From 8a415e03caff993e812c643890841fff9ca667ca Mon Sep 17 00:00:00 2001 From: bflatastic Date: Wed, 7 Oct 2026 01:19:25 +0700 Subject: [PATCH] Render Queue: Render Settings templates apply from the menu (#117) Choosing Draft Settings (or any entry) in a Render Queue item's Render Settings menu did nothing when the menu had been moved up to fit the window. egui hit-tests a fixed-position Area where it was asked to go, not where it was moved, so the popup's "pressed outside" check was true over its own entries and the menu closed on the press, before the click. widgets::pressed_outside tests the press against the rect the popup was drawn at. Every popup with the same check uses it: popup_menu (Render Queue, Effect Controls, dropdowns), the colour popup, the Preview and viewer popups, the panel menu, the Flowchart, OpenType and font menus. Regression test: ui_render_queue.rs picks Draft Settings from a menu moved up to fit a 1000 px window; it failed before the fix. Co-Authored-By: Claude Opus 5.5 --- crates/ui-egui/src/header.rs | 2 +- crates/ui-egui/src/panels/misc.rs | 3 +- crates/ui-egui/src/panels/mod.rs | 2 +- crates/ui-egui/src/panels/precomp.rs | 2 +- crates/ui-egui/src/panels/text_panels.rs | 4 +- crates/ui-egui/src/panels/viewer_tools.rs | 4 +- crates/ui-egui/src/widgets.rs | 11 +++++- crates/ui-egui/tests/ui_render_queue.rs | 46 +++++++++++++++++++++++ 8 files changed, 63 insertions(+), 11 deletions(-) create mode 100644 crates/ui-egui/tests/ui_render_queue.rs 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"); +}