mirror of
https://github.com/storytold/lightcraft.git
synced 2026-10-26 02:59:17 -06:00
UI: align Local folder rows, paint dock gaps, bottom-align the tool strip
- Local folders in the sidebar are drawn at their tree indent like Albums and Folders: the icon lines up with the rows above and the disclosure triangle sits in the gutter left of it; children nest by 16 pt (#1097). - The central panel that hosts the dock is filled with chrome, so the gaps the dock leaves between panes no longer show the dark clear colour (#1098). - The tool strip's bottom group is two buttons and one gap tall, so Info ends at the bottom margin as Presets starts at the top one (#1099). Each fix has a headless regression test that fails without it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
b3d392b499
commit
aeb21ac813
@@ -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);
|
||||
|
||||
@@ -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:?}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user