Commit Graph
1237 Commits
Author SHA1 Message Date
Claude 3ba43b20c7 Effect Controls: file parameters get Choose…, Apply Color LUT asks for its LUT on apply, and text parameters get a field (#514, #473)
Cause: Apply Color LUT's only parameter, `lut`, was ParamUi::Hidden (set only
through commands), so the effect could not be used from the UI. Effect
Controls also had no widget for any ParamUi::Text parameter (only EXtractoR's
channels had one), so Cryptomatte's Selection, the OCIO effects' config file /
file / config names and Colorama's Output Cycle could not be set either.

Fix: a new ParamUi::File { filters } for paths. Effect Controls shows the
file's name (None when empty) and a Choose... button that opens the host's
file dialog with those extensions and sets the path (prop.set, so agents use
the same command). As in After Effects, applying an effect with an empty file
parameter from the UI (Effect menu, Last Effect, drops, Effects & Presets)
opens the file dialog straight away; cancelling keeps the effect without a
file. Apply Color LUT's LUT (.cube) and the OCIO Config File (.ocio) and File
(.cube/.3dl/.csp/.cc/.ccc/.cdl/.spi1d/.spi3d/.spimtx) use it. Text
parameters get a single-line field that sets the value when it loses the
focus (Esc keeps the old text). Saved projects are upgraded on open: a
Hidden or Text parameter whose spec is now a File parameter takes the File
UI and keeps its path (effects::migrate). docs/effects.md regenerated.

This covers #473's first part (the Selection field); picking mattes in the
viewer, the layer popup from the file's metadata, the GPU path and quoted
commas are separate.

Tests: effects hidden_and_text_paths_become_file_parameters (migration); ui
file_and_text_parameters_have_widgets (the LUT is asked for on apply and
inverts red to cyan, the Choose... button picks another LUT, typing into
Cryptomatte's Selection sets it).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QiAJeFttZp83Fh7YuzAR5f
(cherry picked from commit 403863f703999d19d3a83d9976f76bdc7577e682)
2026-10-10 19:48:25 +00:00
Claude 40e87709d5 Import: numbered stills import as stills unless the Sequence option is on; dropped folders make sequences (#431)
Cause: SequenceOptions::from_params defaulted `sequence` to true for
file.import, so any numbered still (IMG_0001.jpg) brought its whole numbered
run from its folder, and several picked or dropped stills with a common
prefix became one sequence. Dropped files never asked, and the Import
dialog's "<format> Sequence" checkbox started ticked.

Fix, matching After Effects: file.import takes picked and dropped files as
stills unless `sequence: true` (the Import dialog's Sequence checkbox, which
now starts unticked; Force Alphabetical Order implies it), while the numbered
stills inside a picked or dropped folder still import as sequences
(SequenceOptions::folders), and `sequence: false` (Alt-drag of a folder)
imports a folder's files one by one. Replace Footage, reload, proxies and
render outputs keep detecting their sequence (SequenceOptions::default and
from_params are unchanged), and mediaBrowser.import keeps bringing a numbered
still's run unless `sequence: false`. docs/footage.md and docs/agents.md
describe the new defaults.

Tests: sequence import_takes_stills_unless_asked_for_a_sequence and engine
numbered_stills_import_as_stills_by_default (one dropped still and two picked
IMG_ stills stay stills, a dropped folder makes one sequence, the checkbox
brings the run); the existing sequence tests pass `sequence: true`; ui
import_dialog_offers_the_image_sequence_option (unticked by default: a still;
ticked: the run) and a_dropped_folder_imports_as_one_sequence_with_sequence_interpretation
(two dropped frames: two stills; the folder: one sequence).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QiAJeFttZp83Fh7YuzAR5f
(cherry picked from commit aa3c31db9ab3a774ee2f8ba867e1bafc1fce7aba)
2026-10-10 19:48:25 +00:00
Claude 86d47dc3fe Snapping and Align: shape layers meet at their visual edges, with no gap (#352)
Cause: render::content_bounds (used by snapping's targets and source, Align,
Distribute, the viewer's bounding box and handles) took shape layers' bounds
from draw_bounds, the rasteriser's safety rectangle: 1 px of anti-aliasing
padding around fills and width x miter limit / 2 + 1 around strokes. Two
squares snapped edge to edge therefore stayed 2 px apart, or much more with
strokes.

Fix: shapes::content_bounds uses visual bounds, as After Effects' layer bounds
do: the path bounds plus half the stroke width (through the draw's group
scale), with no anti-aliasing or miter padding. Rendering keeps draw_bounds.

Tests: render shape_content_bounds_are_the_visual_edges (a 100 x 100
rectangle is exactly +-50, +-55 with a 10 px stroke); engine
shape_layers_align_by_their_visual_edges (widths 100 / 110, Align Left puts
both edges exactly at 0).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QiAJeFttZp83Fh7YuzAR5f
(cherry picked from commit bfd0abd7437ad1d38652e3aef51f9ba5dea9100b)
2026-10-10 19:48:25 +00:00
Claude 9b7f8cf005 Effects: Write-on draws a stroke along the animated brush, honouring Stroke Length and Brush Spacing (#536)
Cause: write_on_plan drew a single dab at the current Brush Position. Stroke
Length, Brush Spacing and the Paint/Brush Time Properties were declared but
unused, so an animated brush showed a moving dot that dropped every earlier
mark.

Fix: as After Effects describes Write-on, a dab is laid every Brush Spacing
seconds from the layer's start up to the current time, each lasting Stroke
Length seconds (0: for ever). Their positions come from the host's parameters
at their times (EffectHost::params_at, which the renderer implements). Paint
Time Properties > Opacity and Brush Time Properties > Size (or Size &
Hardness) keep each dab's own opacity / size; otherwise the current values
apply to all. One frame draws at most 4096 dabs (longer strokes space them
out and join them). Without a host the current dab draws, as before. Colour
and hardness stay one per stroke (the paint plan has one colour and hardness),
so Paint Time Properties > Color and per-dab hardness aren't modelled. The GPU
path uses the same plan.

Test: write_on_accumulates_a_stroke_along_the_animated_brush (the report's
fixture: start and midpoint marks stay at 0.5 s with the end still clear, all
marks at 1 s, Stroke Length 0.25 keeps only the recent trail, Brush Spacing
0.5 gives three separate marks, per-dab opacity).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QiAJeFttZp83Fh7YuzAR5f
(cherry picked from commit c82c9aeb685eab0cb36ca69c0b337781f4bad258)
2026-10-10 19:48:25 +00:00
Claude 77dfd44658 Puppet: a pin added to a bent mesh holds it where it is; nothing jumps (#318)
Cause: puppet::solve binds each moving pin to its nearest mesh vertex and fixes
that vertex at position + (vertex - rest), an offset in rest space with no
rotation or scale. puppet.addPin set a new pin's position to the click, so in
a bent region the bound vertex was pulled to where it would be unrotated, and
the mesh jumped as soon as the pin was added.

Fix: addPin picks the position that keeps the bound vertex at its current
deformed place: the vertex's deformed position minus the rest offset. On an
unbent mesh that is the click itself (round-off is snapped back to it), so
existing pins behave as before.

Test: a_pin_added_to_a_bent_mesh_moves_nothing (two pins, one moved to bend
the layer, then pins added at three places on the bent part: at most a few
edge pixels change from solver round-off; before the fix about 250 did).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QiAJeFttZp83Fh7YuzAR5f
(cherry picked from commit 16151203b304d565b425873fdd33ae082cae483b)
2026-10-10 19:48:25 +00:00
Claude 7e0e641606 Edit: Copy with a property selected copies that property, and Paste sets it on the selected layers (#493)
Cause: edit.copy handled text editing, selected keyframes, effects and shape
items, then fell back to copying the selected layers. Selecting a property
selects its keys, so an animated property copied, but an unanimated one (or
the Transform group, whose keys prop.select doesn't select) copied the whole
layer, and Paste added a duplicate layer instead of setting the property.

Fix: with properties (or the Transform group) selected, Edit > Copy copies, as
After Effects does, each animated property's keys (the selected ones, else all
of them, through keys.copy) and each unanimated property's value into a new
value clipboard keyed by match path. Edit > Paste sets those values on the
same properties of the selected layers (a key at the CTI where the target is
animated) and pastes the keys at the CTI, as one undo step (keys.paste now
honours `merge`). Keys alone still answer with their count, as before.

Test: copy_a_selected_property_pastes_onto_the_same_property (static
Position, then the Transform group with animated Opacity: values and keys
land on layer B, no layer is added, one undo step each).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QiAJeFttZp83Fh7YuzAR5f
(cherry picked from commit 8267519ebe20e889196e538de23b85b04a1fa76a)
2026-10-10 19:48:25 +00:00
Claude 09b3dbaad9 Merge branch 'fix/f3': community issue fixes
# Conflicts:
#	crates/ui-egui/src/widgets.rs
2026-10-10 19:48:24 +00:00
Claude 33dc3e72fa Merge branch 'fix/f2': community issue fixes 2026-10-10 19:48:18 +00:00
Claude d3e34156e5 Merge branch 'fix/f1': community issue fixes
# Conflicts:
#	crates/ui-egui/src/panels/text_panels.rs
2026-10-10 19:48:18 +00:00
Claude fbe0f7e1ce Cache bars: cached frames show at 1/3 resolution (#595)
The cache bars count the frames of `shown_series`, which rebuilds the
series from the frame key's scale in thousandths (333 -> 0.333). The key's
`opts` hashed the render options with the f64 scale, and the frames had been
rendered at 0.3333..., so the series never matched and the bars stayed empty
at Third (and any resolution that isn't a whole number of thousandths, such
as Custom 1/7).

`opts_hash` now hashes the scale rounded to the key's thousandths, which the
key carries anyway.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QiAJeFttZp83Fh7YuzAR5f
2026-10-10 19:32:18 +00:00
Claude e96638093c Workspaces: saved workspaces last between sessions (#320)
Window > Workspace > Save as New Workspace and Save Changes to this
Workspace kept the layout only in memory (`UiState::saved_workspaces` /
`saved_floating`): nothing wrote them, so they were gone after a restart.

They are now written to `workspaces.json` in the settings store (the config
folder on desktop, OPFS on the web) when a workspace is saved, renamed or
deleted, and read back when the app starts; a file that doesn't read is
ignored.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QiAJeFttZp83Fh7YuzAR5f
2026-10-10 19:32:18 +00:00
Claude 485e4bb59d Interface: Hangul is probed after the other scripts, so a catch-all face never draws Arabic (#616 follow-up)
Without a Korean font the Hangul lookup can land on Unifont, which also
covers Arabic but can't join it; registered first, it took Arabic too.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QiAJeFttZp83Fh7YuzAR5f
2026-10-10 19:17:26 +00:00
Claude 3bde85dfdb Timeline: Ctrl/Cmd+wheel and a trackpad pinch zoom time (#428)
egui turns Ctrl/Cmd+wheel and a pinch into a zoom factor (zoom_delta)
rather than a scroll, and the Timeline only zoomed on Alt+wheel, so
both did nothing over the layers. They now zoom time around the
pointer, like Alt+wheel. (B stays Set Work Area Start as in After
Effects; Split Layer is Ctrl/Cmd+Shift+D.)

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QiAJeFttZp83Fh7YuzAR5f
2026-10-10 19:16:48 +00:00
Claude 69bf806ff5 Graph Editor: buttons that don't fit the bar move to a » menu instead of being cut off (#457)
The Graph Editor's bottom bar lays out 17 text buttons in one row
(about 1100 px) on a painter clipped to the graph area, so on a
normal-width Timeline the right-hand ones (keyframe interpolation and
easing) were cut off and unreachable. Buttons that would pass the bar's
right edge now go, in order, to a » menu at its end; picking one there
runs the same action on the next frame. Hidden buttons register as
timeline.graph.more.<id> while the menu is open.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QiAJeFttZp83Fh7YuzAR5f
2026-10-10 19:16:48 +00:00
Claude e152dfd1d1 Essential Graphics: Scale keeps its Constrain Proportions link, on by default (#338)
A Scale property added to Essential Graphics fell into the generic Point
control: two independent fields with no link, so dragging one value
distorted the layer, unlike After Effects, whose Scale control keeps the
chain link. It now draws the link (on by default) with one field per
axis; linked, an edit scales the others in proportion (Alt edits one),
using the Timeline's constrained() (moved to widgets). The link state is
the property's own, shared with the Timeline's chain link.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QiAJeFttZp83Fh7YuzAR5f
2026-10-10 19:16:47 +00:00
Claude 76c9abad50 Viewer: the 3D Renderer menu shows the comp's renderer and switches it (#307)
The Composition panel's bottom bar drew a hard-coded "Classic 3D"
dropdown that ignored the comp's renderer and did nothing when clicked,
so an Advanced 3D comp still read Classic 3D. It now shows the comp's
renderer (translated) and, as in After Effects, its menu switches it
through comp.settings (one undo step); the automation label is the
renderer's name.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QiAJeFttZp83Fh7YuzAR5f
2026-10-10 19:16:36 +00:00
Claude 29d9ba5fbb i18n: the Home screen's buttons, pages, tiles, columns and empty states go through the interface catalog (#574)
panels/home.rs was in the converted list, but most of its text was drawn
straight from English literals (New Project, Open Project…, the page
tabs, the quick-start tiles, the filter placeholder, the column
headings, the empty states, the Opened/Kind cells and the version line),
so a Chinese interface still showed them in English. They now go through
tr()/tr_args(), with rows in both Chinese catalogs (Traditional's Join
the ArtCraft Discord row was also still English). Automation labels stay
English. The Opened column's plurals are separate rows so each language
words them itself.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QiAJeFttZp83Fh7YuzAR5f
2026-10-10 19:16:28 +00:00
Claude a207b19b12 #336: Ctrl+\ / Cmd+\ maximizes or restores the application window
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QiAJeFttZp83Fh7YuzAR5f
2026-10-10 19:16:07 +00:00
Claude dbbc0e2662 #577: font previews are scaled and moved to fit inside their row
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QiAJeFttZp83Fh7YuzAR5f
2026-10-10 19:16:03 +00:00
Claude bf5e6c6cec #581: the Discord pill fits its label and field text sits on the field's middle
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QiAJeFttZp83Fh7YuzAR5f
2026-10-10 19:16:03 +00:00
Claude e4916e5ca9 #513: clicking off a mask's path, even on its layer, deselects its points
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QiAJeFttZp83Fh7YuzAR5f
2026-10-10 19:16:03 +00:00
Claude ad7df67602 #360: the Project row menu's New Comp from Selection uses the whole selection
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QiAJeFttZp83Fh7YuzAR5f
2026-10-10 19:16:03 +00:00
Claude 7210b37bb2 Format effect.list's parameter metadata (#631 follow-up)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QiAJeFttZp83Fh7YuzAR5f
2026-10-10 18:51:45 +00:00
Claude 7a95a0d138 Project: a single unselected folder or data-file drag still reports why; index math saturates (#634 follow-up)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QiAJeFttZp83Fh7YuzAR5f
2026-10-10 18:51:45 +00:00
ClaudeandNigel Ihza Firdausy 72b2e9774e Dock: test the empty-dock reopen with the reporter's Minimal workspace (#629 follow-up, test from #630)
Co-Authored-By: Nigel Ihza Firdausy <[email protected]>

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QiAJeFttZp83Fh7YuzAR5f
2026-10-10 18:51:45 +00:00
Claude 954e89c661 Merge pull request #638 from FrederoxDev/feat/color-hex-input 2026-10-10 18:51:16 +00:00
Claude 1c68166b2e Merge pull request #634 from Ni-zav/fix/project-multiselect-drag-351-s20261010T1753Z-f4c2 2026-10-10 18:51:15 +00:00
Claude c742145b28 Merge pull request #631 from Ni-zav/fix/effect-parameter-catalog-s20261010T163100Z-6b8a 2026-10-10 18:51:15 +00:00
Claude 1c086fb134 Merge pull request #629 from MarekSpodBiedry/issue-529 2026-10-10 18:51:15 +00:00
Claude 5727754a54 Merge pull request #628 from MarekSpodBiedry/issue-505 2026-10-10 18:51:15 +00:00
Claude 8a65313226 Merge pull request #627 from MarekSpodBiedry/issue-446 2026-10-10 18:51:15 +00:00
Claude 3f840ab37f Merge pull request #626 from MarekSpodBiedry/issue-467 2026-10-10 18:51:14 +00:00
Claude da06c42de1 Media: OpenEXR files with HTJ2K compression say it isn't supported (#482)
The exr crate reads HTJ2K (OpenEXR 3.4's High-Throughput JPEG 2000) headers
but can't decompress the pixels, and the image paths turned that into a
misleading "no non-deep rgb channels" (multi-layer files) or imported the
file and showed a transparent frame (single-layer files).

`exr_channels::unsupported_compression` reads the headers; import (probe)
and frame decoding fail with "<file>: OpenEXR HTJ2K compression isn't
supported yet: re-save the file with ZIP, PIZ or DWAA compression".
Decoding HTJ2K itself needs a pure-Rust JPEG 2000 Part 15 decoder and is
left for later.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QiAJeFttZp83Fh7YuzAR5f
2026-10-10 18:51:12 +00:00
Claude 34f2955585 Media: an audio file that fails to decode no longer stays silent for good (#324)
With the Conformed Audio folder on (the default), the conform thread wrote
whatever `decode_audio` returned, and that returned silence when the source
couldn't be opened or a read failed. The conformed file is found by the
source's path, size and date, so one failed decode (an MP3 the decoder
stumbled on once) kept the footage silent in previews and exports in every
later session, while a renamed copy played.

The conform path now decodes with `try_decode_audio`, which fails instead of
returning silence; a failed conform removes its temporary file, writes
nothing and isn't retried this session (reads go to the decoder). Playback
reads still fall back to silence. CONFORM_VERSION goes to 3 so the silent
files earlier builds wrote are ignored and conformed again.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QiAJeFttZp83Fh7YuzAR5f
2026-10-10 18:51:07 +00:00
Claude 605b7c3a25 Scripting: scripts can't grant themselves file and network access (#590)
A running script reached `prefs.set` through `app.run` and could turn on
Settings > Scripting & Expressions > Allow Scripts to Write Files and Access
Network (or turn off Warn User When Executing Files), which the File, Socket
and system.callSystem gates then read: the permission the user withheld was
the script's to take.

Commands a script issues now go through `Session::execute_for_script`, which
marks the session as running for a script for the command and everything it
runs (engine.batch, ScriptUI callbacks, scheduled tasks). While it is,
`prefs.set` refuses any change that would leave the Application Scripting
settings different (compared on the resulting settings, so dotted keys,
whole-page objects and batches are all covered), and a script's
`prefs.reset` keeps them as the user set them. Command scripts read from a
file (File > Scripts > Run Script File... with .json/.jsonl) count as
scripts too. The UI, CLI, control channel and MCP still change them.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QiAJeFttZp83Fh7YuzAR5f
2026-10-10 18:50:57 +00:00
Claude 58e91fafbf Viewer: the locked viewer shares the main viewer's display key (#619 follow-up)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QiAJeFttZp83Fh7YuzAR5f
2026-10-10 18:50:32 +00:00
Claude ff56c5ad24 Audio: the short preview buffer is Linux-only and within the device's range (#614 follow-up)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QiAJeFttZp83Fh7YuzAR5f
2026-10-10 18:50:32 +00:00
Claude 4de11be339 Time: in Frames style a typed number is a frame count (#610 follow-up)
parse_timecode reads bare digits as timecode pairs, so 00100 went to
frame 30 (1 s) instead of frame 100.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QiAJeFttZp83Fh7YuzAR5f
2026-10-10 18:50:32 +00:00
Claude a3cc4d04b0 Merge pull request #614 from vbasky/fix/554-linux-audio-preview 2026-10-10 18:49:52 +00:00
Claude 312ece2b5c Merge pull request #610 from vbasky/fix/530-current-time 2026-10-10 18:49:52 +00:00
Claude a9c15f8bbf Merge pull request #623 from MarekSpodBiedry/issue-531 2026-10-10 18:49:51 +00:00
Claude 0f626e8d45 Merge pull request #622 from MarekSpodBiedry/issue-532 2026-10-10 18:49:51 +00:00
Claude 49e6e834ae Merge pull request #621 from MarekSpodBiedry/issue-533 2026-10-10 18:49:51 +00:00
Claude b723503785 Merge pull request #620 from MarekSpodBiedry/issue-534 2026-10-10 18:49:51 +00:00
Claude 6538fb175f Merge pull request #619 from MarekSpodBiedry/issue-546 2026-10-10 18:49:51 +00:00
Claude ea10c73294 Merge pull request #618 from MarekSpodBiedry/issue-547 2026-10-10 18:49:51 +00:00
Claude 485e691e52 Merge pull request #616 from MarekSpodBiedry/issue-599 2026-10-10 18:49:50 +00:00
BFlat 1b4d47f34f Merge pull request #639 from storytold/claude/community-pr-review-lwtrlr
Land community PRs, batch 2, with follow-ups
2026-10-11 01:44:49 +07:00
Claude 25adc36e21 Merge remote-tracking branch 'origin/main' into int2 2026-10-10 18:44:27 +00:00
FrederoxDevandClaude Opus 5.5 c8657bf214 UI: one After Effects-style colour picker with a hex field for every colour input
Every colour input opens one picker laid out like After Effects' Color
Picker: a square and slider for the channel chosen with H / S / B /
R / G / B, the new colour over the original, HSB and RGB fields, a hex
field (#RRGGBB, RRGGBB, #RGB) and OK / Cancel. Changes apply live;
Cancel, Escape and the original swatch restore the starting colour.

The 3D light dialog, Essential Graphics and Layer Styles passed sRGB
values to egui's color_edit_button_rgb(a), which reads them as linear,
so picked colours were stored darker (#808080 became #373737). They and
the other dialogs now use the shared sRGB colour button.

The picker and colour helpers live in color_picker.rs; widgets
re-exports them.

Tests: ui_color_hex.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-10 19:26:20 +01:00