diff --git a/crates/ui-egui/src/header.rs b/crates/ui-egui/src/header.rs index fdedbc07..d2ec5f8a 100644 --- a/crates/ui-egui/src/header.rs +++ b/crates/ui-egui/src/header.rs @@ -287,7 +287,7 @@ pub fn color_popup(ui: &mut egui::Ui, id: egui::Id, pos: egui::Pos2, c: &mut [f3 } }); }); - if ui.input(|i| i.pointer.any_pressed()) && !area.response.contains_pointer() || ui.input(|i| i.key_pressed(egui::Key::Escape)) { + if widgets::pressed_outside(ui.ctx(), &area.response) || ui.input(|i| i.key_pressed(egui::Key::Escape)) { ui.data_mut(|d| d.insert_temp(open_id, false)); } changed diff --git a/crates/ui-egui/src/panels/misc.rs b/crates/ui-egui/src/panels/misc.rs index 87229014..dfe0458b 100644 --- a/crates/ui-egui/src/panels/misc.rs +++ b/crates/ui-egui/src/panels/misc.rs @@ -285,8 +285,7 @@ fn dropdown_row(app: &mut EffectcraftApp, ui: &mut egui::Ui, r: Rect, key: &str, } }); }); - let outside = - ui.input(|i| i.pointer.any_pressed()) && !area.response.contains_pointer() && !r.contains(ui.input(|i| i.pointer.interact_pos()).unwrap_or_default()); + let outside = widgets::pressed_outside(ui.ctx(), &area.response) && !r.contains(ui.input(|i| i.pointer.interact_pos()).unwrap_or_default()); if chosen.is_some() || outside || ui.input(|i| i.key_pressed(egui::Key::Escape)) { ui.data_mut(|d| d.insert_temp(pid.with("open"), false)); } diff --git a/crates/ui-egui/src/panels/mod.rs b/crates/ui-egui/src/panels/mod.rs index c7484859..20717ee5 100644 --- a/crates/ui-egui/src/panels/mod.rs +++ b/crates/ui-egui/src/panels/mod.rs @@ -184,7 +184,7 @@ pub fn panel_menu_popup(app: &mut EffectcraftApp, ui: &mut egui::Ui) { } }); }); - if close || (ui.input(|i| i.pointer.any_pressed()) && !area.response.contains_pointer() && !area.response.hovered()) { + if close || crate::widgets::pressed_outside(ui.ctx(), &area.response) { ui.ctx().data_mut(|d| d.remove::<(PanelKind, egui::Pos2)>(id)); } } diff --git a/crates/ui-egui/src/panels/precomp.rs b/crates/ui-egui/src/panels/precomp.rs index 1a59fc9f..1ee55738 100644 --- a/crates/ui-egui/src/panels/precomp.rs +++ b/crates/ui-egui/src/panels/precomp.rs @@ -304,7 +304,7 @@ pub fn mini_flowchart(app: &mut EffectcraftApp, ctx: &egui::Context) { } }); }); - let outside = ctx.input(|i| i.pointer.any_pressed()) && !area.response.contains_pointer(); + let outside = crate::widgets::pressed_outside(ctx, &area.response); if let Some(id) = open { app.session.open_comp(id); close = true; diff --git a/crates/ui-egui/src/panels/text_panels.rs b/crates/ui-egui/src/panels/text_panels.rs index ac97d106..de4f6c93 100644 --- a/crates/ui-egui/src/panels/text_panels.rs +++ b/crates/ui-egui/src/panels/text_panels.rs @@ -387,7 +387,7 @@ fn opentype_popup(app: &mut EffectcraftApp, ui: &mut egui::Ui, id: egui::Id, pos for (aid, r, label) in rects { app.auto.add(&aid, r, &label); } - let outside = ui.input(|i| i.pointer.any_pressed()) && !area.response.contains_pointer() && !area.response.hovered(); + let outside = widgets::pressed_outside(ui.ctx(), &area.response); if outside || ui.input(|i| i.key_pressed(egui::Key::Escape)) { ui.data_mut(|d| d.insert_temp(open_id, false)); } @@ -584,7 +584,7 @@ fn font_popup(app: &EffectcraftApp, ui: &mut egui::Ui, id: egui::Id, pos: egui:: }); }); }); - let outside = ui.input(|i| i.pointer.any_pressed()) && !area.response.contains_pointer() && !area.response.hovered(); + let outside = widgets::pressed_outside(ui.ctx(), &area.response); if chosen.is_some() || outside || ui.input(|i| i.key_pressed(egui::Key::Escape)) { ui.data_mut(|d| d.insert_temp(id.with("open"), false)); } diff --git a/crates/ui-egui/src/panels/viewer_tools.rs b/crates/ui-egui/src/panels/viewer_tools.rs index 73407a93..9448c6cb 100644 --- a/crates/ui-egui/src/panels/viewer_tools.rs +++ b/crates/ui-egui/src/panels/viewer_tools.rs @@ -454,9 +454,7 @@ fn popup(app: &mut EffectcraftApp, ui: &mut egui::Ui, id: &str, anchor: Rect, it } }); }); - let outside = ui.input(|i| i.pointer.any_pressed()) - && !area.response.contains_pointer() - && !anchor.contains(ui.input(|i| i.pointer.interact_pos()).unwrap_or_default()); + let outside = widgets::pressed_outside(ui.ctx(), &area.response) && !anchor.contains(ui.input(|i| i.pointer.interact_pos()).unwrap_or_default()); if chosen.is_some() || outside || ui.input(|i| i.key_pressed(egui::Key::Escape)) { ui.data_mut(|d| d.insert_temp(pid.with("open"), false)); } diff --git a/crates/ui-egui/src/widgets.rs b/crates/ui-egui/src/widgets.rs index 61cb9c5e..0c6586c5 100644 --- a/crates/ui-egui/src/widgets.rs +++ b/crates/ui-egui/src/widgets.rs @@ -229,6 +229,15 @@ pub fn checkbox(ui: &mut Ui, rect: Rect, on: bool, t: &Tokens, id: egui::Id) -> resp } +/// A pointer press this frame landed outside a popup `Area` (its `show` response). egui +/// hit-tests a fixed-position area where it was asked to go, not where it was moved to fit the +/// window, so `contains_pointer` / `hovered` are false over a menu shifted up from the bottom of +/// the window and the menu would close on the press before its entry is clicked (#117). The +/// area's response rect is where it was drawn. +pub fn pressed_outside(ctx: &egui::Context, area: &Response) -> bool { + ctx.input(|i| i.pointer.any_pressed() && !i.pointer.interact_pos().is_some_and(|p| area.rect.contains(p))) +} + /// Show a popup menu anchored at `pos` with string options; returns the chosen index. pub fn popup_menu(ui: &mut Ui, id: egui::Id, pos: egui::Pos2, options: &[String], current: Option) -> Option { let mut chosen = None; @@ -254,7 +263,7 @@ pub fn popup_menu(ui: &mut Ui, id: egui::Id, pos: egui::Pos2, options: &[String] }); }); }); - let clicked_outside = ui.input(|i| i.pointer.any_pressed()) && !area.response.contains_pointer() && !area.response.hovered(); + let clicked_outside = pressed_outside(ui.ctx(), &area.response); if chosen.is_some() || clicked_outside || ui.input(|i| i.key_pressed(egui::Key::Escape)) { ui.data_mut(|d| d.insert_temp(open_id, false)); } diff --git a/crates/ui-egui/tests/ui_render_queue.rs b/crates/ui-egui/tests/ui_render_queue.rs new file mode 100644 index 00000000..1ec5d9e4 --- /dev/null +++ b/crates/ui-egui/tests/ui_render_queue.rs @@ -0,0 +1,46 @@ +//! Render Queue panel input: choosing a Render Settings template from an item's menu (#117). + +use effectcraft_engine::Session; +use effectcraft_engine::project::render_queue::RenderQuality; +use effectcraft_ui_egui::EffectcraftApp; +use effectcraft_ui_egui::dock::PanelKind; +use egui::{Event, Modifiers, Pos2, pos2}; +use egui_kittest::Harness; +use egui_kittest::kittest::Queryable; +use serde_json::json; + +fn click_at(h: &mut Harness<'_, EffectcraftApp>, p: Pos2) { + h.input_mut().events.push(Event::PointerMoved(p)); + h.input_mut().events.push(Event::PointerButton { pos: p, button: egui::PointerButton::Primary, pressed: true, modifiers: Modifiers::NONE }); + h.step(); + h.input_mut().events.push(Event::PointerButton { pos: p, button: egui::PointerButton::Primary, pressed: false, modifiers: Modifiers::NONE }); + h.run_steps(2); +} + +/// The Render Settings menu is taller than the room below the item, so it is moved up to fit +/// the window; its entries must still take the click instead of the menu closing on the press. +#[test] +fn render_settings_template_applies_from_a_menu_moved_to_fit_the_window() { + let mut s = Session::default(); + s.execute("file.openDemoProject", json!({})).unwrap(); + s.execute("renderQueue.add", json!({})).unwrap(); + let id = s.project.render_queue[0].id; + assert_eq!(s.project.render_queue[0].settings.name, "Best Settings"); + let mut app = Some(EffectcraftApp::new(s)); + let mut h = Harness::builder().with_size(egui::vec2(1600.0, 1000.0)).build_eframe(|_| app.take().expect("app")); + // Twirl the item open, as clicking its twirl does. + h.ctx.data_mut(|d| d.insert_temp(egui::Id::new("rq-ui"), (vec![id], None::))); + h.state_mut().show_panel(PanelKind::RenderQueue); + h.run_steps(4); + let dd = h.state().auto.find(&format!("renderQueue.item.{id}.renderSettings")).expect("Render Settings menu").clone(); + let dd_center = pos2(dd.rect[0] + dd.rect[2] / 2.0, dd.rect[1] + dd.rect[3] / 2.0); + click_at(&mut h, dd_center); + let entry = h.query_by_label_contains("Draft Settings").expect("the Draft Settings entry").rect(); + assert!(entry.max.y < dd_center.y, "the menu was moved up to fit the window (entry {entry:?}, menu button at {dd_center:?})"); + click_at(&mut h, entry.center()); + let it = &h.state().session.project.render_queue[0]; + assert_eq!(it.settings.name, "Draft Settings"); + assert_eq!(it.settings.quality, RenderQuality::Draft); + assert_eq!(it.settings.resolution, 0.5); + assert!(h.query_by_label_contains("Quality: Draft").is_none(), "the menu closed after the choice"); +}