mirror of
https://github.com/storytold/lightcraft.git
synced 2026-10-30 21:18:11 -06:00
Merge PR #1491
# Conflicts: # crates/ui-egui/src/menus.rs # docs/parity-checklist.md # docs/ui-parity.md
This commit is contained in:
@@ -123,6 +123,8 @@ const LAYOUT: &[(&str, &[&str])] = &[
|
||||
"view.photoGrid",
|
||||
"view.squareGrid",
|
||||
"@Grid Info",
|
||||
"view.thumbSizeUp",
|
||||
"view.thumbSizeDown",
|
||||
"view.detail",
|
||||
"view.compare",
|
||||
"view.survey",
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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() }
|
||||
|
||||
@@ -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}"#),
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 (🟡) |
|
||||
|
||||
@@ -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 |
|
||||
|
||||
Reference in New Issue
Block a user