Windows and Linux already marked Yes and No. Cancel had no letter, so the prompt now shows (C)ancel and C cancels. macOS still hides the letters and keeps the shortcut. (#4582)
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Context menus inherited a 6 pt item gap that belonged to neither button, so a drag across Merge Down or an adjustment list fell into dead space. Popup menus now stack their rows, and Pro rows use a square hover. (#4602)
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Effect eyes sat in the same column as layer eyes, so a long list read as extra layers. The eye, its hit target and the label now sit to the right of that column. (#4572)
Co-authored-by: Brandon Thomas <echelon@gmail.com>
The contrast glyph put a smaller semicircle inside the ring, which read as concentric rings on a layer thumbnail. It is now one circle with a filled half. (#4561)
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Tutar is a sum of money. Filter and adjustment Amount strings now use Miktar, the word for a quantity. (#4350)
Co-authored-by: Brandon Thomas <echelon@gmail.com>
The thin scrollbar faded its handle to the same opacity as the track, so scrolling a menu showed a bar with no position. The handle stays opaque and the resting bar is slightly wider. (#4611)
Co-authored-by: Brandon Thomas <echelon@gmail.com>
A negative Parent, font or normal-sheet index was cast to a huge usize, which skipped the name fallback and could hide the text. Those indexes now have to fit in a usize, and an infinite dpi uses 72. (#4429)
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Feather r50 on 24MP was at ~81ms kernel / ~130ms scenario against the
100ms P27 budget. The cosine-series line() allocated a fresh
n-usize run table per row (thousands of ~48KB Vecs on a 24MP mask).
Reuse one buffer per worker thread via a thread-local; no arithmetic
touched, oracle tests (Photoshop-mapped + direct-Gaussian <=1/255)
prove identical output.
bench_24mp (24MP ellipse, best of 5, x86-64 release):
feather r50: 80.8 -> 70.7ms (-12.5%)
feather r250: 144.1 -> 133.6ms (-7.3%)
feather r1000: 301.7 -> 287.7ms (-4.6%)
Full perf_scenarios P27 with patch: p50 119.3ms (was over by ~30ms,
now over by ~19ms); remaining gap is session/snapshot overhead,
follow-up.
Co-authored-by: Brandon Thomas <echelon@gmail.com>
As in Photoshop, a Channels panel row can be dragged onto the footer's
buttons: New Channel duplicates it (the layer mask's row as
"<layer> Mask copy"), Delete deletes an alpha or spot channel, and Load
Channel as Selection loads it. The layer mask's row on Delete asks
"Apply mask to layer before removing?", like its Layers thumbnail.
Colour channels can't be deleted, so Delete ignores them. The dragged
name follows the pointer and the button under it is highlighted.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Free Transform: H and V skew fields in the options bar
Photoshop's Free Transform options bar has H and V skew fields after the
angle; ours stopped at the angle (ui-parity: "Options bar H/V skew
fields | missing", gaps G5).
The bar now reads the box as Photoshop does: the frame scaled (W, H),
skewed (H, V) and rotated about the reference point (`Parts`). Typing a
skew leans the box about the reference point and keeps W, H and the
angle. A box can be read with any V skew, so the session keeps the typed
one (`TransformSession::skew_v`, serde default). Skews stop at ±89°.
A sheared box used to read as taller (H = 100/cos h %); it now reads as
an H skew with H at 100 %. Readouts without skew are unchanged, flips
included.
Docs: ui-parity row done, G5 and target-app-parity notes updated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* i18n(sr): translate the Free Transform skew field tooltips
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>
* fix(picker): the screen color picker no longer crashes on DX12 or traps the overlay on Windows (#4443)
Two Windows-only faults in the Pick screen color window:
- Created always-on-top, the monitor-sized picker window makes the DX12
backend fail swapchain creation (0x887A0001, DXGI_ERROR_INVALID_CALL);
wgpu then panics in Surface::get_current_texture_view. The window is now
created at the normal level and raised once its first frame is painted.
- As a deferred viewport the picker was painted on its own, and the main
window stopped being redrawn while it was up on Windows (OpenGL). A click
or Esc only stored an answer that the main window's tick() never took, so
the overlay could not be left. It is now an immediate viewport, painted
inside the main window's frame.
Verified on Windows 11 / Intel UHD 630: DX12 (default) opens, picks and
cancels; OpenGL picks and cancels; macOS still picks and cancels. Adds a
test that the picker viewport is not built always-on-top.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Screen picker: raise the window level once per picker window, not every frame
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Status bar: wrap fullscreen zoom scrubbing at horizontal edges (#3344)
* Status bar: preserve keyboard editing and handle fullscreen edge crossings
* widgets: initialise the new FieldOpts field after merging main
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>
Opening a file whose type uses a font that isn't installed said nothing
in the desktop app, so the text silently rendered in a fallback face.
The CLI and automation already reported it; Photoshop warns on open.
The open notice now lists each missing family, with the engine's
existing warning text (type_extra_cmds::missing_font_warnings), for both
the synchronous open and the background open the desktop app uses.
Tests: opening_type_in_a_missing_font_warns,
background_open_warns_about_missing_fonts.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Make Edit Transform Rotate and Skew use their own handle drags.
Rotate and Scale both started Free Transform, so a corner drag scaled the box. Skew only remapped edge handles, so corners still scaled. Give Rotate its own mode that turns the box from handles, and skew a corner along its adjacent edge (#3941).
* Rotate mode: fix the Skew doc comment and rustfmt
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>
The Serbian catalog (#4436) and the Soft themes (#4378) landed together, so
sr.tsv lacked their four strings and the ui-egui i18n completeness tests failed.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* perf: reuse sampled composite across channel visibility toggles
* test: borrow full composite for sampled-grid oracle
* test: compare the entire channel view after invalidation
* style: keep channel cache construction readable
* fix: format channel test and share band renderer
* fix: invalidate sampled channel composite after edits and previews
* refactor: keep channel rendering optimization isolated from compositor
* test: cover both partial-damage and full channel redraw paths
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
The preview treated every drag, including Alt plus the right button, as a stroke. That gesture now sets the diameter and density, and a plain left drag still paints. Fixes#4702.
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Sample every visible layer in Content-Aware Fill by default.
An empty layer above the photo used to report a successful fill and stay empty, because only the active layer was sampled. The result is still written to the active layer. sampleAllLayers can be set to false to keep the old behaviour.
Fixes#4026
* Sample other layers in Content-Aware Fill only when asked or the layer is empty.
Reuse the Remove tool's composite, which writes the active layer's format, and leave Photoshop's Sample All Layers checkbox off unless the window has no pixels on this layer. An explicit false still reads only this layer.
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
A CMYK document with several layers, masks or effects is flattened through the
RGB compositor. Its TIFF and JPEG exports were written as sRGB RGB, losing the
document's mode and ICC profile. The composite is now converted back through the
document's CMYK profile (relative colorimetric with black point compensation)
inside each parallel worker, written as four ink channels plus straight alpha at
the document depth, and tagged with that profile (the built-in coated CMYK
profile when the document is untagged). Invalid, non-CMYK or unusable profiles
are rejected. RGB-only formats keep the direct sRGB composite, and a single
plain raster layer keeps its native samples.
This fixes the output mode and tagging only: the RGB roundtrip does not
preserve original ink separations, black generation or CMYK-native blending.
Claude-Session: https://claude.ai/code/session_01BSB7fMQUccqFQekksDGWpo
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
inpaint::fit fits each channel as gain · source + bias, the gain the
ratio of the patches' deviations within the level's range and the bias
clamped after it. On nearly flat patches that ratio is noise, so the
gain often sat at its limit; scaling about zero, it moved a bright mean
further than Default's (or High's) bias could bring back, each channel
by a different amount: a tinted blotch in a sky gradient (up to 14
levels at Default, 27 at High).
For nearly flat patches (deviations under about 5 levels) whose means
are already within the bias of each other, the gain is now kept where
the bias can still match the means. Textured patches, and flat ones of
clearly different colours (Content-Aware Move's Color, measured against
Photoshop), are fitted as before.
Fixes#4706
Co-authored-by: Brandon Thomas <echelon@gmail.com>
#4217 used rayon's par_iter in ui-egui unconditionally, but rayon is a
native-only dependency there, so cargo xtask wasm failed on main.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* panic_hunt: don't fuzz the print commands into a real printer
file.print and file.printOneCopy spool to CUPS, so every run of the adversarial-params test
printed a blank page on the default printer. The hunt now skips those two ids (their params
are validated like everyone else's; spooling is not what the test is for).
* Guard the print spooler itself and the CI environment
- print_cmds: disable_printing() (a process-wide flag the panic hunt sets) and the
PHOTOCRAFT_NO_PRINT environment variable make spool_pdf refuse before running lp; dryRun
still renders and reports the command. Test.
- CI: PHOTOCRAFT_NO_PRINT=1 and LPDEST=photocraft-no-such-printer, so a regression fails
loudly instead of printing. Documented in docs/development.md › Environment variables.
* Type: type on a path, glyphs follow the curve (#4365)
Add TextShape::Path (path, start offset, flip). The text is laid out as one
point-text line and each glyph is placed rigidly on the flattened path by arc
length, turned to the tangent at its centre; glyphs past the end are hidden and
a degenerate path falls back to straight text. New commands type.attachToPath
(work path or shape layer subpath) and type.detachFromPath. TextShape is no
longer Copy. .pcraft round-trips the new variant.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Automation policy: type.attachToPath/detachFromPath take document vector paths, not host files (#4365)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* docs: regenerate scorecard after merging main
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>
* feat: linux caption buttons follow the desktop: layout, style and icon theme
The title bar's minimize/maximize/close buttons now match the desktop:
- Layout and side: GNOME button-layout via the settings portal (live),
KDE kdeglobals ButtonsOnLeft/Right, else gtk-3.0 settings.ini.
- Style family (GNOME round, Breeze chevrons, Windows) follows the
desktop, then the GTK theme.
- Glyphs come from the icon theme's window-*-symbolic.svg (freedesktop
lookup with Inherits and hicolor; unlisted theme directories such as
symbolic/ui are searched lazily), rasterized with resvg at the real
pixel size, recoloured and cached; missing icons fall back to the
built-in vector glyphs. Override: PHOTOCRAFT_CAPTION_ICONS=<theme>|none.
- Theme and portal work runs on a worker thread (10-22 ms per theme).
* feat: linux caption buttons follow the desktop: layout, style and icon theme
The title bar's minimize/maximize/close buttons now match the desktop:
- Layout and side: GNOME button-layout via the settings portal (live),
KDE kdeglobals ButtonsOnLeft/Right, else gtk-3.0 settings.ini.
- Style family (GNOME round, Breeze chevrons, Windows) follows the
desktop, then the GTK theme.
- Glyphs come from the icon theme's window-*-symbolic.svg (freedesktop
lookup with Inherits and hicolor; unlisted theme directories such as
symbolic/ui are searched lazily), rasterized with resvg at the real
pixel size, recoloured and cached; missing icons fall back to the
built-in vector glyphs. Override: PHOTOCRAFT_CAPTION_ICONS=<theme>|none.
- Theme and portal work runs on a worker thread (10-22 ms per theme).
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* i18n(sr): add a Serbian (Latin, Ekavian) UI catalog
Adds sr.tsv with 4,022 rows: every key the i18n-coverage tool measures
plus the union of the dynamic labels the other catalogs translate. The
catalog is written in Latin script, Ekavian; every Serbian locale
(sr-RS, sr-Latn-*, sr_RS@latin and Cyrillic sr-Cyrl-*) resolves to it.
Plurals use the one/few/many rule (plural_russian) with three forms per
@plural entry.
Original translations from the plain meaning of the English labels; no
proprietary localisation resources were used (ATTRIBUTION.md and
LICENSE-translations.txt updated). Tests cover locale resolution,
contextual and id overrides, placeholders, and plurals across teens and
compound counts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Info.plist: declare the Serbian localization
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Dejan Port <snork1982@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Open .kra and .krz with paint layers (8/16-bit RGBA and gray, any offset), nested
and pass-through groups, names, visibility, opacity, locks, blend modes and
transparency masks, plus the image's ICC profile and resolution. Vector, filter,
fill, file and clone layers and the other mask kinds are left out with a warning;
float, CMYK and Lab documents open as Krita's merged image (preview). Import only:
Save suggests .ora. Checked against 35 Krita 5.2.9-authored files.
Co-authored-by: Scott Campbell <251750033+scottcampbelldata@users.noreply.github.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
RenderVectorMasks keyed compiled masks by the address of the VectorMask it
was given. hiding_mask/effects_source pass temporary clones, whose addresses
get reused, so a later layer received an earlier layer's compiled mask.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The built-in "Pure" group's hues are vivid but not pure (its green is
#28C850), so picking it for text looked dull next to Photoshop's #00FF00.
Add a Photoshop-style "RGB" group (#FF0000, #FFFF00, #00FF00, #00FFFF,
#0000FF, #FF00FF) right after Grays. Pure is kept unchanged so existing
documents, actions and saved swatches that use it keep their colours.
* Content-Aware Move: live preview of the result while the pointer rests (#4385)
A cancellable, debounced background job shows the move/extend result during the drag and in the Transform On Drop box.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* docs: regenerate scorecard after merging main
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Some DNG writers state a white level the sensor never reaches (an iPhone
15 Pro Max Bayer DNG states 4095 and clips at 3567). Development clips
each channel at 1 after white balance, so red and blue reached 1 while
green stayed at 0.85 and blown highlights turned magenta. On decode, a
per-colour pile-up clearly below the white level is now raised to it, as
in storytold/lightcraft#586.
* Type: fall back to installed Indic fonts for document text (#4383)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* docs: regenerate scorecard after merging main
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Fix macOS system dialog localization
* Info.plist: declare the Hungarian and Turkish localizations too
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>
The dialog had no colour control at all: the engine defaulted the From
colour to the foreground and the spec's `color` param rendered nothing.
It now shows a Color row (swatch opens the Color Picker, seeded from the
foreground) plus a pipette that arms a canvas eyedropper: the next click
on the image samples the merged composite into the field, like the
Curves picker and Photoshop's Replace Color.
Co-authored-by: highperfocused <highperfocused@pm.me>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>