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 <noreply@anthropic.com>
This commit is contained in:
bflatastic 2026-10-07 01:19:25 +07:00
parent ea25e402b1
commit 8a415e03ca
8 changed files with 63 additions and 11 deletions

View File

@ -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

View File

@ -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));
}

View File

@ -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));
}
}

View File

@ -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;

View File

@ -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));
}

View File

@ -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));
}

View File

@ -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<usize>) -> Option<usize> {
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));
}

View File

@ -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::<u64>)));
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");
}