diff --git a/crates/ui-egui/src/panels/dialogs.rs b/crates/ui-egui/src/panels/dialogs.rs index 5ae99e18a..f14d7338b 100644 --- a/crates/ui-egui/src/panels/dialogs.rs +++ b/crates/ui-egui/src/panels/dialogs.rs @@ -308,6 +308,8 @@ pub fn show(app: &mut LightcraftApp, ctx: &egui::Context) { let widest = if settings { ctx.data(|d| d.get_temp::(widest_id)).unwrap_or(0.0).min(screen.width() * 0.9) } else { 0.0 }; let mut content_w = 0.0f32; let mut body_h = None; + // before the window: drawing it closes a dropdown the click lands outside of + let popup_was_open = egui::Popup::is_any_open(ctx); let shown = egui::Window::new(crate::i18n::tr(&title)).id(window_id) .collapsible(false) .resizable(import) @@ -1491,7 +1493,7 @@ pub fn show(app: &mut LightcraftApp, ctx: &egui::Context) { } if let Some(w) = &shown { // (the import review keeps its hand-picked photos however the click lands) - if app.ui.settings.close_dialogs_outside && !import && clicked_backdrop(ctx, w.response.rect) { + if app.ui.settings.close_dialogs_outside && !import && clicked_backdrop(ctx, w.response.rect, popup_was_open) { close = true; } } @@ -1595,11 +1597,12 @@ 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 +/// popup is open (a dropdown, a date picker's calendar) only closes the popup: `popup_was_open` is +/// read before the window is drawn, since drawing it already closes the popup. Only a press made /// 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 { +fn clicked_backdrop(ctx: &egui::Context, window: egui::Rect, popup_was_open: bool) -> 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 @@ -1618,7 +1621,7 @@ fn clicked_backdrop(ctx: &egui::Context, window: egui::Rect) -> bool { (at(true), at(false), i.pointer.primary_clicked()) }); if let Some(p) = press { - let outside = !window.contains(p) && !egui::Popup::is_any_open(ctx); + let outside = !window.contains(p) && !popup_was_open; ctx.data_mut(|d| d.insert_temp(press_id, outside)); } let Some(release) = release else { return false }; diff --git a/crates/ui-egui/src/tests_dialogs.rs b/crates/ui-egui/src/tests_dialogs.rs index aefe4863b..2e96b59e7 100644 --- a/crates/ui-egui/src/tests_dialogs.rs +++ b/crates/ui-egui/src/tests_dialogs.rs @@ -128,6 +128,28 @@ fn a_click_outside_closes_the_dialog_when_settings_allow_it() { assert!(h.app.ui.dialog.is_none(), "the next click on the backdrop does"); } +/// With a dropdown open in the dialog, a click on the backdrop only closes the dropdown, even when +/// it arrives pressed and released in one frame (the dropdown closes while the dialog is drawn). +#[test] +fn a_click_outside_an_open_dropdown_keeps_the_dialog() { + let mut h = demo([1400.0, 1000.0], json!({"view": "detail", "right": "edit"})); + h.app.ui.settings.close_dialogs_outside = true; + h.app.run("dialog.export", json!({})).unwrap(); + h.step(); + h.step(); + let r = h.request("ui.clickWidget", json!({"id": "combo:exportPreset"}), T); + assert_eq!(r["ok"], true, "{r}"); + h.step(); + assert!(egui::Popup::is_any_open(&h.view.ctx), "the preset dropdown is open"); + let at = egui::pos2(10.0, 990.0); + let press = |pressed| egui::Event::PointerButton { pos: at, button: egui::PointerButton::Primary, pressed, modifiers: egui::Modifiers::NONE }; + h.events.extend([egui::Event::PointerMoved(at), press(true), press(false)]); + h.step(); + h.step(); + assert!(!egui::Popup::is_any_open(&h.view.ctx), "the click closes the dropdown"); + assert!(h.app.ui.dialog.is_some(), "but not the dialog"); +} + #[test] fn modal_dialogs_close_background_menus() { let mut h = demo([1400.0, 1000.0], json!({"view": "detail", "right": "edit"}));