5189 Commits
Author SHA1 Message Date
6bb15d2278 Integration: keep another instance's shift tool switch choice (#5878)
Two instances migrate the same version 1 file (useShiftKeyForToolSwitch
false -> true), so both save bases hold false. When B turns the switch off
and saves version 2, A's next unrelated save wrote its migrated true over
that choice. write_prefs now drops the migration delta from the base when
the file on disk is already version 2 or later.

Test: another_instances_shift_tool_switch_choice_beats_this_ones_migration
(fails without the fix: true written over false).

Co-authored-by: Higgs32584 <55243596+Higgs32584@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-22 21:39:04 +02:00
95c3af68c3 Integration: prove doc.replaceSmartObject replaces and undoes (#8150)
The rpc test only checked the returned layer id, so a no-op that echoed
{"layer": id} passed, its undo check passed on doc.place's own step, and
the bad-layer case asserted on an empty reason. It now checks the smart
object's file name and rendered centre pixel (42 -> 200 after the
replace, back to 42 on undo, 200 on redo), that every refusal changes
nothing, and that layer 999999 fails with "no such layer".

The bridge row in docs/control-protocol.md claimed doc_replace_smart_object
maps onto layer.smartObjects.replaceContents; the desktop refuses that
command (ambient path), like doc_place's file.placeEmbedded. The row now
says so and points to the headless / workspace server method.

Co-authored-by: VishnuByrraju <112673081+VishnuByrraju@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-22 21:36:42 +02:00
David Mann b7eabd4990 Integration: merge main (#6254) 2026-10-22 21:36:35 +02:00
249eae743f Integration: make Oil Paint's round_i32 exact for every f32 (#8556)
round_i32 added 0.5 in f32, which rounds up at the binade crossing:
0.49999997 (0.5 - 2^-25) + 0.5 becomes 1.0, so round_i32 gave 1 (and -1 for
the negative) where f32::round gives 0. The sum is now taken in f64, where
it is exact for every f32 the i32 cast does not saturate; still no roundf call.

rounding_matches_the_standard_library now covers +-0.49999997, the ties and
8388607.5, and a new test compares every f32 bit pattern in [-4096, 4096]
against f32::round (0.6 s). Both fail on the previous head. The code comment
and docs/oil-paint-performance.md no longer claim the f32 sum is exact.

Co-authored-by: ebosido444-ai <316167330+ebosido444-ai@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-22 21:31:24 +02:00
fd8a2856d4 Integration: line the layout's ink box up with the TySh ink box (#6094)
refine_legacy_point_origin moved the layout's logical box (line x0,
baseline - ascent) onto the TySh boundingBox, but Photoshop writes
boundingBox as the tight ink box (Arial 120 pt "..." has bounds top
-102.98, boundingBox top -11). Legacy point text whose ink starts below
the ascent line ("...", "_", lowercase) opened up to ~116 pt too low.

Match like with like: use the layout's unrounded text-space ink bounds
(new render::ink_bounds). The PR's tests built boundingBox from the
logical box, so they now write bounds = layout box and boundingBox =
ink box and pin the exact anchor; legacy_origin_lines_up_ink_with_the_
ink_bounding_box covers "Name", "..." and "_" and fails without this.

Co-authored-by: bogusyogi <102761952+bogusyogi@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-22 21:29:29 +02:00
David Mann 1e3323f64c Integration: merge main (#6966) 2026-10-22 21:28:41 +02:00
David Mann 87905d448f Integration: merge main (#8118) 2026-10-22 21:25:47 +02:00
David Mann f9efad49d6 Integration: merge main (#5937) 2026-10-22 21:16:38 +02:00
4e80150200 Integration: export artboard assets at the full board, no preference (#8254)
Photoshop's Generator always exports an artboard named like an asset at the
board's bounds (generator-assets renderer: canvasBounds = layer.artboard),
with no setting. The PR put that behind a new Export preference that
defaulted to the trimmed, non-Photoshop output. Image Assets now always tries
artboard_document first and falls back to layer_document; the preference,
its dialog label and its 20 translations are gone (scorecard prefs back to
main's 21 of 176).

Test: image_assets_keep_full_artboards writes a transparent 40x30 artboard
with an 8x6 logo through file.generate.imageAssets and expects board.png
at 40x30 (the trimmed path gives 8x6) and logo.png still trimmed to 8x6.

Co-authored-by: IbrahimOezhan <89545837+IbrahimOezhan@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-22 21:16:12 +02:00
cc498e537e Integration: keep the save base complete after the prefs migration (#5878)
The save base after loading kept only the keys the file had, so a key an
older file lacks was written as this instance's default over another
instance's change (two instances on one v1 file: B sets the theme, A saves
anything, the theme reverts). A version 3 file was also written back as
version 2. The base is now what was loaded, with only the version and
tools.useShiftKeyForToolSwitch put back as the file had them (a newer
version is left alone), the way repair_invalid patches keys.

Tests: an_old_sparse_file_keeps_another_instances_change,
a_newer_files_version_is_kept_on_save (both fail with the previous base).

Co-authored-by: Higgs32584 <55243596+Higgs32584@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-22 21:15:53 +02:00
David MannandClaude Opus 5.5 018d3605b1 Integration: merge main (#6094)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-22 21:13:36 +02:00
77caa32765 Integration: keep a hidden layer's eye box empty (#8496)
The PR widened the eye hit area to the whole column (#8467) but also painted
a 12x12 sunken well in a hidden layer's eye box. The maintainer decided on
#1662 that a hidden layer's eye box stays empty, as in Photoshop (and the
hidden fx-row eye still does), so the well goes; the full-column hit area and
its test stay. New test a_hidden_layers_eye_box_paints_nothing checks, in all
five themes, that nothing is drawn inside a hidden layer's eye column while a
visible layer's eye is; it fails with the well. Also drops the trailing blank
line at EOF of layer_row_ui/tests.rs (cargo fmt).

Co-authored-by: kobaltgit <159009675+kobaltgit@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-22 21:10:34 +02:00
f40366f1a8 Integration: keep only the segment of a lossless-JPEG tile, test the sum through decode (#6041)
A compression-7 frame may declare up to four times its segment's samples
(ljpeg::decode is capped at 4x the tile), and read_plane returned the whole
frame, so each retained tile held up to 4x what retained_segment_bytes
charges it: tiles declared 256x256 holding 512x512 frames pass a 1 MiB
budget while keeping 2 MiB. The lossless-JPEG arm moves into
lossless_jpeg(), which now truncates to the segment's rows (all the scatter
reads) and releases the spare capacity.

The PR's test only called the helper; deleting the call in read_plane kept
it green. decode_charges_the_retained_tiles_together drives decode on an
18x18 CFA in four padded 16x16 LJ92 tiles: 1500 B -> LimitExceeded, 2048 B
-> Ok. lossless_jpeg_keeps_only_the_segment pins the truncation. The doc
comment now says the output plane is alive alongside the sum.

Co-authored-by: Muhammad Saad <188063027+saad-mughal435@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-22 21:09:26 +02:00
David Mann 34146281ea Integration: merge main (#8254) 2026-10-22 20:57:14 +02:00
edea110601 Integration: refuse Paste Into a Lock-All group before pasting (#6352)
With the mask commands now honouring Lock All, Paste Into / Paste Outside with a
child of a Lock-All group active committed edit.paste and then failed on the
refused layer.layerMask.revealAll, leaving an unmasked pasted layer behind an
Err. paste_into now checks the destination group's locks first and refuses
cleanly. Test: mask_lock_all::paste_into_a_lock_all_group_refuses_before_pasting
(layer count and history unchanged). Also moves mask_lock_all.rs into main's
one-binary tests/it layout.

Co-authored-by: osaidahmed <170216456+osaidahmed@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-22 20:52:59 +02:00
aiongg fd4c1f4d15 Actions: a step dialog takes only the step's values
Copying a recorded step's params into its dialog skips `__` keys: they are
the dialog's own markers, and the command never receives them.
2026-10-22 18:50:15 +00:00
aiongg 0eb76eda4d Actions: the gate judges a step before its dialog opens
A playback under an automation gate stops at a step the gate denies, as it
does for steps without a dialog, instead of opening that step's dialog.
2026-10-22 18:50:15 +00:00
aiongg 7d73232023 Actions: a failed step dialog command ends its paused playback
abandoned() drops the pause's record as stopped() does, so the recording a
paused call suspended comes back once and the record doesn't keep refusing
actions.record, actions.delete and actions.move afterwards.
2026-10-22 18:50:15 +00:00
James ChapmanandClaude Opus 5.5 dbacc5a795 Keep a paused action playback's automation gate when it resumes
The pause is recorded in the app (`PausedPlaybacks`): the action, the next
step and the gate it was playing under, keyed by a token that is all the
step dialog's field holds. `resume` plays from that record under its gate
(or the one already installed), and refuses a token it doesn't know.
Cancelling drops the record. A playback a person started stays ungated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 18:50:15 +00:00
aiongg 993e3046df Automation: exported layers keep linked smart objects as pixels in workspace sessions
`Headless::export_layers` calls `export_layers_with` outside `Session::execute`, so no session gate was installed when `encode` chose whether a PSD/PSB embeds a linked smart object's file. `export_layers_with` enters the session's gate itself, as `Session::execute` does, and a workspace session's layer export writes such a layer as pixels like `doc.save`.
2026-10-22 18:41:32 +00:00
aiongg a89554eb83 Automation: dialog fields can't add the Replace prompt's marker
The Replace prompt (`__replaceSaved`) routes `confirm` and `cancel` like the other dialog markers, so `ROUTES` lists it and `ui.dialog.set` / `ui.dialog.open` refuse to add it.
2026-10-22 18:41:32 +00:00
James ChapmanandClaude Opus 5.5 ebdddbe1f5 Stop automation re-routing dialogs to file-writing handlers
`dialogs::confirm` picks a dialog's handler by marker fields (`__diskChanged`,
`__export`, `__web`, `__print`, ...). Only the app sets them now:

- `ui.dialog.set` and `ui.dialog.open`'s `fields` refuse to add a key any of
  confirm's or cancel's `owns()` predicates route on
  (`dialogs::is_route_marker`, which probes the predicates themselves). A
  dialog's own fields, private ones included, stay settable.
- `dialogs::confirm`, when driven by automation (a control request's gate or
  synthetic input), judges a handler that writes files itself as the file
  command it stands for: file.save, file.export.exportAs,
  file.export.saveForWebLegacy or file.print.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 18:41:32 +00:00
James ChapmanandClaude Opus 5.5 a2fe5b2458 Gate linked smart-object reads on re-render
A smart object's Linked source path is document data (a PSD placed layer's
Idnt, or a path a .pcraft stores). `read_file`, the single disk read point
for linked files, now rechecks the path with the gate the engine already
carries through dispatch, `Session::edit` and background jobs, as the
contents commands already did. A refused path is handled like a missing
file: the layer keeps its last rendering. Trusted sessions install no gate
and are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 18:41:32 +00:00
James ChapmanandClaude Opus 5.5 b4d6d3f639 Don't embed linked smart-object files in automation PSD saves
PSD/PSB and layered TIFF export embed the file a Linked smart object names
(lnk2). Add `read_linked_files` to `photocraft_io::ExportOptions` and
`PsdExportOptions` (default on, so trusted saves are unchanged), threaded
through nested PSB and layered TIFF export. When it is off, such a layer is
written as pixels with the existing warning. Automation turns it off in
`Headless::save` (workspace), the desktop's `save_automation_with`, data-set
export for a gated session, and `file_cmds::encode` when a session gate is
installed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 18:41:32 +00:00
David Mann 9095272487 Integration: merge main (#6352) 2026-10-22 20:38:21 +02:00
James ChapmanandClaude Opus 5.5 25def299d7 Gate Pixel Replacement paths in data-set export
`image.applyDataSet` judges the Pixel Replacement image paths a data set
names with the session's automation gate. `export_data_sets_with`
(doc.exportDataSets / doc_export_data_sets) now does the same: the check is
factored out of `apply_data_set` and runs for every planned data set before
anything is written. Test in automation rpc tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 18:33:32 +00:00
bommerts 6425a23309 Merge main and preserve sharp CPU rendering during animated zoom 2026-10-22 13:58:21 -04:00
David Mann 9487065314 Integration: merge main (one test binary per crate, #8644)
# Conflicts:
#	docs/file-format-parity.md
#	docs/gaps.md
#	docs/scorecard.md
#	scorecard/file_compat.toml
2026-10-22 19:55:06 +02:00
Dylan M. Taylor 6649b78526 Use chrono instead of hand-rolled UTC conversion (#8234)
* Use chrono for UTC/calendar conversion instead of hand-rolled copies

Replaces the civil_from_days copies behind the app's log timestamp, the
Measurement Log's date/time, and the perf report date with chrono as a
workspace dependency (reusing the version already in the tree). Output formats
are unchanged and pinned by the existing tests. AGENTS.md now says time goes
through chrono.

Follow-up to storytold/cadcraft#1785

* Never panic on a clock outside chrono's range; keep 5-digit years unprefixed

chrono's From<SystemTime> unwraps internally, so a clock past year 262143
panicked. logging::timestamp and analysis_cmds::now_iso now read the duration
since the epoch and go through the fallible DateTime::from_timestamp, reading
an out-of-range or pre-epoch clock as the epoch as the old saturating code
did. All three ISO helpers (timestamp, now_iso, perf::iso_utc) write the year
with {:04} rather than %Y, which prefixes a '+' past year 9999.

Tests cover the far-future and year-10000 cases for timestamp and iso_utc.

* Don't build an unrepresentable SystemTime in the far-future test

UNIX_EPOCH + Duration::from_secs(u64::MAX / 2) overflows Windows'
SystemTime and panicked before timestamp() ran, failing the Windows
test job. Build it with checked_add and only assert where it exists.

* time: preserve old out-of-range date conversion (no epoch fallback)

`DateTime::from_timestamp(...).unwrap_or_default()` silently reset any
instant past chrono's year range to 1970-01-01, changing output the old
hand-rolled Howard-Hinnant code converted exactly (e.g. 10_000_000_000_000 s
was 318857-05-20 17:46:40, not the epoch). Restore the old conversion domain
with a cycle-based helper that routes the calendar through chrono (the
calendar repeats every 400 years = 146_097 days, so month/day/time come from
chrono and only the year is carried in i128, keeping every i64 exact).

- apps/photocraft/src/logging.rs: `timestamp` via `utc_parts`; keep the
  pre-1970 epoch clamp; u64 secs past i64::MAX saturate, not epoch.
- crates/engine/src/analysis_cmds.rs: extract pure `iso_from_unix` (cycle
  based) from `now_iso` so the conversion is tested directly.
- xtask/src/perf.rs: `iso_utc` cycle-based, accepts the full u64 range the
  old code did.

Pinned old outputs in tests:
  logging u64::MAX/2 s -> 292277026596-12-04T15:30:07.000Z (was epoch),
          10_000_000_000_000 s -> 318857-05-20T17:46:40.000Z
  iso_from_unix 10_000_000_000_000 -> 318857-05-20T17:46:40Z,
          -10_000_000_000_000 -> -314918-08-13T06:13:20Z, i64 extremes
  iso_utc u64::MAX -> 584554051223-11-09T07:00:15Z (was epoch),
          10_000_000_000_000 -> 318857-05-20T17:46:40Z

* Don't build a year-318857 SystemTime unchecked in the logging test

Windows' SystemTime only reaches about year 30828, so
UNIX_EPOCH + Duration::from_secs(10_000_000_000_000) panicked before
timestamp() ran and failed the Windows test job. Build it with
checked_add and assert where the platform can hold it. The year-10000
case fits everywhere and stays unconditional.

* Name the 400-year range extension as the one exception to the time rule

The rule said never to hand-roll calendar math, but the range fix
(shifting by whole 400-year Gregorian cycles so dates past chrono's year
range keep their old output) is cycle arithmetic, so the rule and the code
disagreed. The rule now names that exception: the library still computes
month, day and time, and only whole cycles are carried in the year.

* Say the library computes month and day; time-of-day is plain arithmetic

Narrows the range-extension wording: the date library computes the
calendar fields, and splitting seconds into hours, minutes and seconds
is ordinary arithmetic, which is what the code does.
2026-10-22 13:54:10 -04:00
bommerts bd195276e9 Merge current main and preserve compact clipping evidence 2026-10-22 13:35:05 -04:00
David MannandClaude Opus 5.5 607a84de75 Integration: the Color panel gesture test picks a swatch of another colour after merging main
Main's #5568 (now in apply_color) skips a shape that already has the
picked colour. The test's last drag ends on black, the first swatch, so
that swatch click added no step. It now clicks the second swatch and
checks the foreground changed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 19:26:25 +02:00
David MannandClaude Opus 5.5 f5533e5d82 Integration: merge main into batch 6
Main merged #5940, #6472, #7838 and #7922 directly. Kept from the batch:
the snapshot/History-follow identity at the history limit (#5940,
c7ef3676a; main still had the 3-tuple HistorySpot) and the strokeless
shape at a 0 px width step (#6472, 962539607; main still added the
stroke). Main already carries the other two fix-ups (34c9d0ea4, dff8bfd0e).

Overlaps with main resolved:
- #5842 vs main's #5568: one path, apply_color, tool-gated as #5842
  has it, with #5568's float fill, kept fill opacity and lock skip;
  shape_fill_ui's duplicate foreground_changed/gesture dropped; #5568's
  tests run with a shape tool.
- #6230 vs main's #5180: one Type tool path click (path_target), no
  second type.attachToPath; vertical type does not start type on a path,
  as main has it; main's path_at/path_cursor and canvas arm dropped.
- #5452 moved the shape swatch to shape_paint_ui.rs: main's hex field
  (#6201) ported there.
- type_extra_cmds::with_text takes main's dpi form, pub(crate) for
  type_area_cmds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 19:20:26 +02:00
Khawar Zamman Wani 6d2d910c65 Merge remote-tracking branch 'origin/main' into feat/6000-color-panel-modes
# Conflicts:
#	crates/ui-egui/src/i18n/cs.tsv
#	crates/ui-egui/src/i18n/de.tsv
#	crates/ui-egui/src/i18n/el.tsv
#	crates/ui-egui/src/i18n/es.tsv
#	crates/ui-egui/src/i18n/fr.tsv
#	crates/ui-egui/src/i18n/hu.tsv
#	crates/ui-egui/src/i18n/id.tsv
#	crates/ui-egui/src/i18n/it.tsv
#	crates/ui-egui/src/i18n/ja.tsv
#	crates/ui-egui/src/i18n/ko.tsv
#	crates/ui-egui/src/i18n/nl.tsv
#	crates/ui-egui/src/i18n/pl.tsv
#	crates/ui-egui/src/i18n/pt-br.tsv
#	crates/ui-egui/src/i18n/ru.tsv
#	crates/ui-egui/src/i18n/sr.tsv
#	crates/ui-egui/src/i18n/tr.tsv
#	crates/ui-egui/src/i18n/uk.tsv
#	crates/ui-egui/src/i18n/vi.tsv
#	crates/ui-egui/src/i18n/zh-hans.tsv
#	crates/ui-egui/src/i18n/zh-hant.tsv
#	crates/ui-egui/src/panels.rs
#	docs/gaps.md
#	docs/scorecard.md
#	docs/ui-parity.md
2026-10-22 22:38:42 +05:30
aiongg ffa400c754 Keep tests correct and docs current with one test binary per crate
- gpu: the test modules share one GPU_LOCK, and timing and vector_mask
  take it too, so one process drives one wgpu device at a time.
- ui-egui: the wgpu tests in pattern_preview_canvas, adjust_preview_gpu,
  marquee_autoscroll and layout_perf take gpu_lock().
- affinity compound_limits: the child process selects its test by the
  module path (compound_limits::<name>); with --exact, the bare name
  matches no test and the child passes without running the check.
- xtask scorecard reads crates/io/tests/it/corpus.rs.
- Docs and comments name the new test paths and the --test it filters.
2026-10-22 16:55:34 +00:00
aiongg a6428d3dfb Build each crate's integration tests as one binary
Every tests/*.rs file is its own executable that links the whole crate
graph. Move each crate's test files into tests/it/ as modules of one
tests/it/main.rs; helper modules under tests/common move with them.

These keep their own binary (own process):
- engine panic_hunt: a process-wide panic hook counts every panic.
- engine text_gamma: changes the process-wide text gamma.
- engine smart_filter_stages, mask_cache_purge: assert on global caches.
- codecs pnm_peak_memory: reads the process's peak RSS.
- photocraft mac_menu_appkit: harness = false.
2026-10-22 16:55:34 +00:00
aiongg 1370c7a642 Integration: #7811 cargo fmt on process_outputs 2026-10-22 15:47:53 +00:00
6d3092f243 Integration: merge main, resolve conflicts, JPEG Options docs and encoder test (#7883)
Merges main (8f09d122d). Conflicts: the replay_save doc comment (main added
file.export.exportAs replay) and the status lines and history rows of
docs/file-format-parity.md and docs/gaps.md; both sides kept.

The docs called FILE-215-19 done and said only Save As and Save a Copy ask.
Photoshop's JPEG Options also has Matte, the progressive scan count
(Scans 3-5) and the size preview, none of which is offered, and Baseline
Optimized writes the same file as Baseline for RGB (4:2:0). The row is now
partial with those gaps named, and the docs say File > Save asks too.
docs/scorecard.md is regenerated.

Adds jpeg_options_ui::tests::the_answer_reaches_the_encoder. Every UI test
used a fake exporter, so dropping `opts.encode.jpeg_format` from
ExportSettings::io_options (the desktop and web save path) left them all
green; this test writes a real JPEG through io_options and fails without it.

Co-authored-by: Michael Higgins <55243596+Higgs32584@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 15:45:30 +00:00
a2a6f1e13f Integration: Zoom Resizes Windows shows the whole image, clear of the scrollbars (#7662)
A Zoom tool click or wheel zoom keeps the clicked spot under the pointer, so
the view centre moves off the image's middle. The window then grew to the
image's size around that off-centre view: a 400x300 image zoomed to 200% about
(50, 50) showed grey on the left and top and lost its right and bottom edges.
The size also left out the 12 pt scrollbars, which are drawn over the canvas
and hid the image's right and bottom edges.

The window now makes room for the scrollbars, and a window that grows around
the whole image centres it in the area the scrollbars leave free (any flip,
rotation, pixel aspect and display scale). A window the screen caps keeps the
zoomed spot where it is. Scrubby zoom and the Animated Zoom hold re-anchor the
view only when the zoom changes, so their release frame and the frames at the
zoom limit keep that centred view. A maximized or full-screen window keeps the
size the user gave it.

Co-authored-by: Nguyễn Minh Long <181250616+KLBMinhLong@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 15:42:54 +00:00
893b54ea8c Integration: keep the font menu's arrows walking with recent fonts (#3559)
Each arrow step in the open font menu applies its family, the options bar,
Character and Glyphs panels record it as the most recent font, and the next
frame hoisted it to row 0: with Recent Fonts on (default 10), Down then
alternated between two families and Up stayed on the first row (CodeRabbit's
open thread on #3559). The menu now keeps the recent group it opened with.

As in Photoshop, the recent families now head the menu in their own group
above a divider and keep their alphabetical place below it, instead of
moving out of it. The arrows start from the current family's alphabetical
copy (or the row the last arrow chose), and only that row is marked.

Tests: open_menu_arrows_walk_on_through_recent_fonts,
options_bar_menu_arrows_walk_on_while_recording_recent_fonts and
open_menu_on_a_recent_font_steps_through_the_alphabet fail without the
fix; recent_fonts_appear_first_in_the_menu pins the group, divider and
alphabetical copies.

Co-authored-by: Reynold Hu <88558425+reynold-hu@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 15:42:42 +00:00
81925d7769 Integration: Raw Defaults resolution on Revert, Batch, Stack, headless open and CLI convert (#7811)
The resolution preference reached File › Open, but the other ways a raw
becomes a document of its own still opened it at 72 ppi: File › Revert
(a raw opened at 240 dropped to 72), Batch / Image Processor / Lens
Correction batch (a raw saved as PSD came out at 72 while Open + Save
gave 240), Load Files into Stack, the headless MCP `doc.open`, and the
CLI (`convert` at 72 while `run`/`info`, which open headless, now take
the default 300). They now apply the same `raw_defaults` rule; Batch
passes the user's value into its scratch sessions.

Tests that fail without this change (72 vs 240/300):
revert_batch_and_stack_open_a_raw_at_the_raw_defaults_resolution
(engine, one assertion per path), doc_open_gives_a_raw_the_raw_defaults_resolution
(automation), convert_opens_a_raw_at_the_raw_defaults_resolution (CLI).

Co-authored-by: Korg1ch <57859048+Korg1ch@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 15:42:42 +00:00
b526378de4 Integration: 32-bit Hue/Saturation with Photoshop's saturation factor, finite at lightness 1 (#5781)
The 32-bit branch cut the slider to floor(|s| * 255) / 255, which only matches
Photoshop near 25 and 50 %: measured at every whole percent from -100 to +100
on a 32-bit document, Photoshop lowers by 1 - floor(|s| * 256) / 256 and raises
by 255 / (255 - floor(s * 254)) taken to 1/256 and capped at 32767/256 (201 of
201 exact). +100 gave 512x where Photoshop gives 128x, +90 9.8x for 9.44x.

It also kept the HSL saturation unclipped, which is inf at HSL lightness 1 and
0 and negative above 1: (2, 0, 0), red at +1 stop, turned NaN with any edit,
Hue +40 alone included, where Photoshop gives (2, 1.333, 0). The 32-bit branch
now scales the chroma around the lightness instead (same result in gamut),
on the CPU and in the shader, in whole numbers on both. Colorize clamps its
saturation before the 32-bit scaling, so a PSD's out-of-range value renders
the same on the GPU.

Tests (fail without this): thirty_two_bit_saturation_steps_match_photoshop,
thirty_two_bit_hue_saturation_keeps_bright_colours, the GPU parity
hdr_hue_saturation_bright_colours. Photoshop-oracle floor 188 -> 190 of 258,
measured after merging main.

Co-authored-by: Timothy Daly <100327160+timothydaly1@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 15:42:03 +00:00
f1d947de8a Integration: test that a and b round after the opacity mix (#5771)
The PR's test checks L at full opacity only, so it still passes when a and b are left unrounded or
the rounding moves before the opacity mix, or truncates. New test: Curves taking a 140 to 151.4 at
50% opacity in an 8-bit Lab document must show a 146 (145.7 unrounded, 145.5 rounded before the
mix, 145 truncated: all three fail).

Co-authored-by: timothydaly1 <100327160+timothydaly1@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 15:41:21 +00:00
dff8bfd0e4 Integration: a unit per field, no Percent, not remembered, as in Photoshop (#7922)
Photoshop's Create Rectangle switches only the right-clicked field
(Width 35.28 mm, Height still 100 px), lists the six length units
without Percent, and opens in pixels again after OK. The PR shared one
unit between both fields, offered Percent, and saved the unit on every
OK, so after one use with the rulers in px the dialog stayed in px even
after the rulers changed to cm, though no unit was ever picked.

Now each field has its own widthUnit / heightUnit (set only by the
menu, never remembered), and the menu is rulers::unit_menu_of with
LENGTH_UNITS. The new test right-clicks the real field and clicks the
menu item through egui input.

Co-authored-by: Korg1ch <57859048+Korg1ch@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 15:41:21 +00:00
34c9d0ea4d Integration: History follows the history, not every revision, and never moves a visible row (#5940)
The panel scrolled with `scroll_to_rect(current, Some(Align::BOTTOM))` on every revision
change. In egui an explicit alignment always realigns, and the revision also changes when
nothing in the history does (selecting a layer, a channel eye). So clicking a visible older
state slid it to the bottom under the pointer, Undo hid the greyed redo states, a layer click
threw a user who had scrolled up back to the bottom, and selecting a snapshot scrolled it out
of view.

Now the panel follows the history position (document, past/redo counts, current name, and the
newest past state, which also changes at the state limit), skips it while a snapshot is
selected, and scrolls without alignment, so only a hidden current row moves into view. New
tests: a clicked visible state stays put, Undo keeps the redo state in view, a layer click keeps
the scroll, a selected snapshot stays in view, a new state at the limit is revealed; the first
three fail on the PR head, the others without the snapshot guard / the newest-state key.

Co-authored-by: nrq6993-a11y <342501791+nrq6993-a11y@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 15:41:21 +00:00
aiongg 64fb74f1e7 Integration: #8143 the branchless floor test accepts any NaN for NaN inputs
MSVC's floor quiets a signalling NaN while branchless_floor keeps its payload, so the Windows job failed the bitwise compare; every other input still compares bit for bit.
2026-10-22 15:40:11 +00:00
aiongg 22d912a108 Integration: #8142 #8535 Quick Export accepts a .exr path
Main's extension check (#8519) refused .exr, the format #8142 adds to Export Preferences; the path's extension now may also name EXR.
2026-10-22 15:40:08 +00:00
aiongg add0fbc706 Integration: merge main (through #8562)
contributors/people.toml: kept both sides' new entries (r4's list and main's benandez).
2026-10-22 15:40:05 +00:00
aiongg 4819f5b7d4 Merge PR #8372: Compose: effect maps that don't fit in the effect cache are built over the render rect, not rebuilt whole on every render (#8330) (new head) 2026-10-22 15:40:02 +00:00
aiongg 9cba44307d Merge PR #8236: Add interactive drag positioning to print preview (new head)
crates/ui-egui/src/file_ui.rs: kept r4's print_params and added the new head's doc comment on print_confirm.
2026-10-22 15:40:00 +00:00
aiongg eb199bcefe Merge PR #7721: fix(actions): refuse lossy fractional Crystallize ATN export (new head) 2026-10-22 15:39:55 +00:00