diff --git a/crates/ui-egui/src/font_menu.rs b/crates/ui-egui/src/font_menu.rs index 8e61f291e4..590b93a2fa 100644 --- a/crates/ui-egui/src/font_menu.rs +++ b/crates/ui-egui/src/font_menu.rs @@ -367,7 +367,11 @@ pub fn font_menu( let state_id = ui.scope_id().with(&id).with("font-menu"); let mut st: MenuState = ui.data(|d| d.get_temp(state_id)).unwrap_or_default(); ui.data(|d| d.get_temp::(filters_id())).unwrap_or_default().apply(&mut st); - let target = ui.data(|d| d.get_temp(wheel_fields_id(ui.ctx().viewport_id()).with("target"))); + // The viewport is read outside `ui.data`. The closure of `ui.data` runs with the context + // locked, and a second lock taken inside it blocks once a font-sample thread is waiting to + // request a repaint. + let wheel_fields = wheel_fields_id(ui.ctx().viewport_id()); + let target = ui.data(|d| d.get_temp(wheel_fields.with("target"))); let target_changed = st.wheel_target != target; if target_changed { st.wheel_order = None; @@ -435,10 +439,8 @@ pub fn font_menu( out = Some(FontPick::Wheel(steps)); } } - let queued = ui.data(|d| { - d.get_temp::(wheel_fields_id(ui.ctx().viewport_id())) - .is_some_and(|fields| fields.queued.iter().any(|event| event.field == field.id)) - }); + let queued = + ui.data(|d| d.get_temp::(wheel_fields).is_some_and(|fields| fields.queued.iter().any(|event| event.field == field.id))); if drawn || (!field.contains_pointer() && !queued) { st.wheel_order = None; ui.data_mut(|d| d.remove::(field.id.with("wheel"))); diff --git a/crates/ui-egui/src/tests/font_menu.rs b/crates/ui-egui/src/tests/font_menu.rs index 47d6258a1d..7ab5378e14 100644 --- a/crates/ui-egui/src/tests/font_menu.rs +++ b/crates/ui-egui/src/tests/font_menu.rs @@ -1058,6 +1058,54 @@ fn open_menu_with(id: &str, left: f32, width: f32, screen: egui::Vec2, look: Men drawn } +/// The field is drawn while other threads ask the context for repaints, as the threads that +/// render the samples do when they finish. A read of the context made while the menu already +/// holds one waits behind such a request, and a debug build of egui panics after ten seconds. +#[test] +fn the_field_draws_while_other_threads_request_repaints() { + use std::sync::Arc; + use std::sync::atomic::{AtomicBool, Ordering}; + + /// Stops the threads when the test ends, failed or not. + struct Stop(Arc); + impl Drop for Stop { + fn drop(&mut self) { + self.0.store(true, Ordering::Relaxed); + } + } + + let ctx = egui::Context::default(); + crate::theme::install_fonts(&ctx); + let stop = Stop(Arc::default()); + let (started, running) = std::sync::mpsc::channel(); + let threads: Vec<_> = (0..4) + .map(|_| { + let (ctx, stop, started) = (ctx.clone(), stop.0.clone(), started.clone()); + std::thread::spawn(move || { + let _ = started.send(()); + while !stop.load(Ordering::Relaxed) { + ctx.request_repaint(); + } + }) + }) + .collect(); + for _ in &threads { + running.recv().expect("the thread started"); + } + // The wheel on: the field also looks up its queued wheel input. + let look = MenuLook { wheel: true, ..Default::default() }; + for _ in 0..2000 { + let mut out = ctx.run_ui(egui::RawInput::default(), |ui| { + font_menu(ui, "test-repaints", "Inter", 200.0, None, look); + }); + out.textures_delta.clear(); + } + drop(stop); + for thread in threads { + thread.join().expect("the thread ended"); + } +} + /// A narrow field, such as the Control bar's, opens a menu wide enough for the filters on their /// rows and for the families' names, each drawn whole beside its sample. #[test]