diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index d4520bfa8..11fb0984f 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -75,6 +75,8 @@ mod tests_dialog_fit; #[cfg(test)] mod tests_dialogs; #[cfg(test)] +mod tests_edit_layout; +#[cfg(test)] mod tests_error_kinds; #[cfg(test)] mod tests_filmstrip; diff --git a/crates/ui-egui/src/panels/edit.rs b/crates/ui-egui/src/panels/edit.rs index d67f35cc4..0a6b27bcd 100644 --- a/crates/ui-egui/src/panels/edit.rs +++ b/crates/ui-egui/src/panels/edit.rs @@ -165,7 +165,10 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId) { ui.horizontal(|ui| { ui.label(egui::RichText::new(crate::i18n::tr("Profile")).font(t.font(13.0)).color(t.text_dim)); let name = app.session.profile_info(&d.profile.id).map(|(name, _)| name).unwrap_or("Color"); - let r = crate::widgets::dropdown(ui, "profile", crate::i18n::profile_label(&d.profile.id, name), t.font(15.0), t.text_label); + // a long name is cut short before Browse Profiles: a wider row would widen the whole panel's + // content, and the sliders below would run past its edge + let max = ui.available_width() - 28.0 - ui.spacing().item_spacing.x; + let r = crate::widgets::dropdown_within(ui, "profile", crate::i18n::profile_label(&d.profile.id, name), t.font(15.0), t.text_label, max); egui::Popup::menu(&r).show(|ui| profile_menu(app, ui, &d)); ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { if crate::widgets::icon_button( diff --git a/crates/ui-egui/src/tests_edit_layout.rs b/crates/ui-egui/src/tests_edit_layout.rs new file mode 100644 index 000000000..7c6d79ff0 --- /dev/null +++ b/crates/ui-egui/src/tests_edit_layout.rs @@ -0,0 +1,68 @@ +//! The Edit panel at its narrowest with a long profile applied: the profile row fits (#1759). + +use egui::{Rect, 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::edit::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 a_long_profile_name_leaves_the_edit_panel_its_width() { + let width = crate::state::RIGHT_WIDTH.min; + let (texts, widgets) = panel(width); + let rect = |id: &str| widgets.iter().find(|(w, _)| w == id).unwrap_or_else(|| panic!("{id} not drawn")).1; + let dropdown = rect("dropdown:profile"); + let browse = rect("icon:profileBrowser"); + assert!(dropdown.right() <= browse.left(), "the profile menu {dropdown:?} runs into Browse Profiles {browse:?}"); + let name = texts.iter().find(|t| t.galley.job.text == LONG_NAME).unwrap(); + assert!(name.galley.elided, "a name wider than the row is cut short"); + assert!(name.visual_bounding_rect().right() <= dropdown.right(), "the name leaves its menu"); + // every slider drawn, from the profile's Amount down + rect("sliderValue:profile.amount"); + rect("sliderValue:light.exposure"); + for (id, r) in widgets.iter().filter(|(id, _)| id.starts_with("slider")) { + assert!(r.right() <= width, "{id} {r:?} is cut off by the {width} pt panel"); + } +} diff --git a/crates/ui-egui/src/widgets.rs b/crates/ui-egui/src/widgets.rs index 67fa4d394..0318dcbb5 100644 --- a/crates/ui-egui/src/widgets.rs +++ b/crates/ui-egui/src/widgets.rs @@ -691,8 +691,15 @@ fn star_lit(i: u8, rating: u8, hover: Option) -> bool { /// A borderless dropdown button: `text` followed by a painted chevron (no font glyph needed). pub fn dropdown(ui: &mut Ui, id: &str, text: &str, font: egui::FontId, color: Color32) -> Response { + dropdown_within(ui, id, text, font, color, f32::INFINITY) +} + +/// [`dropdown`] at most `max` wide, chevron included: a longer `text` is cut short with …, and +/// shown in full on hover. +pub fn dropdown_within(ui: &mut Ui, id: &str, text: &str, font: egui::FontId, color: Color32, max: f32) -> Response { let t = Tokens::get(ui.ctx()); - let galley = ui.painter().layout_no_wrap(text.to_string(), font, color); + let galley = one_line(ui.painter(), text, font, color, max - 20.0); + let elided = galley.elided; let size = vec2(galley.size().x + 20.0, galley.size().y.max(20.0)); let (r, resp) = ui.allocate_exact_size(size, Sense::click()); resp.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::ComboBox, true, text)); @@ -700,7 +707,7 @@ pub fn dropdown(ui: &mut Ui, id: &str, text: &str, font: egui::FontId, color: Co let c = if resp.hovered() { t.text } else { color }; ui.painter().galley(pos2(r.left(), r.center().y - galley.size().y / 2.0), galley, c); paint(ui.painter(), Rect::from_center_size(pos2(r.right() - 10.0, r.center().y), vec2(16.0, 16.0)), Icon::ChevronDown, c); - resp + if elided { resp.on_hover_text(text) } else { resp } } #[cfg(test)]