diff --git a/crates/ui-egui/src/panels/dialogs.rs b/crates/ui-egui/src/panels/dialogs.rs index c15e126bf..a53da854c 100644 --- a/crates/ui-egui/src/panels/dialogs.rs +++ b/crates/ui-egui/src/panels/dialogs.rs @@ -1515,16 +1515,31 @@ pub fn show(app: &mut LightcraftApp, ctx: &egui::Context) { if app.ui.dialog.as_ref().is_some_and(|now| *now != at_start) { return; } + if !close { + let frame = ctx.cumulative_frame_nr(); + ctx.data_mut(|d| d.insert_temp(egui::Id::new(BACKDROP_SHOWN), frame)); + } app.ui.dialog = if close { None } else { Some(dlg) }; } +/// The last frame a dialog was drawn and stayed open, for [`clicked_backdrop`]. +const BACKDROP_SHOWN: &str = "dialog-backdrop-shown"; + /// A click on the dimmed backdrop around the dialog: it closes the dialog like Esc. Pressed and /// released outside the window, so a slider dragged past its edge doesn't close it; a press while a /// popup is open (a dropdown, a date picker's calendar) only closes the popup. Only a press made -/// while the dialog is shown counts, so the click that opened it never closes it. (Read from the -/// raw events: egui forgets the press origin on release, and a scripted click is one frame.) +/// while the dialog is shown counts: in the frame a dialog appears, its input is the click that +/// opened it (a quick click on a slow frame presses and releases in that one frame), so it never +/// closes it. (Read from the raw events: egui forgets the press origin on release.) fn clicked_backdrop(ctx: &egui::Context, window: egui::Rect) -> bool { let press_id = egui::Id::new("dialog-backdrop-press"); + let frame = ctx.cumulative_frame_nr(); + // the same frame too: a second pass of it gets no input + let shown = ctx.data(|d| d.get_temp::(egui::Id::new(BACKDROP_SHOWN))).is_some_and(|f| f + 1 >= frame); + if !shown { + ctx.data_mut(|d| d.remove_temp::(press_id)); + return false; + } let (press, release, clicked) = ctx.input(|i| { let at = |down: bool| { i.events.iter().rev().find_map(|e| match e { diff --git a/crates/ui-egui/src/tests_dialogs.rs b/crates/ui-egui/src/tests_dialogs.rs index f08eb62b3..aefe4863b 100644 --- a/crates/ui-egui/src/tests_dialogs.rs +++ b/crates/ui-egui/src/tests_dialogs.rs @@ -114,6 +114,18 @@ fn a_click_outside_closes_the_dialog_when_settings_allow_it() { h.step(); assert!(h.app.ui.dialog.is_none(), "a click on the backdrop closes the dialog"); assert_eq!(h.app.session.catalog.photo(active).unwrap().rating, rating, "the click doesn't reach the panel below"); + // the click that opens a dialog lands outside its window; a quick one on a slow frame arrives + // pressed and released in that one frame + let gear = h.app.widgets.iter().find(|(id, _)| id == "icon:copyGear").map(|(_, r)| r.center()).unwrap(); + let press = |pressed| egui::Event::PointerButton { pos: gear, button: egui::PointerButton::Primary, pressed, modifiers: egui::Modifiers::NONE }; + h.events.extend([egui::Event::PointerMoved(gear), press(true), press(false)]); + h.step(); + h.step(); + assert!(h.app.ui.dialog.is_some(), "the click that opens a dialog doesn't close it"); + let r = h.request("ui.clickWidget", json!({"id": "star:bottom:5"}), T); + assert_eq!(r["ok"], true, "{r}"); + h.step(); + assert!(h.app.ui.dialog.is_none(), "the next click on the backdrop does"); } #[test]