Commit Graph

1343 Commits

Author SHA1 Message Date
Tanish Manku 2a2980771e Mask view test: a commit that takes the live stroke uploads nothing it already showed 2026-10-10 23:28:32 -07:00
Tanish Manku c5703f5632 Releasing a brush stroke no longer freezes: the commit takes the pixels the live stroke already drew (P32: 136 ms → 0.8 ms)
Brush phase 4. On release the canvas dropped its live stroke and ran `paint.stroke` over every
point again (the whole stroke re-rendered), then re-composited the stroke's bounding box, although
the canvas already showed exactly those pixels. Long strokes froze for 100–250 ms at mouse-up
(#2068).

- `Session::offer_live_stroke` hands the finished live stroke to the next command. A
  `paint.stroke` / `paint.pencil` whose command, document revision, params, seed and points match
  it exactly takes its pixels instead of rendering (result `"fromLive": true`). Anything else
  ignores the offer and renders as before. It still runs through the normal dispatch
  (authorization, journal, one history step), and a replay renders identical pixels.
- The canvas feeds the release frame's last points to the live stroke first, then remembers the
  stroke's per-step damage for one revision (`CommittedStroke`): each canvas cache re-composites
  only the steps it hadn't shown, usually nothing, instead of the bounding box.
- P32 (brush release on a long stroke) becomes measurable: perf_scenarios times a 600-point
  200 px soft stroke on 24 MP. 136 ms → 0.83 ms on the GPU canvas, 257 ms → 0.71 ms on the CPU
  canvas (RTX 5090, release). Budget 16 ms, not enforced until a baseline run.
- Scorecard: TOOL-213-10 (⌃⌥-drag resize with readout) is done; TOOL-213-7 (live strokes for every
  brush-based retouch tool) is partial (Spot Healing, Mixer Brush, Background Eraser and Pattern
  Stamp still aren't live).
2026-10-10 23:23:02 -07:00
Nigel Ihza Firdausy e59bb2909b
feat(ui): allow disabling Studio canvas background dots (#2317)
* feat(prefs): persist an optional canvas dot pattern setting (#2264)

* feat(canvas): respect interface canvas dot preference (#2264)

* test(prefs): cover canvas dot toggle persistence and reset (#2264)

* Translate the Show canvas dots preference and regenerate the scorecard

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

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 02:15:06 -04:00
Elvis33LE 29f1ff9e90
Design: bounded GPU stack prefix reuse for property edits (#1164)
* docs: design bounded GPU stack prefix reuse

* docs: advanced blending stays on the CPU; cache admits whole grid chunks and serves partial rects
2026-10-11 02:08:08 -04:00
Elvis33LE a8ad82dd46
GPU composite: reuse unchanged lower-stack chunks for property edits (#1169)
* perf(gpu): reuse unchanged lower-stack composite chunks

* test(gpu): verify half-float composite checkpoint reuse

* docs: record measured GPU composite reuse gains and limits

* perf(gpu): admit whole grid chunks, serve partial damage as sub-rect copies

Cache entries were keyed by the exact damage rectangle, so brush strokes
(a new partial rect every frame) filled the budget with entries that
never hit again. Entries are now whole chunk-aligned, chunk-sized grid
cells: a capture merges into the cell it overlaps at the rectangle's
offset, a read is served as a sub-rect copy of the covered part, and
coverage is tracked per cell so a resume only happens where every pixel
of the requested rectangle is cached. After a full render, any in-bounds
partial rectangle hits.

Regression test: two series of disjoint strokes inside the same chunks
leave composite_cache_bytes unchanged (no entry-per-rectangle growth),
a full refresh still finds its chunks and a top-layer edit afterwards
reuses them. The old partial-damage test now expects the covered-chunk
hit and checks outside-the-document renders admit nothing.

* docs: record the GPU reuse measurement in target-app-parity.md

The honest assessment moved from roadmap.md to target-app-parity.md on
main; keep main's roadmap and add only the dated measurement.

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

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 02:07:48 -04:00
skylar taylor-barrick 97136485f0
Merge pull request #3097 from skylartaylor/codex/ui-suite-integration-20261010
Adopt shared docking while preserving panel controls and workspaces
2026-10-10 21:40:26 -06:00
skylar taylor-barrick d0a18cfd83 UI.9: keep browser bundle under size budget on current main 2026-10-10 20:56:34 -06:00
Brandon Thomas 7926161595
Release prep 0.7.0: Linux riscv64 tarball + version bump (#2965)
* Release prep 0.7.0: add Linux riscv64 tar.gz to the release; bump to 0.7.0

- release.yml: new linux-riscv64 job, ported from GridCraft: cross-compiled on ubuntu-24.04
  against ports.ubuntu.com riscv64 libs, tar.gz only, photocraft-cli --version smoke-tested
  under QEMU. The draft-release job now needs it, so SHA256SUMS.txt covers it.
- packaging/linux/package.sh: CROSS_ARCH / CROSS_TARGET / CROSS_COMPILE / EMULATOR support.
- linux_libs: search /usr/lib/riscv64-linux-gnu on riscv64 (was the x86_64 triplet).
- README and docs/releasing.md list the riscv64 tarball.
- Version 0.6.0 -> 0.7.0 (cargo xtask version set), Cargo.lock refreshed; contributors refreshed.

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

* canvas: crisp elliptical marquee ants again in an unrotated view

#2732 (a27d7175) built the elliptical marquee's live ants in document space and snapped every
vertex to the half-pixel grid on its own. Near the edge midpoints the nearly flat arcs became
1 px stairs, whose antialiased steps are grey, so the ants no longer contrast on white or black:
drag_preview_canvas's marquee test fails on main (ubuntu and windows). In an axis-aligned view
(rotation a multiple of 90 degrees) draw the ellipse inscribed in the snapped screen box again,
as before #2732; rotated views keep the mapped ellipse.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 21:36:30 -04:00
skylar taylor-barrick 77f50330c6 UI.9: refresh shared panel integration against current main 2026-10-10 19:30:56 -06:00
Fabio Lussi 953390a36e
Navigator preview fills its group; no scroll fade over dock panels (#2726) (#2818)
* Let the Navigator preview fill its dock group, drop the scroll fade (#2726)

The Navigator drew a fixed 150 pt preview inside the dock's outer
ScrollArea, so a taller group left dead space below the zoom slider,
and a shorter one scrolled with egui's scroll fade painting a dark
gradient over the thumbnail. The same fade darkened the Color field
and swatches at the bottom of their groups.

The Navigator tab now lays itself out (like Layers and History): the
preview takes the group's height above the zoom controls, keeps the
document's aspect and stays centred. The dock's outer scroll areas no
longer paint the fade; their scroll bar already shows there is more.

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

* Navigator wheel test: point below the preview, which now fills the group

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

---------

Co-authored-by: Fabio Lussi <198260595+itrycodinganditsbad@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-10 19:32:48 -04:00
Fabio Lussi 3c70d3b34f
Layer > Rasterize > Layer Style bakes effects; Rasterize Layer converts vector masks (#2245) (#2669)
* Add Layer > Rasterize > Layer Style; Rasterize Layer converts vector masks (#2245)

Nothing could bake layer effects into a layer's pixels, and Rasterize >
Layer refused a raster layer that has a vector mask.

New command layer.rasterize.layerStyle (Layer > Rasterize > Layer Style,
and in the Layers context menu). It renders the layer by itself through
the compositor at 100% opacity in Normal mode, so the effects, fill
opacity and masks end up in the pixels, including those past the canvas.
The layer's own opacity, blend mode, knockout and Blend If stay layer
properties. Type, shape, fill and Smart Object content is rasterized
first, all in one history step. With a Normal layer and Normal-mode
effects the composite is unchanged at 8, 16 and 32 bit. Effects in
another mode are evaluated against the isolated layer's transparent
backdrop, because one layer of pixels cannot hold several blend modes.

Rasterize > Layer and All Layers now also turn a vector mask into a
pixel mask ("all vector data") and leave effects live, as Photoshop
does. The menu enablement follows both changes.

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

* Regenerate docs/scorecard.md after rebase (#2245)

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

---------

Co-authored-by: Fabio Lussi <198260595+itrycodinganditsbad@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-10 19:20:33 -04:00
Fabio Lussi 7bea7dae1c
Wake the UI when the first Linux appearance read lands late (#2798) (#2941)
The appearance worker stored the first portal/gsettings answer and
signalled readiness, but only SettingChanged updates requested a
repaint. If Read replied after the 250 ms start-up wait, an idle Auto
window kept its fallback palette until some unrelated event.

Route the initial read and later signals through one publish step that
repaints the registered context whenever the value changes, and move
the thread/wait setup into start() so a test can drive it without D-Bus.

Co-authored-by: Fabio Lussi <198260595+itrycodinganditsbad@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-10 19:19:27 -04:00
Sir Starch bfa523f916
fix(theme): subscribe before reading Linux appearance (#2833) 2026-10-10 19:15:38 -04:00
Ehsan Fazli d9ae73e95f
feat(ui): live font preview on hover (#2431)
* feat(ui): preview fonts on hover

* test(ui): exercise font preview through eframe

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-10 19:11:27 -04:00
Valentin Klein 325c5b3194
Tools: Single Row Marquee and Single Column Marquee (#2837) (#2848)
Both tools join the Marquee flyout (no key, outside the M / Shift+M cycle,
as in Photoshop). A click selects the one-pixel row (column) under the
pointer across the whole canvas; a press-drag moves the line until the
release. The selection goes through select.rect, so Shift adds, Alt
subtracts, Shift+Alt intersects and the options-bar modes apply; Feather
applies, Anti-alias and Style/Width/Height are greyed. Selection-mode
cursor badges, the selection right-click menu and snapping cover them.
Original row/column glyphs with an ATTRIBUTION.md row; tool names in all
catalogs; README tool list and the G6 / ui-parity rows updated.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-10 19:10:34 -04:00
Fabio Lussi 9d3ba499ad
Layer > New > Background from Layer; the Background stays at the bottom (#2626) (#2663)
* Add Background from Layer and keep the Background at the bottom (#2626)

Layer > New > Background from Layer was missing, and the Background was a
locked layer that Layer > Arrange (with an explicit layer), Reverse and
dragging in the Layers panel could move, or put other layers below.

The new layer.new.backgroundFromLayer command renders the active layer as
it looks over the background colour, drops it to the bottom and locks it
as the Background, in one undo step. It is unavailable when the document
already has a Background, and for groups and adjustment layers.

Arrange refuses to move the Background and stops other layers above it;
layer.moveTo and Reverse refuse edits that would move it off the bottom.
The Layers panel shows no drop indicator for those drops.

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

* Parity floor 629; the empty-space drop test uses a document without a Background

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

---------

Co-authored-by: Fabio Lussi <198260595+itrycodinganditsbad@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-10 19:09:26 -04:00
dhiaaddin 1e0f8844eb
Font menus: add an Arabic sample for families that cover Arabic (#2535) 2026-10-10 19:06:05 -04:00
Valentin Klein 5b778b49f2
Move tool: highlight the layer Auto-Select would pick on rollover (#2834) (#2843)
* Move tool: highlight the layer Auto-Select would pick on rollover (#2834)

With the Move tool and Auto-Select on (or ⌘ held while it is off), hovering
the canvas outlines, in the theme accent, the layer (or group, per the
Auto-Select target) that a click there would pick. Preferences › Tools ›
Show Highlight on Rollover turns it off (on by default).

The hover asks the click's own hit test (pick_cmds::auto_select_target, now
shared with layer.pickAt), so a type layer is outlined anywhere inside its
text and fully locked layers are clicked through, as on a click. The answer
is cached per document revision and pointer pixel. Nothing is drawn while a
button is down, a drag or transform runs, or the selected pixels would move.

Part of #214 (scorecard TOOL-214-4, now partial).

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

* Move hover: follow main's moves_selected_pixels_with rename

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-10 19:04:19 -04:00
Sir Starch 67e34a1d4c
Themes: add Breeze Light and Breeze Dark (#2276) (#2335)
Two Studio-layout themes for KDE Plasma desktops: original colour values
chosen to sit close to Breeze's cool blue-grey surfaces and blue accent
(and its 5 px radius) so the app blends into a Plasma desktop, without
copying Breeze's colour schemes. Text readability comes first.

- theme.rs: ThemeKind::BreezeLight / BreezeDark. Every text colour (text,
  secondary, faint, accent, danger, warning) holds 4.5:1 on every surface
  and on hovered, pressed and selected states; the primary button is a
  deeper blue that takes white text and stays above 4.5:1 while it
  darkens on hover and press. Breeze's own inactive text and link blue
  are only about 2.8:1 on its light surfaces, so ours are darker. The
  close button goes red on hover.
- prefs: Theme/LightTheme::BreezeLight ("breezeLight") and
  Theme/DarkTheme::BreezeDark ("breezeDark"): Preferences > Interface,
  `prefs.set`.
- Window > Theme > Breeze Light/Dark Theme (`window.theme.breezeLight`,
  `window.theme.breezeDark`) and `ui.set {"theme": ...}` (aliases:
  breeze, kde, plasma for the light one; ...dark for the dark one).
- i18n: the four new labels in all 16 catalogs.
- docs/ui-design.md and ATTRIBUTION.md (original work, credits Breeze as
  the inspiration; Breeze's schemes are LGPL-2.0-or-later and none of
  their values, files or code are copied).
- Tests: names/aliases, surface order and blue-grey character, the
  contrast matrix for both themes, and the preference/mode selection.
2026-10-10 19:01:53 -04:00
FinnNk 0c4016a4db
feat(ui): hold Alt+Space to zoom out, as in Photoshop (#2877)
* feat(ui): Alt turns a held temporary Zoom In into Zoom Out

Alt reverses the Zoom tool's direction, but a held Zoom In (Cmd+Space)
ignored it: with Zoom Out unbound or rebound, Cmd+Alt+Space zoomed in.
held_tool now reports Zoom Out whenever Zoom In is the held binding and
Alt is down, unless Alt is part of the Zoom In key itself (a rebound
Alt+Z still zooms in). The default keys don't change here.

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

* feat(ui): hold Alt+Space to zoom out, as in Photoshop

Photoshop zooms out with Alt+Space (Option+Space on the Mac), usually
pressed as Space first, then Alt, then click. PhotoCraft's Zoom Out
(hold) default was Cmd+Alt+Space only, so Space then Alt still panned.

Zoom Out (hold) now defaults to Alt+Space. Holding Cmd+Alt+Space
matches both Cmd+Space and Alt+Space equally; the previous commit's
Alt rule resolves that to Zoom Out, so it keeps zooming out. Saved
overrides are kept. Holding Space first also avoids the Mac's Spotlight
shortcut and the Alt+Space window menu on many Linux desktops.

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

* chore(contributors): credit FinnNk by GitHub profile name

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 18:56:04 -04:00
Valentin Klein 6911075037
Edit: Copy/Paste with no selection keeps type, shape and group layers (#2670) (#2686)
* Smart Objects: Copy/Paste with no selection keeps the smart object (#2428)

Photoshop's layer-based copy (CC 2018 on) copies the active layer whole
when there is no selection. Edit > Copy always lifted the layer's pixels,
so Paste made a raster layer from a smart object. Copy now also keeps the
smart object layer on the clipboard (with its pixels for Paste Into,
masks, channels and other apps). Paste recreates it with contents of its
own, converted to the destination's colour and depth and placed as the
pixels would be. With a selection, Copy still takes pixels.

The colour/depth/contents-id handling of layer.copyToDocument moves into
shared helpers (adapt_copies, translate_copies) used by both.

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

* Edit: Copy/Paste with no selection keeps type, shape and group layers (#2670)

Photoshop's layer-based copy, verified in Photoshop: with no pixel
selection, Copy takes a type layer, a shape layer or a group (its whole
subtree) whole, and Paste / Paste in Place recreate it instead of a
pixel layer. #2428 did this for smart objects; this extends the same
path. Pixel layers, several selected layers and any copy with a
selection still take pixels.

A group has no pixels of its own, so Copy (now enabled for a group with
no selection) puts its composite on the clipboard for Paste Into, masks
and other apps.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-10 18:50:04 -04:00
Fabio Lussi 37cb99eb1f
Let dock tabs be dragged to reorder within their group (#2272) (#2774)
Dock tabs only sensed clicks, so a drag on a tab fell through to the
strip behind it and moved the whole group; there was no per-group tab
order to change.

DockLayout gains tabOrder (tab names per group, like hidden_tabs, so the
Pro/Studio Color/Swatches swap is safe). It is read leniently: unknown
groups, names and wrong types fall back to the default order instead of
dropping the saved layout. It is drawn through visible_tabs, while
dockTabs and the panel bodies keep using the original indices.

Tabs on the dock strips now sense drags: dragging one along its strip
shows an insertion line and drops it there, a plain click still selects,
and a tab dragged off the strip moves the group as the strip does.
Lock Workspace stops both. The order is part of ui.set {dock}, the
remembered panelLayout and saved workspaces.

Co-authored-by: Fabio Lussi <198260595+itrycodinganditsbad@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 18:47:36 -04:00
Brandon Thomas 9c90365c45
ui-egui tests: restore the joined helper (fixes ui-egui test build after #2924 and #2497 both removed one) (#2947)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 18:46:16 -04:00
Valentin Klein 4916a0f088
Pencil: 1 px lines step pixel by pixel, no doubled corners (#2139) (#2806)
Small aliased tips (dabs at most a pixel apart) now walk each segment
pixel by pixel on the line between the pixels holding its ends, one dab
per pixel along the major axis at the pixel's dab centre. Distance
spacing put two dabs in a row wherever the line crossed a pixel corner,
which left L-shaped doubled pixels at every step of a Pencil line.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 18:45:57 -04:00
Valentin Klein 93599a1837
Smart Objects: Copy/Paste with no selection keeps the smart object (#2428) (#2653)
Photoshop's layer-based copy (CC 2018 on) copies the active layer whole
when there is no selection. Edit > Copy always lifted the layer's pixels,
so Paste made a raster layer from a smart object. Copy now also keeps the
smart object layer on the clipboard (with its pixels for Paste Into,
masks, channels and other apps). Paste recreates it with contents of its
own, converted to the destination's colour and depth and placed as the
pixels would be. With a selection, Copy still takes pixels.

The colour/depth/contents-id handling of layer.copyToDocument moves into
shared helpers (adapt_copies, translate_copies) used by both.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-10 18:43:46 -04:00
Fabio Lussi 87f74cc8d2
Type: tabs advance to Photoshop's default tab stops (#1881) (#2733)
#1942 stopped tabs from shaping as .notdef by laying them out as a
space, so a tab was only one space wide. Layout now widens each tab to
the next default stop (every 36 pt, from the anchor of point text or the
left edge of the box). Paragraph text breaks lines with the real tab
widths: it starts from a safe upper bound (space + one interval) and
re-breaks until the widths agree, so expanded tabs never overflow the
box and lines don't wrap early. The text keeps its \t, so PSD round
trips are unchanged. Tab in the Type tool now types a tab.

Co-authored-by: Fabio Lussi <198260595+itrycodinganditsbad@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 18:38:53 -04:00
xsvm 13e478cc6e
Filter Gallery: an unknown option is a bad-params error (#2548 follow-up) (#2599)
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-10 18:37:25 -04:00
Brandon Thomas e1a13013b6
ui-egui tests: drop the shadowing joined closure (fixes clippy dead-code on main) (#2924)
#2782 and #2853 both fixed the save-dialog test's path expectations; together they left a
top-level joined() helper shadowed by an identical local closure, so clippy -D warnings fails
with "function joined is never used". The test now uses the documented helper.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 18:34:51 -04:00
Nigel Ihza Firdausy 7687380604
feat(prefs): add search across preference sections and settings (#2679)
* feat(prefs): search visible settings across sections (#2593)

Filter the Preferences sidebar by section and editable field labels,
switch to the first match, and show matching settings in the editor.
Keep the draft intact while searching and hide Reset Section during filtering.

* Preferences search: translate the field hint and empty state; rustfmt

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

* Merge fixups: pass the new search argument in a main test; search test uses a visible Type setting

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

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 18:32:04 -04:00
Fabio Lussi 7c9e7e5ae3
Keep the type style when a data set replaces text (#2742) (#2785)
Applying a textReplacement value cleared the layer's character and
paragraph runs, so the text re-flowed from the summary fields (family,
size, colour) and lost weight, italic, tracking, underline, caps and the
rest of the run style. The new text now takes the first character's style
and the first paragraph's settings, as in Photoshop. The value is kept
verbatim, as before.

Co-authored-by: Fabio Lussi <198260595+itrycodinganditsbad@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 18:31:50 -04:00
Ahmed Hosny 308c3369c0
Control: timeout errors name the likely causes (#2489) (#2497)
* Control: timeout text derives from the reply deadline (#2489 review)

The server deadline and both timeout replies now share one constant:
REPLY_DEADLINE drives the serve thread's clock and the text is built from
it, so the message can never claim a number the server no longer uses.

* Tests: drop the duplicate joined() helper left by #2782 and #2853 both landing

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

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 18:27:03 -04:00
Fabio Lussi 07e7a73636
Show non-square pixels on the canvas (#2594) (#2718)
View > Pixel Aspect Ratio and its Correction toggle only stored a value:
the canvas transform (ViewXform) had no notion of pixel aspect, so the
image was always drawn with square pixels.

ViewXform now carries the displayed pixel aspect ratio and stretches
document x by it before rotation and flip. Drawing, pointer-to-document
mapping, pan and zoom-about deltas, rulers, grid, Fit/Fill on Screen,
scrollbars, the Navigator's view box and paste-in-place visibility all
go through it. Correction is on by default, as in Photoshop. A corrected
view draws through the CPU canvas path, like a flipped one, since the
GPU shader assumes square pixels. The document's pixels are unchanged.

Co-authored-by: Fabio Lussi <198260595+itrycodinganditsbad@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-10 18:25:24 -04:00
Arthur Wagner 46af2e6e30
Workspaces: preserve the embedding host menu and rail flags (#2620)
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 18:22:30 -04:00
skylar taylor-barrick 254450f38e UI.8: pin final shared dock action labels 2026-10-10 16:20:57 -06:00
Arthur Wagner 2194c7c58f
Status bar: show document dimensions in the ruler unit (#2618) 2026-10-10 18:18:19 -04:00
Valentin Klein faf2639fb8
Web: File › Open opens every selected file (#2802) (#2807)
The web file dialog called rfd's pick_file() and ignored the request's
`multiple` flag, and FileDialogAnswer could only carry one file's
contents. Pick several files when File › Open asks for them, answer
with the new ContentsMany variant, and open each as its own document,
reporting a failed file without stopping the rest (as the desktop's
multi-path open does).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 18:15:47 -04:00
Gustavo Ziger 1a59f6f6a3
Fix brush picker closing immediately on trackpad taps (#2772)
Ignore outside-click dismissal during the opening frame.
Add regression coverage for repeated opening after outside clicks.
2026-10-10 18:10:12 -04:00
Valentin Klein f9f65447f8
Layers: Layer via Copy duplicates every selected layer (#2777) (#2796)
With several layers selected and no pixel selection, Cmd+J now copies every
selected layer in one undo step (each copy above its original, the copies
selected), reusing Duplicate Layer's multi-layer path. With a pixel
selection it still copies the active layer's selected pixels.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 18:09:03 -04:00
Valentin Klein 444323ee74
Warp Text: the dialog lists Style, orientation, then Bend and distortions (#2689) (#2819)
The schema dialog draws its fields in the order of the command's params
doc, which put Orientation last, below the three sliders. Photoshop's
Warp Text dialog has Style, then Horizontal/Vertical, then Bend,
Horizontal Distortion and Vertical Distortion. Reorder the params doc.

Live preview and opening with the layer's current warp already landed
in #2539 (after v0.6.0).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 18:06:09 -04:00
llduyll10 27e25c2cb4
Crop/marquee readout and Properties W/H/X/Y in the ruler unit (cm, mm, in…) (#2334)
* Crop / marquee size readout in the Units & Rulers unit (cm, mm, in…) as Photoshop shows it

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

* Properties: Transform W/H/X/Y and Canvas W/H in the Units & Rulers unit (cm, mm, in…), typed values converted back to px

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

* Merge origin/main: size readout test follows the trimmed unit format

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-10 18:03:32 -04:00
Elvis33LE 03f4cc0de1
Correct Photomerge roadmap status (#2831) 2026-10-10 17:57:51 -04:00
mroxso d9e6cee908
Tests: build save-dialog expectations with Path::join so they pass on Windows (#2745) (#2853)
Co-authored-by: highperfocused <highperfocused@pm.me>
2026-10-10 17:57:41 -04:00
Fabio Lussi 46d170b19b
Free Transform frames the whole type layer; Distort/Perspective greyed for type (#2630) (#2701)
* Free Transform on type: whole-layer frame, grey Distort/Perspective (#2630)

With a selection active, Free Transform on a type layer framed only the
selected part of the text and the preview lifted half of the rendered
glyphs, but the engine then transformed the whole type layer, so the box
and the result disagreed. Type, shapes and smart objects always move
whole, so transform_bounds no longer intersects their frame with the
selection and the preview no longer lifts pixels out of them.

Distort and Perspective stayed enabled for editable type and then failed
on commit. They are now greyed for type layers (menu bar, marquee and
transform-box context menus), as in Photoshop, unless an unlinked mask or
channel is targeted.

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

* transform_type_tests: set CanvasToolMenu::pasteboard (#2630)

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

---------

Co-authored-by: Fabio Lussi <198260595+itrycodinganditsbad@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 17:57:26 -04:00
Reynold Hu a3d2a6d486
Place: wire Resize Image During Place into the implicit fit (#204) (#2538)
* feat(engine): Resize Image During Place drives the implicit Place fit (#204)

* Merge origin/main; regenerate docs/scorecard.md

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

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 17:53:25 -04:00
Gert Pellin - GPE a27d717597
canvas: the marquee hinting follows a rotated view like the release does (#2732)
The live ants for the Rectangular/Elliptical Marquee (and Object Selection)
were an axis-aligned screen box built from the two projected corners of the
drag. A rotated view projects the drag's document rectangle onto a rotated
one, and the box through two of its corners is a different shape in a
different place: on a 30 degree view a corner landed 21 x 50 px away from
where the selection it previews was committed. The release was always right,
only the hinting was drawn elsewhere (#2512).

marquee_drag_ants builds the outline in document space and maps every point
through the view instead: four corners for the rectangle, the ellipse
inscribed in it for the elliptical marquee, with as many vertices as its size
on screen asks for. Every point is snapped to the half-pixel grid like every
other ants outline (marching_ants_segments), so the lines stay crisp at any
zoom. Unrotated the mapping is only a scale, and the corners come out where
they always did.

Tests drag a marquee in a 30 degree view for both tools and check the drawn
outline against the selection the release commits: each corner is the mapped
corner of the dragged rectangle, and each point of the ellipse maps back onto
it.

Co-authored-by: switch87 <8284204+switch87@users.noreply.github.com>
2026-10-10 17:51:20 -04:00
Valentin Klein 63fee8c07c
wasm: gate native-only constants so wasm32 builds without dead-code warnings (#2808) (#2813)
PAR_MIN_PIXELS / PAR_CHUNK_PIXELS (photocraft-cms) and OPENABLE
(photocraft-engine) are only used by native-only code (the rayon paths
and list_images). Give them the same cfg so wasm32 check and clippy
-D warnings are clean. No behaviour change.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 17:48:19 -04:00
Valentin Klein 1320dac5bf
Spot Healing: Create Texture keeps the grain, joining blocks along min-error cuts (#2104) (#2762)
Create Texture (inpaint::synthesize) averaged each block 50/50 into the
already-synthesised overlap. Averaging two unrelated grain patterns cuts
their standard deviation by about 1/sqrt(2), so the fill came out smoother
than its surroundings (75 % of the grain for one 30 px dab), and every
overlapping stroke, which samples the previous ones, lost more (57 % after
eight). Blocks are now joined along Efros & Freeman's minimum-error
boundary cut (Image Quilting, SIGGRAPH 2001): each pixel is copied from
one block or the other, never averaged.

Content-Aware and Proximity Match already keep the grain on main since
#2202 (patch copies instead of voting blur); the new test guards all three.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 17:46:41 -04:00
Valentin Klein ab3e6ebe81
Brush: Wet Edges acts on the whole stroke, not each dab (#2088) (#2721)
Wet Edges multiplied every dab by its own rim profile and max-accumulated the
dabs, so the rims of neighbouring dabs showed through the stroke as a chain of
rings. Dabs now accumulate normally and record how deep they reach each pixel
(1 - rn); the stroke keeps the deepest value per pixel and applies the
wet-edge transfer to its accumulated coverage when composited (live preview,
tail preview, commit and retouch coverage alike). The interior of the stroke is
evenly light and only the stroke's own rim is darker.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 17:41:14 -04:00
twentymls d3ae5aad0f
Smart filter edits: re-render from cached stages, in a background job (#2739)
Showing, hiding or editing a smart filter re-rendered the whole stack from the
placed source on the UI thread: on a 3000x4498 layer with five filters every
click on a filter's eye froze the window for about 1.5 s with no feedback.

- Each render keeps the placed source and the result after every visible filter
  (blake3 of the source contents, the placement and the filters so far), so an
  edit reruns only the filters from the edited one up. Edit > Purge > All drops
  them.
- Smart filter edits apply the edit at once (so bad params still fail
  immediately) and re-render as a background job when started with
  Session::start: the window keeps drawing, with progress and Esc to cancel
  between filters. Session::execute is unchanged.
2026-10-10 17:38:59 -04:00
Valentin Klein d0127f9294
Eraser: Block mode (#2770) (#2778)
* Pencil/Eraser: round Pencil cursor and Eraser Pencil mode (#2662)

The Pencil cursor always outlined the square grid_square footprint, but the
aliased dab is a disc from 4 px on. It now draws a circle on the same
pixel-grid square when the tip is round, and the square for 1-3 px tips
whose dab really fills it.

The Eraser options bar gains Photoshop's Mode: Brush / Pencil (replacing
the blend-mode dropdown the Eraser never used). Pencil mode commits
paint.pencil with erase: true (hard, aliased, full flow), live and on
release, and shows the Pencil's cursor. Block mode is left for later.

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

* Eraser: translate the Pencil mode label in every catalog

The Eraser's Mode dropdown added tl!("Pencil"), which had no entry in the
cs, el, ko, zh-hans and zh-hant catalogs, so every_tl_literal_is_translated
failed.

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

* Eraser: Block mode (#2770)

Photoshop's Eraser Mode is Brush / Pencil / Block; #2694 added Pencil.
Block erases a hard, aliased square of 16 screen pixels, so 16 / zoom
document pixels, whatever the brush's size, hardness, opacity or flow.

- paint: BrushSettings.square (with aliased) rasterises the dab's whole
  pixel-grid square (serde: omitted when false, so saved brushes are
  unchanged).
- engine: paint.pencil "block": true builds that brush from the stroke's
  "zoom" (brush_cmds::block_size: round(16 / zoom), 1..5000 px); a zoom
  that is not a positive number is an Err. Params doc updated.
- ui: Mode dropdown gains Block; the Block-mode Eraser commits
  paint.pencil with "block": true, shows the block's square cursor and
  greys out Size/preset, Hardness, Opacity and Flow.

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

* Eraser: translate the Block mode label

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-10 17:35:55 -04:00