Previous / Next Frame also answer to Cmd+Left / Cmd+Right Arrow, and Back / Forward
10 Frames to Cmd+Shift+Left / Right, next to the Page keys (After Effects' second
default keys). A command spec can now list several default shortcuts
(`alt_shortcuts`, `cmd!(…, ["PageDown", "Cmd+ArrowRight"], …)`); menus and tooltips
keep showing the first.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Port of storytold/photocraft#2445. Each theme in the appearance picker gets a
star at the right edge of its row. Starred themes are saved with the
preferences and listed in a right-click menu on the header's appearance button,
next to Appearance Settings, so switching between favorites takes one click.
- The Layer panel keeps its own magnification and pan (`ui.layer_zoom`,
`ui.layer_pan`) with the Composition panel's magnification menu in its
footer; the wheel / pinch zooms around the pointer, the Hand tool,
Space-drag and a middle-drag pan, and the Zoom tool clicks zoom in
(Alt: out). Navigation never paints or makes Roto Brush strokes.
- View > Zoom In / Zoom Out / Fit / Actual Size act on the Layer panel
while it is focused, else on the Composition viewer as before.
- A Roto Brush click or drag in the Composition panel says the brush
works in the Layer panel; double-clicking a layer there with a Roto
Brush tool opens it in the Layer panel (as After Effects).
- Opening another layer resets the Layer panel to Fit.
Tests: crates/ui-egui/tests/ui_layer_panel_zoom.rs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The picker marked every pass it drew, including the one it closed in, so
for one pass after closing the dialog Escape handler still stepped aside
and a quick second Escape did nothing. The mark is now set only while the
picker stays open and cleared in the pass it closes (OK, Cancel, Escape or
a click outside); the picker also consumes the Escape it handles.
Test: the_escape_after_the_picker_closes_cancels_the_editor presses Escape
to close the picker and again in the very next pass, with no settling
frames: the editor cancels. It fails on the previous marking.
Problem: Object Matte computed its frames only on demand while rendering, so
playing a shot stalled on every untracked frame, there was no progress or way
to stop, and nothing kept the mattes once computed.
Fix: Object Matte runs through Roto Brush's job machinery (one job slot,
cancellable with roto.cancel, a worker thread on the desktop, a Web Worker in
the browser, never the UI thread). objectMatte.propagate computes the frames
around the reference frame (clipped to the layer) outward, filling the same
content-keyed segmentation cache the renderer reads; the engine's sync now
queues an instance for background tracking when its frames or prompt change.
objectMatte.freeze stores the final mattes in a new hidden Frozen parameter
(Roto Brush's FrozenCache format) under a key of the input, prompt, span,
model and matte settings; renders use them without segmenting while the key
matches; a frozen prompt is refused until objectMatte.unfreeze. Freeze is one
undo step and is dropped if the prompt changed meanwhile.
objectMatte.status reports progress, frozen state and computed frames. Effect
Controls shows Freeze / Unfreeze and Track / Stop with a status line (progress
banner). The frontend now polls Roto Brush / Object Matte jobs on their own:
before, a finished job (and so a Freeze result) was only picked up while a
Warp Stabilizer job was active. Job progress is set before the worker starts.
Tests: engine tests_roto::object_matte_background_job_cancel_and_freeze
(queued by the edit, returns at once with progress, cancel keeps a partial
result, completion computes all 25 frames, frozen renders recompute nothing,
unfreeze recomputes, undo restores); effects object_matte job_frames and
frozen-matte tests (stored pattern used, key mismatch and damaged data
ignored); ui-egui ui_object_matte Effect Controls Freeze end to end.
docs/agents.md: Object Matte section; docs/effects.md regenerated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: Object Matte could only be applied through the objectMatte.apply
command; After Effects 26.2 picks the object directly in the viewer (click it,
or drag a box around it).
Fix: an Object Matte tool (with Roto Brush & Refine Edge in the Alt+W slot,
icon drawn in code). In the Composition viewer a click sets a point prompt and
a drag a box prompt (its centre the point) on the footage, solid or comp layer
under the pointer, converted to layer pixels through the layer's transform.
The layer's Object Matte (the selected one, else its last) is re-prompted;
Shift adds another. Everything goes through objectMatte.apply, which gained
`effect` to re-prompt an instance (point, box or no box, reference frame; the
reach kept unless given), so each prompt is one undo step; the effect is then
selected, so its point controls show in the viewer. Hover draws a crosshair
and the outline of the layer that would get the matte (a live matte preview
would need a segmentation per pointer move). Automation ids:
viewer.objectMatte.crosshair / .target / .box, tools.ObjectMatte.
Tests: ui-egui ui_object_matte (hover, click, drag box re-prompts the same
effect, Shift-click adds one, undo steps); engine
tests_roto::object_matte_reprompt_updates_the_instance. Checked with the
headless snapshot example (hover crosshair, box drag isolating a disk).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: while a preview played, the RAM preview cache evicted the least
recently used frame. The frames just ahead of the playhead had been rendered
(prefetched) before the frames just shown, so once the cache was full they
were evicted first; prefetch asked for them again, but its queue ran in
request order, so they waited behind every frame further ahead. Playback
ran into a gap right ahead of the playhead while frames far ahead stayed
green, re-rendered the same frames over and over (about four renders per
frame shown when the preview was longer than the cache) and stalled for
seconds. The prefetch window could also reach further ahead than the cache
holds.
Fix: the playing preview tells Frames where it is (Frames::set_playhead).
Evictions then take frames the preview does not reach first (other series,
behind it without looping), least recently used first, then the frame it
reaches last; prefetch renders the frame it reaches first; queued prefetch
frames it no longer reaches are dropped (and leave the in-flight set). The
prefetch window stops at what the cache holds. The viewer puts a CPU frame
that needs no averaging on screen without copying its pixels.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: floating panels lived inside the main window only, so on a
two-screen setup no panel could go onto the second monitor (#486).
Fix: panel menu > Move Panel to New Window (and the agent/control command
`window.panelWindow {panel, window?}`) undocks the panel if needed and shows
its floating group in a native egui viewport the user can drag to any screen;
Move Panel to Main Window or closing the window brings it back. The window's
place and size are kept in the workspace (sanitised against hostile values)
and restored on the next start. Browser and test builds, which embed
viewports, show it as an in-app window.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: scrubbing a long movie showed the frame under the pointer
seconds after the pointer stopped. Every viewer frame a free frame-pool
thread had started kept running: each seeked the movie's one decoder and
decoded up to a GOP, and the newest frame waited behind all of them on
the decoder's lock (45 min 1080p H.264/HEVC, GOP 250: 1.8-3.6 s for the
last frame of a drag; a single jump is 0.1-0.4 s). FilmCraft's decoder
already polls a cancellation flag, but nothing set one.
Fix: FootageSource::cancellable / called_off and CancellableFootage (a
per-render view whose frame reads run under the render's own flag, so
reads on other threads answer to the right render). MediaPool runs them
under filmcraft_media::cancel::with_cancel. The frame workers render
through it, and a newer viewer frame calls off the comp's running viewer
frames of other times. A called-off render is not cached (frames, disk,
layer cache). Drag latency is now 0.1-0.3 s.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A mouse click gives a Project row no keyboard focus, and the arrow keys
only move keyboard focus when a row already had it, so Delete /
Backspace, Cmd+D and Enter keep working for mouse users, also after an
arrow key moved the selection. Pin that down with a headless test.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: app.scheduleTask(code, delay, true) queued the task and then
skipped it (eval_with_tasks: `if repeat { continue; }`), so repeating
tasks - what ScriptUI panels use for polling and refreshing - never ran.
One-shot tasks scheduled by handlers or in the Script Console never ran
either.
Fix: tasks live per script context (crates/script/src/tasks.rs) with a
due time. Frontends run the due ones every frame through the new
ScriptUi::run_tasks hook (the egui app does; agents and headless hosts
use the `script.runTasks` command). Script-host threads get a Tick
message, inline hosts (Script Console, web) run in their kept context.
A repeating task runs every `delay` ms until app.cancelTask, until it
throws (reported once, then cancelled), or until its owner goes: the
script's last window closing, or - for scripts without windows and the
console - the project being closed or replaced (ScriptUi::project_epoch).
Bounds: at least 50 ms between runs, at most 64 pending tasks per
script, one run per pass (no catch-up bursts), task code nested too
deeply is refused, the engine's loop and recursion limits apply. Ticks
only reach the hosts of the session that scheduled them.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: with the Project panel focused the arrow keys did nothing (After
Effects selects the previous / next item, extends with Shift and opens /
closes folders with Right / Left). The custom-drawn rows, sort headers,
twirls, label swatches, proxy switches and footer buttons reached
AccessKit with no name or role, the empty list area and each row's name
cell were unnamed extra Tab stops, a Tab-focused row showed no focus
ring, and the arrows then moved egui's spatial focus instead.
Fix: Up / Down select the previous / next row (Shift extends from the
anchor), Right opens and Left closes the selected folder or selects the
parent; the moved-to row scrolls into view. A row with keyboard focus
keeps the arrows, focus follows the selection and the panel takes focus.
Rows are AccessKit tree items named "<name>, <type>" with selected,
level and expanded state; headers and row / footer buttons are named
buttons; one Tab stop per row and a focus ring.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The panel catalog (crates/ui-egui/src/i18n/ui.rs) has grown to 496 rows since
Ukrainian last matched it on 9 October, so `uk` fell back to English for the Home
screen, the viewer's toolbar tooltips and popups, the Appearance settings, the
solid dialog and the project, import, export and colour-management forms.
Add those rows, in the Chinese catalogs' order and under their section comments,
so the catalogs stay parallel. `uk` now carries 495 of the 496 rows: `RGB` reads
the same in Ukrainian, so it keeps no row and falls back to the English source,
which is what the catalog's own rule asks for.
Terminology follows the Ukrainian menu catalog in i18n.rs so the two agree:
Full/Half/Third/Quarter as Повна/Половина/Третина/Чверть, Draft as Чорнова,
Wireframe as Каркас, Custom... as Власна..., Region of Interest as Область
інтересу, footage as матеріал, solid as суцільний колір, Default as Стандартний.
Dark and Light take the neuter form the catalog's existing Darker/Lighter rows
(Темніше/Світліше) already use, so one row reads correctly both as an Appearance
Mode and as a theme card option.
`format_ago` picks between one and many, while Ukrainian declines the noun for 1,
for 2–4 and for 5 and up, so the relative times use the invariant unit
abbreviations (хв, год, дн.), which read correctly for every count. A per-language
plural rule would be the fuller fix and is left for its own change.
ROADMAP.md and docs/localization-parity.md carry the new count.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Problem: the Render Queue had no single-file uncompressed output, so
handing a render to an external encoder meant a lossy codec, ProRes,
or an image sequence plus a separate WAV.
Fix: add the "QuickTime (Uncompressed)" Output Module format. Frames
are stored as QuickTime 'raw ' samples: 8-bit RGB (depth 24, rows
padded to even bytes) or ARGB with RGB + Alpha (depth 32), written
through FilmCraft's existing QuickTime muxer with PCM audio
(16/24-bit or float) like ProRes. Format name `uncompressed` on the
control channel; disk-space estimate and Sample Format menu included.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: ScriptUI controls never sent mouse events to scripts.
addEventListener("mousedown" | "mouseup" | "mousemove" | "mouseover" |
"mouseout" | "click") listeners never ran, so custom-drawn controls
(onDraw + mouse, as DuScriptUI builds them) could not react to the pointer.
docs/plugins.md listed this as missing.
Fix: scriptui.js builds a ScriptUI MouseEvent (clientX/clientY relative to
the target, button, detail = click count, screenX/Y, modifier keys,
getModifierState, stopPropagation, preventDefault) and delivers it in the
capture, target and bubble phases. addEventListener takes the capture flag.
The parent walk is bounded, so a script-made parent cycle cannot hang it.
Each published control lists the mouse events a listener wants
(Widget::mouse, inherited from containers). The new scriptui.mouse
command dispatches one event with clamped, finite values and rejects
unknown types and disabled controls. The egui ScriptUI view hit-tests the
pointer over script windows and docked panels, skipping covered layers and
windows blocked by a modal dialog. It sends mouseover/mouseout,
mousemove, mousedown/mouseup with click counts, and a click carrying the
pointer details in place of the plain click, so a checkbox still toggles
once. Handlers run under the existing script limits; a throwing listener
reports an error and the window keeps working.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: After Effects 26.2 spreads a Ctrl+Alt (Cmd+Opt) hot-text scrub
over the selected layers by selection order (first unchanged, last the
whole change, the ones between even shares). Our multi-layer scrub
(#491) always moved every selected layer by the same change.
Fix: prop.set takes `proportional` (with `selected` + `offset`); each
participating layer, the scrubbed one included, gets index/(n-1) of the
change in selection order, in one undo step; the first layer is left
alone (no key added). The Timeline value editor sends it while Ctrl+Alt
(Cmd+Opt) is held; that chord no longer fine-scrubs or unlinks Scale.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: the Media Browser lists .mxf files, but File > Import's dialog
filtered them out and media::is_importable refused them, although the
pinned FilmCraft codecs behind crates/media already demux MXF (AVC,
MPEG-2 / XDCAM / D-10, VC-3 / DNxHD, ProRes, PCM audio) by content.
Fix: add "mxf" to VIDEO_EXTENSIONS and to the File > Import and Set
Proxy dialog filters. Tests check MPEG-2 4:2:2 long-GOP + 24-bit PCM and
DNxHD 36 MXF against ffmpeg frames (>35 dB, measured 64-67 dB), audio
level, and that the Import dialog offers MXF. AVI stays open: FilmCraft
has no AVI demuxer.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: the Media Browser listed .dpx files and After Effects imports
DPX and Cineon scans (stills and numbered sequences), but import
refused them: nothing could decode either format, and .cin was not even
an image to the browser, so its runs never grouped into sequences.
Fix: a small pure-Rust reader written from the public SMPTE 268M and
Kodak Cineon specifications (crates/media/src/dpx.rs): luma, RGB, RGBA
and ABGR at 8/10/12/16 bits, 10-bit filled method A/B, 12-bit in 16-bit
words, both byte orders, lines padded to 32-bit words, unrotated
orientations and the pixel aspect ratio; the codec names the transfer
(log, linear, ...) and code values are shown as stored. RLE, signed,
float, Y'CbCr, packed 10/12-bit and transposed files are refused with a
clear error; every header field is bounds-checked and the data length
verified before allocating. Probe and frame decode use it first;
is_importable, the Media Browser kinds and the import dialog list
.dpx/.cin.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: EffectCraft shipped only 8 original text-animator presets against
After Effects' 621; there was no Transitions category (ranked gap #7).
Fix: an original preset library, crates/engine/presets/transitions.json
(23 presets on our own Transition effects and layer opacity: fades,
directional and diagonal wipes, clock and fan reveals, blinds and slats,
pixel/mosaic dissolves, card flips, CC grid, radial scale, line sweep and
jaws wipes; In and Out). New commands anim.applyBuiltinPreset (adds the
effects with values and keyframes from the current time, one undo step,
reports any path that doesn't fit) and anim.builtinPresets. Effects &
Presets lists them under * Animation Presets > Transitions; double-click
applies. Sidecar and ATTRIBUTION.md entry; gaps/effects-parity updated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: After Effects 26.3's font menu can show only variable fonts.
EffectCraft's font menus (Character panel, Properties ▸ Text) had no such
filter, and text.fonts / MCP list_fonts didn't say which families are
variable.
Fix: the font scan marks a face variable when its table directory has an
fvar table declaring axes (header read only, no extra file loading). The
font menu gets a "Variable fonts only" checkbox above the results; it
combines with the typed search, keeps the keyboard in the search field and
stays set while the app runs. text.fonts reports `variable` per family and
takes `{variable: true}`; list_fonts passes it through. Catalog rows for
ja, zh-Hans, zh-Hant and uk.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: In Advanced 3D, an extruded shape or text layer was one mesh in
its paint colour. After Effects colours the front, bevel, side and back
of an extrusion separately (shape Contents ▸ Add ▸ Front/Bevel/Side/Back,
text Animate ▸ Front/Bevel/Side/Back), and EffectCraft had none of it.
Fix: The extruder can split its mesh by surface (front cap, bevel rings,
walls, back cap). Shape layers get Front / Bevel / Side / Back contents
items (layer.addShapeItem kinds frontColor…backColor, one item per
group, colouring every paint of the group and its nested groups; the
innermost wins). Text animators get Front / Bevel / Side / Back Color,
blended over the character's fill or stroke by the selection. Paints
with a surface colour render as one mesh per surface. Menus, the
Contents Add: pop-up and the menu translations list them. The other
surface material properties (Ambient, Diffuse, Specular, Metal,
Reflection, ...) remain.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: the Media Browser lists .tga and .hdr files (and groups them into
image sequences), but media::is_importable had neither extension and the
still decoder never tried them, so importing them failed.
Fix: enable the image crate's built-in tga and hdr decoders (no new
dependency), add both to STILL_EXTENSIONS and the File > Import dialog, and
pick a still's format by signature with an extension fallback, since Targa
has no signature. Radiance HDR is linear float light and is sRGB-encoded like
OpenEXR (super-white kept); Targa alpha goes through the existing
unlabeled-alpha interpretation.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: with shape layers selected, the Tools bar's Fill Options / Stroke
Options (None, Solid Color, Linear Gradient, Radial Gradient, and the
swatch's Alt-click cycle) only changed the defaults for new shapes.
shape.fillStroke had no kind, so a selected layer's fill or stroke could
not be removed or turned into a gradient from the toolbar; it had to be
edited in the Timeline. The swatch also kept showing the tool's kind, not
the layer's.
Fix: shape.fillStroke takes fillType / strokeType. None switches the
layers' Fills and Gradient Fills (or Strokes) off; another kind switches
them on and turns them into that kind, keeping the uid and the
properties both kinds share (blend mode, opacity, width, dashes...), and
gives a group with paths and no such item one (gradients span the
group's paths). It reports the first layer's fillType / strokeType, and
the toolbar shows them. The kind picks in the options popup and the
Alt-click cycle apply to the selected shape layers, one undo step each.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: After Effects 26 imports FBX models into Advanced 3D comps; File > Import rejected
.fbx, so FBX product shots and scenes could not be used.
Fix: a clean-room FBX 7.x reader in effectcraft-model (binary node records with 32/64-bit
offsets and zlib arrays, ASCII syntax) and a scene builder: meshes with normals, UVs and
per-polygon materials, the Model hierarchy with the SDK transform (pivots, offsets, pre/post
rotation, rotation order, geometric transform), Lambert/Phong materials onto
metallic-roughness with embedded or relative textures, and GlobalSettings axes turned to
Y-up. Wired into import, probing, the media browser and the file dialog. Hostile input
(truncation, corruption, huge arrays, deep nesting) fails with an error.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: Mesh Warp keeps its Distortion Mesh in a hidden text parameter,
and the viewer only draws handles for point parameters (Bezier Warp's),
so with Mesh Warp selected there was no grid and nothing to drag: the
effect could not warp anything from the UI.
Fix: with a Mesh Warp effect (or one of its parameters) selected and
layer controls shown, the viewer draws the (rows+1)x(columns+1) grid
over the layer in effect space, with a handle on every vertex
(automation id viewer.meshWarp.<fx>.<row>.<col>). Dragging a vertex
writes its offset to the `mesh` parameter through prop.set, one undo
step per drag; a mesh that does not match Rows/Columns starts from the
undistorted grid, as it renders.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: After Effects 26.3 pastes copied vector artwork as shape layers. Edit ▸ Paste only
knew the app's own clipboard, so SVG markup on the system clipboard was ignored; turning SVG
into shapes needed an imported footage file and Create Shapes from Vector Layer.
Fix: a new `edit.pasteSvg {svg, name?, comp?}` command builds a shape layer from SVG text with
the same conversion as Create Shapes from Vector Layer (paths, fills, strokes, gradients, clip
masks), above the selected layer with the SVG canvas centred in the comp, as one undo step;
oversized, non-SVG or shapeless text is rejected. In the UI, Ctrl/Cmd+V (and the native Edit ▸
Paste) run it when the system clipboard holds SVG. Copying in the app already replaces the
system clipboard with a note, so SVG there is newer than the app's clipboard.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: After Effects 26.3 copies the current composition frame to the
system clipboard; EffectCraft could only write it to a file (Save Frame As).
Fix: new command comp.copyFrame {comp?, time?, scale?} renders the frame as
Save Frame As ▸ File does and emits Event::CopyImage (pixels not
serialized); the egui frontend puts it on the clipboard after checking the
buffer size. In the Composition menu (translated) and the viewer's context
menu; drivable by id over the control channel / MCP.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: After Effects 26 imports STL models besides glTF and OBJ; we
refused .stl files, and the Media Browser and import dialog did not list
them as models.
Fix: a pure-Rust STL reader in effectcraft-model (binary and ASCII,
detected from the contents; several ASCII solids and polygon loops;
VisCAM/SolidView and Materialise Magics facet colours as plain
materials; Z-up turned to the model's Y-up; faceted normals from the
winding). Truncated, empty or non-finite input is an error, never a
crash. .stl is now a model extension for import, probing, the Media
Browser, the import dialog and the system report.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: dropping a PSD/PSB file on the window imported it straight away.
The drop handler always passed `importAs` (Settings > Import > Default Drag
Import As) to `file.import`, and the Import Photoshop File form only opens
when `importAs` is missing, so the dialog that File > Import > File...
shows never appeared for drops.
Fix: a drop that includes a Photoshop file leaves `importAs` out, so the
form asks (other drops keep passing the preference). For dropped files the
form's Import Kind starts at Default Drag Import As; File > Import keeps
starting at Composition. The engine already falls back to the preference
for `drag` imports without `importAs`.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: with the project's Time Display Style set to Frames, the
current-time display showed frame numbers but the Timeline ruler kept
labelling its ticks in seconds:frames (`00:15f`). The label format was
hard-coded and never read the project settings.
Fix: a `ruler_label` helper prints the frame number in Frames mode the
same way the current-time display does (`{:05}`, counted from Start
Numbering Frames At), and keeps seconds:frames for the other styles.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Settings > Appearance > UI Scale offered only 75, 100, 125, 150, 175 and
200 %, so a size between 75 and 100 wasn't possible. It is now the shared
numeric field (Kind::Int(75, 200, "%")): type or drag any whole
percentage. The engine already stored the scale as a clamped integer and
the UI applies it with set_zoom_factor, so the range, the default, live
preview, Cancel and saved preferences are unchanged; the preset list
goes.
Numeric settings now apply a typed number once it is entered (Enter or
leaving the field), not at every keystroke: typing 85 used to apply the
partial 8, clamped to 75, and resize the whole interface before the 5.
Dragging still applies as it moves.
Tests: the UI Scale test types 85 a key at a time (nothing applies and
the field keeps the keyboard until Enter), Cancel, 50 and 300 clamp to
75 and 200, 90 with OK, and a new app with the saved preferences starts
at 90 %. The GPU Information transaction test types 125.
The stop colour now uses widgets::srgb_color_button, the After Effects-style
picker every colour input opens (#638): hex field, HSB/RGB fields, OK and
Cancel, sRGB as the project stores colours. The stop keeps its alpha.
The dialog Escape handler only stepped aside for egui popups, and this
picker isn't one, so in a modal dialog Escape with the picker open closed
the dialog too. The picker now marks the pass it was open in
(color_picker::open_recently), and the handler leaves Escape to it: Escape
restores the colour the picker opened with and closes only the picker.
Tests: ui_gradient_editor clicks inside the picker's colour field over the
modal editor, types a hex code, closes the picker by clicking outside it
and saves the stop; a new test checks that Escape and the picker's Cancel
restore the stop's colour and keep the editor and its other edits. That
test fails without the Escape change. ui_color_hex still passes.
Settings > Previews > GPU Information was fixed text ("Compositing: CPU, N
threads... Display: egui on wgpu."): it never named a GPU, even while the GPU
compositor was running, so a card looked undetected. The System Report's GPU
line only said whether the compositor started.
sysinfo::gpu_summary builds the graphics lines from the session: the compositor's
adapter, or the reason compositing is on the CPU, and the adapter the window
itself got. The dialog uses it.
This does not explain why a particular card isn't used for compositing; that
needs the reporter's System Report (Help > System Report).
Tests: engine gpu_summary_names_the_adapter_or_says_why_there_is_none and UI
gpu_information_names_the_adapter_and_why_the_gpu_is_off (fails with the old
text). The six engine tests that fail on this Windows machine (relative footage
and render-queue paths) fail on main too.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The Timeline's "Edit Gradient…" on a gradient property (Gradient Fill /
Gradient Stroke ▸ Colors, Gradient Overlay) only painted its label. It now
opens a Gradient Editor (prop.editGradient): opacity stops above a preview
bar and colour stops below. Click to add a stop (it takes that track's
own value there), drag or type a Location to move it, set its opacity or
colour (egui's colour picker), Delete removes it (never the last of a
track, never while typing). The tracks stay in order while editing and the
selection follows its stop.
The editor works on a draft. OK sets the property once with prop.set at
the composition time it opened, snapped to the frame, with
timeBase: "comp" (offset and stretched layers get the key where it was
opened): one undo step, and nothing at all when the draft is unchanged, so
OK adds no keyframe. Cancel and Escape change nothing; Escape with the
picker open closes only the picker. On an animated gradient only the
keyframe at that time changes; when its stop counts no longer match a
neighbouring keyframe's (gradients only blend with equal counts) the
editor says so. The draft is saved in the typed form, so one colour stop
and each stop's RGBA survive.
prop.editGradient checks its target is a gradient property before the UI
opens anything, so agents get an error for a wrong one. New Gradient
color_at / opacity_at sample one track each (sample() multiplies opacity
in, which would apply it twice to a new stop).
Tests: ui_gradient_value (fails on main: the value had no automation id),
ui_gradient_editor (11), gradient_editor and keyframe unit tests.
Every viewer request spawns a job on the frame pool. A pool thread blocked
inside a render (waiting on a parallel loop's join) runs other pool work
meanwhile, rayon's injected jobs included: the spawn of the next request
started a whole frame render on top of the waiting one. Dragging makes a
new revision, and a new viewer frame, every UI frame, so renders nested
without bound on every thread, each holding its buffers (a large image's
full-resolution copies), until all memory was used. Holding the layer still
makes no new revisions, which is why only moving it did this.
A spawn now drains the queue, and one that lands inside a render on its
thread returns at once; the render below takes the queued jobs when it is
done. Each pool thread renders one frame at a time.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QiAJeFttZp83Fh7YuzAR5f
The Scrub helper and the real-properties test discarded FullOutput, so the
first pass' font-atlas texture delta panicked on drop in debug builds —
which is how the FreeBSD CI gate failed. Consume the delta the way the
rest of the crate's egui tests do.
Preserve legacy selection repair, modifier routing, viewer lifecycle and workspace persistence through the shared docking integration. Qualify the complete workspace gate by refreshing the 18 stale Audio Spectrum hashes for the existing linear renderer, with an independent synthetic-tone regression.
The file dialogs were blocking rfd::FileDialog calls made inside eframe's frame,
that is inside winit's event handler. On macOS the dialog runs a nested modal
event loop there; a file dragged over the window while it was open reached
winit's handler again, which refuses re-entry with a panic across the
Objective-C boundary, aborting the app (Import with Cmd+I, then a drag).
Hooks gains file_dialog_async: a host that sets it shows a command's file dialog
(Import, Open, Save As, Save Frame As, presets, guides, templates, Collect Files,
Relink...) without blocking, and the command runs with the answer on a later
frame (menus::dialog_answered, drained at the start of each frame); a cancelled
dialog runs nothing. The macOS app implements it with rfd::AsyncFileDialog, a
sheet on the window answered when it closes, outside the frame. Other platforms
keep the blocking dialogs (winit queues their re-entrant events), as do the few
panel buttons that pick a file in place (Settings paths, shortcut import/export,
effect file pickers).
Test: with a non-blocking hook, Save As and Save Frame As return at once without
the blocking dialog, the save happens when the answer arrives from another
thread, and a cancelled dialog writes nothing. The macOS sheet itself needs a
manual check (Cmd+I, drag a file over the window, pick or cancel).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QiAJeFttZp83Fh7YuzAR5f
widgets::keep_focus (Project panel rename and comment, Render Queue, Essential
Graphics and Timeline fields) selected the whole text whenever it saw its field
without the keyboard focus, not just on the frame the editor opened. Anything
that took the focus for a frame (an input method, another handler) then
re-selected the name, so the arrow keys seemed to do nothing and Backspace erased
the whole name, as reported on Linux. The editor now remembers the pass it was
last shown: it selects all only when it opens and otherwise just takes the
keyboard back with the caret where it was.
The harness does not reproduce the report's Linux behaviour itself (arrows reach
the field there); the tests cover the path that did: a unit test drops the focus
for a frame and checks the caret survives (and that reopening selects all), and a
kittest renames a Project item and a Timeline layer with Left / Right / Up /
Down / End and Backspace, editing part of each name.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QiAJeFttZp83Fh7YuzAR5f
Motion-path hits were only handled for the Selection tool, so the Pen slot's
tools did nothing on a layer's motion path, and every Pen slot tool showed the
same crosshair. As in After Effects, on a motion path:
- Add Vertex (and the Pen) add a Position keyframe at the clicked frame dot,
with the path's value there (prop.addKey), so the path keeps its shape;
- Delete Vertex removes the clicked keyframe (keys.select + keys.delete);
- Convert Vertex switches it between Linear and Auto Bezier spatial
interpolation (keys.interpolation {spatial}).
motion_path now reports its frame dots for the hit test. Over the viewer the Pen
slot tools draw their own pen glyph (Pen, Add, Delete, Convert, Mask Feather) at
the pointer in place of the system cursor, and the Pen shows Add Vertex's pen over
a mask, shape or motion path it would add a point to.
Test: ui_viewer clicks a two-key motion path with each tool and checks the added
key, its spatial interpolation both ways, its removal, and the pointer.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QiAJeFttZp83Fh7YuzAR5f