Commit Graph
137 Commits
Author SHA1 Message Date
OneSlipandClaude Opus 5.5 077f4a507f GPU-FULL: more of the render on the GPU, loupe drafts from GPU textures (#1065)
* GPU-FULL-1: LUT profiles, Refine Edges and soft proofs render on the GPU

These three sent the whole loupe / compare / export render back to the CPU
pipeline (media::develop and lightcraft_gpu::render declined them).

- LUT profiles: the .cube table rides in the finish kernel's aux buffer
  (LUT_* parameters); lut_apply mirrors Lut3d::apply (trilinear, domain),
  applied after grain and blended by the profile amount, as on the CPU.
- Refine Edges: the cross guided filter Auto Mask already ran on the device,
  now with a combine mode (max for strokes, mix by sqrt(refine) for Refine),
  applied to the combined alpha before the mask's invert and amount.
- Soft proofing: proof-space gamut mapping and the destination / display
  gamut warnings in the kernel (PROOF_* parameters, GAMUT_WARN and warning
  colours generated from the CPU constants), for output spaces and displays.

Equivalence tests (RTX 5070, DX12): LUT profiles at three amounts and a
non-default domain, Refine 15/60/100 on two masks, proofs in 16 combinations
of space, output, display and warnings - all max 1 LSB, mean <= 0.0005 LSB.
The tests also check that strong refine and the destination warning change
the image (so they aren't comparing two unchanged renders).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* GPU-FULL-2: spot removal, defringe and the histogram on the GPU

Spots and defringe made every render that had them (and every white
balance or spot edit) read the resampled image back, run the scene-linear
stage on the CPU and upload it again. Now only the ProRAW local tone map
(off by default) does.

- Defringe: OkLab pass, separable lightness min/max filter, per-pixel
  apply. Parameters come from the new optics::defringe_params, which the
  CPU stage now uses too.
- Spots: new spot module (spot_prep / spot_apply over each spot's box, the
  Heal tone correction as device Gaussians), spot by spot in place so each
  reads what the earlier ones left, as on the CPU. Remove's automatic
  source: spot_score rates the 48 candidates on the device and only the
  scores come back. Layout, candidates, spoke directions, radii and ranking
  are shared helpers in pipeline::spots (spots::apply uses them; behaviour
  unchanged, pipeline tests green).
- Histogram: counted with atomics on the device on Histogram::of_srgb8's
  sampling grid (exact), instead of a CPU pass over the readback.

Tests: spots (clone, heal, auto remove incl. near the edge where no
candidate fits, brushed strokes, feather 0, cut by the edge, overlapping)
and defringe (purple/green, hue ranges, preview and full size) max 1 LSB;
the device histogram equals the CPU count of the returned image for small,
sampled (>1 MP) and windowed renders; the scoring kernel picks the CPU's
candidate. (The edge case caught a real bug: "doesn't fit" was written as a
decimal literal that rounds below f32::MAX.) Unit tests that create the
device now take turns with the crash-sentinel test, which swaps a global.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* GPU-FULL-2: fix the spot kernels' build; GPU tests fail when kernels don't build

07d4680b's spot module declared `const F32_MAX = bitcast<f32>(...)`, which
naga can't evaluate as a constant: the kernels failed to build, the device
was unavailable, and the equivalence tests skipped as if there were no
adapter - so that commit's GPU tests didn't actually run. f32::MAX is now a
function, and the equivalence suite (and the device unit test) fail instead
of skipping when an adapter exists but the kernels don't build.

Re-verified on the RTX 5070 (DX12), all on the device: 21 equivalence tests
incl. the 8 spot cases (max 1 LSB), defringe, the exact device histogram,
and the auto-source unit test.

Adds an ignored thumbnail throughput bench (tests/thumbs_bench.rs).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* GPU-FULL-3: thumbnails render on the GPU once the device is ready

Every cached job (grid and filmstrip thumbnails, preset and profile
previews, faces, quick stand-ins) was forced onto the CPU. They now use the
GPU when lightcraft_gpu::ready() - so they never block on device creation
at startup (1.8 s incl. kernels here) and render on the CPU until then.
Views and exports keep waiting for the device as before.

tests/thumbs_bench.rs (ignored): six workers rendering 256 px thumbnails of
512 px sources with preset-like settings (NR, sharpening, clarity, grading,
a gradient mask): RTX 5070 3.6 ms each vs 4.3-5.2 ms on 32 CPU threads,
with the cores left free for raw decoding.

Engine and UI suites: unchanged (only the 12 failures that predate this
branch on Windows: tests_sync paths, tests_model_setup).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* GPU-FULL-4: the loupe's drafts are drawn straight from the GPU

Rendering and drawing now share one device: the desktop app creates the
window's wgpu device with the adapter's limits and hands it to the GPU
renderer (lightcraft_gpu::adopt_window_device, on a background thread at
startup, under the crash sentinel; a device that doesn't suit falls back to
one of the renderer's own, as before).

A drag's drafts of the views are RenderJob::on_screen: the render ends in an
Rgba8Unorm texture (lightcraft_gpu::render_display -> Output::Display, new
`present` kernels: unwritten-pixel count, row padding for the copy) and
egui-wgpu draws it as a native texture (ui-egui ScreenTextures, implemented
by the app) - no readback, no conversion to egui pixels, no re-upload. The
readback's checks run on the device (atomic unwritten count, device
histogram). Renders that need pixels keep the readback: the settled full
render (view cache, overlays, screenshots), thumbnails, exports, overlays,
windows that spots grew, a monitor profile.

- engine: RenderJob::on_screen, RenderResult::frame (ScreenFrame).
- ui-egui: Tex::native / Tex::id() (21 draw sites), Renderer
  set_screen_textures, natives per slot (freed when pixels return),
  ui.inspect perf.gpuWindowDevice / screenTextures / mainOnGpu.
- app: WindowTextures, window device limits, LIGHTCRAFT_GPU_WINDOW=0 to opt
  out.

Tests: crates/gpu/tests/display.rs adopts a device in its own process and
checks the texture holds exactly the readback's pixels (padded and
unpadded widths, draft and full), the histogram matches, and overlays /
grown windows come back as pixels. In the app (RTX 5070, a 45 MP CR3 from
the user's library copy): all 90 drafts of a drag drawn from GPU textures,
1.69 ms vs 2.05 ms median per draft render with the readback; the frame
screenshot shows the image correctly; pixels return after the drag.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* GPU-FULL-2: the denoise runner tests take turns with the crash-sentinel tests

Building a denoise runner creates a device and compiles kernels under the
init marker. Running in parallel with backend::tests, it could arm the marker
the test had just set, so "is removed afterwards" failed now and then. The
runner tests now hold DEVICE_TEST_LOCK while they build, like the render tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* GPU-FULL-4: fix the build after merging upstream main

Upstream's QuickJob panic guard builds a RenderResult (add `frame: None`),
its idle-trim test builds a Tex (add `native: None`), and its on_exit test
builds App (add the WindowTextures slot).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* GPU-FULL-2: upstream's blur tests take turns with the crash-sentinel tests

gaussian_matches_cpu and bench_kernels create the device, so they hold
DEVICE_TEST_LOCK like every other device test in lightcraft-gpu.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Tests: the same-folder export test compares paths separator-blind (Windows)

The written path is the original's folder (`\` on Windows) joined with `Exports/IMG_1.jpg`;
the expected one had `\Exports`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Tests: the folder-drop tests find folder widgets separator-blind (Windows)

The folder tree spells its paths with `/`; the tests built widget ids from Windows paths with
`\`, so none was found and every test failed on Windows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Tests: the Windows path fixes take the same form as in the other PR branches

So the branches merge into main without conflicts between their copies of the fix.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* GPU-FULL: headless screenshots read back GPU-drawn views; LIGHTCRAFT_GPU_LIMITS caps the adopted device

Review of PR #1065:
- A headless `ui.screenshot` taken mid-drag showed the photo as a white
  rectangle: the GPU-resident draft left no CPU copy. With keep_pixels
  (control channel) the renderer now keeps each view's frame, and
  `cpu_textures` reads it back through `ScreenTextures::read`
  (`lightcraft_gpu::read_texture`). Drawing still never reads back, so
  drags keep the speed-up.
- Adopting the window's device skipped LIGHTCRAFT_GPU_LIMITS; `Gpu::adopt`
  now caps its limits the same way `Gpu::new` does.

Tests: a_headless_screenshot_reads_back_a_view_drawn_from_the_gpu (ui-egui),
the_limits_override_caps_an_adopted_device (gpu); the display test uses
read_texture and skips through lightcraft_testkit::skip!.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* GPU-FULL: review fixes (CodeRabbit 2026-10-21) and slideshow/print draw GPU-drawn views

- the window-device adoption is announced before its thread starts: until it finishes, the
  settings' warm-up and renders don't create a device of their own (they raced it)
- the device error / device-lost handlers go on only after the kernels build, so a declined
  window device keeps the window's own handler
- releasing a slot frees its GPU-resident texture and kept frame
- a soft proof into a colour space drafts full size on the GPU; only one to a device profile
  counts as proofed for `handles`
- docs: Fill spots and device-profile soft proofs render on the CPU
- slideshow, slide layout and print draw `Tex::id()` (the GPU texture when there is one)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* GPU-FULL: the display test skips when LIGHTCRAFT_GPU=0 switches the GPU off

It panicked ("GPU rendering is off") instead of skipping like the other GPU tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* GPU-FULL: a view readback never panics on a lost device

read_texture (headless screenshots of GPU-drawn views) polled the device
directly. On a lost device wgpu 30 panics in Device::poll; it now goes
through the same guard as the renderer's own polls (poll_device): a lost
device returns an error, stops the GPU and is not polled again. Review of
PR #1065. Also quiets a browser-build dead-code warning on OnScreen::Frame.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* GPU-FULL: an HDR edit's histogram shows the HDR range on the on-screen GPU path too

develop_on_screen returned the GPU's SDR histogram for both its Display and Image arms; it now
applies lightcraft_pipeline::hdr_histogram as develop_reporting does (None for drafts, windows,
proofs and non-HDR edits, which keep the GPU's histogram).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* GPU-FULL: the Navigator draws the photo's texture as it is drawn in the loupe

The Navigator falls back to the main view's texture before the photo has a thumbnail. It drew
`t.tex`, which is stale (or the 1x1 placeholder) when the main view holds a GPU-resident draft;
it now draws `Tex::id()`, like every other draw site.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* GPU-FULL: the egui 0.37 lifecycle test's texture names no native id

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* GPU-FULL: rustfmt the lifecycle test

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* GPU-FULL: the second window fits the frame's size; freed GPU frames drop their views

The second window sized its image from the texture handle, which is only a
1x1 placeholder for a GPU-resident render; it now uses the frame's size.
When a GPU-resident render can't be shown and every native texture is
freed, the views pointing at them (Before, Region, Compare tiles) are
dropped too, so they render again as pixels instead of drawing a freed id.
Test: freeing_gpu_frames_drops_the_views_drawn_from_them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(gpu): LIGHTCRAFT_GPU_ADAPTER picks the card for rendering too

The variable chose the card only for AI Denoise and the face detector
(two copies of the same enumerate-and-match code); photo rendering
always took the first high-performance adapter, on a two-card machine
the one driving the screens.

One helper, backend::pick_adapter, now serves the render device
(Gpu::new), the denoise network and the face detector: the first
hardware card whose name contains the value (any case), else the
high-performance one. A name that matches no card is logged and falls
back instead of failing. backend::may_adopt_window_device says whether
rendering may share the window's device (only for the window's card).
Name matching is unit-tested without a GPU; the equivalence tests print
the card they ran on. Documented in docs/gpu-pipeline.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* GPU-FULL: a view switching to a GPU texture releases its pixel image

Review of #1065 (CodeRabbit nitpick): when a slot that had pixels switched to
a GPU-resident texture, its egui texture kept the full-size image, never drawn
while the native texture is set. Tex::show_native now swaps it for a 1x1
placeholder on the switch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(gpu): LIGHTCRAFT_GPU_ADAPTER picks among several matches in a defined order and logs once

Review of #3428:
- several cards match: the one whose whole name it is, then dedicated before built-in before
  virtual cards, then wgpu's order (backend::choose_named_adapter); the log names every match
  and the one used;
- no match: the default choice, with a warning that lists the cards;
- a name matching only a software renderer: never used, and the warning says so;
- each line is logged once (backend::log_once), though the render device, AI Denoise and the
  face models each ask;
- tests on fake adapter lists: a_fake_adapter_list_picks_in_a_defined_order,
  a_choice_is_logged_once.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* GPU-FULL: the release note gives the GPU's documented bound against the CPU (3 levels, mean under half a level)

Review of PR #3544.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* GPU-FULL: the window's device isn't adopted when LIGHTCRAFT_GPU_ADAPTER names another card

With #3428 merged, Gpu::new already makes the render device on the named card; the window's
device was still adopted first, so the app rendered on the screen's card whatever the variable
said. backend::may_adopt_window_device (tested on names in backend.rs) now decides: another
card named renders on a device of its own, and the window stays on the screen's card.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 05:10:32 +01:00
aiongg e05c4f7e7c Merge track r3-t08
# Conflicts:
#	crates/meta/README.md
#	crates/ui-egui/locales/cs.json
#	crates/ui-egui/locales/de.json
#	crates/ui-egui/locales/es.json
#	crates/ui-egui/locales/fi.json
#	crates/ui-egui/locales/fr.json
#	crates/ui-egui/locales/id-formats.json
#	crates/ui-egui/locales/id.json
#	crates/ui-egui/locales/it.json
#	crates/ui-egui/locales/ja.json
#	crates/ui-egui/locales/ka-formats.json
#	crates/ui-egui/locales/ka.json
#	crates/ui-egui/locales/pl.json
#	crates/ui-egui/locales/pt-br.json
#	crates/ui-egui/locales/ru.json
#	crates/ui-egui/locales/sv-formats.json
#	crates/ui-egui/locales/sv.json
#	crates/ui-egui/locales/uk.json
#	crates/ui-egui/locales/vi.json
#	crates/ui-egui/locales/zh-hans.json
#	crates/ui-egui/locales/zh-hant.json
#	crates/ui-egui/src/panels/grid.rs
#	crates/ui-egui/tests/it/main.rs
#	docs/gaps.md
#	docs/library-shortcuts.md
#	docs/parity-checklist.md
#	docs/raw-parity.md
#	docs/user/guides/export-photos.md
#	docs/whats-new.md
2026-10-31 02:31:16 +00:00
aiongg 8415fd6a90 Merge track r3-t06
# Conflicts:
#	docs/parity-checklist.md
#	docs/whats-new.md
2026-10-31 02:30:12 +00:00
aiongg 67b636329a Merge track r3-t03
# Conflicts:
#	crates/ui-egui/src/tests/drop_import.rs
#	crates/ui-egui/tests/it/main.rs
#	docs/gaps.md
#	docs/parity-checklist.md
#	docs/whats-new.md
2026-10-31 02:29:52 +00:00
aiongg e8566e839a Merge PR #2952: LR-EDIT-COLOR-TEMP: a one-field white balance partial keeps the other field's look (#2910)
Conflicts: docs unioned
2026-10-31 02:09:50 +00:00
aiongg e595d9de5e Merge PR #3408: Control: pointer and scroll gestures refuse numbers past any screen instead of queueing NaN positions
docs/whats-new.md: both entries kept, newest first.
2026-10-31 02:07:04 +00:00
Bruno-Alumn 7283694154 Control: pointer and scroll gestures refuse numbers past any screen instead of queueing NaN positions 2026-10-30 19:51:48 +01:00
tylerphotosandCodex 6b1fc20053 LR-IMP-ADD-DIALOG: render import previews when the camera JPEG is too small
Require a covering embedded image only for renderable import candidates. Keep the available camera JPEG for preview-only raws and failed or unavailable render fallbacks; ordinary quick views and grid thumbnails retain their immediate stand-in behavior. Also release import preview state when canceled through the control channel.

Co-Authored-By: Codex <noreply@openai.com>
2026-10-29 11:59:23 -05:00
tylerphotosandCodex 8ab4641011 LR-IMP-ADD-DIALOG: finish import thumbnail sizing review (#2056)
Reconcile with current modules, preserve all locale catalogs, reuse small-source preview jobs, release dismissed review state, and keep exact-entry Escape inside the slider.

Co-Authored-By: Codex <noreply@openai.com>
2026-10-29 07:56:08 -05:00
helgehelge123 840e33a311 Merge branch 'main' of https://github.com/storytold/lightcraft into space-hold-pan
# Conflicts:
#	crates/ui-egui/src/lib.rs
2026-10-27 02:49:16 +01:00
Chad ElliottandClaude Opus 5.5 0a76419c4e fix(mcp): #3069 resend a request that crossed the app's idle-close notice
A request sent just as the app expires an idle control connection crosses
the app's id-less idle-close notice: the client's pre-send check saw an
open connection, wrote the request, and read the notice as its reply, so
`--connect` and the MCP server failed a command the app never read.

The app reads nothing after its idle timeout, so such a request never ran.
The app now marks the notice `"closed": "idle"`, and `Remote::call` sends
a request whose reply is that notice once more on a new connection (it
also matches the notice's exact text, for apps from before the marker).
At most one resend per call, whatever the reason; a reply with an id or
any other error is returned as before.

`idle_close_reconnects_without_replaying_a_command` now forces both
orders (notice before the next request, and a request crossing it)
instead of racing the notice; with main's client it fails every run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 11:08:24 -05:00
helgehelge123 bb59018de6 Merge remote-tracking branch 'origin/main' into space-hold-pan
# Conflicts:
#	crates/ui-egui/src/control.rs
#	docs/control-protocol.md
2026-10-25 23:48:05 +01:00
hnoerenbergandClaude fa7e5b9cb4 LR-EDIT-COLOR-TEMP: a one-field white balance partial lands where a mode answers its own white (#2910)
Reviewing #2952 turned up two more shapes of the same bug, both on the path this branch is about.

`effective_wb` ignores the temp / tint stored on an `As Shot` white balance — it answers the file's
as-shot white — and on a raw edited in Kelvin it also ignores a named preset mode's stored numbers,
through `WbMode::preset`. So a one-field partial that names its scale, `wb {temp: 5800, scale:
"kelvin"}`, merged into such a photo, was stored and then read by nothing: the panel shows 5800 K
while the render stays at as shot, and the stored tint no longer matches what renders. An unmarked
partial already materialized a Custom value through the relative-scale conversion; the named-scale
ones never did. The photo's preparation now writes out the white the photo really renders as Custom
values before the partial lands, for `As Shot` and for preset modes on a Kelvin source. `Auto` keeps
its mode — its gray is resolved and stored when the mode is set, not at render time — and a partial
that names a `mode` itself stays authoritative. Sources without the Kelvin scale (a raw whose camera
has no matrices, a rendered file) had the same swallow, and the number reaches their render now.

The issue asks for one photo-aware merge in every caller that writes settings onto a photo, and names
preset apply. `preset.apply` ran `Preset::apply` → `apply_partial` → `DevelopSettings::merged` and
never saw the photo's scale, so a preset authored by hand naming one white balance number still had
the original bug; measured on a photo at 5000 K / +12, it landed the photo on the relative scale and
moved the tint it never mentioned. The preparation and the normalize-back now wrap `Preset::apply`
per photo, with the amount blending and the mask merging untouched, which also puts an amount below
100 % on a single scale. Presets the app writes (`preset.create` → `extract_groups`) and the crs /
XMP importers name mode, temp, tint and scale together and so land in the branch that behaves
exactly as before.

Tests: the `As Shot` and preset-mode cases in the #2910 merge test, and the new
`preset.apply` test, fail before this commit (the first stores the number in a mode that renders the
photo's old white; the second demotes the photo to `Legacy`); the same test then pins that
`preset.apply` and `develop.merge` agree on a one-field white balance, which is the point of having
one merge.

Docs: the rule for an agent is in `docs/control-protocol.md` and `docs/mcp.md`, recorded on the
`LR-EDIT-COLOR-TEMP` row of `docs/parity-checklist.md`, and in `docs/whats-new.md`.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-10-25 21:54:35 +01:00
hnoerenbergandClaude 049034e247 LR-EDIT-COLOR-TEMP: a one-field white balance partial keeps the other field's look (#2910)
`develop.merge`, `lightcraft-cli render --settings` and MCP `set_develop` merged a white balance
partial that names only `temp` or only `tint` straight into the photo's settings, so the field the
partial leaves out — stored on the photo's scale — is re-read on the partial's scale. On a raw
edited in Kelvin that moves the green/magenta balance of photos the partial never mentioned:
`wb {temp: 5800}` on a photo at 5000 K / +12 tint lands on the relative scale, a shift of ~2.5 tint
units.

Resolve a one-field white balance partial against the photo: bring its stored white balance onto
the partial's scale with the photo's own as-shot white, merge, then put the result back on the
scale that photo is edited on. The inverse of `legacy_to_kelvin` is the composition of the two
Bradford adaptations run backwards: `lightcraft_pipeline::local::kelvin_to_legacy`. It maps the
source's own white to the relative scale's 6500 / 0 exactly, and answers None for a white the
relative scale cannot hold. Partials naming both fields or neither, presets and the crs / Luminar
importers are untouched: they always write `temp`, `tint` and `scale` together.

Tests: the round trip over the slider's reach, in both directions, and hostile inputs
(`crates/pipeline/src/local.rs`); the merge cases both ways plus the unchanged ones, and a restated
value that must not move the white at all (`crates/engine/src/tests_color.rs`). Both fail before
the change.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-10-25 21:54:35 +01:00
aiongg 672baaea46 Merge lightcraft#2913 (egui 0.37) into docking-adoption; craft-ui and craft-docpath at craft-libs main a091680 2026-10-25 17:36:59 +00:00
skylar taylor-barrick c627ce7210 egui-037: preserve modal input ownership and packaged font notices 2026-10-25 13:14:34 +01:00
skylartaylor e7144adc1f M0.37: reconcile current LightCraft main and qualify the migration 2026-10-25 07:47:22 +01:00
aiongg 7291abdc14 Merge upstream main into docking-adoption 2026-10-25 05:55:03 +00:00
aiongg 31ec62cd03 Info panel Target Photo edits, Add Keywords cursor request and Fixed keys follow-ups (#2889)
* LR-PREF-SHORTCUTS: the shortcuts panel's Fixed keys ignore keys saved for unknown commands

* KEYC-METADATA: an Add Keywords request the Keywords panel can't take in time expires

* LR-META-INFO: Target Photo saves an Info edit to the photo it shows
2026-10-25 00:58:10 -04:00
aiongg c3feda15c0 LR-META-INFO: the Info panel shows Selected Photos by default; gaps.md lists its change notes once 2026-10-25 03:36:14 +00:00
skylartaylor c057ccc23f Migrate LightCraft to egui 0.37 and reconcile dependency and font locks
Preserve current main, adapt frame/input and IME handling, and keep library
check results stable across layout passes. Include the matching Nix font lock
and reviewed attribution changes.
2026-10-25 02:54:42 +01:00
aiongg 02aa1e0f59 Docs: only the five split zones of ui.dock move have PascalCase names 2026-10-24 23:41:54 +00:00
aiongg a27d8c05d4 Docs: ui.dock above and below name the target's group 2026-10-24 22:19:29 +00:00
aiongg 5b7a4e9a94 Docs: ui.dock move lists the tab, above and below zones 2026-10-24 21:31:46 +00:00
aiongg 5ce171da23 Docking: the workspace, its commands and their parser come from craft-ui (craft-libs 16fb3d4)
craft-ui and craft-docpath are pinned to craft-libs 16fb3d46e1eaa0d455465e4e66a3ac7b17b55bd3.
The saved workspace is craft_ui::docking::Workspace<Panel>; settings written
before it had a version load as version 1. ui.dock params parse as the shared
DockCommand and apply through Workspace::apply_all. LightCraft keeps its
panels, permissions, default layout, photo-view checks, the fallback beside
the photo view for a panel with no recorded place, and its settings file.

The photo view takes side splits (accepts_splits on PROTECTED), so panels
still open and move beside it.

Tests: a version 1 workspace file loads and reopens its hidden panel; a drag
of a group by its bar moves both of its panels.
2026-10-24 19:23:08 +00:00
helgehelge123andClaude Opus 5.5 abf0caf201 KEY-TOGGLEZOOM: hold Space over a tool to pan and click-zoom (#1895)
With Crop, Masking, Remove, Red Eye or a picker on the photo, a held
Space lends the pointer to the view until the key is up, as Lightroom's
temporary Hand / Zoom: a click zooms to the click-zoom ratio at the
point clicked (or back to Fit), a drag pans the zoomed photo, and the
tool gets neither. A tap (under 0.4 s, no click) still toggles the zoom,
on its release. Space's auto-repeat no longer toggles the zoom again.

ui.key takes action press|down|up so a script can hold a key over a
ui.pointer drag.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 16:08:51 +02:00
dotCooCoo 0b1b47ee7b LR-META-INFO: the Info panel can show metadata for the selected photos
With several photos selected, the Info panel offers Show Metadata For:
Target Photo (the default) shows the active photo's values as before,
and Selected Photos shows the values the selected photos share. A text
field or GPS where they differ is empty with a Mixed hint, and
Copyright Status shows Mixed. What is typed there is saved to every selected
photo, and a Mixed field typed in and left empty clears it on all of
them. A field left as it was saves nothing, even when the choice
changes during the edit. The choice is saved with the app settings, and
`view.infoSelectedPhotos {on?}` sets it for an agent.
2026-10-24 07:03:31 -07:00
skylartaylor 50df646183 M0.37: port LightCraft UI, input and capture lifecycle to egui 0.37 2026-10-24 08:02:12 +02:00
francoisgnandClaude Opus 5.5 cf93349071 Merge #2701@9c7bdbd44 (exists docs, root-safe permission test) into round3a-integration
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 06:56:55 +03:00
francoisgnandClaude Opus 5.5 9c7bdbd44c #1290: exists docs match the code; the permission test skips as root
CodeRabbit on #2689 (head 496291276):
- docs/control-protocol.md, `app.recentLibraries`'s doc, `is_missing`'s
  doc: a folder that can't be listed is `exists: null` (couldn't tell), not
  `false`, since 142e496f5; the launch window still shows why for every
  reason.
- a_failed_lookup_doesnt_mark_a_recent_library_missing skips (System) when a
  folder with no permissions can still be read, as when running as root.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 06:54:39 +03:00
francoisgnandClaude Opus 5.5 8ada8a196a Merge #2701@142e496f5 (a failed lookup isn't "not found"; no timer on found libraries) into round3a-integration
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 05:34:24 +03:00
francoisgnandClaude Opus 5.5 142e496f5b #1290: a failed lookup isn't "not found"; recent libraries aren't checked on a timer
CodeRabbit on #2689:
- `unavailable` used `exists()` / `is_dir()`, which are false for any
  error: a parent folder that can't be read or a share that timed out came
  out as "Not found" and greyed the entry out. Only an absent path is "not
  found" now; other lookup errors are "This library couldn't be checked:
  <reason>" (LookupFailed), and a folder that refuses to be listed is no
  longer counted as missing either (it is there: permissions, macOS
  privacy), so neither is greyed out and opening says why.
- Every recent library was checked again every 30 s while the app ran,
  waking external drives and network shares. A library not found is looked
  for again every 30 s (a drive plugged back in; an absent path wakes
  nothing); the others only when the window comes back to the front, for
  the 8 entries shown; paths never checked are checked once.
Tests a_failed_lookup_doesnt_mark_a_recent_library_missing and
recent_libraries_are_checked_again_only_when_the_window_comes_back fail with
the old lookup / the old timer / no timer for libraries not found.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 05:34:11 +03:00
francoisgnandClaude Opus 5.5 5223c80954 Merge #2701@da434b059 (presets dropped with no library open are refused) into round3a-integration
crates/ui-egui/src/lib.rs: the batch's handle_dropped_files refuses presets
while no library is open, as #2701 does. The drop card says so, and its
"photos wait" line is desktop-only (the web build's drops import into its
session as before). photos_dropped_with_no_library_open_wait_for_one also
drops a preset; LR-IMP-DRAGDROP row updated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 22:05:14 +03:00
francoisgnandClaude Opus 5.5 da434b0598 #1290: presets dropped with no library open are refused, not lost
Presets belong to a library (presets.json, <library>/Profiles): dropped
while the launch list or the failed-library window is up, they went into
the session the library opened then replaced. They are refused with a
toast now; the photos of the same drop still wait for the library. Docs:
What's New, control protocol, the #1290 tracker row.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 22:02:11 +03:00
francoisgnandClaude Opus 5.5 edecba5ae8 Merge #2701@52ce53622 (photos dropped with no library open wait for one) into round3a-integration
crates/ui-egui/src/lib.rs: the batch's handle_dropped_files holds the photos
(library_problem::hold_dropped) before the Lightroom / running-import
checks. The drop card (drop_overlay.rs, LR-IMP-DRAGDROP) says the photos
wait instead of announcing an import or a review; test
photos_dropped_with_no_library_open_wait_for_one drops through
`ui.dropFiles`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 21:55:16 +03:00
francoisgnandClaude Opus 5.5 52ce536225 #1290: photos dropped on the launch list wait for the library chosen
While no library is open (the launch list, or the window of a library that
failed to open), a drop went into the in-memory session that the library
chosen then replaced, so the photos were gone from view. They now join the
command line's files (`pendingImport`) and are imported into the library
that opens (or the temporary session), with a toast saying so. Found by
CodeRabbit on #2689. Test
photos_dropped_on_the_launch_list_go_into_the_library_chosen fails with
the hold disabled.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 21:52:44 +03:00
francoisgnandClaude Opus 5.5 be51f07495 Round 3a: Lightroom import docs say when the destination question is asked; one revision table
CodeRabbit on #2689 (head b840f3f29):
- docs/control-protocol.md, docs/lightroom-catalog-import.md (#2685): the
  destination window opens only when settings.lightroomImport is "ask"; the
  three places that said it always asks now say so.
- docs/parity-checklist.md: two dated prose entries cut the revision table,
  so the rows after them (#2685's among them) fell outside it. They are rows
  now, and the batch's rows read newest-first.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 21:51:28 +03:00
francoisgn 0d64d9ef36 Merge PR #2701 (review round e2210ed14) 2026-10-23 19:18:59 +03:00
francoisgnandClaude Opus 5.5 e2210ed14e #1290: a failed open or an unreadable catalog never marks a recent library missing
Review round: whether a recent library is there is kept apart from whether it can be
opened. `Unavailable::is_missing` (not found, a file, no library, a folder that can't be
listed) is the only answer that greys out File ▸ Open Recent Library and makes
`app.recentLibraries` say `exists: false`; an unreadable catalog or a failed check gives
`null`. A failed open from the launch window (say another program has the library)
drops the answer so it is checked again, instead of storing the open's error as one; the
launch list keeps the error on its row. Test: Open on a locked library from the launch
window, then the menu entry stays enabled and `exists` is true (fails if a failed open
marks it missing); an unreadable catalog gives `exists: null`. Docs worded to match.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 19:18:52 +03:00
francoisgn 22a8343ec7 Merge PR #2701 (review round a4ed555d4) 2026-10-23 18:46:55 +03:00
francoisgnandClaude Opus 5.5 a4ed555d45 #1290: recent libraries are checked on workers, never on the UI thread
Review round (CodeRabbit + an adversarial review, handled together):
- The launch list starts with every row "Checking…" and a worker per path checks it (a
  share whose server is gone, or a sleeping disk, can take seconds or never answer; one
  that hangs doesn't hold the others). After 5 s a row says the drive isn't answering;
  it can still be opened. Once every answer is in, the selection moves to the first
  library that can be opened, unless the person moved it.
- File ▸ Open Recent Library and `app.recentLibraries` no longer look at folders either
  (the menu is rebuilt every frame): they use the same answers, refreshed after 30 s, when
  a library is opened, removed from the list, or fails to open. `app.recentLibraries`
  gives `exists: null` and `checking` until a check answered.
- Reasons are found on the worker and put into words on the UI thread, in the interface
  language. A check whose worker dies ends as "This library couldn't be checked".
- Each check holds an activity row ("Checking recent libraries"; quick ones don't show).
- Docs: control protocol (`checking`, `available`, `exists`), What's New; fr strings.
Tests: slow check never holds the window; late answers keep the person's selection; the
menu takes its answers from a worker (fails with the old is_dir()); reasons in French; one
hung drive doesn't hold the others; a dying check ends; reopening clears a missing answer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 18:46:42 +03:00
francoisgn 81a9e8b298 Merge PR #2701: #2701 3fe02631e LRC-LIB-CATALOG-SWITCH: choose what opens at launch, the last library
# Conflicts:
#	crates/ui-egui/locales/fr.json
#	crates/ui-egui/src/panels/settings.rs
#	docs/parity-checklist.md
#	docs/whats-new.md
2026-10-23 16:39:33 +03:00
francoisgn 2fe9101153 Merge PR #2685: #2685 e9b383978 LRC-LIB-CATALOG-IMPORT: ask whether a Lightroom catalog merges into th
# Conflicts:
#	crates/ui-egui/locales/cs-formats.json
#	crates/ui-egui/locales/de-formats.json
#	crates/ui-egui/locales/es-formats.json
#	crates/ui-egui/locales/fi-formats.json
#	crates/ui-egui/locales/fr-formats.json
#	crates/ui-egui/locales/fr.json
#	crates/ui-egui/locales/id-formats.json
#	crates/ui-egui/locales/it-formats.json
#	crates/ui-egui/locales/ja-formats.json
#	crates/ui-egui/locales/ka-formats.json
#	crates/ui-egui/locales/pl-formats.json
#	crates/ui-egui/locales/pt-br-formats.json
#	crates/ui-egui/locales/ru-formats.json
#	crates/ui-egui/locales/sv-formats.json
#	crates/ui-egui/locales/uk-formats.json
#	crates/ui-egui/locales/vi-formats.json
#	crates/ui-egui/locales/zh-hans-formats.json
#	crates/ui-egui/locales/zh-hant-formats.json
#	crates/ui-egui/src/panels/dialogs.rs
#	docs/parity-checklist.md
#	docs/whats-new.md
2026-10-23 16:39:12 +03:00
Claude 3fe02631e6 #1290: list the recent libraries read-only
The launch window no longer probes each recent library with the lock check
of File > Open Library (which created catalog.lock and wrote
catalog.lock.owner into every free library at every launch, including
other drives, network shares and sleeping disks). A row is now checked by
looking only: the path exists, is a folder, can be listed, holds a
LightCraft catalog, and that catalog opens for reading. A library another
program has open is found when Open is pressed: the open fails with the
lock owner's message, the window stays and greys the row out.

New test: listing (and drawing the window) leaves every library folder
byte-identical (contents and modification times); it fails with the old
probe. Docs (What's New, parity row, control protocol) say so.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AbegtYaPuwqZ81pGfELsFK
2026-10-23 11:37:04 +00:00
Claude 2c70336fc3 #1290: choose what opens at launch, the last library or a list of recent ones
Settings > General > At launch: Open the last library (default, as before)
or Ask me. With Ask me the desktop app opens nothing and the library-problem
window lists the recent libraries instead (name, folder, when last opened;
missing, locked or no-longer-library ones greyed out with the reason; the
last one selected): Open, Open Another..., New Library..., Remove from List,
Quit, with arrows / Return / Esc. Alt/Option held in the first 2 s after the
window appears shows the same list once over the library that opened (winit
reports no modifiers before the first frame; macOS only on a change).

File > Open Recent Library > Remove from List and
app.recentLibraries {remove: path} forget one entry; nothing on disk is
touched. The list reports lastOpened, kept under a new settings key
(recentLibrariesOpened) so older files and versions keep their list.

--library and LIGHTCRAFT_LIBRARY keep priority (launch_plan), --memory never
asks, lightcraft-cli never reads ui.json. Settings are saved while the list
shows; command-line files go into whichever library opens.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AbegtYaPuwqZ81pGfELsFK
2026-10-23 10:49:51 +00:00
Claude e9b3839780 #2672: ask whether a Lightroom catalog merges into the open library or gets a new one
LRC-LIB-CATALOG-IMPORT. Before File ▸ Import Lightroom Catalog… starts, a
window asks where the catalog goes: merge into the open library (named,
with its photo count; preselected when it is empty) or create a new
library named after the catalog file, next to the open library (both
editable, Choose… picks the folder). The new library is made and opened
through app.newLibrary (its rules: an empty or new folder, refused while
another import runs), then the catalog is imported into it. Cancel does
nothing. Open Library… on a Lightroom catalog goes through the same choice.

Opt-out: Settings ▸ General ▸ When importing a Lightroom catalog (Ask
where to import it / Always merge into the open library), saved as
ui.json settings.lightroomImport (lenient, default ask); the window's
"Don't ask again" picks Always merge. A session in memory merges without
asking, as before.

file.importLightroom takes {into?: open|new, name?, location?} to choose
without the window. UI layer only: library.importLightroom, the engine
and lightroom_import.rs are unchanged.

Tests: decision table (setting x library on disk/in memory x photos x
into), params, default name; headless window / merge / new library /
cancel / empty library / setting / Don't ask again persisted / folder
not empty / into / in memory / stale library; tests_open_lightroom
follows the new window. Format messages in all 17 catalogs, plain
strings in French. Docs: lightroom-catalog-import, control-protocol,
parity-checklist, whats-new.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AbegtYaPuwqZ81pGfELsFK
2026-10-23 10:19:04 +00:00
Nathaniel LedfordandClaude 266f623480 LR-IMP-DRAGDROP: show what a file drop will do and open the import review
While files are dragged over the window a veil and a card say what dropping
does (review N items, import N presets, open the face model; close the open
dialog first when one is open). Dropped photos and folders open the import
review like File > Import; Settings > Import can import them at once. A drop
while a dialog is open is refused with a toast. The web build now reports
unreadable and failed drops. Adds the control request ui.dropFiles.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-10-22 01:58:44 -04:00
aiongg 727e66babe Merge track r2-t03 into next
# Conflicts:
#	docs/gaps.md
#	docs/parity-checklist.md
#	docs/ui-parity.md
#	docs/whats-new.md
2026-10-22 02:53:00 +00:00
aiongg c61ad57601 Merge PR #2425: LR-IMP-ADD-DIALOG: preserve album hierarchy in Import Photos
docs/parity-checklist.md: header keeps 2026-10-21 and lists both sides' change notes
2026-10-22 02:39:44 +00:00
tylerphotosandCodex 9ceeb85316 LR-IMP-ADD-DIALOG: keep import thumbnail review changes focused (#2237)
Update from main and remove Windows fixtures shared with #2004. Supply both
missing labels in all 13 catalogs and use the shared ControlSpec slider for
keyboard steps, exact entry and reset. Add localization and idle-keyboard
regressions, document control IDs, and retain the partial #2056 scope.

Co-Authored-By: Codex <noreply@openai.com>
2026-10-21 16:30:45 -05:00