From 0cf062f508685da7461315854812ac60fc3ffcc2 Mon Sep 17 00:00:00 2001 From: Marco Arnulfo Date: Wed, 14 Oct 2026 12:58:40 +0200 Subject: [PATCH] LR-PROF-BROWSER: long profile names are cut short in a narrow panel (fixes #1634) --- crates/ui-egui/src/lib.rs | 2 + crates/ui-egui/src/panels/profiles.rs | 22 +++---- crates/ui-egui/src/tests_profiles_layout.rs | 70 +++++++++++++++++++++ 3 files changed, 79 insertions(+), 15 deletions(-) create mode 100644 crates/ui-egui/src/tests_profiles_layout.rs diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 37fb86df3..783e4ce10 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -109,6 +109,8 @@ mod tests_preset_import; #[cfg(test)] mod tests_preview_limit; #[cfg(test)] +mod tests_profiles_layout; +#[cfg(test)] mod tests_quit_unsaved; #[cfg(test)] mod tests_sam_setup; diff --git a/crates/ui-egui/src/panels/profiles.rs b/crates/ui-egui/src/panels/profiles.rs index 6294d1e64..d6564d083 100644 --- a/crates/ui-egui/src/panels/profiles.rs +++ b/crates/ui-egui/src/panels/profiles.rs @@ -12,7 +12,7 @@ use super::presets::cover_uv; use crate::icons::{Icon, paint}; use crate::state::RightPanel; use crate::theme::Tokens; -use crate::widgets::{divider, icon_button, register}; +use crate::widgets::{divider, icon_button, one_line, register}; use crate::{HoverPreview, LightcraftApp}; /// Long edge of the variant thumbnails (px). @@ -35,13 +35,9 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId) { let (cr, _) = ui.allocate_exact_size(vec2(ui.available_width(), 34.0), Sense::hover()); let name = app.session.profile_info(&d.profile.id).map(|(name, _)| name).unwrap_or("Color"); ui.painter().text(pos2(cr.left() + 24.0, cr.center().y), Align2::LEFT_CENTER, crate::i18n::tr("Profile"), t.font(13.0), t.text_dim); - ui.painter().text( - pos2(cr.left() + 84.0, cr.center().y), - Align2::LEFT_CENTER, - crate::i18n::profile_label(&d.profile.id, name), - t.font(14.0), - t.text_label, - ); + // a long name is cut short (with …) at the panel's edge + let g = one_line(ui.painter(), crate::i18n::profile_label(&d.profile.id, name), t.font(14.0), t.text_label, cr.width() - 84.0 - 24.0); + ui.painter().galley(pos2(cr.left() + 84.0, cr.center().y - g.size().y / 2.0), g, t.text_label); if d.profile.id != "lc.color" { super::edit::control(app, ui, &d, "profile.amount", true); ui.add_space(6.0); @@ -174,13 +170,9 @@ fn cell_ui( painter.rect_stroke(thumb, 3.0, Stroke::new(1.0, t.text_dim), egui::StrokeKind::Outside); } let name_color = if applied { t.text } else { t.text_label }; - painter.text( - pos2(cell.left() + 2.0, thumb.bottom() + 12.0), - Align2::LEFT_CENTER, - crate::i18n::profile_label(p_id, p_name), - t.font(12.0), - name_color, - ); + // cut short (with …) to the cell, so it never runs into its neighbour; the full name on hover + let g = one_line(painter, crate::i18n::profile_label(p_id, p_name), t.font(12.0), name_color, cell.width() - 2.0); + painter.galley(pos2(cell.left() + 2.0, thumb.bottom() + 12.0 - g.size().y / 2.0), g, name_color); // favourite star (always shown when set, on hover otherwise) let fav = app.session.profile_favorites.iter().any(|f| f == p_id); let star = Rect::from_center_size(pos2(thumb.right() - 13.0, thumb.top() + 13.0), vec2(20.0, 20.0)); diff --git a/crates/ui-egui/src/tests_profiles_layout.rs b/crates/ui-egui/src/tests_profiles_layout.rs new file mode 100644 index 000000000..ccc521151 --- /dev/null +++ b/crates/ui-egui/src/tests_profiles_layout.rs @@ -0,0 +1,70 @@ +//! The profile browser in a narrow right panel: names stay inside their cells (#1634). + +use egui::{Rect, pos2, vec2}; +use serde_json::json; + +use crate::{LightcraftApp, Services, headless::HeadlessView}; + +const LONG_ID: &str = "lut:kodak-portra-400-plus-one-stop-overexposed"; +const LONG_NAME: &str = "Kodak Portra 400 Plus One Stop Overexposed"; + +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) -> (Vec, Vec<(String, Rect)>) { + let mut app = LightcraftApp::new(lightcraft_engine::Session::with_demo(), Services::default()); + app.session.lut_profiles.push(lightcraft_engine::cmd::lut_profiles::LutProfile { + id: LONG_ID.into(), + name: LONG_NAME.into(), + group: "Film Stocks".into(), + file: String::new(), + }); + app.run("develop.profile", json!({"id": LONG_ID})).unwrap(); + let photo = app.session.active().unwrap(); + let ctx = egui::Context::default(); + 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, 4000.0), 1.0, 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::profiles::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); + } + (texts, widgets) +} + +#[test] +fn profile_names_stay_inside_their_cells_in_a_narrow_panel() { + let width = 250.0; + let (texts, widgets) = panel(width); + let cell = widgets.iter().find(|(id, _)| id == &format!("profileCell:{LONG_ID}")).unwrap().1; + // the cell's name sits below its thumbnail; the header's copy is above every cell + let name = texts.iter().find(|t| t.galley.job.text == LONG_NAME && t.pos.y >= cell.top()).unwrap(); + assert!(name.galley.elided, "a name wider than its cell is cut short"); + let bounds = name.visual_bounding_rect(); + assert!(bounds.left() >= cell.left() && bounds.right() <= cell.right(), "{bounds:?} spills out of {cell:?}"); + let applied = texts.iter().find(|t| t.galley.job.text == LONG_NAME && t.pos.y < cell.top()).unwrap(); + assert!(applied.visual_bounding_rect().right() <= width, "the applied profile's name leaves the panel"); + // built-in names too: each one stays out of its neighbour's cell + for p in lightcraft_engine::presets::PROFILES { + let cell = widgets.iter().find(|(id, _)| id == &format!("profileCell:{}", p.id)).unwrap().1; + let name = texts.iter().find(|t| t.galley.job.text == p.name && cell.contains(pos2(cell.center().x, t.pos.y))).unwrap(); + let bounds = name.visual_bounding_rect(); + assert!(bounds.left() >= cell.left() && bounds.right() <= cell.right(), "{}: {bounds:?} spills out of {cell:?}", p.name); + } +}