Commit Graph
503 Commits
Author SHA1 Message Date
Tanish Manku 8c142f7e3b Merge pull request #11292 from indy2kro/fix/11271-space-hand-view-menu
Hand held by Space: right-click opens the view menu, as in Photoshop (#11271)
2026-11-11 00:30:10 -07:00
Tanish Manku 0fc597634a Merge pull request #10384 from ingling/pdf-spot-plates
PDF: spot channels as overprinting Separation plates (FILE-215-17)
2026-11-11 00:15:13 -07:00
Tanish Manku dc855ae902 Merge pull request #6864 from hidoba/codex/curves-followup-fixes
Fix remaining grayscale Levels/Curves target mismatches
2026-11-10 23:57:32 -07:00
Michael Higgins 9d5be5ec8e Bridge: never send the control token to a listener that is not the app
## Summary

`photocraft-cli mcp --bridge` sent the control token in clear as its first frame, to whatever had accepted the connection: a `[::1]` listener reached through a `localhost` lookup, or any process holding `127.0.0.1:<port>` while the app was not listening. This PR fixes both cases:

- **Literal loopback only.** `BridgeClient` parses its address into a socket address and connects to it. Only `127.0.0.1:<port>` and `[::1]:<port>` are accepted. `localhost:<port>` means `127.0.0.1:<port>` (what the desktop app binds) and is never looked up.
- **Challenge authentication.** The bridge authenticates with `auth.challenge {clientNonce}` → `{serverNonce, serverProof}` → `auth {proof}`. Both proofs are HMAC-SHA256 keyed by the token over both nonces, each under its own label. The client checks the server's proof before it sends its own and never falls back to sending the token. The token never crosses the wire, so a listener that is not the app gets only a random nonce.
- **One shared server implementation.** `security::ServerHandshake` replaces `authentication_reply`, and both the desktop control server and `photocraft-cli serve` use it. The servers still accept the one-frame `auth {token}`, so existing scripts and older clients keep working. A repeat `auth.challenge` after authenticating gets "already authenticated", like a repeat `auth`.
- HMAC comes from RustCrypto's `hmac` 0.12 with `sha2` 0.10 (`sha2` was already in the lockfile). The proofs are compared without an early exit.
- `docs/control-protocol.md` documents the handshake, the exact HMAC message, and the bridge's address rules. The book's protocol summary is updated too.

Compatibility: a new bridge needs an app from the same release. Against an older app it fails with "refused challenge authentication … the control token was not sent". It does not downgrade.

Known limit: the channel is plain TCP with no TLS, so nothing binds the handshake to the connection. A party sitting between a bridge and a real app could relay the handshake without learning the token. On loopback that needs it to hold the port the bridge dials while the app listens on another, which the literal-address rule above narrows. Older clients that still send `auth {token}` keep their old exposure until they upgrade.

Closes #9777

## Tests

New in `crates/automation/tests/it/bridge_token.rs`:

- `localhost_reaches_the_ipv4_app_not_an_ipv6_listener`: a listener on `[::1]:P` gets no connection, and the app on `127.0.0.1:P` does.
- `a_listener_that_is_not_the_app_never_receives_the_token`: on both loopbacks, a listener that stays silent, refuses, or fakes the challenge receives only the challenge frame, never the token. The error says why.
- `the_bridge_authenticates_to_the_headless_server_by_challenge`: end to end against `rpc::serve_tcp`. A wrong token is refused before its proof is sent.

Unit tests: the handshake in both directions, and that wrong, missing or replayed proofs, the token after a challenge, a second challenge and malformed nonces are all refused (`security.rs`). `loopback_socket` address parsing (`bridge.rs`). The desktop server answering a challenge, then dispatching (`control_server.rs`). The existing bridge and MCP fakes now run the real `ServerHandshake`.

Run locally (Windows 11, targeted tests only):

- **Fail before.** I put upstream's `crates/automation/src/bridge.rs` (the old client) back and ran `cargo test -p photocraft-automation --test it bridge_token`: **3 failed**. The `[::1]` listener received `{"method":"auth","params":{"token":"<the token>"}}`, a `127.0.0.1` listener received the raw token, and the headless test got "authentication required" instead of the refusal. The original audit tests (`audit_nv6_bridge_sends_token_to_ipv6_impostor`, `audit_nv6_bridge_sends_token_to_any_ipv4_holder`) fail the same way on upstream/main.
- **Pass after.**
  - `cargo test -p photocraft-automation --lib --test it -- bridge security tcp`: 23 + 21 passed. This covers the new tests, the existing bridge, MCP-bridge, security and serve TCP tests, and the legacy-token tests that still pass unchanged.
  - `cargo test -p photocraft control_server`: 6 passed, including the new challenge test. The legacy-token tests still pass.
  - `cargo clippy -p photocraft-automation --all-targets -- -D warnings`: clean.
  - `cargo fmt`: clean.
- `Cargo.lock` adds only `hmac` 0.12.1, plus `subtle` under `digest` (already in the lock). Checked with `cargo metadata --locked`.

Not run here: the full gate (`cargo xtask ...`, the workspace-wide tests, wasm, clippy on the desktop app crate). That is left to CI.

API note: the workspace-internal `security::authentication_reply` is replaced by `ServerHandshake::reply`, which returns an `AuthStep`.


<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
<!-- coderabbit-ai-generated:v1 -->

## Summary by CodeRabbit

* **Security**
  * Control connections support challenge-response authentication, verifying possession of the configured token without sending it. Token-in-frame authentication remains available for scripts.
  * The MCP bridge accepts literal loopback addresses and maps `localhost` directly to IPv4 loopback. It rejects servers that refuse or cannot complete challenge authentication.
* **Bug Fixes**
  * Rejected authentication exchanges close the connection; successful authentication allows requests to proceed.
* **Documentation**
  * Control protocol and bridge authentication behavior are documented.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-11-11 07:35:17 +01:00
Vlad Grankovsky 333566a616 Merge upstream main 609e2aa72 into curves follow-up 2026-11-10 19:51:17 +01:00
Tom InglingandClaude c77f589184 Merge storytold main (0d08cb7dd) into pdf-spot-plates
One conflict, the gaps.md header: main's line (2026-11-08, G9 Column
Size now leads it) with this branch's G10 spot-plate item in front.
Main's changes to control-protocol.md and pdf/text.rs merged cleanly;
the doc.save row keeps `pdfSpotPlates`.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-11-10 13:48:01 -05:00
ec97a146f5 Support ordered automation read roots with safe save provenance (#3023)
* fix(cli): reject repeated automation read roots

* feat(automation): add ordered read roots with isolated saves

Stage source and regression tests for GGEO-AI02 validation before updating PR #3023. Repeated reads accumulate; writes remain single-root and implicit saves retain their selected capability provenance.

* style: format multi-root automation extension

* test: use fixed pixel chunks in read-root regression

* docs: reconcile control protocol with upstream TGA options

* docs: state multi-root capabilities directly in API summaries

* test: construct regression actions through upstream defaults

* Clarify headless and desktop directory automation reads

* Format the shared root parser and relocated service regressions

* test: adapt automation import callback to ImportOptions

* test: expect read-root diagnostic for denied smart-object input

* test: reject repeated write roots while allowing ordered read roots

* test: assert explicit empty-root parser diagnostic

* test: exercise leaf-link save using a supported image filename

---------

Co-authored-by: FredApps <FredApps@users.noreply.github.com>
Co-authored-by: Codex <codex@openai.com>
2026-11-10 14:28:12 +01:00
Tom InglingandClaude Fable 5.1 d08a0bfaba Merge storytold main (009b93c5e) into pdf-spot-plates: plates on artboard pages
Main rewrote the PDF writer since e4e2607c8: a page per artboard with
page labels, writer cancellation (`ctl`), an export memory budget, and
PDF import. This branch adds spot plates, an optional page image and an
XMP packet. The merged writer keeps both:

- `print_pdf_pages` numbers its objects dynamically: per page its page,
  contents, image and one XObject per plate; then the shared font, ICC
  profile, overprint ExtGState and XMP metadata stream, each only when
  present, the selection font and last the page-label tree. The XMP
  packet is a parameter now, not a page field, since it is the
  catalog's.
- `export(doc, spot_plates, ctl)` / `export_with(doc, spot_plates, xmp,
  ctl)`. An artboard page carries its own region of every plate
  (`plates_of` takes the page rect), so a two-board document separates
  into two pages of named plates; a Multichannel artboard document is
  pages of plates alone. The export budget counts one byte a pixel per
  plate alongside the composite. New test:
  artboard_pages_carry_their_region_of_each_plate.
- lib.rs passes `ctl` through; the PDF arm keeps the XmpEmbed gate.
- file_cmds.rs: main's `maximizeCompatibility` and this branch's
  `pdfSpotPlates` both listed in the four spec strings and the import.
- docs: gaps.md and file-format-parity.md headers take main's line with
  this branch's items in front; the PDF parity row keeps main's new
  read column and notes and this branch's plate notes, plus that an
  artboard page carries its region of each plate; control-protocol.md's
  doc.save row lists both new params.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-09 14:46:47 -05:00
Cristi Radu 1be423b30e Merge upstream main
# Conflicts:
#	docs/control-protocol.md
2026-11-09 19:34:53 +02:00
David Mann d2330fccea Merge pull request #12402 from TrevorBrown-dev/fix/actions-drag-trash-new-12373
Actions panel: a row dragged onto the trash is deleted, onto Create new action duplicated
2026-11-09 16:28:21 +01:00
TriwarrandFredApps 7c64653d5c Add toolbar side toggle (#11193)
* Add a toolbar side toggle and dock-edge resizing

- Let the toolbox sit on the canvas' left or right, persisted in the panel
  state and switchable from the toolbar header and preferences.
- In the compact arrangement, stack dock, rail and toolbox on the dock's
  physical edge so the toolbox cannot cover the rail.
- Make the dock panel's inner edge the single resize handle: it drives the
  panel width and the dock total (split size and column widths) in the same
  frame, so the rail and dock content stay bound with no reserved room and
  no second handle. The docking layout's own column-edge handle is covered;
  the bar is the theme separator at rest and the accent on hover/drag.
- Persist the layout, keep the split size in sync, handle RTL, and test the
  toolbar side state.

* Toolbar side: dock-edge only with a lone canvas; test every theme

- Use the between-dock-and-canvas layout only when nothing is docked beside the canvas: that view drops the canvas from craft-ui's split, so with a panel there the standard layout keeps it visible and dockable.
- Add canvas_alone_layout/canvas_alone and a unit test for a lone canvas versus a split canvas side.
- Add a kittest that toggles the toolbar Left/Right across every ThemeKind::ALL entry, asserting the panel lands on the requested physical side.
- No dependency changes: Cargo.toml and Cargo.lock are identical to main, so the repository's exact pins are untouched.

* Toolbar side: keep the canvas a drop target during dock drags

While a panel is being dragged, draw the standard layout even with the toolbar at the dock edge. It keeps the canvas in craft-ui's split, so a panel can still be dropped beside it; after the drop the canvas is no longer alone and the standard layout stays. The between-dock-and-canvas layout resumes for a lone canvas once the drag ends.

* Toolbar side: stable drag layout, unwind-safe dock flag, clippy

- Restore the dock-only thread-local through a drop guard, so an unwinding show does not leave a later standard layout canvas-less.
- Do not switch the dock layout mid-drag: craft-ui records its areas on the press, and changing the shape cancels the drag. The layout stays stable; a panel beside the canvas still selects the standard layout before the drag begins.
- Fix the CI clippy errors: explicit auto-deref in end_dock_node/resize_end_dock and field reassignment in the toolbar_side test.

* Toolbar side: anchor the Edit Toolbar flyout; borrow the canvas layout

- more_slot_flyout takes the resolved ToolbarSide instead of the text direction, so a right-hand toolbar in LTR (and a left-hand toolbar in RTL) anchors the flyout away from the canvas like the tool flyouts.
- canvas_alone borrows the saved layout and treats the native-dock legacy layout as canvas-only, avoiding a per-frame clone and traversal.

* Toolbar side: only a horizontal canvas split uses the dock-edge layout

A saved workspace can put the canvas above another panel in a vertical root split; canvas_alone_layout now requires SplitAxis::Horizontal so that shape keeps the standard layout. The guard test covers the vertical case.

* Toolbar side: document the functions this PR adds

Covers ToolbarSide::resolve/other, canvas_alone_layout, the DockOnlyGuard drop, and the two new tests. Docstring coverage of the PR's own additions is now complete; no behaviour change.

* docs: refresh the scorecard after merging main

The merge left docs/scorecard.md stale against the combined settings table; regenerated with cargo xtask scorecard, which is what the gate's scorecard step checks.

* Toolbar side: honor the workspace lock, empty docks and the end-dock shape

- The dock-edge resize handle is inert while the workspace is locked (hover sense, no highlight) and set_end_dock_total refuses to write then; the drag test covers a locked workspace.
- An empty dock takes only the rail's width instead of a clamped blank column; dock_is_empty reports it and a test empties the dock.
- canvas_alone_layout now requires the second child to be the Dock itself, and end_dock_node/resize_end_dock follow horizontal splits only, so a layout the handle cannot resize keeps the standard layout instead of snapping back.
- Tests: DockOnlyGuard restores the flag on drop and unwinding; the theme test clicks the toolbar's move button instead of setting state; a dock-edge harness drags the edge, checks it persists and that a locked workspace refuses it; axis tests and a two-column ui.set {dockWidth} test.
- The flyout indicator follows the resolved toolbar side, the rail keeps its chrome fill and 4/8 inset with the separator inside it, ATTRIBUTION counts the new icon (116), and docs/control-protocol.md describes dockWidth as the whole end dock.

* Toolbar side: floating windows on empty docks, iconic widths, dockWidth columns

- An empty dock still runs the docking pass on the full remaining area, so Studio's floating Properties window keeps rendering with or without the rail; the dock column itself takes no width. A Studio regression builds an empty dock with a floating Properties group and asserts the pass runs and the window is drawn.
- Dock width now uses each column's displayed width (iconic_width while collapsed to icons), and resizing/scaling keeps iconic widths in step. A collapse-to-icons regression checks a 36-point column not reserving 290.
- ui.set {dockWidth} scales every column to the requested total directly instead of resizing only the first column and then finding the sum already equal. The two-column test also covers a target smaller than the other column's expanded width (250 from [300, 180]) and asserts per-column widths.

* Toolbar side: iconic widths, edge release and modal handling, side persistence

- An iconic-only dock keeps its narrow displayed width instead of being raised to the expanded
  minimum, and the dock-edge panel records its laid-out width for a geometry regression.
- The release frame applies its final pointer position before dropping the drag, so the dock keeps
  the width the user released at.
- The dock-edge handle is inert (and any stored drag is dropped) while a modal dialog is open.
- set_display_width changes iconic_width while iconic and leaves the columns expanded width for
  expanding again; the regression resizes an iconic column and checks the restore.
- Clicking the toolbar side persists the panels snapshot immediately through persist_panels, even
  when the dock is not drawn, while Remember Workspace Changes is on.
- The locked-drag test compares against the pre-drag width instead of only settled equality.

* Toolbar side: defer persistence while moving, translate the side labels

- persist_panels defers while the dock persistence pass sees a native window move/resize (the
  same native_moved signal), so a click during an OS resize cannot save intermediate geometry;
  it requests the usual 100 ms repaint and leaves the dock pass to save when the geometry is
  stable.
- The toolbar-side tooltip/accessible labels now pass both literals directly to tl!, and all 34
  registered catalogs carry translations for Move Toolbar to the Right/Left.

* Toolbar side: keep in-progress dock geometry out of toolbar saves

- native_moved caches its answer for the frame, so the dock pass and persist_panels see the same
  movement result even though only the first call observes the change.
- persist_panels defers while a dock-edge drag is stored, a pointer is down or a native window
  moved; it stores a pending flag and requests the usual 100 ms repaint.
- persist_pending runs in the frame tick (before preferences save) and retries the deferred save
  once geometry is stable, so the toolbar side is not lost even when the dock was not drawn.
- The dock persistence pass also skips while the dock-edge drag start is stored, covering a
  synthetic/accessibility click with no pointer button down.

* Toolbar side: keep a same-width dockWidth request a no-op

The dockWidth rewrite scaled and committed the legacy layout even when the requested width
already matched, so a later `ui.set {dockTabs}` took the customized-workspace path and expanded
the Properties group. Restore the early return for an unchanged displayed width and cover it:
setting the current dock width keeps `app.ui.docking` None.

Fixes the `a_new_adjustment_layer_brings_properties_to_the_front` failure on CI.

* Toolbar side: recheck native bounds, expire stale dock-edge drags

- native_moved keeps movement already seen in the frame but compares the bounds again on every
  call, so a later dock save is not fooled by the early persist_pending check caching false
  before the docking UI updates a floating window.
- dock_panel records the frame it rendered; persist_pending drops a stored dock-edge drag start
  once the panel stopped rendering (Shift+Tab hides the dock mid-drag), so deferred saves resume.
- The same-width dockWidth test also selects a tab in a collapsed group and asserts the tab,
  the collapsed state and that no workspace was customized.

* Toolbar side: draw the release width, clear drags when the dock empties

- The release frame draws the panel at the final pointer width just stored, instead of the width
  read before the drag ended, so the panel and toolbar match the saved dock width.
- The empty-dock path clears a stored dock-edge drag start, so a dock that empties mid-drag cannot
  leave deferred saves blocked.
- Tests: nested DockOnlyGuard (flag found set stays set), and a queued `ui.set {dockWidth}`
  through the rendered dock pass checking a two-column dock totals 420 points.

* Toolbar side: cancel the dock-edge drag when resizing is unavailable

- A disabled parent UI (screen picking) never delivers drag_stopped, so dock_panel now clears a
  stored dock-edge drag whenever resizing is unavailable: locked, modal, empty dock or disabled
  UI. The same condition makes the handle hover-only, so it cannot start a new drag while
  disabled.
- Regression: start a resize, disable the UI, release, re-enable and move the pointer; the drag
  is cleared, the dock does not jump and persistence can resume.

* Toolbar side: bound the dock-edge layout and enforce column minima

- The dock-edge panel leaves the toolbar and the 200-point canvas minimum room, so a narrow
  window shrinks the dock instead of the canvas.
- The empty dock runs the docking pass in a child ui that does not consume the parent layout, so
  the toolbar and canvas keep their space.
- Column scaling keeps the 220-point panel minimum: minima are allocated first, the rest is shared
  proportionally, and requests below the sum of minima stop there.
- The dock metrics (empty, iconic-only, width) come from one layout snapshot per frame.
- Removed dead empty-dock branches, narrowed the content-edge interceptor to one point either
  side, corrected the toolbar target comment, used the drag press origin, and documented the
  Side::End assumption.
- Tests: narrow-window canvas minimum, empty dock composed with toolbar and canvas, minimum-aware
  two-column widths, and the empty compose regression.

* Toolbar side: bound the dock-edge layout, enforce panel minima, match iconic widths

- The dock-edge panel leaves the toolbar and the 200-point canvas minimum room, so a narrow
  window shrinks the dock instead of the canvas.
- The empty dock runs the docking pass in a child ui that does not consume the parent layout, so
  the toolbar and canvas keep their space.
- Column scaling allocates each column's 220-point panel minimum first and shares the rest
  proportionally; requests below the sum of minima stop there.
- Iconic columns use craft-ui's renderer rule: iconic_width.unwrap_or(36).clamp(36, 240), including
  the real collapse case that leaves iconic_width unset.
- Dock metrics come from one layout snapshot per frame; dead empty-dock branches are gone; the
  content-edge interceptor is one point either side; the toolbar target comment is corrected and
  the Side::End assumption documented.
- Tests: narrow-window canvas minimum, empty dock composed with toolbar/canvas, minimum-aware
  two-column widths, collapse via Action::SetIconic leaving iconic_width unset, and the 36..240
  clamp. The press-origin change was reverted: Sense::drag starts immediately.

* Toolbar side: distribute refused width, draw actual widths, invalidate stale drags

- scale_columns moves the total toward the request in passes, freezing columns that hit a bound
  (iconic 36..240, expanded minimum 220), so width refused by the iconic cap reaches columns that
  can still grow.
- set_end_dock_total returns the width actually stored; the dock panel uses that width during the
  drag, on release and after, instead of forcing the requested or minimum width.
- The custom drag stores docking_generation; a workspace replacement clears the stale gesture
  before any width mutation.
- Clamp bounds are normalized against the available maximum (including iconic columns) and
  resizing is disabled when the window cannot fit the column minima, so narrow layouts cannot
  panic.
- Tests: iconic-cap surplus redistribution, actual-vs-requested panel geometry during and after a
  drag, workspace replacement during a drag, and a 300pt window resize without panic.

* Fix dock-edge resize bounds and cancellation

* Preserve explicit dock width across legacy migration

* Document dock code, dedupe widths, harden toolbar_side load

- Add doc comments to the functions and tests this PR touches (Docstring Coverage).
- Share the toolbar width (toolbar_width/toolbar_columns), the rail widths and
  CANVAS_MIN_WIDTH instead of repeating the values; no values change.
- Document dockWidth as the requested model width, which a narrow window may draw
  narrower in either layout, and test model 520 / drawn <= 364 / canvas >= 200.
- Document that a dockWidth change is shared equally among columns within their
  minimums and iconic caps; tie ICONIC_WIDTH/ICONIC_MAX_WIDTH to craft-ui's
  DockStyle defaults (36/240) in a test.
- Replace the DOCK_ONLY thread-local and its guard with show_layout(dock_only).
- Move the test-only probe writes behind panel_docking::record, with no-op stubs
  outside tests.
- Read an unknown or malformed saved toolbar_side as Start instead of dropping
  the whole panel state; ui.set still refuses it.

* Test visible dock chrome in narrow windows; name panel body minimum

- The narrow-window dockWidth test now checks the docking controls craft-ui drew:
  hit_rects are full widget rects (before clipping), so the visible part of each,
  intersected with the dock, must stay inside the dock and off the canvas and
  the toolbox, with at least one control visible.
- Name the 200-point guard for drawing a panel's body PANEL_BODY_MIN_WIDTH,
  separate from CANVAS_MIN_WIDTH; no value change.

* test(ui-egui): assert dock controls are clipped at the panel edge

The narrow-window test intersected each hit rect with the dock and then asserted the
intersection lies inside the dock, which always holds. Check each dock control's clipped
interact_rect against the dock instead, and add a squeezed-column test where a control's
full rect runs past the panel edge, so the clip is actually exercised.

* ci: retrigger pull_request workflows after resolving the merge conflict

No changes. GitHub did not queue Actions for 3652ede, the push that
resolved the conflict with main.

* test(ui-egui): assert saves resume after a disabled UI cancels a dock-edge drag

The test's doc says saves must resume once a disabled UI drops the held drag, but nothing
checked it. Assert that the held drag is not saved, that its width is saved as soon as the
drag is cancelled (while the UI is still disabled), and that a width change made after
re-enabling reaches the saved layout too.

* test(ui-egui): cancel a dock-edge drag on a window shrink and check the dockWidth column split

constrained_width_does_not_panic_while_resizing now starts the drag in a wide window and
shrinks the window below the column minima while it is held: the drag must be cancelled and
the dock must stay within the window. Both ui_set_dock_width tests now check how the change is
split between the columns, not only the total.

* test(ui-egui): tighten dock tests per CodeRabbit review 5824274070

- disabling_the_ui_cancels_a_dock_edge_drag: assert the drag is cancelled on the first disabled
  frame, before the pointer is released.
- all_iconic_totals_stop_at_both_bounds_without_losing_expanded_widths: check the stored column
  widths sum to the expected total after each request.
- explicit_width_request_preserves_queue_and_native_state_until_invalid_workspace_recovery: add
  a changed-width (520) case next to the no-op one.
- the_toolbar_side_toggle_works_in_every_interface_theme: with the toolbar on the right, a grouped
  tool's flyout opens to the left of its button.

* test(ui-egui): assert stored widths and native rect per CodeRabbit review 5824824938

- constrained_width_does_not_panic_while_resizing: the stored column widths captured before the
  window shrinks are unchanged after the cancel and after the release.
- explicit_width_request_preserves_queue_and_native_state_until_invalid_workspace_recovery: the
  native state keeps the exact rectangle the test saved until recovery, not just some state.

* Keep the toolbar side when a workspace saved none

A workspace saved before the toolbar could move has no panels.toolbar_side,
so applying it reset a right-hand toolbar to Start. Keep the current side
when the saved value is missing or unreadable; a valid saved side still wins.

* Cover a saved start side and the other panel flags on workspace apply

---------

Co-authored-by: FredApps <FredApps@users.noreply.github.com>
2026-11-09 15:51:29 +01:00
6121e884ba Merge origin/main into PR #12402 (docs/ui-parity.md change log: keep both rows)
Co-Authored-By: Trevor Brown <trevorbrown.dev@gmail.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 15:49:09 +01:00
Brennan StehlingandClaude Opus 5.5 aac3312bfb Perf: one frame stage record across egui's passes; the web keeps slow frames too (#9328) (#10103)
* Perf: the web keeps slow frames too; its clock is performance.now() (#9328)

now_ms() reads web_time::Instant, which is performance.now() on wasm, so
the web build records slow frames, reports perf.slowFrames and logs them
to the browser console. The stroke trace is off on the web for another
reason: it has no environment, stderr or files to write to.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Perf: one frame stage record across egui's passes (#9328)

egui runs a frame's UI again when a pass asks for a discard (a sizing
pass). The second pass opened a new stage record after the first pass
closed its own, so a frame of two 30 ms passes was never slow and the
stroke trace gained a 0-move row per extra pass.

finish_frame now adds each pass's UI time to the record and leaves it
open while ctx.will_discard() says egui will run the pass again; the
last pass closes it. ui_start keeps the first pass's input time.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Perf: a frame's stroke flag covers every pass

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-11-09 15:49:05 +01:00
David Mann fbcb6798e0 Merge pull request #9021 from devstivi/apply-to-all-unsaved-prompt
Offer Apply to All in the unsaved-changes prompt
2026-11-09 14:23:44 +01:00
FredApps 885a6ddf27 Merge pull request #9864 from Higgs32584/fix/8806-rasterize-click-replay
fix(ui-egui): the rasterize prompt no longer replays the click that opened it (#8806)
2026-11-09 11:55:33 +01:00
Trevor BrownandClaude Opus 5.5 c1a109f894 Actions panel: the drop target is outlined only, so its icon stays visible (#12373)
The hover highlight filled the footer button with the opaque hover colour over the icon. It is
now a 1.5 px accent outline. control-protocol.md's Actions panel paragraph mentions the two drags.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Model-Effort: Opus 5.5 drove and wrote
2026-11-08 15:06:50 -05:00
David Mann 32ce08364b Merge pull request #9991 from ljkx/feat/raw-xmp-crop
Camera Raw sidecars: apply the crop, stop reporting RawFileName (#9985)
2026-11-08 16:55:43 +01:00
David Mann 2ffe5d1008 Merge pull request #12102 from Bobderto/fix/12085-zoom-menu-centre
Zoom tool menu: keep the view centre for Zoom In and Out
2026-11-08 16:51:19 +01:00
Vlad Grankovsky 3d57a9afeb Merge upstream main 6ab991571 into curves follow-up
Keep recorded channel initialization shared with Record Again and retain upstream Photo Filter and Action playback behavior.
2026-11-08 14:13:01 +01:00
Cristi RaduandClaude Opus 5.5 5f3502cc3f Hand held by Space: right-click opens the view menu, as in Photoshop (#11271)
With another tool selected, holding Space and right-clicking the image
now opens the Hand tool's view menu (Fit on Screen, Fill Screen, 100%,
200%, Print Size, Zoom In/Out) instead of nothing. The selected tool is
kept, and the menu closes if it changes. ⌘Space and ⌥Space hold the Zoom
and still open no menu. Agents get the same with ui.pointer space: true.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 07:10:18 +02:00
David Mann 100997baaa Merge origin/main into feat/raw-xmp-crop
# Conflicts:
#	crates/ui-egui/src/dialogs/camera_raw_ui/open.rs
#	crates/ui-egui/src/dialogs/camera_raw_ui/sidecar.rs
#	docs/file-format-parity.md
2026-11-08 01:36:16 +01:00
8bf381f2d9 Find Layers: keep the Name text on every run path; no focus for a hidden filter row (#11238)
CodeRabbit found three things after the last fix-up:
- `engine.execute` and action steps without `name` replied `selected: []`
  while the panel listed the layers its kept Name text matches. `run` now
  fills in the panel's Name text for every path, as the menu did.
- With Filter Options > Hide, Find Layers left a focus request that took
  the caret once the row was shown again. In the reference application the
  row stays hidden and showing it later does not focus the field, so the
  request is only made for a drawn row.
- The control protocol said the selection never changes; the Name filter
  deselects a selected layer it hides (#10879). The doc now says so and
  describes each entry point and the reply.

Tests for the first two fail without the fix.

Co-Authored-By: NaufalDzz <NaufalDzz@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 20:24:22 +01:00
Vlad Grankovsky 2ed691bcc5 Merge upstream main e6ba4aca4 into curves follow-up 2026-11-07 20:12:52 +01:00
David Mann 0f0e76c44b Merge remote-tracking branch 'origin/main' into pr-11238
# Conflicts:
#	crates/ui-egui/src/panels/layer_filter_ui.rs
#	docs/control-protocol.md
2026-11-07 19:37:29 +01:00
83ffc233d8 Merge main into #9021; move the Apply to All change to dialogs/discard_ui
Main moved discard_ui.rs to dialogs/discard_ui.rs and its tests to
dialogs/discard_ui/tests.rs; the change and its tests now live there.

Co-Authored-By: devstivi <devstivi@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 17:47:20 +01:00
Dan 4c46db6206 fix(ui): retain held-Z menu anchoring 2026-11-07 16:14:56 +00:00
Dan 593c7d487b fix(ui): keep Zoom menu steps centred on the view 2026-11-07 16:09:14 +00:00
David Mann e3f3465fe8 Merge pull request #10840 from TrevorBrown-dev/feat/gradient-overlay-drag
Layer Style: a drag on the image moves a Gradient Overlay (#10763)
2026-11-07 16:11:57 +01:00
David Mann 67e3457b02 Merge pull request #10001 from indy2kro/feat/8064-psd-maximize-compatibility-param
Saves take maximizeCompatibility for a PSD/PSB's merged image (#8064)
2026-11-07 14:44:52 +01:00
David Mann 41aa2b86f5 Merge pull request #3693 from funkvay-star/feat/levels-reset-clipping
Levels: Alt-drag clipping display (#3328)
2026-11-07 13:38:09 +01:00
Vlad Grankovsky d9400a7cb8 Merge upstream main 048ae7977 into curves follow-up 2026-11-07 01:41:38 +01:00
Vlad Grankovsky 283de2c506 Merge upstream main 8df2b7a2e into curves follow-up 2026-11-07 01:27:10 +01:00
David Mann 927c63d394 Merge pull request #11022 from Hamza0101/fix/crop-overlay-and-fill-defaults
Crop: Always Show Overlay by default, Fill resets on another open document, and an untouched frame shows only itself (#10894, #10930)
2026-11-07 01:11:21 +01:00
David Mann fd890eaca4 Merge pull request #11669 from davidmann4/feat/crop-task-bar-10890
Crop: Contextual Task Bar under the crop frame, as a reusable task-bar widget (#10890)
2026-11-06 22:49:21 +01:00
David Mann 7ecfccbd64 Merge pull request #11643 from davidmann4/integration-batch-19
Integration batch 19: epic PRs (WIP)
2026-11-06 22:48:55 +01:00
Cristi RaduandClaude Opus 5.5 20900a5432 Say what an unset maximizeCompatibility does in app.save (#8064)
Unset or null follows Preferences › File Handling: Always and Ask When Saving
write the real merged image, Never the placeholder, and app.save opens no dialog.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 21:07:10 +02:00
Cristi RaduandClaude Opus 5.5 0c449729d7 Say Bitmap, Indexed Color and Duotone keep the merged image (#8064)
The PSD writer always writes the real merged image for those modes (Bitmap and
Indexed are saved without layers; Duotone is exempt), so maximizeCompatibility
false does not give them the placeholder. The MCP params, control protocol and
CLI help now say so.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 21:07:10 +02:00
Cristi RaduandClaude Opus 5.5 a1bb9f003f Saves take maximizeCompatibility for a PSD/PSB's merged image (#8064)
Photoshop's Maximize Compatibility checkbox, for one save: `maximizeCompatibility`
(true or false) on Save, Save As, Save a Copy, `app.save`, serve `doc.save`, MCP
`doc_save`/`doc_export`, Batch, Image Processor and Layers to Files decides the
merged image over Preferences > File Handling > Maximize PSD and PSB File
Compatibility; unset, the preference decides. Anything but a boolean is refused
before anything is written. The CLI takes `--no-maximize-compatibility`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 21:07:10 +02:00
David MannandClaude Opus 5.5 e078c560ec Address CodeRabbit review: task bar tile, inspect and ui.set merge
- Draw the Crop task bar only on the active document's tile: a click into
  another tile activates its document mid-pass, and the tile drawn after it
  put the new frame's bar on itself for that frame.
- ui.inspect reports the bar hidden (and no rect) as soon as it can't show,
  not only after the next draw.
- ui.set merges panels.task_bar field by field: {"at": [x, y]} keeps a pin,
  {"pinned": true} keeps a dragged place; unknown nested fields are refused.
- Tests for each (each fails with its fix reverted), plus the task bar's
  preset dropdown picking 1 : 1 in sync with the options bar.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 19:41:21 +01:00
David Mann d1d913e7e6 Merge remote-tracking branch 'origin/main' into feat/crop-task-bar-10890 2026-11-06 19:24:17 +01:00
David Mann 7919429708 Merge remote-tracking branch 'origin/main' into feat/crop-task-bar-10890 2026-11-06 17:50:28 +01:00
David MannandClaude Opus 5.5 c7b5373ba8 Crop: Contextual Task Bar under the crop frame, as a reusable task-bar widget (#10890)
The Crop tool gets the reference application's contextual task bar: a small floating
bar centred 11 pt under the crop frame with a grip, the preset dropdown (the options
bar's own, so both stay in sync), Straighten, a ⋯ menu (Hide bar, Reset bar position,
Pin bar position) and, while a crop is pending, Cancel and Done. It follows the frame,
stays inside the canvas, and stays put once dragged or pinned. Window › Contextual Task
Bar (after Tools, as in the reference) shows and hides it.

The bar itself is a reusable widget (widgets/task_bar.rs): position rule, grip, ⋯ menu
and the hide/pin/place state in Panels (kept with the panel layout across restarts,
read by ui.inspect › taskBar, set by ui.set {panels}). A context supplies only its
controls and where ⋯ goes; the Crop contents are tools/crop_task_bar.rs. Generative
Expand stays out until #41.

Also: Lucide pin and rotate-ccw icons, six new strings in all 27 catalogues, parity
661/661 (FLOOR raised), control-protocol docs. Three tests about the options bar's
Ratio dropdown now hide the task bar, which shows the same dropdown.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 17:50:20 +01:00
David Mann b5acb4c622 Merge origin/main into integration-batch-19 2026-11-06 17:32:14 +01:00
FredApps aee46a2fc6 Merge remote-tracking branch 'origin/main' into fix/10164-final-prune-u2 2026-11-06 17:28:48 +01:00
David Mann 16348db38b Merge pull request #9138 from davidmann4/fix/9137-control-click-coord-range
ui.click / ui.move reject coordinates beyond the screen range (#9137)
2026-11-06 17:24:27 +01:00
David Mann f38a7dfecb Merge remote-tracking branch 'origin/main' into pr-11238
# Conflicts:
#	crates/ui-egui/src/panels/layer_filter_ui.rs
#	docs/control-protocol.md
2026-11-06 16:25:36 +01:00
David Mann 8df0c8ba54 Merge origin/main into fix/crop-overlay-and-fill-defaults
# Conflicts:
#	crates/ui-egui/src/tools/crop_ui.rs
2026-11-06 16:25:10 +01:00
2a9426cfe2 Merge origin/main into #11092 (Group filter) and fit it to main's filter row
Main added Effect, Attribute, Color, Smart Object and Artboard to the Layers
panel filter. Group is now the last type in the list, and works like Artboard:
`Filter::with_selection` fills the listed rows once per frame from
`group::listed`, so the panel, the deselect-on-apply step (#10879) and the
control channel all see the same rows; `shows_in`/`group_ids` and the
layers.rs change are gone. This also removes the duplicate `mod group;` and
`mod tests;` lines (E0428) and the rustfmt issues from the review.

- `group::listed` walks the tree with a stack instead of recursion and
  returns `LayerId`s.
- `Filter::show_all` is documented: Group keeps each group open or closed.
- Tests go through `Filter::of_app` and `panel_rows`, as the panel does,
  plus a deselect-on-apply case and a document without groups.
- control-protocol.md: `group` added to main's `layerFilter` entry.

Co-Authored-By: Nurana100 <Nurana100@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 14:42:50 +01:00
Khawar Zamman Wani 123b2032c4 Web build: Place runs a slice per frame, so a large PNG no longer freezes the page (#10164, rebased onto main)
The remaining commits of the stack, applied on top of the squash-merged #10147:

- PNG decodes in resumable steps of rows (PngDecodeRows, shared with decode_rows)
- PNG imports as a document in resumable steps (io PngImport, text metadata and
  document build in bounded steps)
- Jobs can run a slice at a time where there are no threads (StepWork,
  cooperative sessions, Session::poll_jobs)
- Place decodes, flattens and resizes in slices (PlaceSteps), reading a path
  and keeping the frame, memory gate and typed memory-budget errors main has
- The web build places dropped and picked files a slice per frame, with a
  bounded place inbox
- A measurement of the stepped place; docs for jobs without threads
2026-11-06 08:51:17 +05:30
David Mann cc361c306d Merge commit '9bef3b2dc8b84c1b6cefc8ce0dc2b01d089cd9e8' into HEAD
# Conflicts:
#	crates/engine/Cargo.toml
#	crates/engine/src/prefs.rs
#	docs/ui-parity.md
2026-11-06 02:16:44 +01:00