* Document tabs: keep whole names, hide what doesn't fit, list every document in the » menu (fixes#4487)
* Document tabs: check the active document in the » menu (#4487)
Like Photoshop's, the » menu marks the active document with a check mark,
drawn as the menu bar's menus draw theirs.
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* fix(format): saving onto a dangling symlink writes through to its target (#6605)
resolve_symlink fell back to the link path when canonicalize failed, so an
atomic save renamed over the symlink and replaced it with a regular file.
Dangling chains now resolve lexically (relative targets included, with a
depth cap), so the bytes land at the target and the link survives; a
symlink loop is a clear error instead of a silent link replacement.
* Follow dangling links without folding .. by text
A folder on the way may itself be a symlink, so leave .. for the OS to resolve.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
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>
Eighteen labels in ko.tsv used a different word than the rest of the
Korean catalog for the same term, or named a menu item by a label the
menu does not use (Reset All to Defaults reads 모두 기본값으로 초기화).
Only translation values change; placeholders and ellipses are untouched.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
- Both dialogs get the Preview checkbox (`PREVIEWED`).
- The canvas shows a filter preview through the preview's own colour management (its profile and
mode), not the committed document's: Assign Profile changes only the profile, so its preview
looked unchanged. The preview keeps the document's id, so Proof Colors still apply.
- Previews run with the app's Color Settings: `working` is the chosen working space, not the
built-in default.
- The `bpc` checkbox (Convert to Profile, Proof Setup) reads "Black Point Compensation" instead of
"Bpc"; every catalog already translates it.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Menu rows prefixed their label with "✔ ", three spaces or nothing, so
unchecked toggles (Lock Guides, Lock Slices) sat at a different x than
checked and plain rows, most visibly in CJK translations. Every row,
submenu headers included, now reserves one fixed-width checkmark column
(the width of "✔ " in the button font) and draws the mark in it.
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* OpenRaster: layer locks no longer change silently on save (#7018)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* ORA: the Background's position lock is not reported as lost (#7018)
Every new document's Background carries a position lock, so the warning made a
plain .ora unsavable in place.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The Elliptical Marquee used a dashed circle and Quick Selection a brush
inside a dashed circular arc, so the two read alike. Photoshop draws the
marquee as a horizontal ellipse and Quick Selection as a brush in an
upright dashed rounded C. New original ellipse-dashed.svg; quick-selection.svg
redrawn; tests check the ellipse is wider than tall and the C runs down
the left edge.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Found while testing crates/engine/src/commands.rs:117-120 (turn) with crates/engine/src/canvas_geom.rs:224 (Turn::affine); applied in crates/engine/src/lib.rs:807-814 (Session::edit)
Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
With Remove after each stroke off, every finished stroke was fed into the
one shared tint trail, whose point counter still held the previous
stroke's length, so later strokes were stamped only past that count.
Trail::feed_stroke starts each new stroke's count at zero.
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A tall menu reopens where it was last scrolled to, so after reaching Edit ›
Preferences › Settings… or Window › Theme its top rows lie hidden above the
view, over the menu bar. egui opens a submenu whenever the pointer is over its
row, clipped or not: the hidden submenu row under the title opened on the
press, and the release on the title counted as a click outside that submenu,
which closed the whole menu. Hovering the top scroll arrow scrolled the menu
back up, which is why that "fixed" it.
Submenu rows are now drawn like `ui.menu_button` but skip egui's submenu
logic while the pointer is over a part of the row that is scrolled out of
view (an already open submenu, e.g. from the keyboard, is drawn as before).
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
composite_rgba8 removed the white matte from 8-bit RGB composites with
one integer division per colour sample (3 per pixel, ~108 million on a
36 MP file). The result depends only on the sample and its alpha, so the
same expression, now unmatte_u8, fills a 256 x 256 table once per call
and each sample is a lookup. Built per call, not as a static, so no
binary (wasm included) grows by the table.
The new test runs all 65 536 (sample, alpha) pairs through
composite_rgba8 against the replaced loop, kept verbatim as the
reference.
photocraft_psd::ase::parse counted colors toward MAX_SWATCHES but not
group starts, so the number of groups it read grew with the file's
size. An empty group start is six bytes, and parse held about 64 bytes
for each: in a test build, parsing a 60 MB file of 10,000,000 empty
group starts raised the process's peak memory from 63 MB to 705 MB.
swatches.import reads files of up to 512 MiB, and read_file built a
group for each group start before the import refused more than 10,000
groups.
parse now counts group starts and returns LimitExceeded past
MAX_GROUPS (10,000), the most groups the Swatches panel holds. Files
with at most 10,000 groups parse as before, so every file the import
accepted still loads. Files with more were refused before as well; now
parse refuses them before it holds more than 10,000 groups. write also
refuses more than MAX_GROUPS groups, as it refuses more than
MAX_SWATCHES colors.
ase_group_cap_is_enforced_on_hostile_input parses 10,001 empty group
starts and expects LimitExceeded, then parses the same file with one
block fewer and expects all 10,000 groups. too_many_groups checks that
write refuses 10,001 groups and that 10,000 groups are written and
read back.
Frames past the cap collected nothing, so the layers under dividers that
are never closed were lost once those dividers nested past max_depth,
and nothing was left to hoist at the end. A first pass now matches
dividers to folders and only closed dividers open a frame, so every
frame's depth is the final depth of its nodes and the capped tree equals
layer_tree cut at max_depth, still at most max_depth + 1 levels deep.
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Edit > Presets > Preset Manager renamed and deleted brush presets directly, so a rename
left the current preset pointing at the old name (brush.presets.update then failed), accepted
a case-insensitive duplicate name, and a delete left the current preset dangling. It now uses
the same rules as brush.presets.rename / brush.presets.delete through two small shared
helpers (rename_at, forget_deleted_preset). Patterns and custom shapes are unchanged.
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Document tab says "Quick Mask" in Quick Mask mode, as in Photoshop
In Quick Mask mode, painting and Free Transform target the mask, not the
layer. The tab still read "(Layer 1, RGB/8)", so a user who pressed Q by
accident and then rotated saw the red overlay turn with the box and the
layer stay put, with nothing on screen saying why. The tab now reads
"(Quick Mask/8)", the same condition `paint_target` routes on.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Credit arnav-hiwarkar in the About window
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Keep contributors/people.toml as on main
The branch removed another contributor's entry; the added entry only repeated the username.
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>
Found while testing crates/engine/src/notes_cmds.rs:105
Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
The probe read the FileChooser `version` property and only counted a few D-Bus
error names as "absent". zbus wraps property errors in `Error::FDO`, and a portal
with no FileChooser backend doesn't export the interface at all, so the read fails
with `InvalidArgs` ("No such interface"). Neither matched, the probe said
"inconclusive", and Save As / Export did nothing without the "No file dialog can
open" notice.
Introspect the portal object and look for the interface instead, and treat
InvalidArgs, UnknownMethod and NameHasNoOwner (as `FDO` or `MethodError`) as absent.
Fixes#5739
Claude-Session: https://claude.ai/code/session_01AfRHB1wHUjrJx6agBYGnVu
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
With no text on the clipboard (an image copied from a browser, a file from Explorer) egui-winit
swallows the Ctrl+V press and sends only the V release; the paste then ran only when that release
still carried Ctrl. Letting go of Ctrl first, as many people do, pasted nothing, so Ctrl+V seemed
to need several tries. A V release whose press never arrived, after Ctrl was held and before any
other key, is now that swallowed Ctrl+V and pastes once. A plain V tap (the Move tool) and the
web build, which always gets the key-down, are unchanged.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Keep backend defaults within supported limits while preserving the
adapter's texture and buffer maxima. Add regressions for V3D shader
variables, weaker GL adapters, minimum alignments and capable devices.
* Krita: uncompressed tiles are read as interleaved pixels (#7082)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* kra: the agent-surface test writes its raw tile interleaved, like Krita (#7082)
The krita.rs fixture wrote its flag-0 tile as byte planes, which only the old
decoder read back correctly.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Snap To Layers built each layer's edges and centre from transform_bounds, which clips the
layer to the selection. With a selection, every layer's targets were the selection's own
bounds, so dragging the outline with a marquee snapped it back to where it started (shown
as layer alignments) and beat nearby guides and document edges. Layer targets now use the
layer's whole content bounds, as in Photoshop; the moving box (layer_rect) is unchanged.
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
They acted inside a circle fitted to the bounds' shorter side, so on a wide
or tall image only a central disc changed. Like Photoshop's, they now act
inside the ellipse inscribed in the bounds (selection or layer), reaching all
four edges; square bounds are unchanged.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The pixel-grid walker stepped between the pixels holding each segment's ends. Smoothing splits
a Shift-click line into pieces along it (so does the pointer on a Shift-drag), and snapping every
piece to its own pixels bent the line into a jagged staircase; rounding the half-pixel ties away
from zero also made the line depend on which end it started from. The walker now samples the
segment's own line at each column (or row) centre and resolves ties on the pixel grid, so the
pieces of a straight line paint exactly the pixels of the whole line, both ways.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* GPU: a group's effect cache follows its vector mask and children's Blend If (#5706)
group_key, which decides when a styled group's effect shape and distance
fields are rebuilt on the GPU, hashed the group's children and layer mask
but not the group's own vector mask, nor each child's Blend If and
Advanced Blending. A group's Stroke or Drop Shadow kept the old outline
after those changed. Hash them, as the CPU's layer_identity does.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* GPU: pass the pin callback in the group key test after main's signature change
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Session::offer_live_dab clears any earlier offer before converting, so a
refused offer can't leave an older one behind (test fails without it).
- retouch_ui::finish_stroke records whether the commit took the live pixels
(PhotocraftApp::retouch_from_live), and the ui-egui release test asserts
it, so a second render on release can't pass unnoticed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Covers #5044's two names: BMDoHyeon (family "BM DoHyeon", guessed "BM Do Hyeon") and Daum_SemiBold (guessed "Daum_Semi Bold"). Both resolve to their installed faces and aren't reported missing on main, through the exact PostScript lookup (#1844) that missing-font checks follow since #608.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
fontSize is in points and type.create renders it at resolution/72 px
per point, but the bar height, label height and baseline treated it as
pixels. At 300 ppi a 12 pt label (50 px) overlapped the bar. Lay out in
font_size * dpi / 72 px; type.create still gets points. 72 ppi is
unchanged.
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
paste_lorem decided whether to insert into existing text from the active
layer, while with_text edits the layer the "layer" param names. With a
pixel layer active, {"layer": T} made a new paragraph layer and left T
unchanged. An explicit "layer" now always goes through with_text, so a
type layer gets the text and any other layer is an error; without
"layer" the behaviour is unchanged.
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The "Open color-less EXR as ACEScg" option was only applied by file.openAs; background
opens (File > Open, drag-drop, Open Recent via Session::start_open) and file.openExrParts
kept io's linear-sRGB default. Both now retag through tag_exr_colorspace, start_open before
the Ask When Opening prompt so it sees the ACEScg profile.
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(atn): a Paint Bucket step keeps its explicit colour through .atn export and import (#7199)
* test(atn): an explicit Paint Bucket colour survives an .atn round trip (#7199)
* fix(engine): flip and rotate presets turn about the selection's bounds, as Free Transform does (#6928)
* test(engine): cover flips and turns about the selection (#6928)
Found while testing crates/engine/src/variables_cmds.rs:285-292 (import_data_sets_text; the file is read in import_data_sets at line 279)
Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
Found while testing crates/algo/src/camera_raw.rs:592 (value taken unchecked in crates/engine/src/lens_cmds.rs:137 raw_params)
Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>