`supported` was only called on desktop builds, so the wasm32 check
warned that it was never used. The browser build, which has no system
locale here, now resolves Match System through supported(None)
(English) instead of a literal.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Files dropped on the window or picked in the Import dialog were probed
one after the other on the UI thread: the app froze while a video was
read, nothing said what was being imported or how many files were
left, and only the last new item was selected. People ended up
importing the same file again and again.
file.import takes `background`: the files are probed in a background
job (Window > Progress, jobs.list / jobs.cancel) that names the file
being read and how many there are ("b.png (2 of 3)"); then they are
added in one undo step, all selected (the Project panel scrolls to
them, a file already in the project arrives again as in After
Effects) and a toast reports the count ("Imported 3 items", with the
first error). Drops and the Import dialog use it, and an Importing
card at the bottom left shows the job with its progress bar and
Cancel. Agents and scripts still import at once by default. The
toast registers an automation id.
Test: background_imports_report_progress_and_select_what_arrived
(engine), dropped_files_show_import_progress_then_the_items_they_made
(ui_drag_drop).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Moving a puppet pin keys its Position, but the Timeline didn't show
those keys where people look for them. U revealed every pin's Position
as one more flat "Position" row under the layer, indistinguishable from
the layer's own Position and from each other, and the reveal was a
snapshot: a pin placed or moved after U (or with the layer twirled open
and Effects closed) got keys that no row showed.
As in After Effects, revealed properties inside an effect now show
under the effect and its groups (Puppet > Mesh 1 > Deform > Puppet
Pin 1 > Position), twirled open by the reveal and closable like any
group; Transform and mask properties stay as they were. Placing, moving
or recording a pin in the viewer keeps its Position in view: on a
twirled-open layer its groups open, and a U reveal takes it in. A
collapsed layer stays collapsed. The keys select, move and delete like
any others.
Test: puppet_pin_keys_show_under_their_pins_in_the_timeline,
placing_a_pin_opens_its_groups_on_a_twirled_open_layer (ui_viewer).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Learn workspace opens the Home screen on its Learn tab, but no other workspace closed it, so
after Learn the tutorials stayed over Default, Review and the rest until a project was created or
Home was toggled. It wasn't about clicking quickly: any switch away from Learn kept them. Leaving
Learn for another workspace now closes the Home screen it opened.
Test: ui_panels leaving_the_learn_workspace_closes_its_tutorials (clicks through the workspace
tabs: the Home screen and the tutorials show only on Learn).
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>
get_project / render.backend reported "gpuAcceleration": true with
"active": "cpu" and no reason, which read as a broken GPU. The
headless MCP server only attaches the GPU compositor when started with
--gpu, which docs/agents.md didn't mention, and a failed --gpu said
only "no usable GPU adapter" (the adapter's error went to a log no
one sees). The session now keeps the host's reason there is no GPU
compositor (accel_note): the CLI's "headless: renders on the CPU
unless started with --gpu", or the desktop app's compositor setup or
retirement error. The status reports it as `why` whenever it renders
on the CPU (or that the project's renderer is Software Only). --gpu
failures include the adapter's error (Gpu::try_headless).
docs/agents.md documents `mcp --gpu`.
Test: render_backend_says_why_it_renders_on_the_cpu (engine).
Checked with effectcraft-cli mcp and mcp --gpu.
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>
Scaling a layer by a bounding-box handle didn't snap, so it couldn't
be scaled exactly to the comp. With snapping on (Cmd/Ctrl toggles it
during the drag), the dragged handle now snaps like a dragged layer's
feature: to the comp's edges, corners and centre and to other layers'
edges, corners, centres and anchor points, as the Snapping options
allow. A corner snaps both axes, an edge its own axis; with Shift the
corner slides along its line from the anchor point to the nearer
snapped edge, so the proportions stay and the layer lands on, say, the
comp's size.
Test: handle_drags_snap_to_the_comp_edges.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Shift-dragging the work area's begin or end handle now snaps it to the
current-time indicator, keyframes, layer in and out points and markers
within 8 px, with the snap candidates the CTI and keyframe drags use.
The work area bar itself drags too, as in After Effects: it moves the
work area and keeps its length, and with Shift the nearer end snaps.
Test: shift_dragging_the_work_area_snaps_to_the_current_time.
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>
Clicking (or marquee-selecting) keyframes in the Timeline selected only
the keys. Their property and layer stayed unselected, so the Graph
Editor, which shows the selected properties or else the animated
properties of the selected layers, came up empty until the property
was selected separately. As in After Effects, the Timeline's key
selection now selects the keys' properties and layers too (Shift adds
to them). keys.select takes this as `selectProperties`; the Graph
Editor's own key clicks leave what it shows alone.
Test: a_key_clicked_in_the_timeline_shows_in_the_graph_editor
(ui_keyframes).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The logger kept warnings in memory and wrote to a file only after Help
▸ Enable Logging, and it ignored RUST_LOG. An app that never opens its
window (macOS 27 on Apple Silicon, #234) therefore left no trace. Now
RUST_LOG (a level, or env_logger-style `target=level` directives)
also prints what it lets through to stderr, including eframe, winit
and wgpu. The desktop app logs its start-up milestones: opening the
window, the graphics adapter, the app created, the first frame drawn,
the window being asked to close, and the window closed.
Test: rust_log_filters_by_level_and_target (engine logging). Checked
by running the app with RUST_LOG=info and quitting it over the control
channel.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Tools bar had Fill and Stroke swatches and a Stroke Width, kept in
the interface only, so agents couldn't set them and new shapes could
only be a solid fill with a solid stroke. As in After Effects:
- With a shape layer selected and a shape tool or the Pen active, Tool
Creates Shape / Tool Creates Mask choose whether drawing adds shapes
to the layer or masks on it (Fill and Stroke hide while drawing
masks).
- Clicking the word "Fill" or "Stroke" opens Fill Options / Stroke
Options: None, Solid Color, Linear Gradient or Radial Gradient, the
blend mode and the opacity. The swatches show the kind; Alt-click
steps through the kinds.
- New shapes and Pen paths are painted with them: a Gradient Fill or
Gradient Stroke across the shape for the gradient kinds.
The options are engine state (EditorState.shape_tool) set with the new
shape.toolOptions command; shape.newShape and shape.newPath use them
unless given paint parameters (which now include fillType, fillBlend,
fillOpacity and their stroke twins; layer.newShape takes them too). A
Pen path without paint parameters now takes the Tools bar's stroke
(none by default) instead of a fixed 2 px white one. layer.addMask also
draws rounded rectangle, polygon and star masks, so every shape tool
makes a mask on a selected footage or solid layer (rounded rectangles,
polygons and stars made a new shape layer).
Test: shape_tool_options_paint_new_shapes,
masks_of_every_shape_tool_kind,
tool_creates_mask_draws_a_mask_on_the_selected_shape_layer,
fill_options_paint_the_next_shape.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Edit > Duplicate (Ctrl+D) with a shape group or other shape item
selected in the Timeline duplicated the whole shape layer. As in After
Effects it now duplicates the selected items inside their group, each
copy above its original and numbered on ("Rectangle 1" gives
"Rectangle 2"), and selects the copies. With only layers selected the
layers are duplicated as before.
Test: duplicate_with_shape_items_selected_duplicates_them_in_place,
ctrl_d_duplicates_the_selected_shape_group_in_its_layer.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Edit > Copy with a shape group (or a path, fill, stroke or path
operation) selected in a shape layer's Contents copied the whole layer,
and Paste made a new layer. As in After Effects, Copy now copies the
selected items, Paste puts them into each selected shape layer's
Contents (above its selected item, in that item's group, else on top,
with names kept unique there: "Rectangle 2"), and Cut removes them, so
Cut + Paste moves them. Pasting with no shape layer selected is an
error rather than a new layer. Copying anything else clears the shape
item clipboard, as it does the other clipboards.
Test: shape_contents_copy_cut_and_paste_between_shape_layers,
ctrl_c_and_ctrl_v_copy_a_shape_group_into_another_shape_layer.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With a shape layer selected, the Rectangle, Rounded Rectangle, Ellipse,
Polygon and Star tools made a new shape layer for every shape. After
Effects draws into the selected shape layer instead: the shape becomes a
new group on top of its Contents ("Rectangle 2", ...), placed by the
group's Transform. The new shape.newShape command does that (in the
layer's space, so a moved or scaled layer still gets the shape where it
was drawn), and makes a new shape layer only when no unlocked shape
layer is selected. The viewer's shape tools use it; masks on other
selected layers are unchanged. layer.newShape and the Pen's
shape.newPath share its layer and Contents helpers.
Test: shape_tools_draw_into_the_selected_shape_layer,
shape_tool_draws_into_the_selected_shape_layer.
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>
Dragging Project items onto the Create a new Composition button at the
bottom of the Project panel did nothing. As in After Effects, the
button now takes the drop and runs File > New Comp from Selection with
the dragged items (the selection when the dragged item is in it): one
item becomes a comp of its size, frame rate and duration holding it,
and several open the New Composition from Selection dialog. The button
is outlined while items are dragged over it.
Test: project_items_dropped_on_new_comp_make_a_composition.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Right-clicking the Effect Controls panel did nothing outside an
effect's header. As in After Effects, a right-click anywhere a control
has no menu of its own (the empty area, property names, buttons such
as Reset) now opens the Effect menu: Effect Controls, Last Effect,
Remove All, Manage Effects and every effect category, applying the
chosen effect to the selected layers. It is the menu bar's own Effect
menu tree and drawing code; an effect header keeps its own menu.
Test: right_click_in_effect_controls_shows_the_effect_menu.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Spacebar started or stopped the preview on its press, so holding it to
pan the Composition panel (Space+drag already panned there) also
toggled playback. As in After Effects, plain Spacebar now runs its
shortcut when it is released, and only if no mouse button went down
while it was held and the press didn't go to a text field; key
auto-repeat toggles nothing more. Shift+Spacebar and the numpad
preview shortcuts still run on the press.
Holding Spacebar over the Timeline's ruler and time graph is the Hand
tool there too: dragging scrolls the time graph (when zoomed in, never
before the comp's start or past its end) instead of moving bars, keys
or markers.
Test: spacebar_taps_preview_and_held_spacebar_pans_the_viewer,
spacebar_drag_scrolls_the_time_graph.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The interface language was English unless Japanese was chosen in
Settings ▸ General ▸ Language. Language now has a Match System choice
(`general.language` = `system`), the new default: the operating
system's interface language where EffectCraft has a translation
(Japanese), else English, as After Effects installs in the system's
language. English and Japanese can still be chosen; a language chosen
before stays. The browser build stays in English with Match System,
since it has no Japanese font of its own.
The system language comes from sys-locale (MIT OR Apache-2.0, no
dependencies on the desktop platforms), on desktop builds only.
Tests: match_system_uses_the_system_language_where_there_is_a_catalog
(i18n), interface_language_is_validated_persisted_and_backward_compatible
(engine).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A press in the Composition panel always acted on the topmost layer
under the pointer, so a selected layer could not be dragged where
another layer covered it: the press selected the layer in front. As in
After Effects, a selected layer under the pointer now comes first: it
stays selected and a drag moves it. With no selected layer there the
topmost layer is picked as before, and Shift still toggles the topmost
one. The context menu's list of layers under the pointer shares the
same hit test.
Test: viewer_press_prefers_a_selected_layer_under_the_pointer
(ui_viewer).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Project panel's vertical scroll bar clamped its thumb between 16
points and the track's height. When the track was shorter than 16
points (an empty project in a 1280 x 800 browser window at device pixel
ratio 2, or any short panel), that clamp panicked on the first frame:
the browser canvas went blank and the app stopped responding. The
thumb is now at most the track's length, and both scroll bars share
the calculation.
Test: a_short_project_panel_draws (ui_project_rows).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* About: contributors compiled in from contributors.json (grab bag + table, sortable, name toggle)
About EffectCraft now has tabs About · Contributors · Models, following the craftrules standard
(standards/contributors.md): contributors/contributors.json is generated by
craftrules/scripts/contributors.py and baked into the binary by crates/ui-egui/build.rs as static
tables. Each contributor line: @username, opt-in display/real name, PRs, commits, lines +/−/Δ,
binary assets +/−, first/last commit. Sortable by every column; alphabetical ignores case and
the leading @. Every control registers an automation id (about.tab.*, about.credits.*).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Contributors: self-submitted names in contributors/people.toml; AGENTS.md credit-yourself section
build.rs reads contributors/people.toml next to the generated contributors.json and shows a name only
for entries the generator verified as written by that person (craftrules standards/contributors.md).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Contributors: regenerate (verified people.toml entries)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
- Selecting a mask in the Timeline (its row or Mask Path), or a shape's
Path, selects all its points, so dragging one in the viewer moves the
whole mask, as in After Effects. Delete still removes the mask, and
selecting another property deselects the points.
- Mask Feather has the chain link (Constrain Proportions, on by
default, Alt edits one value) and no longer takes negative values;
Scale's link shows as a toggle too.
- Dragging in the Project panel's empty area draws a selection box
(Shift / Cmd adds); a click there deselects.
Tests: selecting_a_mask_selects_its_points,
mask_feather_values_are_linked_and_not_negative,
dragging_in_the_empty_area_box_selects_items.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018gnTnH3Jmk5zr4tMq1Mc9f
- With a shape layer selected, the toolbar shows its Fill, Stroke and
Stroke Width (with any tool) and editing them changes the layer's
fills and strokes, as in After Effects (new shape.fillStroke, one
undo step per picker or drag). Shapes drawn without a stroke get one
when a stroke colour or width is set, so changing the stroke now works.
- Alt+Shift+P (A, S, R, T) reveals the property it keyed on each layer.
- A click in the time graph below the last layer deselects the layers
(it only deselected keyframes), so a shape tool draws a new shape
layer instead of a mask on the selected image.
Tests: toolbar_fill_and_stroke_paint_selected_shape_layers,
toolbar_stroke_edits_the_selected_shape_layer,
add_position_key_reveals_position,
clicking_below_the_layers_deselects_them.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018gnTnH3Jmk5zr4tMq1Mc9f
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
An 11000 x 2200 comp at Full resolution renders on the CPU, and the
frame went to egui as one texture over the device's 8192 px limit:
the upload failed every frame (a panic in debug builds) and the window
stopped drawing. Creating a comp that size at Full did the same.
Frames, snapshots, the channel / exposure view, other viewers, the
Layer and Footage panels and the Roto Brush overlay now go up averaged
down by the smallest whole factor that fits. The full frame stays
readable for pixel readouts.
Tests: frames_over_the_texture_limit_are_averaged_down and
full_resolution_frames_wider_than_the_texture_limit_fit (panicked
before).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018gnTnH3Jmk5zr4tMq1Mc9f
Since #103, a preview with audio stayed silent until every frame from
the playhead to the end of the preview (or what fits in the RAM
preview) was cached. The prefetch only renders a few frames ahead, so
on a normal work area the sound came near the end or on the second
loop, even when the comp rendered in real time.
The held sound now also starts once the silent preview has kept real
time for a quarter second with the prefetch window ahead cached. A
comp that can't render in real time still waits for the cache, as #103
intended; a stall clears the measured rate so the sound doesn't cut in
and out.
Test: preview_with_audio_sounds_once_rendering_keeps_up (a 30 s comp
sounds within 2 s, without skipping a frame first).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018gnTnH3Jmk5zr4tMq1Mc9f
Clicking an item's Output To file name opened an inline editor on the
name template (`[compName].[fileExtension]`), so there was no way to
pick a folder from there. Choose File… in the menu beside it did open
a dialog, but the project save dialog: filtered to .ecproj and opening
wherever the OS chose.
As After Effects' Output Movie To, clicking the file name now opens a
save dialog at the current output (its folder and name) for the output
format's extension, and the chosen file becomes the output. Choose
File… opens the same dialog. Alt-click still edits the template in
place, and hosts without a save dialog (the web) keep the inline
editor. The desktop `pick_save_file` hook opens in the folder of a
default path.
Test: clicking_the_output_file_name_opens_the_save_dialog.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Save as New Workspace stored the layout and `window.workspace` could
switch to it, but the Workspace submenu was a fixed list of the
built-in workspaces, so a saved one never appeared and could not be
chosen from the menu.
The submenu now has a `savedWorkspaces` dynamic node after the
built-ins (DynCtx carries the saved names), in the in-window and native
menus, with a check mark on the current one as the built-ins have.
The in-window submenu was also too short to show it. egui lays out a
new submenu in a default 400 pt area, and the scroll area inside took
that height and kept it, so Window ▸ Workspace (514 pt) opened with its
last rows (Undocked Panels and below, where saved workspaces go)
scrolled out of view. Submenus now open at their full height and scroll
only when taller than the window.
Tests: saved_workspace_is_listed_in_the_workspace_menu (picks it from
the menu bar), saved_workspaces_are_in_the_native_workspace_menu.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Cause: a `DockNode::Stack` laid its panels out from fixed heights (Preview
46 pt) with the rest shared, but only split gutters were interactive, so
the right column's panels could be expanded or collapsed and never resized.
Fix: the gap below a stacked panel drags the nearest open panels above and
below it against each other, neither going below 40 pt. The new sizes are
stored as the entries' heights, so they persist in the layout like split
sizes. The last panel without a height keeps sharing the rest, and when no
panel takes the rest, or space runs short, the heights scale so the stack
fills its column. Split and stack gaps share one `gutter` helper.
Automation ids: `dock.gutter.<path>s<i>` for the gap below stacked entry i.
Test: ui_tabs `stacked_panel_gaps_resize_the_panels` (drag, persistence,
collapse/expand, minimum); dock unit tests `stacked_panels_fill_their_column`
and `dragging_a_stack_gap_trades_height_between_its_neighbours`.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
The Project panel (like other panels) runs comp.open while it draws, so the
new comp became the active comp mid-frame. Later in the same frame the
active, locked viewer drew and recorded the active comp as its own. On the
next frame on_open_comp compared that record with the opened comp, found
them equal and skipped the lock, so the locked viewer switched comps.
The active viewer no longer records its comp while an OpenComp event is
pending (it asks for a repaint instead); on_open_comp then sees the comp
the locked viewer really shows and routes the new one to an unlocked or
new viewer.
The viewers test's click helper now releases a frame after the press, as
a hand does: a press in a tab strip focuses the shown panel first, and a
same-frame press and release refocused it after the tab's own click.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Pan Behind gesture ignored modifiers other than Ctrl (snapping): it
always moved Anchor Point and Position together and never constrained the
move, and the tool's button had no double-click action.
- Alt held as the drag begins moves the Anchor Point alone (Position is
left as it is, so the layer shifts).
- Shift keeps the move to the axis dragged along most; Move and Pan
Behind now share that rule (one_axis).
- Ctrl+double-clicking the Pan Behind tool's button runs
layer.centerAnchor (Center Anchor Point in Layer Content).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Pan Behind drag passed its layer to the snapper's exclude list, which
dropped all of that layer's targets: its corners, edge midpoints and centre
as well as the anchor being dragged. The layer's box doesn't move in comp
space while the anchor pans (Position compensates), so those points are
valid targets.
The engine's per-layer snap targets are now one function, layer_targets,
which can leave out the anchor point. The gesture records its layer's
targets without the anchor as the drag begins and passes them to the
snapper (snap_with) as extra targets.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each Project panel row registered its click / drag area after the twirl,
proxy indicator, label swatch and comment cell drawn on it, so the row sat
on top and took their clicks: a folder's arrow selected the folder instead
of opening it, and the swatch never opened the label menu.
The row's area now goes in first and those widgets sit on top of it. A
single click on the arrow twirls the folder; a click on the swatch opens
the label menu (project.setLabel). Clicks, double-clicks and drags on the
rest of the row work as before; a single click on the comment cell still
selects the row, and a double-click there now edits the comment.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Its setup wrote the Timeline's old global reveal field, which no longer
filters on its own since reveals are kept per layer.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The navigator drew handles on both ends but the whole bar was one drag
area that scrolled. Each end is now a handle (timeline.navigator.start /
.end) that moves that side of the visible span while the other stays:
pulled in, the ruler zooms in; stretched to the track's ends, it shows
the whole comp again. The middle still scrolls at the current zoom.
; (timeline.zoomFrameToggle) zooms in to frame level around the current
time and, pressed again, back out to the whole comp. timeline.zoomFit
stays as the plain fit command (no shortcut). Both go through set_zoom.
Tests: navigator_ends_drag_to_zoom and
semicolon_toggles_frame_level_and_the_whole_comp (real pointer drags).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Alt+wheel's zoom took its lower limit from the current zoom rather than
the comp's fit and kept the ruler's start where the pointer had put it,
so on long comps it stopped zoomed out on a slice from the middle of the
comp (0:05:02–0:10:27 of a 10-minute comp). The = / - zoom used a
different limit again.
Both now go through set_zoom: the limit is the zoom that fits the comp
(fit_pps, the same formula the ruler and the zoom slider use), and
reaching it switches the ruler back to fitting the whole comp from 0.
Test: alt_wheel_zooms_out_until_the_whole_comp_shows (real Alt+wheel
events on a 10-minute comp); it failed before (Some(1.135) px/s, start
still offset).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Layer ▸ Time ▸ Enable Time Remapping (and the layer menu's entry) added
the property and its two keyframes but left the layer twirled closed, so
nothing visibly changed. When the command turns remapping on, the
layers that have it now twirl open on Time Remap, through the same
per-layer reveal as RR. Turning it off reveals nothing.
Test: enabling_time_remapping_reveals_time_remap.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
U (and P, S, R, T, A, E, M, F, L) kept one reveal filter for the whole
Timeline and replaced the set of open layers with the selected ones, so
revealing on one layer closed every other layer and dropped their
reveals.
Each layer now has its own filter (TimelineState::layer_reveal; an open
layer without one shows its whole tree). A shortcut applies to the
selected layers (all layers with none selected) through apply_reveal and
leaves the others as they are; `reveal` stays the last shortcut's filter
for toggling and Shift-adding. U's results replace only the target
layers' earlier picks, so U after UU shows the keyframed properties
again. anim.reveal tells the Timeline which layers it looked at, so a
layer with nothing keyframed closes. Twirling a layer by hand, Ctrl+`
and Collapse All clear the filters they cover.
Test: reveal_shortcuts_leave_unselected_layers_alone.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>