* Add an opt-in WinTab backend for pen pressure on Windows
Windows Ink stays the default. Preferences > Tools > Pen Tablet has a Tablet
input dropdown (Windows Ink (default) / WinTab). WinTab opens a WinTab 1.4
system context on PhotoCraft's window and polls pressure and the eraser end
(tilt on devices that report orientation axes; rotation only with a pen and
driver that report twist) into the stylus feed. The row says to turn Windows
Ink off in the tablet's software utility; the choice applies at next launch.
- crates/tablet: Windows-only wintab/wintab-sys backend. Devices with
orientation axes use the orientation packet; pressure-only devices open a
packet without it and report neutral tilt. Leaving is the driver's hardware
proximity message (WT_PROXIMITY) and the packet's out-of-context status, with
a long fallback for drivers that never report either. Overlap priority
follows window focus, and the message hook requests a frame for each WinTab
message so pressure-only strokes update without cursor movement.
- apps/photocraft: install the backend when the preference is on; poll it at
the start of each frame; the installed wake repaints the UI.
- ui-egui: TabletPollFn service; the Windows-only Pen Tablet row with the
Tablet input dropdown and the note.
- engine: tools.tabletInput (windowsInk/winTab), default windowsInk.
- docs: hardware-parity WinTab gap entry, qualifying tilt to orientation axes
and rotation to twist-reporting pens.
Tested locally on a Wacom tablet with Windows Ink off: pressure, tilt and the
eraser end reach the brush (rotation where the pen reports twist); the default
path is unchanged.
* WinTab: a message that finds the state borrowed is skipped, not a panic in the hook
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* WinTab: rustfmt
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Scorecard: count the Tablet input setting
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Present the live scratch state as current configuration rather than the cause of a file's refusal. Explain that configured scratch capacity cannot override caller/build limits or replace non-spillable memory. Keep the typed error and admission policy unchanged, and add wording regressions.
Formatting and whitespace checks passed. No local build or test execution; validation awaits hosted CI.
Converting a document (layer copy to another mode/profile, Image > Mode,
Convert to Profile with or without dither, Duotone inks) converted run colours
but not the colours set by character/paragraph styles and Basic Paragraph, so
converted runs showed a false local override and stopped following later
style edits. Duplicate Layer > New Document dropped the source's Basic
Paragraph, giving inheriting type false overrides.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With 1 GiB or less of free RAM, or 512 MiB or less of commit headroom, the
hard limit was exactly 0, so even a small file was refused with a message that
didn't say memory was low.
The reserve now shrinks with the measurement: up to a quarter of free RAM opens
automatically, the rest goes to approval, and an approved import always leaves
at least 256 MiB free. Limits are unchanged for automatic opening above 4/3 GiB
free and for approval from 4 GiB free. A refusal set by measured memory now
gives the free amount and says to close other apps and try again, and the
approval prompt warns when little memory would be left.
An unreadable cgroup still measures Some(0) and stays refused.
Windows network folders reported unknown because sysinfo doesn't list them, and an unlisted Linux
NFS or CIFS mount used the numbers of its listed parent. GetDiskFreeSpaceEx (Windows) and statvfs
(Unix) on the directory fix both. macOS keeps sysinfo's figure, which counts purgeable space, when
the listed disk is this volume. A mount that reports no space is flagged in Preferences, in the
hard-limit refusal and in spill::stats, with a hint to set a Scratch Size Limit or use a local disk.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
The 32-bit message ended mid-phrase. It now says the cap applies or less if
less memory is free, and separates the cause from the next step. Windows
scratch tests check the returned numbers belong to the temp directory's volume.
Color Lookup uses the schema dialog, which removes an emptied text key, so
a stale data or file was read over the chosen lut. Key the replacement on
image.adjustments.* rather than the dedicated editors. setParams' replace
now also holds for Shape Blur, which merged before resolving its shape.
layer.smartFilter.setParams merges, so a key the dialog removed stayed
stored. A Channel Mixer filter with Monochrome turned off kept its gray
row, which is read over red: the Red sliders had no effect. The dialog
holds all of an adjustment's settings, so it now passes replace: true;
other callers keep merging.
PngImport decoded the whole image and then copied it into a second, tile surface (about
+400 MB for a 100 MP RGBA8 file, enough to kill a browser tab). A PNG whose rows come in
final order now streams: PngDecodeRows::step_stream hands each group of rows on, they
become tiles at once (the PngStream the one-shot streamed import also uses) and are
dropped, then the prune scan runs in batches. decode_rows is the same step_stream driven
to the end, so there is one path. Interlaced, animated and early-eXIf-oriented files are
decoded whole as before; a late eXIf sends a streamed import back to a whole decode.
The turn upright of an EXIF orientation 2-8 was one whole-buffer pass in the poll of the
last rows; OrientSteps (photocraft-codecs) does it in bands of 64 rows through the same
band writer as Image::oriented, and PngImport runs it as a stage of its own.
Memory-budget errors keep their type and text: the stream is checked as the one-shot
streamed import is, and, like the whole-image path, a layer under 32 MiB is not.
Docs for the stepped place updated.
A place at an offset off the tile grid rebuilt nearly every tile the image touches in
a single poll, which can overrun the ~8 ms slice on a big image. TranslateSteps
(photocraft-raster) builds the target tiles of Surface::translated a tile at a time
with the same code (a shared ShiftBuilder), so the result is bit-identical; the place
runs it as a Placing stage of its own in a cooperative session.
The flatten size refusal named "file.place", which is no command: it now names
file.placeEmbedded or file.placeLinked.
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
- #11458: the straight-crop rows also check that the active layer's pixels
inside the old canvas are the plain crop's, so a fill that recolours them
fails.
- #11295: the Esc case sends the key through shortcuts::handle instead of
calling cancel directly.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
In the app (background jobs on) edit.transform runs as a job and lands
after app.run returns, so renaming the top History step to "Transform
Path" right away renamed the step before it (the second shape's "New
Shape Layer") and left the scale as "Free Transform". Found in the batch
smoke run; the PR's tests run with background jobs off.
edit.transform now takes stepName "Transform Path" (any other name is
refused, as select.transformSelection's stepName from #11444), and the box
passes it instead of renaming afterwards. New tests: the box with
background jobs on (fails without the fix), and the parameter.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Channels: the panel menu has the channel commands, New Spot Channel among them (#11451)
The Channels panel's menu had no channel items, so New Spot Channel... and
the rest could only be reached from a row's right-click menu. It now lists
New Channel..., Duplicate Channel, Delete Channel, New Spot Channel...,
Merge Spot Channel, Channel Options..., Split Channels and Merge
Channels..., each acting on the targeted channel and greyed where it can't:
Duplicate takes a single colour, alpha or spot channel, the composite of a
one-channel image, the Quick Mask or a targeted layer mask. Channel
Options... in Quick Mask mode now opens the targeted alpha or spot
channel's options, not Quick Mask Options. The panel menus run their
commands through the menu bar's enabled rules, so the Layers panel menu
greys its dialog items the same way too. The labels are in every catalog.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Channels: New Channel... and Merge Channels... ask first, from the panel and row menus (#11451)
The panel menu's Merge Channels... ran channel.merge with no parameters, and
the engine merged the first grayscale documents into RGB and closed them with
no choice; New Channel... added a channel straight away. Both now open a
dialog when called without parameters, as New Spot Channel... and Channel
Options... already did: New Channel... reuses the Channel Options dialog
(name, colour indicates, colour and opacity), and a new Merge Channels
dialog picks the mode and the open grayscale document for each channel. The
channel rows' menu had the same two items and goes through the same path.
With parameters (scripts, the New button) both still run at once.
The Merge Spot Channel test now checks pixels, so a delete couldn't pass it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Channels: Merge Channels waits for three grayscale documents and the dialog offers only modes they fill (#11451)
With two grayscale documents open, Merge Channels... was enabled and its
dialog repeated a document for the third channel; OK then failed in the
engine and the dialog closed. The engine's rule now needs three documents
of one size (no mode merges fewer), so the menus grey the item below that.
The dialog offers only the modes the open documents can fill (RGB and Lab
from three, CMYK from four), greys OK with the reason while the picks
repeat, keeps itself open when OK is refused, and comes back with the
engine's message if the merge itself fails.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Channels: the Merge Channels dialog keeps its picks by document id (#11451)
The dialog kept the picked source documents by tab index, and background
work can still run while it is open, so a document moved or closed in the
meantime could leave another one at a picked index; OK would then merge,
and close, the wrong document. The picks are now document ids, resolved to
the current tabs when OK is pressed: a closed pick, or one that is no
longer a grayscale document of the same size, is refused with the reason
and the dialog stays open.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Since #10867 the non-deleting crop turns a 2000 px layer to 2001 px bounds, so
the budget refusal names 200100 x 200100 px.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The donor check indexed the label buffer directly; the bounds are inside
the canvas today, but a lookup that is out of range now counts as no donor
instead of panicking.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* GPU plan: cache adjustment LUT rows between plans (#11498)
adjustment_program rebuilt the Levels/Curves tone tables on every plan
(twice per refresh), about 1-2 ms per Levels layer each time. Keep the
rows of Levels, Curves, Color Balance and Hue/Saturation ranges in a
32-entry most-recently-used cache (photocraft_compose::lut_rows, at
most 32 x 3 x 16 KiB = 1.5 MiB), built outside the lock.
Entries are keyed by a u64 hash of exactly what each row builder reads,
fields destructured in full and floats by bits (-0.0 and 0.0 stay
apart). Hue/Saturation master sliders don't add entries; Hue/Sat ranges
and Color Balance don't depend on depth. Edit > Purge > All drops the
cache, and a filled cache enables it.
Release, per call: Levels 8/16-bit 1.10 -> 0.003-0.018 ms, Curves
0.16 -> 0.018, Color Balance 0.11 -> 0.001-0.015, Hue/Sat ranges
0.05 -> 0.008-0.018. The tables are identical, so rendering is unchanged.
Tests: cached rows match fresh tables bit for bit at 8/16/32-bit, hits
and LRU eviction on a local cache, key coverage, and Purge All
releasing the cache.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* compose: rustfmt the LUT row cache tests (#11498)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* GPU plan: test that plans read cached LUT rows and that curve points key them (#11498)
Plant rows no builder makes under a fresh key and check the planner returns
them, and check that moving a Curves point changes its key (review on #11539).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* compose: prevent in-flight LUT builds from undoing purge
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: FredApps <FredApps@users.noreply.github.com>