LR-BEHAV-DIALOGS: the click that opens a dialog never closes it

A quick click on a slow frame presses and releases in the frame where the
panels open a dialog, and the backdrop check read that press as one on the
backdrop, closing the dialog at once. Input now counts only once the dialog
was already shown the frame before; a pending press is dropped otherwise.

The test opens Copy Settings from the bottom-bar gear with press and release
in one frame and checks it stays open, then that the next backdrop click
closes it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Ketbome
2026-10-20 13:07:20 -03:00
co-authored by Claude Opus 5.5
parent a2a1505953
commit 4a6c43ce06
2 changed files with 29 additions and 2 deletions
+17 -2
View File
@@ -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::<u64>(egui::Id::new(BACKDROP_SHOWN))).is_some_and(|f| f + 1 >= frame);
if !shown {
ctx.data_mut(|d| d.remove_temp::<bool>(press_id));
return false;
}
let (press, release, clicked) = ctx.input(|i| {
let at = |down: bool| {
i.events.iter().rev().find_map(|e| match e {
+12
View File
@@ -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]