In Photoshop a path is deleted by dragging its row in the Paths panel
onto the Delete (Trash) button at the panel's bottom: the row follows
the cursor and releasing over the Trash deletes that path, whichever
row is selected. PhotoCraft's rows only reacted to clicks, so the
drag gave no feedback and a drop on the Trash did nothing.
Saved path, work path and vector mask rows now start a drag. While it
runs the row's name follows the cursor (the ghost the Layers and
History panels use) and the Trash is outlined when under the pointer;
releasing there runs the row's own delete command (path.delete for a
saved or the work path, layer.vectorMask.delete for a vector mask),
reusing the History panel's dropped_on helper. A drag that never sees
its release is dropped next frame.
A shape layer's path row does not start a drag: what deleting it does
is #6552's change, kept separate.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
An adjustment layer whose block no mapper knows (Photoshop 4's `hue `, or a
type newer than PhotoCraft) opened as an empty pixel layer with no notice.
Its block did survive in psd_blocks, but the layer showed as a paintable
empty pixel layer and saved without the "pixel data irrelevant" flag.
Photoshop writes every adjustment layer without pixels and with "pixel data
irrelevant" set (every adjustment layer in the corpus; pixel, type and smart
layers never have it). A record like that which is not a group, matches no
known content kind, and carries exactly one block that is neither a known
layer-information key nor a known adjustment now imports as
Adjustment::Unsupported with that block, plus the usual "could not be read"
warning naming the key. With two or more candidates nothing is guessed and
the layer stays a pixel layer as before. The rule only fires for layers
without pixels, so no pixels can be lost, and the block is written back
verbatim either way.
Export keeps an unknown-key Unsupported block in place while it is
unchanged; before, it was written a second time beside the preserved copy.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The right-click menu of a shape layer's "<Layer> Shape Path" row in the
Paths panel had no Delete Path, while Photoshop offers it enabled.
Measured in Photoshop: with a rectangle shape layer's shape path targeted
in the Paths panel, Delete Path removes the whole shape layer as one
"Delete Layer" history step (another shape layer below is untouched);
it does not leave a full-canvas fill behind.
The row now offers Delete Path, dispatching the existing layer.delete
command for that row's layer (passed explicitly, so a multi-layer
selection is not deleted with it). Saved and work path rows keep
path.delete and a vector mask row keeps Delete Vector Mask.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
After an Alt tap or F10 the in-window menu bar owns the keyboard, and
shortcuts::handle returned before dispatching anything, so Ctrl+D, F7
and the other command shortcuts did nothing until Esc left the bar.
Since the shortcuts run before the menu bar in a frame, the bar can't
let go of the key itself in time.
menu_keys::leave_for_shortcut takes the keyboard off the bar when a
Cmd/Ctrl combo or a function key other than F10 is pressed while the
bar is focused with no menu open; shortcuts::handle calls it before its
early return, so the shortcut runs in the same press. Arrows, Enter,
Esc, letters, Alt+letter and F10 keep driving the bar, and an open menu
still swallows shortcuts.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Selecting a saved path in the Paths panel drew nothing: the canvas overlay
only drew the work path, the active shape layer's path and a targeted vector
mask. The selected saved path is Photoshop's target path, so it is now drawn
with any tool (anchor squares with Path Selection, and with Direct Selection's
overlay), still hidden by View > Show > Target Path and Extras.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Paths these features keep in the document go through the same resolver as
linked smart objects (relative to the document's folder; other computers,
devices and URLs refused):
- A Displace smart filter's `mapPath` and a Color Lookup smart filter's LUT
`file`, re-read on every re-render: a path that is not followed leaves
the pixels as they are, as for a missing map or LUT.
- Apply Data Set's Pixel Replacement images.
- Reload Frame of a video layer's footage.
Apply Data Set and Reload Frame say why a path is not followed.
Save As PSD/PSB, layered TIFF and Package as PSD read a linked smart
object's file to embed it. The path is resolved with the same resolver as
other linked-file reads, against the document's own file (the new
ExportOptions::document_path / PsdExportOptions::document_path, set by the
engine's saves and the app's Save As). A link that is not followed is
written as pixels with a warning that says why, as for a missing file.
Nested smart-object documents resolve against the same document.
A linked smart object's path comes from the document. Every place that
follows it now goes through one resolver (photocraft_io::doc_path, built on
craft-docpath): a relative path is taken from the document's folder, and
only local files or files on the share the document itself is on are
opened. Paths to other computers, device paths and URLs are treated like a
missing file: the layer keeps its last rendering.
- Link watch: such links are not stamped, and no background look starts
when no link of the document is followed.
- Update Modified Content and other re-renders read only followed files;
the error names why a link is not followed.
- Package lists such links as missing instead of copying them.
- Reveal in Finder for a linked layer shows only a followed file; the
document's own Reveal is unchanged. The macOS invocation ends options
with `--`, and Explorer is not asked to select a path containing a comma
or a quotation mark.
- Place Linked stores a relative path as an absolute one.
The document path travels to smart-object renders in a thread-local scope
entered by Session::edit, command dispatch and background jobs.
Co-authored-by: David Mann <daaavid.mann@gmail.com>
The live preview only covered selections up to 256x256 px; larger ones (the
command takes up to 16 M) errored, the UI remembered the failure and showed
nothing until release.
Engine: a preview whose selection (or landed area) is over 256x256 is now
solved on a scaled-down copy of the area around the selection and where it
lands (area-average resize of the target, or of the composite with Sample All
Layers, and of the selection), on a one-layer document of its own, then
enlarged back (bilinear, float samples, so any depth) over the selection's
shape where it lands and, for a move, where it left. The landed content is
sampled through the scaled placement, so rounding the scaled offset does not
shift it. ContentAwarePreview::refine gives the full-resolution job, with the
command's own margin, so the refined preview is the command's result bit for
bit. Every step checks the JobCtx; the session is never touched; bad params
and selections over the command's limit still error. Selections within
256x256 take the unchanged path (same output as before, checked by hash
against main). The bilinear resampler used by Transform On Drop now runs its
rows in parallel (same results).
Measured (release, 24 MP 8-bit document, Ryzen 9 7950X with other builds
running): coarse preview 600x400 0.26 s, 1500x1000 0.38 s, 3000x2000 0.69 s
(256x256 full-resolution preview: 0.21 s); the exact refine takes as long as
the command (0.8 s, 4.1 s, 9.4 s).
UI: cam_preview shows the coarse result, then starts the refine as a second
cancellable job and swaps it in; a failed refine leaves the coarse result
shown, and a failed first job is still not retried.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
`photocraft_compose::change_bounds` returned "anywhere" for every
adjustment layer before looking at `clipped`, although a clipped
adjustment only changes its clip base's pixels. Hiding or showing one
(or undoing that) therefore recomposited the whole canvas, which on a
heavy document fails the GPU memory fit check and falls back to a full
CPU redraw taking seconds to minutes.
`change_bounds` sees one layer and cannot find the base, so a new
`change_bounds_in(layers, k, canvas)` looks at the sibling list: a
clipped adjustment's change bounds are its base's change bounds. It
stays `None` (anywhere) when no unclipped base lies beneath it (the
compositor then draws it as an ordinary adjustment), when the base is
an adjustment (layers clipped to it composite atop the adjusted
backdrop), and, through `change_bounds`, when the base is a
pass-through group holding an adjustment (visible clipped layers make
that group isolated, so they decide whether its adjustment reaches the
layers beneath) or draws outside its bounds. The engine's
`layers_damage`, `step_damage` and `one_sided_bounds` use it.
Tests: a compose unit test that the clipped adjustment's bounds equal
the base's, that every pixel the toggle changes lies inside them, and
the cases that stay unbounded; an engine test that hide, undo, redo,
show and `layer.setProps` visible report the base's rect (top level and
inside a group), and that recompositing only that rect over the
previous canvas equals a full recomposite.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The Lightness slider's track in the Hue/Saturation editor (Properties
panel and dialog, which share it) ran black -> the current hue -> white.
Photoshop's Lightness track is a fixed black -> mid grey -> white ramp
whatever the Hue. lightness_track() now returns that ramp and no longer
takes the hue; the Saturation track still follows the hue. The UI-217-16
scorecard row describes the new track.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Conflicts:
- distort_ui.rs: main's `drop_foreign` (#5092, #5123) closes the Content-Aware Fill workspace
too when another document becomes active or its own is closed, stopping its background
preview as Cancel does; what Apply wrote stays. New test
`the_workspace_is_dropped_when_another_document_becomes_active`.
- sr, tr, vi catalogs: both sides' rows. Dropped es's High, Low, Duplicate Layer and Contract,
which main has now with the same translations.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
With more documents than tiles, the run of shown documents was worked out from the active
index alone, so after activating document 2 in Two-up, a click into the visible document 1
moved the tiles back to [0, 1]. The run is now kept from frame to frame and slides only when
a hidden document becomes active.
paint::color followed a spot colour's RGB equivalent and base colour with
no depth limit. Colours are shared objects, so a spot colour that links to
itself overflowed the stack and aborted the import, and a chain whose levels
link to the next through both took exponential time. Stop after 4 levels of
spot colours defined through spot colours, with a warning.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fixes#3708; part of #1540 (Flatpak needs its own follow-up: each sandbox has its own loopback).
A second launch hands its files to the running instance over an authenticated loopback channel (random port, 32-byte token in a private instance.json, constant-time check, bounded clients, requests and paths). Files are acknowledged only once the UI has taken them; the instance shuts the hand-off down before a CPU-compatibility relaunch; it runs only in the per-user settings mode. --new-instance and PHOTOCRAFT_NEW_INSTANCE=1 opt out.
Thanks to @MrCloudy2 (#2358), whose Linux version first handled the relaunch and per-user endpoints, and @Desayuno64 (#5273), whose shutdown flag and read deadline informed the final design.
Both harnesses built their device with egui_kittest's .wgpu() (egui's default limits) instead of
gpu_canvas::wgpu_setup() (the adapter's own limits, what the app requests), as #5954 fixed for the
background_redraw and navigator tests. On that device:
- canvas_16f's sixteen_bit_gradient_on_screen and thirty_two_bit_preview_exposes_values_above_one
panicked in egui-wgpu ("Failed to create staging buffer for index data", renderer.rs:984), every
run, single-threaded too;
- arbitrary_rotation_preview's two tests failed "the fixture must use the GPU canvas".
Both binaries pass on the app's device.
validate_token accepts uppercase hex and server_token keeps the token
lowercased, but authentication_reply passed the client's token to
token_matches unchanged, which compared bytes exactly. A client sending
the token exactly as configured (--control-token or a token file in
uppercase) was refused; only the bundled bridge, which lowercases, got in.
token_matches now folds ASCII letters on both sides, still without an
early exit. Only A-Z fold, so a non-hex byte never matches a hex digit.
* Save As on Linux: the file type picked sets the extension (#6121, #4328, #4394)
The GTK file chooser behind the desktop portal keeps the typed name when
another file type is picked, and rfd 0.17.2 reads only the file name from
the portal's answer, so Untitled-1.psd with PNG picked was written as a
PSD. On Linux, Save As now asks the portal's FileChooser.SaveFile itself
(over the zbus already used for the portal), reads current_filter, and
gives the name that type's extension: an extension PhotoCraft saves is
replaced, none or another is appended. If that names an existing file, the
dialog opens again under the new name so the portal asks before replacing
it. rfd remains the fallback without a portal, and this can go once an rfd
release reads current_filter (PolyMeilex/rfd#341).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Save As on Linux: keep typed extensions, open on the stem, follow the request handle
Review follow-ups, checked on real GNOME 46 and KDE Plasma 6.7.5 sessions
(Wayland and X11):
- An extension the user typed is kept, as in PolyMeilex/rfd#341: only a
name with none, or still with the extension the dialog opened with, takes
the picked type's. GTK reports the type even when it is untouched, so
art.png with the PSD type left alone was saved as art.psd.
- The dialog opens on the stem (test), as on Windows (#4407): with
test.psd in the name and PNG picked, GTK asked to replace test.psd,
which the save leaves alone. A name that then exists is asked about
again under that name; the rule compares against the suggested name, so
a test.psd GTK completes back still becomes test.png.
- Response 2 (GNOME's dialog closed with Esc) is a cancel: it fell back to
rfd, which opened a second Save As.
- The answer is awaited on the handle SaveFile returns, which a portal
older than 0.9 picks itself (the Request documentation).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* style(automation): format smart filter refresh closure
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
* GPU memory: a graphics card's own memory isn't charged for the document; GPU Memory preference; GPU Memory Full in the status bar
Many layers hit a cliff: past what the GPU memory budget leaves for layer pages, every edit
that redraws the whole document (opacity, visibility, a nudge) goes to the CPU compositor.
Measured on 3000x2000 full-canvas layers (RTX 5090, 31 GB RAM, Memory Usage 8 GB): 150
layers took 2.6 s per such edit, 300 layers 6 s. The budget was Memory Usage minus the
document's pixels and History, which is right where the GPU shares system memory, but a
discrete card's memory doesn't hold the document, so 150 layers of 6 MP (3.6 GB) squeezed
the budget below what they needed.
- Automatic budget: discrete adapters are no longer charged for the document's pixels
(integrated and software adapters still are). 150 layers: 2.6 s -> 25 ms.
- Preferences › Performance › GPU Memory (MB, 0 = Automatic; performance.gpuMemoryMb) sets
the budget outright. 300 layers with 12 GB: 6 s -> 45-72 ms.
- The status bar shows "GPU Memory Full" while the active document's layers don't fit
(both status bar styles); hovering gives the numbers, clicking opens Preferences ›
Performance. Its Graphics rows show the budget and what the layers need. The slow CPU
redraw notice gives the numbers and points at GPU Memory.
- Headless: the preference is a normal pref (prefs.set), and ui.state's perf.timings
reports gpuLayerFit {doc, needed, budget} next to gpuBudgetMb.
Next (owner-approved plan): after #5370, read the card's real limit from the driver
(wgpu memory budget thresholds) and shrink on out-of-memory; then redraw the visible
area on the GPU first when layers still don't fit.
* GPU memory: typed discrete check; the status and budget follow a GPU Memory change at once (measure_fit); perf rows P60/P61 for whole-document edits among 150 full-canvas layers under the app's budget
* GPU memory on top of #5234: memory_budget takes the card's memory (vram) and the GPU Memory value; the document's pixels are charged unless the card's own memory is known; a set GPU Memory is capped at the card's share; benches use video_memory
* perf(gpu): cap the GPU memory budgets by a discrete GPU's own memory
The compositor and canvas budgets came from system RAM only, so a 16 GB card in a 96 GB machine planned for up to 8 GB of compositor pages plus 24 GB of display textures. Detect a discrete adapter's dedicated memory (DXGI on Windows through winsafe, amdgpu sysfs on Linux) and cap each budget at three eighths of it. PHOTOCRAFT_VRAM_MB simulates a smaller card. Fixes#4536.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* perf(gpu): keep a 16-bit canvas on the GPU in 8-bit when floats don't fit
With the canvas budget capped by a small card's memory, a 16-bit document under the float-canvas pixel limit could need more than the budget as Rgba16Float and fall back to the CPU canvas although its Rgba8Unorm set fits. Choose the float canvas only when it fits the budget, which is now shared atomically so format_for can read it while the renderer is locked.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* perf(gpu): detect a card's memory once per adapter, and test the Linux reader
video_memory built a DXGI factory each time it was asked, three times at
startup; it now remembers the answer per (vendor, device), so a GPU retry
that picks another adapter still gets its own. The amdgpu sysfs reader
takes the drm folder as a parameter and is tested against a temporary
folder: the matching card's VRAM, a card without the file, garbage ids, a
malformed size, no match and no folder.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
- The CPU canvas texture and the crop's beyond-canvas bands were reduced to fit MAX_TEXTURE (4096)
whatever the device allows. On a device with a smaller limit (2048 on some older GPUs and WebGL
contexts) a 2600 px document was uploaded at full size: egui asserts and the backend can't
create it. They now fit the smaller of the two. Test: cpu_canvas_texture_fits_the_device_limit.
- background_redraw and navigator GPU tests built their harness with egui's default wgpu limits
instead of the app's wgpu_setup (the adapter's own limits), and the GPU canvas lost the device
on it ("Device is lost", every run on an RTX 5090). They now use wgpu_setup.
- Eleven tests built a wgpu device without photocraft_testkit::gpu_lock, which testkit requires so
devices aren't created concurrently; they now hold it.