From cad024fbff42284b078068aa0f5e0570d0f74cb1 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 8 Oct 2026 20:47:07 +0000 Subject: [PATCH] Part of #372: painted-text layout tests from #376 The tests from @QuirkyTurtle94's #376, a duplicate fix for #372 opened while #379 was in review: they read the painted text shapes (not the registered rects) at 1x and 2x scale, in English, German and Brazilian Portuguese, at panel widths 250-520, and check that tile labels fit unelided inside their tiles, that a long mask name is elided before the eye, and that a long Describe prompt keeps Select inside the panel. They pass unchanged against main's implementation. Co-Authored-By: Elliot Graham Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01912WaK6Rmew3wxZuPyX7E8 --- crates/ui-egui/src/lib.rs | 2 + crates/ui-egui/src/tests_masking_layout.rs | 92 ++++++++++++++++++++++ 2 files changed, 94 insertions(+) create mode 100644 crates/ui-egui/src/tests_masking_layout.rs diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index a76f167f..d64cf899 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -39,6 +39,8 @@ mod tests_library_problem; #[cfg(test)] mod tests_masking; #[cfg(test)] +mod tests_masking_layout; +#[cfg(test)] mod tests_offline; #[cfg(test)] mod tests_panels; diff --git a/crates/ui-egui/src/tests_masking_layout.rs b/crates/ui-egui/src/tests_masking_layout.rs new file mode 100644 index 00000000..74313ed3 --- /dev/null +++ b/crates/ui-egui/src/tests_masking_layout.rs @@ -0,0 +1,92 @@ +//! Check painted text as well as widget rectangles: custom painters can overflow a valid widget. + +use egui::{Rect, pos2, vec2}; +use serde_json::json; + +use crate::{LightcraftApp, Services, headless::HeadlessView, i18n::Locale}; + +const LONG_NAME: &str = "The person standing beside the very long fence across the background"; + +fn text_shapes(shape: &egui::Shape, out: &mut Vec) { + match shape { + egui::Shape::Text(text) => out.push(text.clone()), + egui::Shape::Vec(shapes) => shapes.iter().for_each(|s| text_shapes(s, out)), + _ => {} + } +} + +fn panel(width: f32, scale: f32, locale: Locale, describe: bool) -> (Vec, Vec<(String, Rect)>) { + let mut app = LightcraftApp::new(lightcraft_engine::Session::with_demo(), Services::default()); + app.run("mask.add", json!({"kind": "subject", "name": LONG_NAME})).unwrap(); + app.ui.describe = describe.then(|| ("new".into(), LONG_NAME.repeat(4))); + app.ui.detail_due = Some((100.0, 1)); + let photo = app.session.active().unwrap(); + let ctx = egui::Context::default(); + crate::i18n::set_language(locale); + crate::theme::install_fonts(&ctx); + crate::theme::apply(&ctx); + let mut texts = Vec::new(); + let mut widgets = Vec::new(); + for frame in 0..3 { + let raw = HeadlessView::raw_input(vec2(width, 1600.0), scale, frame as f64 / 60.0, vec![]); + let mut out = ctx.run_ui(raw, |ui| { + egui::CentralPanel::default().frame(egui::Frame::NONE).show(ui, |ui| { + crate::panels::masking::show(&mut app, ui, photo); + }); + }); + out.textures_delta.clear(); + texts.clear(); + for shape in &out.shapes { + text_shapes(&shape.shape, &mut texts); + } + widgets = crate::widgets::take_registry(&ctx); + } + if describe { + assert_eq!(app.ui.describe.as_ref().unwrap().1, LONG_NAME.repeat(4), "layout must not shorten the actual prompt"); + } + crate::i18n::set_language(Locale::En); + (texts, widgets) +} + +#[test] +fn masking_tile_labels_fit_without_eliding_at_supported_panel_widths() { + for locale in [Locale::En, Locale::De, Locale::PtBr] { + for width in [250.0, 270.0, 330.0, 520.0] { + for scale in [1.0, 2.0] { + let (texts, widgets) = panel(width, scale, locale, false); + for (kind, label) in [("background", "Background"), ("luminanceRange", "Luminance"), ("prompt", "Describe")] { + crate::i18n::set_language(locale); + let label = crate::i18n::tr(label).to_string(); + crate::i18n::set_language(Locale::En); + let tile = widgets.iter().find(|(id, _)| id == &format!("maskNew:{kind}")).unwrap().1; + let text = texts.iter().find(|t| t.galley.job.text == label && t.pos.y < tile.bottom()).unwrap(); + assert!(!text.galley.elided, "{locale:?} {width} {label} should be readable in full"); + assert!( + tile.shrink2(vec2(2.0, 0.0)).contains_rect(text.visual_bounding_rect()), + "{locale:?} {width} {scale}: {label} exceeds {tile:?}" + ); + } + } + } + } +} + +#[test] +fn masking_long_names_and_describe_controls_stay_inside_the_panel() { + for locale in [Locale::En, Locale::De, Locale::PtBr] { + for width in [250.0, 270.0, 520.0] { + let (texts, widgets) = panel(width, 2.0, locale, true); + let row = widgets.iter().find(|(id, _)| id == "mask:1").unwrap().1; + let name = texts.iter().find(|t| t.galley.job.text == LONG_NAME).unwrap(); + assert!(name.galley.elided, "long mask names should be elided"); + assert!( + Rect::from_min_max(pos2(row.left() + 32.0, row.top()), pos2(row.right() - 28.0, row.bottom())) + .contains_rect(name.visual_bounding_rect()) + ); + let field = widgets.iter().find(|(id, _)| id == "maskDescribe").unwrap().1; + let button = widgets.iter().find(|(id, _)| id == "button:maskDescribeGo").unwrap().1; + assert!(field.right() <= button.left(), "the prompt must not overlap Select"); + assert!(field.left() >= 24.0 && button.right() <= width - 22.0, "{locale:?} {width}: {field:?} {button:?}"); + } + } +}