browse_a_local_folder compared the browsed folder with backslashes replaced
by slashes, but library.browse keeps the platform's separators; compare
paths instead. A breadcrumb now opens the path cut from the original string,
so a UNC share keeps its leading \ and a bare drive opens its root (C:\)
rather than the current folder on that drive.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Added a function to check if an overlay supports window rendering. Updated overlay handling in various contexts to ensure compatibility with new overlay support logic.
A Custom white balance from a Lightroom catalog or XMP sidecar was taken as absolute Kelvin for every
raw. Raws LightCraft develops relative to their as-shot look (ARW, NEF, RW2, RAF, CR3, CR2, PEF; 6500 K
means as shot) then read a small tweak as a large blue shift: ΔE 22 against Lightroom's export on the
photos of issue #510.
The crs mapper now takes a `Target` (preset, rendered, raw with a measured illuminant, relative-WB raw)
instead of a bool. For relative-WB raws, and rendered files with only `Temperature`, a Kelvin value is
converted to the same mired shift from the packet's `AsShotTemperature` / `AsShotTint`. Without that
reference (Lightroom catalogs keep none for Custom photos) the photo stays As Shot and the import
report lists `Temperature, Tint` as not carried over. Import, sidecar read and catalog import decide
the target the same way through `lightcraft_catalog::relative_wb_format`.
Tests: shift from the issue's numbers (3650 → 3578 K lands at ~6275 K), the As Shot fallback with its
report entry, target selection per file, and the catalog mapper's Custom-WB case.
Each photo's develop settings record the rendering process they are
interpreted by (`DevelopSettings::process`). There is one process, V1, the
pipeline as it is; nothing renders differently (180 CPU 8/16-bit and GPU
renders of procedural scenes, and the saved settings JSON and hash64, are
byte-identical to main).
Settings saved without the field load as V1 (a field-level serde default),
new settings get the latest (`Default`), and V1 is left out when saving, so
files and preview-cache keys stay as they were. A damaged value (anything
but a whole number up to 2^32 - 1) reads as V1 too, so it never costs the
rest of the settings (an unparsable `lc:settings` sidecar is ignored). Reset moves a photo to the
latest process, section and slider resets don't; virtual copies inherit
it; copy / paste / sync / Auto Sync / presets don't carry it; a number from
a newer LightCraft is kept and renders with the newest known process. The
process is not an edit (Edited badges, import-look checks). The base tone
map is chosen per process in one place (`finish::base_tone`, shared by CPU
and GPU). Photo > Update to Current Process (`develop.updateProcess`) moves
selected photos on an older process to the latest as one undo step. Its
menu enablement follows the selection; a call that names photos (`ids` /
`id`) is validated on those instead, through a new opt-in
`CommandSpec::explicit_targets` (execute checks enablement before the
handler sees `ids`). `develop.get` reports the process. The rule and how to add V2:
docs/process-versions.md.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
proxies() fitted the decoded camera JPEG into the sensor proxy's box,
which keeps the JPEG's own aspect and can round a pixel short: an
ILCE-7RM4's 1616x1080 preview, decoded at 384x257, fits a 192x128
profile proxy as 191x128, and collect_pairs rejected the mismatch. So
`lightcraft-cli calibrate` skipped every file from this camera and no
profile could be written.
Resize the JPEG to exactly the sensor proxy's size instead; proxies()
has already rejected references whose aspect differs by more than 2%.
Where fitting already landed on the proxy's size the result is
identical (fit is resize at a rounded size), so working renders don't
change: 34 real ILCE-7RM4 ARWs render bit-identically. On 923 files
from one trip calibrate goes from 0 to 843 accepted.
Refs #515
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From final review: the problem-message example ("rule 2.1: rating is
0–5, not 9") predated the rating check's rewording ("no rating 0–5 is
9"); two doc comments still said Album rules take a plain album; and
What's New described the Album rule's old flat list.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From final review: the field menu mixed languages. The new fields (Copy
Name, In a Stack, File Extension, Video Duration, Keyword Count, People
Count, Shutter Speed, Cropped) and four group headings (Keywords &
People, Description, Camera Info, Develop) were English in every
locale. Russian also lacked fifteen older labels (Pick Flag, Any
Searchable Text, Filename, Color Label, Has GPS…).
All are translated, following each locale's existing terms (Stapel /
Pila / スタック / 堆叠 for stacks, Цветовая метка as in Russian's
other colour-label messages). The i18n test now requires every field
and group label in every language, as it does for choice values and
problem texts. pt-br's album search reads "Pesquisar", like its other
searches.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From final review:
- Every non-number "between" got date pickers, so a text or choice rule
written with "between" (by an agent; the check marks it) offered
calendars that would write dates into it. Only date fields get them
now; a test checks a "Title between" rule.
- An "in the last" N stored as text ("7") showed as 30, and changing
only the unit wrote 30. It is now read as the catalog reads it.
- take_escape's comment no longer claims later egui passes see the key
press (they don't); keying it to the frame just lets it lapse.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From final review: while the rule dialog was open it ran the rule check
several times, the full query for the live count (sorting every match
only to count them) and rebuilt the album list with a loop walk per
album, on every repaint, including pointer moves over a picker. The
sidebar re-checked every smart album's rules on every frame.
Caches::rules_view keeps the dialog's problems, count and album list
until the catalog revision, the rules, the language or (for "in the
last…" rules) the minute changes. Caches::smart_album_problems keeps
the sidebar's ⚠ problems per catalog revision. Both count their scans,
like the album counts, and a test checks that unchanged frames reuse
them and that rule and catalog changes don't.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From final review:
- Cost: ancestors and paths were found by scanning every album for each
row, which grows with the square of the album count (about 1.1 s for
20,000 albums). An id→position index built once per call or frame
makes it cost the folder depth. A test lays out and searches 20,000
albums in nested folders well under its 1 s limit.
- Focus: clicking a folder, or pressing Enter when nothing can be
picked, took the keyboard from the search box, so typing went
nowhere. Both hand it back; a headless test types after a folder
click.
- Scrolling: the highlighted result was scrolled into view every frame,
fighting the mouse wheel. It now scrolls only when ↑ / ↓ moved to it.
The headless harness stops rendering once input is handled, so this
snap-back can't be reproduced there; a test I wrote passed with and
without the fix and was dropped rather than kept as a false guard.
- Folder rows carry widget info (name, open or not) for assistive
tools. Dead code (an unused stack) and a test assertion that couldn't
fail are gone.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From final review: photo.setCaptureTime quietly skipped photos whose
shifted date would leave years 0000–9999. It reported success, with
the photo missing from `captured`. Such a shift is now refused for the
whole selection with how many photos it concerns, and nothing changes.
A date that doesn't read at all is still skipped, as before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From final review: I had refused a time zone or fractions of a second
in rule dates on the premise that capture times are stored local and to
the second. They aren't. Imports write DateTime::to_iso, which carries
EXIF SubSecTime and OffsetTime ("2026-08-14T10:00:00.250+02:00"), so a
rule like "is 2026-08-14T10:00:00.250" did match those photos and was
wrongly refused; saved albums holding such dates would have shown ⚠
too.
rule_date now accepts, after whole seconds, a fraction (1–9 digits)
and a zone (Z or ±hh:mm). Anywhere else ("2026-08-14Z") they are no
date. Issue::DateWithZone and its translations are gone; such values
are NeedsDate. The test matches a photo captured with a fraction and a
zone, and the docs no longer say zones are refused.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From final review: loop detection only followed Album rules. A smart
album's own album field (Filter.album, as set by the filter bar or
album.setRules {rules: {album}}) was ignored, so
"album.setRules {id: A, rules: {album: A}}" was accepted and A became
silently empty. On main it would have recursed to a stack overflow; the
guard already stops that.
Filter::albums_tested covers both, and album_reaches follows it.
Catalog::album_filter_problem reports a loop through the album field;
smart_album_problems (sidebar ⚠, albums.list) and album.setRules
(including fromView) use it. A problem with no rule position displays
without the "rule …:" / "#…" prefix.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From final review: library.filter and album.setRules (without replace)
merged a change onto the current rules and re-checked all of them.
Once a rule stopped checking later (the album it tests was deleted, a
custom label renamed), every later change was refused: the search box,
keyword clicks and the filter bar silently stopped working, since the
UI doesn't show those errors, and partial edits of the album were
refused.
Only rules a change sets ("ruleSet" present) are checked now. Rules
that went stale still show in the dialog and the sidebar's ⚠ until
they're fixed. A regression test deletes the album a filter rule tests,
then changes the rating, edits the album, and still gets refused for a
bad new rule.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From final review: the loop guard bounded the depth of smart albums
testing smart albums, but not repeated work. Each album tested by the
next one twice ("all of: album is X(k-1), album is X(k-1)") doubled the
cost per level, so 25 levels meant 2^25 evaluations per photo, enough
to freeze the sidebar's counts. Any agent or catalog file could build
such a chain.
rules::smart_album_holds remembers each smart album's answer for each
photo during one outermost question, and forgets them when it ends (a
drop guard, however it ends). A regression test counts a 26-deep
doubled chain in well under its 2 s limit; before the fix it ran for
minutes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A flat dropdown doesn't scale to the hundreds of albums and smart
albums a library grows. crates/ui-egui/src/album_picker.rs is a
reusable picker:
- A button opens a popup with a search field, focused on open.
- With nothing typed it shows the sidebar's tree in the sidebar's order
(entries_from uses Catalog::album_children_by_parent): folders with
painted disclosure triangles and folder icons, album / smart-album
icons. Folders are closed except those holding the current choice.
- Typing lists the albums whose "Folder / Name" path holds every word,
in any case, names starting with the query first. ↑ / ↓ move, Enter
picks, Esc closes just the popup.
- Albums that can't be picked stay visible but greyed, the reason on
hover, instead of vanishing.
- Pure, unit-tested helpers: path_label, ancestors, visible_tree,
search, set_depths. Widget ids albumPicker / albumPickerSearch /
albumPickerFolder / albumPickerItem.
The rule editor's Album value uses it. The album being edited and
albums that would loop back are blocked with a reason ("This is the
album you're editing", "It tests this album, so testing it back would
loop"). This also removes the stray divider the old dropdown drew above
"Smart Albums" when no regular album preceded it. New strings are
translated in all seven locales, and the strict i18n test lists them;
the unused "Smart Albums" heading is dropped. Headless tests: browsing
the tree, typing with ↑ / Enter, greyed loops.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A file-path rule test and a sidebar long-name test used a real person's
name as their example. They now use "Lake Como Wedding-Day" and keep
what they check: a multi-word folder name, word order that must not
match, and a hyphen a space must not match.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From review: a loop saved anyway doesn't "match nothing". Inside it, the
album being evaluated counts as empty, so an "isn't" rule on it matches
every photo; the sidebar marks it. Also: Album rules take plain or smart
albums (not "plain" only), the picker lists both, an album that is
smart is no longer a problem, and a broken sentence in the parity row
is mended.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From review: album.setRules with fromView saved the current view
unchecked. Showing smart album A ("Album isn't B") and updating B from
that view made B test itself, which the rule check is meant to refuse.
The view is still saved as it is, but a loop for the album being
updated is refused (Issue::AlbumLoop), and the sidebar's menu item
shows the reason instead of ignoring the result.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From review: the walk through smart-album references kept its seen-set
in a Vec, which is quadratic over long chains. The loop test's comment
now says what a saved loop does: inside it, the album being evaluated
counts as holding no photos.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Summaries showed an Album rule's id ("album isn't 9"): the sidebar
tooltip, the "Rules:" filter chip and albums.list's rulesText for
agents.
RuleSet::describe_with(&Catalog) and Filter::describe_with name the
album ("album isn't “Excluded Photos”"). One that is gone shows as #9
(rules::album_name). The engine's rulesText and Rules chip use them, and
i18n::rules_label takes the catalog for the translated summaries.
Tests at all three levels: catalog (nested groups, a missing album),
engine (rulesText), UI (German summary).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The Album rule's list offers smart albums under a "Smart Albums"
heading (translated in all seven locales). It leaves out the album
being edited and any smart album that tests it, since that would loop
back (album_choices, Catalog::album_reaches).
- The dialog checks rules for the album being edited
(RuleSet::check_for), so a loop set by other means is marked under its
row, OK is disabled, and confirm refuses it.
- Esc with any popup open in a dialog (a dropdown, a menu, a date
picker) closed the dialog with its edits. The dialog now claims Esc
for an open popup before drawing (widgets::take_escape), so only the
popup closes. New headless test for a dropdown, next to the
calendar's.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An Album rule may now name a smart album: "Keywords contain travel"
and "Album isn't Excluded Photos" (red or rejected) leaves out whatever
Excluded Photos matches, as its rules change. Smart albums used to be
refused for this.
Loops:
- RuleSet::check_for(cat, owner) refuses an Album rule that would make
the owner include itself, directly or through other smart albums
(Issue::AlbumLoop). Catalog::album_reaches walks the references with
a seen-set; RuleSet::albums_tested lists them. album.setRules checks
for the album being edited, and smart_album_problems reports a loop
saved anyway.
- Matching never recurses without end: album_contains evaluates a smart
album's rules through rules::evaluating, a thread-local stack of
albums being evaluated with a 32-deep cap. An album already on it (a
saved loop) holds nothing, and the stack is restored by a drop guard
however evaluation ends. A test builds A → B → C → A and checks that
every album returns and reports the loop.
The issue "can't test a smart album" is replaced by the loop issue in
all seven locales. The sidebar counts already refresh while any smart
album uses "in the last…", so an album testing one is covered.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From review:
- Esc with the calendar open also closed the rule editor and lost its
edits. The shortcut layer runs before the UI, and Esc's command (Back)
closes any dialog. Shortcuts now leave Esc to an open popup (any
menu or calendar). The picker marks Esc as handled for the frame
(widgets::take_escape / escape_taken; the mark lasts the whole frame,
since egui may run more than one pass), and the dialog's own Esc
check defers to it. New headless test: Esc closes only the calendar.
- Paging wrapped at the ends (‹ on January 0000 showed December 0000).
step() is now a pure function that stops at years 0000 / 9999; year
pages are aligned blocks of twelve, and the last one holds 9996–9999.
View::open clamps a caller's year and month, so a huge `today` can't
overflow.
- PickedDate::parse reads a time after the day only if it is one
(T10, T10:00, T10:00:00), as the rule check does; "2026-08-14Tjunk"
is no longer shown as a valid day.
- Today is marked in the month and year views too (holds_today).
- Documented: ids must be unique among the pickers on screen; the
picker picks dates, not times (picking a day replaces a typed time).
The i18n test also covers Year / Month / Day.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From review: "2026-08-14Z" (a zone after a date with no time) was
reported as "needs a date" rather than as the time-zone issue it is.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each date field in the smart-album rule editor has a DatePicker beside
its text field. It opens on the rule's date at its precision (or on
today, from the session's clock, passed in rules_editor::Env), and
writes what is picked as 2026, 2026-08 or 2026-08-14. Typing still
works, and opening never changes a rule. In a "between", each date's
picker stops at the other, so the two stay in order.
Headless tests pick a day, a month and a year, move between months and
year pages, keep a "between" in order from both ends, and open an
unreadable date on today without rewriting it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
crates/ui-egui/src/date_picker.rs: a calendar button that opens a popup
to pick a year, a month or a day. egui_extras' DatePickerButton was
considered and left: its weekday names, month names, "Week", "Cancel"
and "Save" are hard-coded English, it only picks whole days, and it
needs a Save click and the jiff crate.
- PickedDate { year, month?, day? } keeps a value's precision (2026
means the whole year): parse / iso / precision / first_day /
last_day. days_in_month, a Monday-first month_grid and in_bounds (any
day of the value in range) are pure and unit-tested.
- DatePicker::new(id, &mut Option<PickedDate>) with precisions(), min(),
max() and today(). It opens on the value at its precision (else
today, on days) and returns a response that is changed() when a date
is picked. Out-of-range cells are disabled.
- It follows the UI language: Year / Month / Day, month titles and
labels from the date headings (date_group_label), and weekday headers
(Mo…Su) translated in all seven locales with "Pick a date". A test
requires them.
- Widget ids for tests and agents: datePicker:<id>,
datePickerPrecision:<p>:<id>, datePickerPrev/Next:<id>,
datePickerCell:<iso>:<id>.
- A code-drawn Calendar icon in icons.rs (original work, covered by its
attribution entry).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Some date values passed the check but never matched. "2026-10-01 10:00"
(a space, not T) and "…T10:00:00+02:00" (a zone) were accepted, while
the matcher compared them as written against capture times stored as
"2026-10-01T10:00:00".
rules::rule_date is now the one reading for the check and the matcher:
2026, 2026-08, 2026-08-14, then T10 / T10:00 / T10:00:00, with a space
working as the T, and a real date and time (25 o'clock and minute 61 are
refused too). A time zone or fractions of a second can never match a
local, whole-second capture time. They get their own issue,
DateWithZone, translated in all seven locales, instead of matching
nothing silently.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From review:
- Keywords and People say "are empty" / "aren't empty", but their
empty-text hint said "is empty". New issues NeedsNamesOrEmpty /
NeedsNamesOrNotEmpty quote their operators, translated in all seven
locales with each locale's own label (sind leer, están vacíos, が空…).
- Russian had none of the 20 smart-album operator labels, so its
editor showed every operator in English and the new hints named
«пусто», which wasn't on screen. All 20 are translated now, and a
test requires every operator label in every language.
- Japanese and Chinese join a field and its problem with the full-width
colon their catalogs use ("タイトル:…").
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>