# Conflicts:
#	crates/ui-egui/src/menus.rs
#	docs/parity-checklist.md
#	docs/ui-parity.md
This commit is contained in:
Brandon Thomas
2026-10-14 22:33:08 -04:00
9 changed files with 186 additions and 5 deletions
+2
View File
@@ -123,6 +123,8 @@ const LAYOUT: &[(&str, &[&str])] = &[
"view.photoGrid",
"view.squareGrid",
"@Grid Info",
"view.thumbSizeUp",
"view.thumbSizeDown",
"view.detail",
"view.compare",
"view.survey",
+18
View File
@@ -91,6 +91,9 @@ pub const UI_COMMANDS: &[UiCommand] = &[
("view.navigate", "Set Image Zoom and Pan", None, ""),
("view.zoomIn", "Zoom In", Some("Cmd+="), "View"),
("view.zoomOut", "Zoom Out", Some("Cmd+-"), "View"),
// Library grids only (Photo Grid, Square Grid, People): the bottom-bar slider's step by key
("view.thumbSizeUp", "Increase Grid Thumbnail Size", Some("="), "View"),
("view.thumbSizeDown", "Decrease Grid Thumbnail Size", Some("-"), "View"),
("view.clipping", "Show Clipping", Some("J"), "View"),
("view.visualizeHdr", "Visualize HDR", None, "View"),
// in grids S expands/collapses stacks (the engine command it shadows)
@@ -827,6 +830,15 @@ pub fn run_ui_command(app: &mut LightcraftApp, id: &str, p: &Value) -> Option<Re
app.ui.zoom = if next == 0.0 { Zoom::Fit } else { Zoom::Percent(next) };
Ok(Value::Null)
}
"view.thumbSizeUp" | "view.thumbSizeDown" => {
// the bottom-bar slider's range, one `THUMB_SIZE_STEP` per call; {} → {size}
if !thumb_size_view(app) {
return Some(Err(format!("{id}: only in Photo Grid, Square Grid or People")));
}
let step = if id == "view.thumbSizeUp" { crate::state::THUMB_SIZE_STEP } else { -crate::state::THUMB_SIZE_STEP };
app.ui.thumb_size = (app.ui.thumb_size + step).clamp(crate::state::THUMB_SIZE_MIN, crate::state::THUMB_SIZE_MAX);
Ok(json!({"size": app.ui.thumb_size}))
}
"view.navigate" => {
// {zoom?: "fit"|"fill"|{percent: number}, pan?: [x, y]} (normalized image centre).
// Validate the complete request before changing either part of the viewport.
@@ -2059,9 +2071,15 @@ pub fn run_ui_command(app: &mut LightcraftApp, id: &str, p: &Value) -> Option<Re
Some(r)
}
/// Views whose bottom bar has the thumbnail-size slider and that draw tiles of that size.
fn thumb_size_view(app: &LightcraftApp) -> bool {
matches!(app.ui.view, ViewMode::PhotoGrid | ViewMode::SquareGrid | ViewMode::People)
}
pub fn ui_enabled(app: &LightcraftApp, id: &str) -> bool {
match id {
"library.selectOnlyActive" => app.session.active().is_some(),
"view.thumbSizeUp" | "view.thumbSizeDown" => thumb_size_view(app),
"dialog.contactSheet" | "app.contactSheet" => !cfg!(target_arch = "wasm32") && app.session.active().is_some() && app.export.is_none(),
"dialog.slideshowExport" | "app.slideshowExport" => {
!cfg!(target_arch = "wasm32") && !app.session.slideshow_export.as_ref().is_some_and(|r| r.running())
+3 -2
View File
@@ -245,13 +245,14 @@ fn right_side(app: &mut LightcraftApp, ui: &mut egui::Ui, full: Rect, compact_zo
let p = child.painter();
let y = r.center().y;
p.line_segment([pos2(r.left(), y), pos2(r.right(), y)], egui::Stroke::new(2.0, t.track));
let f = (app.ui.thumb_size - 90.0) / (480.0 - 90.0);
let (min, max) = (crate::state::THUMB_SIZE_MIN, crate::state::THUMB_SIZE_MAX);
let f = (app.ui.thumb_size - min) / (max - min);
let x = r.left() + f * r.width();
p.rect_filled(Rect::from_center_size(pos2(x, y), vec2(3.0, 14.0)), 1.0, t.thumb_hover);
if let Some(pp) = resp.interact_pointer_pos()
&& (resp.dragged() || resp.clicked())
{
app.ui.thumb_size = 90.0 + ((pp.x - r.left()) / r.width()).clamp(0.0, 1.0) * (480.0 - 90.0);
app.ui.thumb_size = min + ((pp.x - r.left()) / r.width()).clamp(0.0, 1.0) * (max - min);
}
}
if child.min_rect().width() > 0.0 { child.min_rect().left() } else { full.right() }
+3
View File
@@ -204,6 +204,9 @@ pub const ALIASES: &[(&str, &str, &str)] = &[
("F6", "view.filmstrip", "{}"),
("F7", "view.leftPanel", "{}"),
("Shift+M", "version.create", "{}"),
// grid thumbnail size: Classic's key is `=`; `+` (keypad, or Shift+= reported as Plus) agrees
("Plus", "view.thumbSizeUp", "{}"),
("Shift+Plus", "view.thumbSizeUp", "{}"),
("Shift+X", "photo.flag", r#"{"flag": "reject", "advance": true}"#),
("Shift+Z", "photo.flag", r#"{"flag": "pick", "advance": true}"#),
("Shift+U", "photo.flag", r#"{"flag": "none", "advance": true}"#),
+7 -1
View File
@@ -558,6 +558,12 @@ impl AppSettings {
/// Click-zoom ratios offered (percent): 1:1, 2:1, 3:1, 4:1, 8:1.
pub const CLICK_ZOOMS: [u32; 5] = [100, 200, 300, 400, 800];
/// Grid thumbnail size (points): the bottom-bar slider's range, and the `=` / `-` keys' step
/// (`view.thumbSizeUp` / `view.thumbSizeDown`), about a thirteenth of the range.
pub const THUMB_SIZE_MIN: f32 = 90.0;
pub const THUMB_SIZE_MAX: f32 = 480.0;
pub const THUMB_SIZE_STEP: f32 = 30.0;
/// Width limits of a side panel the user resizes (points).
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct PanelWidth {
@@ -1417,7 +1423,7 @@ impl UiState {
}
/// Clamp values restored from disk.
pub fn sanitized(mut self) -> Self {
self.thumb_size = self.thumb_size.clamp(90.0, 480.0);
self.thumb_size = self.thumb_size.clamp(THUMB_SIZE_MIN, THUMB_SIZE_MAX);
self.left_width = LEFT_WIDTH.clamp(self.left_width);
self.right_width = RIGHT_WIDTH.clamp(self.right_width);
self.brush_size = self.brush_size.clamp(0.002, 0.5);
+138 -1
View File
@@ -1,7 +1,8 @@
//! Headless tests of the View ▸ Zoom shortcuts and modifier-wheel zoom: Cmd+= / Cmd+- / Cmd+0
//! (Ctrl on Windows and Linux) zoom the photo and never egui's interface scale, which would
//! otherwise take the same keys at the end of the frame; Ctrl+wheel over the photo in the Edit
//! view zooms it.
//! view zooms it. Also the grid thumbnail-size keys `=` / `-` (Classic's Library Grid keys), which
//! share the physical keys with Cmd+= / Cmd+- but act on the grids only.
use std::time::Duration;
@@ -85,3 +86,139 @@ fn ctrl_wheel_over_the_photo_zooms_it_in_the_edit_view() {
}
assert!((h.app.image_rect.unwrap().width() - zoomed.width()).abs() < 0.01, "a plain wheel changed the zoom");
}
fn grid_view(view: crate::state::ViewMode) -> Headless {
let services = Services { png: None, ..Default::default() };
let mut app = LightcraftApp::new(lightcraft_engine::Session::with_demo(), services);
app.ui.settings.gpu = false;
app.ui.view = view;
let mut h = Headless::new(app, [1400.0, 900.0], 1.0);
h.step();
h.step();
h
}
fn press(h: &mut Headless, k: &str, shift: bool, cmd: bool) {
let r = h.request("ui.key", json!({"key": k, "shift": shift, "cmd": cmd}), T);
assert_eq!(r["ok"], true, "{r}");
h.step();
h.step();
}
#[test]
fn equals_and_minus_step_the_grid_thumbnail_size_and_clamp() {
use crate::state::{THUMB_SIZE_MAX, THUMB_SIZE_MIN, THUMB_SIZE_STEP, ViewMode};
for view in [ViewMode::PhotoGrid, ViewMode::SquareGrid, ViewMode::People] {
let mut h = grid_view(view);
h.app.ui.thumb_size = 220.0;
press(&mut h, "=", false, false);
assert_eq!(h.app.ui.thumb_size, 220.0 + THUMB_SIZE_STEP, "{view:?}: =");
press(&mut h, "-", false, false);
press(&mut h, "-", false, false);
assert_eq!(h.app.ui.thumb_size, 220.0 - THUMB_SIZE_STEP, "{view:?}: - -");
// `+` (Shift+= on a US layout, or the keypad) is the same key for the same job
press(&mut h, "Plus", true, false);
press(&mut h, "Plus", false, false);
assert_eq!(h.app.ui.thumb_size, 220.0 + THUMB_SIZE_STEP, "{view:?}: +");
// clamped at both ends of the bottom-bar slider's range, however many presses
h.app.ui.thumb_size = THUMB_SIZE_MAX - 5.0;
for _ in 0..3 {
press(&mut h, "=", false, false);
assert!(h.app.ui.thumb_size <= THUMB_SIZE_MAX);
}
assert_eq!(h.app.ui.thumb_size, THUMB_SIZE_MAX, "{view:?}");
h.app.ui.thumb_size = THUMB_SIZE_MIN + 5.0;
for _ in 0..3 {
press(&mut h, "-", false, false);
assert!(h.app.ui.thumb_size >= THUMB_SIZE_MIN);
}
assert_eq!(h.app.ui.thumb_size, THUMB_SIZE_MIN, "{view:?}");
// the whole range is walked in whole steps from either end
let mut presses = 0;
while h.app.ui.thumb_size < THUMB_SIZE_MAX {
press(&mut h, "=", false, false);
presses += 1;
assert!(presses <= 20, "never reached the maximum");
}
assert_eq!(presses, ((THUMB_SIZE_MAX - THUMB_SIZE_MIN) / THUMB_SIZE_STEP).ceil() as usize);
}
}
#[test]
fn thumbnail_size_keys_do_nothing_outside_the_grids() {
use crate::state::ViewMode;
for view in [ViewMode::Detail, ViewMode::Compare, ViewMode::Survey, ViewMode::Reference] {
let mut h = grid_view(view);
h.app.ui.thumb_size = 220.0;
let zoom = h.app.ui.zoom;
for (k, shift) in [("=", false), ("-", false), ("Plus", true)] {
press(&mut h, k, shift, false);
}
assert_eq!(h.app.ui.thumb_size, 220.0, "{view:?}");
assert_eq!(h.app.ui.zoom, zoom, "{view:?}: a bare = / - is not a zoom key");
// the command says why, and the menu shows it unavailable
let r = h.request("ui.menu.invoke", json!({"id": "view.thumbSizeUp"}), T);
assert_eq!(r["ok"], false, "{view:?}: {r}");
assert_eq!(h.app.ui.thumb_size, 220.0);
let list = h.request("ui.menu.list", json!({}), T);
let entry = list["result"].as_array().and_then(|a| a.iter().find(|e| e["id"] == "view.thumbSizeUp")).cloned().unwrap_or_default();
assert_eq!(entry["enabled"], false, "{view:?}: {entry}");
}
}
#[test]
fn cmd_equals_and_minus_still_zoom_and_leave_the_thumbnail_size_alone() {
// in the loupe: the photo zooms, the grid size is untouched
let mut h = edit_view();
h.app.ui.thumb_size = 220.0;
key(&mut h, "=");
assert_eq!(h.app.ui.zoom, Zoom::Percent(50.0));
key(&mut h, "-");
assert_eq!(h.app.ui.zoom, Zoom::Percent(25.0));
assert_eq!(h.app.ui.thumb_size, 220.0);
// in a grid Cmd+= / Cmd+- are View ▸ Zoom In / Out, not the size keys
let mut h = grid_view(crate::state::ViewMode::PhotoGrid);
h.app.ui.thumb_size = 220.0;
key(&mut h, "=");
key(&mut h, "-");
assert_eq!(h.app.ui.thumb_size, 220.0, "Cmd+= / Cmd+- changed the grid size");
}
#[test]
fn search_field_keeps_equals_and_minus() {
let mut h = grid_view(crate::state::ViewMode::PhotoGrid);
h.app.ui.thumb_size = 220.0;
let r = h.request("ui.clickWidget", json!({"id": "field:search"}), T);
assert_eq!(r["ok"], true, "{r}");
h.step();
for (k, text) in [("-", "-"), ("=", "="), ("Plus", "+")] {
press(&mut h, k, false, false);
let r = h.request("ui.text", json!({"text": text}), T);
assert_eq!(r["ok"], true, "{r}");
h.step();
}
assert_eq!(h.app.ui.thumb_size, 220.0, "typing in the search field resized the grid");
assert_eq!(h.app.ui.search, "-=+", "the search field did not receive the keys");
}
#[test]
fn thumbnail_size_commands_run_from_the_control_channel_and_persist() {
let mut h = grid_view(crate::state::ViewMode::SquareGrid);
h.app.ui.thumb_size = 120.0;
let r = h.request("ui.menu.invoke", json!({"id": "view.thumbSizeUp"}), T);
assert_eq!(r["ok"], true, "{r}");
assert_eq!(r["result"]["size"], 150.0);
let r = h.request("engine.execute", json!({"command": "view.thumbSizeDown"}), T);
assert_eq!(r["ok"], true, "{r}");
assert_eq!(h.app.ui.thumb_size, 120.0);
let r = h.request("ui.menu.invoke", json!({"id": "view.thumbSizeUp"}), T);
assert_eq!(r["ok"], true, "{r}");
// the size is saved with the rest of the UI state, like the slider's, and restored clamped
let saved = serde_json::to_value(&h.app.ui).unwrap();
assert_eq!(saved["thumbSize"], 150.0);
let loaded = serde_json::from_value::<crate::state::UiState>(saved).unwrap().sanitized();
assert_eq!(loaded.thumb_size, 150.0);
let mut wild = serde_json::to_value(&h.app.ui).unwrap();
wild["thumbSize"] = json!(9000.0);
assert_eq!(serde_json::from_value::<crate::state::UiState>(wild).unwrap().sanitized().thumb_size, crate::state::THUMB_SIZE_MAX);
}
+11 -1
View File
@@ -58,7 +58,17 @@ commit `4e012a59a6d2442ba749f109ea27aadb5c0b11a5` by Gardy, and addresses the ca
[issue #283](https://github.com/storytold/lightcraft/issues/283). That PR was open when checked
on 2026-10-08; this work extends its fix with library culling bindings and tests.
Public behaviour references, checked 2026-10-08:
## Grid thumbnail size
`=` makes the grid thumbnails larger and `-` smaller (`+` also enlarges), as in Lightroom Classic. Each
press moves the bottom-bar slider by 30 pt within its 90–480 pt range and stops at either end. The keys
work in Photo Grid, Square Grid and People (the views with the slider), do nothing in Detail, Compare,
Survey and Reference, and leave text fields alone, so the search box still types `-` and `=`. `⌘=` /
`⌘−` remain Zoom In / Out. The same steps are the View ▸ Increase / Decrease Grid Thumbnail Size
commands (`view.thumbSizeUp`, `view.thumbSizeDown`, reachable from the control channel and MCP, which
return `{"size"}`); the size is saved with the other interface settings.
Public behaviour references, checked 2026-10-08 (thumbnail size: 2026-10-13):
- [Lightroom Classic keyboard shortcuts](https://helpx.adobe.com/lightroom-classic/help/keyboard-shortcuts.html)
- [Lightroom Classic flagging, labelling and rating](https://helpx.adobe.com/lightroom-classic/help/flag-label-rate-photos.html)
+1
View File
@@ -859,3 +859,4 @@ Latin stays upright. Advanced Japanese composition remains open. Regression cove
| 2026-10-12 | minor | LRC-LIB-CATALOG-IMPORT: lasting destination/result, offline records and virtual copies count as added; redundant missing-file probes and staged photo copies removed. Status and summary counts unchanged. |
| 2026-10-13 | minor | LRC-LIB-CATALOG-IMPORT review: offline Local promotion and result dismissal across library switches covered; redundant operation/settings copies removed. Status and summary counts unchanged. |
| 2026-10-14 | patch | LRC-LIB-CATALOG-IMPORT: reconciled with upstream phased progress, cancellation kinds, WAL-only import, translations and current progress tracking. Status and summary counts unchanged. |
| 2026-10-13 | minor | KEYC-COMPARE: `=` / `-` (also `+`) increase / decrease the grid thumbnail size (`view.thumbSizeUp` / `view.thumbSizeDown`, View menu), grids only; status unchanged (🟡) |
+3
View File
@@ -68,6 +68,8 @@ under the UI command that wraps it (a no-op error); the UI command now wins.
**Library culling (KEYC-RATING):** `0–5`, `6–9` and `P/X/U` reach egui on macOS even when displayed in the native menu (adapted from PR #261; issue #283). `Shift+0–9` applies and advances once; `Shift+P` picks and advances in Photo Grid / Square Grid and retains Presets elsewhere. See [library shortcuts](library-shortcuts.md) for the verified Classic mapping and regression coverage. `` ` `` cycles the flag (unflagged → pick → reject → unflagged) and ⌘↑ / ⌘↓ raise / lower it; the native menu leaves ⌘↑ / ⌘↓ to egui so Compare/Survey act on the active photo. Remaining Classic gaps: filter-bar keys.
**Grid thumbnail size (KEYC-COMPARE):** Classic's `=` and `-` increase and decrease the Grid thumbnail size (same keys on macOS and Windows; `+` also works for the larger size here, as Shift+`=` or the keypad). They step the bottom-bar slider by 30 pt inside its 90–480 pt range, in Photo Grid, Square Grid and People only; Detail, Compare, Survey and Reference ignore them, and ⌘= / ⌘− stay Zoom In / Out. See [library shortcuts](library-shortcuts.md).
**Still missing / broken:**
- No command yet: F1 help (verify the rest of the old list: full screen, settings, stacks,
visualize spots and merges have commands now).
@@ -129,6 +131,7 @@ a real screen reader on any platform yet.
| 2026-10-12 | minor | Accessibility: crop handles, spots, gradient handles and mask pins operable by keyboard with AccessKit names and values; Tab order test for the right panels; hand-drawn icons and rows named; 55% → 75%; UI remaining 34–58 h → 32–54 h |
| 2026-10-13 | minor | Shortcuts: Tool Overlay modes for spot and mask pins on ⇧H (H stays Remove; issue #224), listed with the deliberate differences; dropped from the missing list |
| 2026-10-13 | minor | Library culling: backtick cycles the flag; ⌘↑ / ⌘↓ raise / lower flag status |
| 2026-10-13 | minor | KEYC-COMPARE: `=` / `-` grid thumbnail size keys noted in the shortcuts section |
| 2026-10-12 | minor | Accessibility: an AccessKit tree audit test over every view, Develop tool and 13 dialogs; unnamed fields, slider ranges and the Auto Stack slider fixed; 30% → 55%; UI remaining 40–70 h → 34–58 h |
| 2026-10-11 | minor | Zoom row: Compare and Reference get zoom windows; what remains of LR-VIEW-ZOOM is overlays and soft proofing |
| 2026-10-10 | major | Created: measured command and shortcut counts, assessment by area, estimates; moved the shortcut conflicts section here from the checklist |