diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 8248ee5a1..c6067f4fe 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -1322,7 +1322,9 @@ impl LightcraftApp { panels::topbar::show(self, ui); panels::library_problem::banner(self, ui); panels::strip::show(self, ui); - egui::CentralPanel::default().frame(egui::Frame::NONE).show(ui, |ui| docking::show(self, ui)); + // filled: the gaps the dock leaves between its panes show chrome, not the dark clear colour + let chrome = theme::Tokens::get(&ctx).chrome; + egui::CentralPanel::default().frame(egui::Frame::NONE.fill(chrome)).show(ui, |ui| docking::show(self, ui)); self.lights_out(&ctx); panels::second::show(self, &ctx); panels::notices::show(self, &ctx); diff --git a/crates/ui-egui/src/panels/left.rs b/crates/ui-egui/src/panels/left.rs index 78c19b7e9..9dc192e14 100644 --- a/crates/ui-egui/src/panels/left.rs +++ b/crates/ui-egui/src/panels/left.rs @@ -1059,7 +1059,9 @@ fn folder_tree( ui.data_mut(|d| d.insert_temp(revealed_id, target)); } } - let resp = row(app, ui, &format!("local:{path}"), Icon::Folder, name, None, sel, indent + 12.0).on_hover_text(path); + // at the tree's own indent, as in Albums and Folders: the triangle sits in the gutter and the + // icon lines up with the rows above + let resp = row(app, ui, &format!("local:{path}"), Icon::Folder, name, None, sel, indent).on_hover_text(path); let tr = tree_toggle(ui, &resp, indent, open_id, &mut open, format!("folderToggle:{path}")); if !tr.clicked() && resp.clicked() @@ -1112,9 +1114,9 @@ fn folder_tree( let _ = f(path); } }); - if open && indent < 12.0 * 8.0 { + if open && indent < 16.0 * 8.0 { for (n, p) in subfolders(ui, path) { - folder_tree(app, ui, place, &n, &p, indent + 12.0, current, reveal, false); + folder_tree(app, ui, place, &n, &p, indent + 16.0, current, reveal, false); } } } @@ -2093,4 +2095,28 @@ mod tests { let l = local_places(builtin(), &[], Some("/home/example/Pictures/Trip"), None, &["/home/example".into()]); assert_eq!((names(&l.places), l.owner), (vec!["Pictures"], Some(0))); } + + #[test] + fn a_local_folder_lines_up_with_the_rows_above_and_its_triangle_stays_left_of_the_icon() { + use crate::Services; + use crate::headless::HeadlessView; + let mut app = crate::LightcraftApp::new(lightcraft_engine::Session::new(), Services::default()); + let mut view = HeadlessView::new(); + let path = "/lightcraft-test/Pictures"; + let mut widgets = vec![]; + for frame in 0..3 { + let raw = HeadlessView::raw_input(egui::vec2(300.0, 200.0), 1.0, frame as f64 / 60.0, vec![]); + view.run(raw, |ui| { + super::row(&mut app, ui, "all", super::Icon::Photos, "All Photos", None, false, 0.0); + super::folder_tree(&mut app, ui, path, "Pictures", path, 0.0, None, false, false); + }); + widgets = crate::widgets::take_registry(&view.ctx); + } + let rect = |id: &str| widgets.iter().find(|(w, _)| w == id).unwrap_or_else(|| panic!("{id} not registered")).1; + let (all, local) = (rect("label:all"), rect(&format!("label:local:{path}"))); + assert_eq!(local.left(), all.left(), "a top-level Local folder's name starts where All Photos' does"); + let triangle = rect(&format!("folderToggle:{path}")); + // the icon is 16 points wide and ends 8 points before the name + assert!(triangle.right() <= local.left() - 24.0, "triangle {triangle:?} overlaps the icon before {local:?}"); + } } diff --git a/crates/ui-egui/src/panels/strip.rs b/crates/ui-egui/src/panels/strip.rs index ae1d6e557..164204b86 100644 --- a/crates/ui-egui/src/panels/strip.rs +++ b/crates/ui-egui/src/panels/strip.rs @@ -9,6 +9,11 @@ use crate::state::RightPanel; use crate::theme::Tokens; use crate::widgets::icon_button; +/// Height of one tool button. +const BUTTON_H: f32 = 40.0; +/// Space between tool buttons. +const GAP: f32 = 6.0; + pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui) { let t = Tokens::get(ui.ctx()); egui::Panel::right("tool_strip") @@ -23,8 +28,8 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui) { .show(ui, |ui| { let has_photo = app.session.active().is_some(); ui.vertical_centered(|ui| { - ui.spacing_mut().item_spacing.y = 6.0; - let sz = vec2(t.strip_w, 40.0); + ui.spacing_mut().item_spacing.y = GAP; + let sz = vec2(t.strip_w, BUTTON_H); if icon_button(ui, "presets", Icon::Presets, sz, app.ui.presets, has_photo, "Presets (Shift+P)").clicked() { let _ = app.run("panel.presets", json!({})); } @@ -54,14 +59,17 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui) { app.ui.dialog = Some(crate::state::Dialog::About); } }); - // bottom icons + // bottom icons: two buttons and one gap tall, so Info ends at the bottom margin as + // Presets starts at the top one let bottom = ui.max_rect().bottom(); - let mut child = ui.new_child( - egui::UiBuilder::new() - .max_rect(egui::Rect::from_min_max(egui::pos2(ui.max_rect().left(), bottom - 96.0), ui.max_rect().right_bottom())), - ); + let mut child = + ui.new_child(egui::UiBuilder::new().max_rect(egui::Rect::from_min_max( + egui::pos2(ui.max_rect().left(), bottom - (2.0 * BUTTON_H + GAP)), + ui.max_rect().right_bottom(), + ))); child.vertical_centered(|ui| { - let sz = vec2(t.strip_w, 40.0); + ui.spacing_mut().item_spacing.y = GAP; + let sz = vec2(t.strip_w, BUTTON_H); if icon_button(ui, "keywords", Icon::Tag, sz, app.ui.right == RightPanel::Keywords, has_photo, "Keywords (K)").clicked() { let _ = app.run("panel.keywords", json!({})); } @@ -76,3 +84,27 @@ fn separator(ui: &mut egui::Ui, t: &Tokens) { let (r, _) = ui.allocate_exact_size(vec2(t.strip_w - 16.0, 1.0), egui::Sense::hover()); ui.painter().rect_filled(r, 0.0, t.button_border.gamma_multiply(0.7)); } + +#[cfg(test)] +mod tests { + use super::*; + use crate::Services; + use crate::headless::HeadlessView; + + #[test] + fn info_ends_as_far_above_the_bottom_as_presets_starts_below_the_top() { + let mut app = LightcraftApp::new(lightcraft_engine::Session::new(), Services::default()); + let mut view = HeadlessView::new(); + let mut widgets = vec![]; + for frame in 0..3 { + let raw = HeadlessView::raw_input(vec2(300.0, 700.0), 1.0, frame as f64 / 60.0, vec![]); + view.run(raw, |ui| show(&mut app, ui)); + widgets = crate::widgets::take_registry(&view.ctx); + } + let rect = |id: &str| widgets.iter().find(|(w, _)| w == id).unwrap_or_else(|| panic!("{id} not registered")).1; + let (presets, keywords, info) = (rect("icon:presets"), rect("icon:keywords"), rect("icon:info")); + let (top, bottom) = (presets.top(), 700.0 - info.bottom()); + assert!((top - bottom).abs() < 0.5, "Presets starts {top} below the top, Info ends {bottom} above the bottom"); + assert_eq!(info.top() - keywords.bottom(), GAP, "the bottom buttons are spaced like the top ones"); + } +} diff --git a/crates/ui-egui/src/tests_docking_acceptance.rs b/crates/ui-egui/src/tests_docking_acceptance.rs index 0e3c499a7..1dc97892e 100644 --- a/crates/ui-egui/src/tests_docking_acceptance.rs +++ b/crates/ui-egui/src/tests_docking_acceptance.rs @@ -21,6 +21,22 @@ fn dock(h: &mut Headless, p: serde_json::Value) { fn rect(h: &Headless, id: &str) -> egui::Rect { h.app.widgets.iter().find(|(key, _)| key == id).unwrap().1 } + +#[test] +fn the_gaps_between_dock_panes_show_chrome_not_the_dark_clear_colour() { + let mut h = app(); + let image = h.snapshot(std::time::Duration::from_secs(5)); + let pixel = |p: egui::Pos2| image.pixels[p.y as usize * image.size[0] + p.x as usize]; + // each panel's own chrome just inside its edge stroke, and the dock's gap just outside it + let right = rect(&h, "panel:right_panel"); + let left = rect(&h, "panel:left_panel"); + for (panel, inside, gap) in [ + ("Tools", egui::pos2(right.left() + 3.0, right.center().y), egui::pos2(right.left() - 2.0, right.center().y)), + ("Library", egui::pos2(left.right() - 3.0, left.center().y), egui::pos2(left.right() + 2.0, left.center().y)), + ] { + assert_eq!(pixel(gap), pixel(inside), "the gap beside {panel} at {gap:?} is not chrome"); + } +} fn release(h: &mut Headless, pos: egui::Pos2) { h.app.synthetic.push(Event::PointerButton { pos, button: PointerButton::Primary, pressed: true, modifiers: Modifiers::NONE }); h.step();