Resting the pointer in an 18 pt band at a menu's bottom edge crawls the list after a short delay and speeds up the longer it stays there, so long menus stay reachable without letting go of the press-and-hold gesture. Context menus do not crawl.
Picking Alpha / Alpha Inverted / Luma / Luma Inverted in a layer's Timeline
Track Matte menu only dispatched a command when the layer already had a
matte layer, so with "No Track Matte" the click closed the menu and did
nothing. The mode items now run layer.trackMatte (Layer > Track Matte),
which keeps an existing matte and otherwise takes the layer above, or
reports that the top layer has none. The menu ticks the current mode and
the cell's tooltip names the matte layer and the mode (the 104 px cell
has no room for both).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The app's item spacing left 4 px between two menu entries where neither
highlighted and a click did nothing. Menus (context menus, the menu bar,
menu buttons and their submenus) now use a menu style with no row gap and
rows taller by that gap, so they keep their size.
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>
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.
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
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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QiAJeFttZp83Fh7YuzAR5f
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 <noreply@anthropic.com>
Tab in a value being typed (hot text) dropped the keyboard: egui moved
the focus to the next widget, and the inline editors asked for the
focus again every frame, which also reset what keys a field keeps for
itself.
As in After Effects, Tab now commits the value and types in the next
value field of the same panel (the next dimension, then the next
property's), Shift+Tab the previous one. The hot-text fields of each
panel register in drawing order; the dock tags each panel's Ui so the
order stays within it. `keep_focus` asks for the focus only when the
field doesn't have it.
Tests: tab_commits_a_value_and_moves_to_the_next (ui_inline_edit:
Timeline and Effect Controls).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#402 made Effect Controls' rows compact and aligned, as After Effects
draws them, and also added thin separator lines between parameter rows
in Effect Controls and the Timeline, with a Settings > Appearance > Show
Parameter Row Separators preference. After Effects has neither, so the
lines, the preference and the Tokens field go; the compact rows, the
alignment and the automation ids stay.
Saved settings with the old key load as before (unknown keys are
ignored).
Test: ui_parameter_spacing keeps the row geometry, hit target and
nesting tests; the separator tests go with the feature.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The in-window menu bar now works from the keyboard as After Effects' does on
Windows: Alt pressed and released on its own focuses the bar, Left / Right move
between its menus and Down, Enter or Space opens one. In an open menu Up / Down
move the highlight (skipping disabled entries, wrapping around), Right opens the
highlighted submenu and Left closes it (on a plain entry they open the next /
previous menu), Enter or Space chooses the entry and Escape closes the menus.
Hovering an entry moves the highlight there, and a highlighted entry scrolls
into view in long menus.
While a menu, context menu or popup list is open, or the bar has the keyboard,
keys go to the menus only: the app's shortcuts don't run (Space started the
preview behind an open menu) and the panels don't see the keys (Enter on an
entry no longer reaches the Project panel's rename).
Tall menus already scroll inside the window since #277 (ui_scrolling's
a_menu_taller_than_the_window_scrolls). F10 stays View > Switch 3D View > Front,
as in After Effects.
Test: ui_menus (shortcuts_wait_while_a_menu_is_open,
arrow_keys_drive_the_menu_bar, enter_chooses_the_highlighted_entry).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The label menus listed the labels' names only, so a label couldn't be
picked by its colour. Every place a label is chosen now shows its
colour swatch before its name, as After Effects does: the Timeline's
label swatch (which now opens its menu on a click too, not only a
right-click), the Project panel's label swatch and a new Label submenu
in its item menu, Edit > Label, keyframe Label menus and the marker
dialog. One shared widget (widgets::label_entry / label_popup) draws
them; keyframe labels now show the names set in Settings > Labels.
Tests: label_swatch_opens_the_label_menu (ui_project_rows),
the_label_swatch_menu_shows_the_label_colours (ui_timeline_switches),
edit_label_menu_shows_the_label_colours (ui_panels).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Most panels lay out their own rows and scrolled only with the mouse wheel: Effect Controls,
Effects & Presets, the Media Browser, Ease Presets and the Timeline's layer rows had no scroll
bar, Properties and Preview a thin indicator that couldn't be dragged, and Render Queue, History,
Markers, Character, Paragraph and Info didn't scroll at all, so a panel shorter than its content
cut it off. Without a wheel or a working touchpad there was no way down. The Project panel had
the only real scroll bars, hand-rolled, and its vertical one was hit-tested under the rows, so it
only dragged where no row was.
- widgets::scroll_bar: the Project panel's bars generalised (horizontal or vertical, drag the
thumb or press the track to move it there, automation id with "scroll/max"); the Project
panel's two bars and the Timeline's column bar use it, drawn over the rows.
- widgets::PanelScroll: the wheel offset, its clamping and the bar for a panel's own rows, used by
Effect Controls, Properties, Effects & Presets, Media Browser, Ease Presets, Preview, Render
Queue, History (which still follows the current state), Markers, Character, Paragraph and Info.
The Timeline's layer rows get a bar on the right edge.
- Effect Controls: an animated vector's values (a puppet pin's Position) ran under the keyframe
navigator in a narrow panel. They now end before it, moving left over the name, which is cut
short.
Test: ui_scrolling panels_taller_than_their_space_have_a_scroll_bar_to_drag (eleven panels in a
short window: a press on the track and a thumb drag scroll them),
effect_values_stay_clear_of_the_keyframe_navigator.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The menu bar's own menus (Edit has 30 entries) had no scroll area, only their submenus did, so
on a short screen Edit ran past the bottom of the window. The font menu was capped at 420
points and never moved up, the Preview panel's and the viewer's popup lists neither scrolled nor
moved, and the Timeline's Blending Mode and Expression Language submenus didn't scroll. egui's
scroll bars also only appeared while the pointer was over the list, so a list could only be
scrolled with a wheel or touchpad.
- widgets::popup_list: the one popup list behind popup_menu, the font menu, the Preview panel's
dropdowns and the viewer's popups: as tall as the window allows, scrolling beyond, moved up to
fit the window.
- widgets::menu_scroll: the same for menus and submenus (the menu bar's, Blending Mode, the
Expression Language menu, the viewer's Select menu).
- Scroll bars always show (solid scroll style), so they can be dragged.
- A first window that doesn't fit the screen is maximized: the 1680 x 1020 default on a smaller
screen is only shrunk to the monitor's size, so with its title bar it ran under the taskbar and
cut menus off at the bottom of the screen even when they fitted the window.
Test: ui_scrolling a_menu_taller_than_the_window_scrolls (Edit in a 520-point window scrolls
its last entry into view and it takes the click); overflows_screen unit test.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A dockable panel (our own design) for the keys.easePreset commands. A
value graph shows the working curve with its two handles, which drag
to edit it; Apply eases the selected keyframes with it and From Keys
loads the curve between the first selected pair. Every preset is a
thumbnail drawn in code; a click loads it and applies it to the
selected keys. Save Current keeps the working curve under the name in
the field, and Rename and Delete act on the selected user preset.
Wide panels put the presets beside the graph; narrow ones below it,
the graph shrinking to leave a row of presets, scrolled with the wheel.
The panel state (working curve, selection, name) is serde in the UI
state, and every control registers an automation id (easePresets.*).
widgets::text_field is the search field without its magnifier.
Test: clicking_an_ease_preset_eases_the_selected_keys_and_handles_edit_the_curve
(ui_keyframes); every_panel_registers_automation_ids_for_its_widgets
covers the new panel.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Dragging a layer already snapped its feature nearest the pointer to
other layers' edges, corners, centres, anchor points and path points
and to the comp's edges and centre, and Cmd/Ctrl held during the drag
already turned snapping on or off against the checkbox. What was
missing were After Effects' Snapping options beside the Tools bar's
checkbox. The new menu turns Snap Edges Extended off (a layer's edges
then snap only along the layer, not across the comp) and each feature
family (edges, corners, centres, anchor points, mask and shape path
points) on or off, for the dragged layer's snap features and for the
targets alike. Agents set them with view.snappingOptions.
Layers whose eye is off (or that a solo hides) are no longer snap
targets, mask and shape path points of the dragged layer snap too, and
while snapping is on the feature that snaps is boxed during the drag.
Test: layer_drag_snaps_to_other_layers_edges_ctrl_toggles_and_options_apply
(UI), snapping_options_pick_the_targets and
snapping_options_command_sets_and_toggles (engine).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each layer switch toggled on its own click only, so hiding or locking
many layers took a click per layer. As in After Effects, pressing a
switch in the A/V Features or Switches column now sets it at once, and
dragging up or down gives the same switch of every layer the pointer
passes the state the first one got, whatever their own state was
(rows passed in a quick drag included). The whole drag is one undo
step: layer.setSwitch takes a `merge` key like other gesture edits.
A single click still changes only that layer, so clicking the eye of
one selected layer doesn't change the others (Layer > Switches still
sets all selected layers).
Test: dragging_over_layer_switches_sets_them_all.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Trim Paths, Merge Paths, Repeater and the other path operations
existed (layer.addShapeItem) but no menu added them to a shape layer.
Like After Effects, a shape layer's Contents row now has "Add:" with
Group, Rectangle, Ellipse, Polystar, Path, Fill, Stroke, Gradient Fill,
Gradient Stroke and the path operations; with a shape group selected
they go into it. layer.addShapeItem gains the empty Path and Gradient
Stroke kinds.
A 3D text or shape layer in a Classic 3D comp now shows Geometry
Options with "Change Renderer…", which opens Composition Settings on
its 3D Renderer tab (Advanced 3D comps show the extrusion options).
Popup menus are as tall as the window allows and open upward near its
bottom: the 20-entry Add menu was cut at 420 px, hiding Trim Paths.
Tests: shape_contents_add_menu_adds_path_operations,
classic_3d_shape_layers_offer_change_renderer.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018gnTnH3Jmk5zr4tMq1Mc9f
Cause: the click-to-edit fields (hot numbers and revolutions, the Timeline
time display and layer comments, Project rename/comment, the Render Queue
output path, Essential Graphics control names) opened a text edit with
egui's default caret at the end of the text, so typing appended to the old
value. The Timeline time display, layer comment and Essential Graphics name
editors also re-requested focus before checking `lost_focus()`, which hid
the loss, so Enter, Escape and clicking away never closed them.
Fix: a shared `widgets::keep_focus` keeps an open inline editor focused and
selects its whole text on the frame it opens (`widgets::select_all`, which
the layer rename now reuses). Those editors check for a lost focus first,
and Escape now cancels in the time display and the Essential Graphics
name editor.
Test: ui_angles `typing_replaces_the_clicked_value` (degrees and revolutions
in Effect Controls, Timeline and Properties, typed without Ctrl+A);
ui_inline_edit `project_rename_replaces_the_name` and
`timecode_field_replaces_commits_and_cancels`.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An angle's revolutions were painted text: only the degrees could be
dragged or typed, so full turns needed the dial or typing past 360.
fx_widgets::angle_field draws `0x+45.0°` as two hot numbers: the
revolutions (widgets::hot_int_at, ten pixels a turn, fractions adding up
between frames so slow drags still step; a typed value is truncated) and
the degrees, each keeping the other. Effect Controls, the Timeline and
the Properties panel use it, so they read and edit angles alike (the
Timeline showed `-1x-40.0°` and Properties had its own split).
Automation ids: timeline.prop.<uid>.revolutions,
effectControls.prop.<uid>.revolutions, properties.prop.<uid>.revolutions.
Regression test: angle_revolutions_scrub_and_take_typing drags and types
the revolutions in all three panels.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Choosing Draft Settings (or any entry) in a Render Queue item's Render
Settings menu did nothing when the menu had been moved up to fit the
window. egui hit-tests a fixed-position Area where it was asked to go,
not where it was moved, so the popup's "pressed outside" check was true
over its own entries and the menu closed on the press, before the click.
widgets::pressed_outside tests the press against the rect the popup was
drawn at. Every popup with the same check uses it: popup_menu (Render
Queue, Effect Controls, dropdowns), the colour popup, the Preview and
viewer popups, the panel menu, the Flowchart, OpenType and font menus.
Regression test: ui_render_queue.rs picks Draft Settings from a menu
moved up to fit a 1000 px window; it failed before the fix.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The details header (item name, size, duration, codec) and the "Select an item" hint were painted
with no width limit, so in a narrow Project panel they ran past its right edge. They now stop
10 px short of it and end in "…" when they don't fit (widgets::text_fit, a shared helper), and
are registered for automation (project.details.*).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Layer ▸ Layer Settings on a solid or adjustment layer opens Solid Settings on the layer's
solid (it ran an empty command before): name (renames the layer and the solid), size, Pixel
Aspect Ratio, colour, and "Affect all layers that use this solid". It starts off, as in After
Effects, so changing a solid other layers share gives this layer its own copy.
`layer.settings {pixelAspect?, affectAll?}` → `{layer, solid?}`; `layer.newSolid {pixelAspect?}`.
- Nulls ask for their name; text and shape layers have no settings (the entry is disabled).
- Composition Settings / New Composition and Solid Settings register an automation id for
every control (`dialog.comp.*`, `dialog.solid.*`). Composition Settings takes any typed frame
rate besides the list, and New Composition starts from the last settings used.
- Colour buttons in both dialogs show the stored sRGB colour (`widgets::srgb_color_button`);
egui's linear picker showed it lighter.
- Docs: agents, parity.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- footage.check: background footage check after File > Open (Progress panel), not an undo step
- timeline: track-matte / parent menus built only while open (was O(n^2) per frame);
expression-row lookup per layer; panels hold Arc<Comp> snapshots instead of deep clones
- Project panel: virtualised vertical scrolling, selection reveal, thumbnails rendered off
the UI thread
- effectcraft-cli bench --ops (engine::perf + ui-egui::bench)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Effect Controls: Lumetri RGB and hue/saturation curve graphs, Colorama's
palette wheel, Glow's colour map strip and Arbitrary Map curves, Reshape
correspondence buttons and viewer handles (all prop.set, one undo step per
gesture, automation ids). Preview panel scrolls; Composition/Timeline tabs
get close, label swatch and viewer lock; Effect Controls tab the layer
swatch; dropdowns elide long text. Fix the browser start (no default disk
cache folder on wasm32). Browser smoke test covers frame workers, waiting
renders and the Media Browser.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>