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 <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
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 <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>
Preserve Project multi-selection when dropping onto the Timeline or Composition viewer. Keep stack order and drop position, skip non-layer project items, and add pointer-drag regression coverage.
- automation / cli autosave restart tests: the MCP instructions embed the
auto-save state as JSON, so a Windows path's backslashes are escaped there;
look for the JSON form of the path.
- ui_gpu: egui_kittest prefers a CPU adapter, which on Windows is WARP on
DirectX 12, where FXC fails to compile the compositor's `pointwise` kernel
("compilation aborted unexpectedly"), so the viewer stayed on the CPU and
the test failed before comparing anything. Build the harness on the adapter
the headless compositor picked (falling back to egui_kittest's choice).
- ui_audio_scrub (#103 test): with a fixed 2 s comp, a machine with many
render threads cached the whole preview before the first step, so the sound
rightly started at once and no silent frame was counted (3 of 24 runs
failed with 8 copies in parallel). Size the comp to four times the frames
playback requests per step, so caching takes at least four steps (0 of 24).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every pointer move replaces the comp's content identity, so the frame on
screen takes a new cache key at every step while its render is still in
flight; the viewer only ever showed an exact key, so the picture froze on
the last completed frame until the drag ended. While a drag or a property
scrub is running, show the newest frame of the same comp, frame, scale,
view and render options that is ready — a render behind, as After Effects
does — and go back to exact frames when it stops.
The drag preview was a 1 px axis-aligned rectangle for every shape tool,
which read as no preview at all against most comps. Draw the tool's own
path instead (rectangle, rounded rectangle, ellipse, polygon, star), from
the engine's own geometry, filled with the Tools bar's Fill and outlined
in its Stroke, with a caption saying where the shape lands — a mask on
the selected layer, into the selected shape layer, or a new shape layer.
The ghost and the released shape go through one normalisation, so the
drag shows exactly what it commits: Alt (Option) draws from the press
point and Shift squares the box, both live. The drag also starts at the
press point now, not where the 6 px drag threshold was crossed, so the
shape is drawn from where the button went down, as in After Effects.
Cause: clipboard events carry no modifiers, and the Cut and Copy/Paste
mapping read i.modifiers, egui's state after all of the frame's events.
A Shift+Delete with Shift released in the same frame became Ctrl+X (no
deletion), and a Ctrl+X followed in the same frame by Ctrl up and Shift
down became Shift+Delete: the selected items and the layers using them
deleted without the prompt.
Fix: walk the frame's events in order from the modifiers held when it
began (the state the last pass ended with, recorded each pass), so each
Cut, Copy and Paste uses the modifiers at its place in the stream. When
that state isn't known (a pass was skipped), a Cut is never Shift+Delete.
Only Shift alone counts, not Ctrl/Alt+Shift.
Tests: a_cut_is_read_with_the_modifiers_where_it_is_in_the_frame queues
modifier changes and a Cut in one frame (Shift then Cut and Shift up;
Ctrl, Cut, Ctrl up, Shift down; and the mixed orders); the Windows
Shift+Delete helper now sends Shift, Cut and Shift up as one frame. They
fail on the previous mapping and pass now.
With the Project panel focused, Delete Project Items Without Confirmation
runs project.delete on the selection: items compositions use go without
the prompt, with their layers, in one undo step. In the other panels it
does nothing, and like every shortcut it doesn't fire while typing or
with a dialog open.
On Windows the windowing layer sends Shift+Delete as Cut (the old Cut
key), which was mapped to Ctrl+X: in the Project panel, which has no
clipboard, a Cut with only Shift held is Shift+Delete now. Elsewhere it
still cuts.
Tests (ui_project_delete): Shift+Delete as a key and as Windows' Cut
deletes a comp, a solid in use, footage and a folder without the prompt
and one undo restores them; a multi-selection with a folder holding a
solid in use goes together; an empty selection deletes nothing; the next
Delete still asks after Shift+Delete or an answered prompt; it does
nothing in the Composition panel, while renaming or with a dialog open;
Shift+Backspace, Alt/Ctrl+Delete and Ctrl/Alt+Shift+Delete don't delete.
The deleting tests fail without this change.
What: the Composition panel's Resolution (Auto, Full, Half, Third, Quarter, Custom) is now a
property of the composition (`Comp::resolution`, moved with its `Resolution` type from the UI
into the project crate) instead of one global viewer setting. Switching to another comp shows
that comp's resolution, switching back shows the first one's again, and saving and reopening the
project keeps them. `view.res.full|half|third|quarter|custom` and the new engine command
`view.res.auto` set it (headless too, with an optional `comp`); `ui.set {viewer: {res}}` sets
the active comp's and `ui.inspect` still reports `ui.viewer.res`; `comp.info` reports
`resolution` and `resolutionFactor`; View ▸ Resolution's check marks come from the engine.
Why: After Effects saves each composition's Resolution with the project (CompItem.resolutionFactor
is a per-comp attribute). As there, changing it marks the project modified but adds no undo step:
undo, redo and History jumps carry the current values over to the restored state. It is not part
of the comp's pixels (`Comp::same_pixels` ignores it and `Comp::pixel_form` normalises it), so
changing it keeps the comp's RAM and disk cache keys. Auto is not written (serde default and
skip_serializing_if), so projects saved before open with Auto and older versions read new files.
The magnification is not saved per comp: Adobe's documentation doesn't say it is.
Tests: project `comp_resolution_is_saved_and_old_projects_load_with_auto`,
`resolution_factors_and_scales`; engine
`each_comp_keeps_its_resolution_outside_the_undo_history_and_in_the_file` (switching comps,
no undo step, undo/redo keep it, save/open round trip, menu check marks, Custom dialog);
ui-egui `each_comp_keeps_its_viewer_resolution` (the viewer popup on comp A, comp B through
`ui.set`, back to A, unchanged frame cache identity, save/open). They don't build before the
change (no `Comp::resolution`, no `view.res.auto` engine command).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Cause: two things kept Arabic out of a layer name on Windows.
- Windows switches the keyboard layout with Alt+Shift. Releasing Shift
before Alt left Alt held alone, and the in-window menu bar (#279)
counted the release as an Alt tap: it took the keyboard focus, the
rename field closed, and the letters typed next were swallowed by the
menu bar. The Project panel's rename field lost them the same way.
- The UI fonts are Inter plus the CJK system fallbacks, so Arabic,
Hebrew, Indic and Thai names that did arrive (pasted, or from a
project) showed as missing-glyph boxes.
Fix:
- An Alt tap only counts when Alt went down with no other modifier
held, so Alt+Shift in either release order is no tap; a plain Alt tap
still focuses the menu bar.
- The UI registers installed system faces for Arabic, Hebrew, Devanagari,
Bengali, Gurmukhi, Gujarati, Tamil, Telugu, Kannada, Malayalam and Thai
after the CJK ones, the same way (native only; the web build has no
system fonts). A face covering several scripts is registered once
(Segoe UI: Arabic and Hebrew; Nirmala UI: every Indic script).
- The text engine's fallback tries each script's plain platform families
(Segoe UI, Nirmala UI, Leelawadee UI, Geeza Pro, Kohinoor, Noto…)
before any covering face, so neither the UI nor a text layer gets a
calligraphic face that merely sorts first.
- The Timeline's rename field registers `timeline.layer.<id>.nameEdit`.
egui 0.36 shapes each run of one face, so Arabic letters join and a word
reads right to left; it has no bidirectional layout, so the words of a
multi-word Arabic name (and mixed Arabic/Latin) still follow each other
left to right.
Tests: ui_inline_edit a_layout_switch_keeps_the_name_field_and_arabic_
names_are_kept (Alt+Shift both ways in the layer rename, Arabic typed and
stored; Hebrew/Devanagari/Thai in the Project panel; a plain Alt tap
still focuses File; failed before: the field closed), theme
installed_script_fallbacks_draw_arabic_hebrew_indic_and_thai_names
(failed before: replacement glyph for Arabic) and
arabic_words_are_joined_and_read_right_to_left, text
other_scripts_fall_back_to_a_plain_system_family and the script
classification.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A same-named file is used only when kind, size, codec, and for moving pictures the rate and duration agree. Symlinks are not followed, and duplicate names are left alone.
What: each property row that can take an expression (it has a stopwatch) now
has a property pick whip in the Parent & Link column, as in After Effects
CC 2018 and later (automation id `timeline.prop.<uid>.propertyPickWhip`).
Dragged onto another property's name or one of its values, it runs
`prop.pickWhip`: the reference is added as the property's expression, or
replaces the one it has, and the expression is enabled. A drop on one value
picks that dimension (`position[1]`).
The parent, expression and property pick whips share one widget and one
drop path (rubber band, autoscroll, dimension pick); only the expression
pick whip inserts at the cursor of an expression being edited.
Pick whips (expression and property) now also drop on properties in
Effect Controls: its property rows register as targets while a pick whip
is dragged, with their per-dimension value fields.
Why: the last part of #363. Adobe's docs say the Property Link pick whip
adds the expression without enabling expressions first, and a pick whip
outside an edit replaces the whole expression text. No modifier keys for
the property pick whip are documented, so none are implemented.
Tests: `the_property_pick_whip_links_a_property_to_another` (ui_inline_edit)
drops it on a property's name, on one of Position's values, on a property
with a disabled expression (replaced and enabled), on an Effect Controls
slider and one value of a point, and on itself (no change). It fails
before the change; with Effect Controls' targets removed it fails at the
slider. Looked at the result with the headless snapshot.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Right-clicking a property's name in the Timeline did nothing, so
Position's dimensions could be separated only from the Graph Editor.
As in After Effects, it now opens the property's menu: Reset, Edit
Value…, Separate Dimensions (Position and, once separated, X / Y / Z
Position, checked), Add / Remove Expression and Add Property to
Essential Graphics. Only Position separates, as in After Effects
(Scale stays one property).
`prop.reset` without `default` used to leave the value as it was: it
now gives the property its own default (an effect parameter's, or a
Transform property's, shared with Layer ▸ Transform ▸ Reset), which
also makes the Properties panel's Reset reset the values. `prop.set
{layer, prop}` without `value` opens the value dialog, like
`keys.set`.
Tests: timeline_property_names_have_the_property_menu
(ui_blank_context_menus), reset_gives_a_property_its_default (engine).
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>
The inline expression field took its height from the saved
expression, so lines typed into it ran over the rows below until the
expression was committed.
Its height now comes from the text being typed (up to 8 lines, as
before), and its bottom edge drags it to any height in whole lines, as
in After Effects; the dragged height is kept per property in the
Timeline state (`expr_lines`).
Tests: the_expression_field_grows_while_editing_and_drags_taller
(ui_inline_edit).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Dropping the expression pick whip on one of a property's values (the X
of Position) referenced the whole property. As in After Effects, a
drop on one value now picks that dimension: `transform.position[0]`;
a drop on the name still picks the whole property.
`prop.pickWhip` takes `target.dimension`.
The rest of #363 is unchanged: inserting at the cursor landed with
#284, and After Effects' property pick whip in the Parent & Link
column is left for later.
Tests: pick_whip_picks_one_dimension_of_the_target (engine),
the_expression_pick_whip_picks_the_dimension_it_is_dropped_on
(ui_inline_edit).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A pick whip dragged to the top or bottom of the Timeline's layers left
them where they were, so a property out of view couldn't be picked.
While a pick whip (expression or parent) is held within a row of the
top or bottom edge of the layers, or past it, the layers scroll that
way, faster the deeper, as in After Effects.
Tests: the_pick_whip_scrolls_the_timeline_at_its_edges
(ui_inline_edit).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Time Display Style could only be changed in Project Settings. As in
After Effects, Ctrl-click (Cmd-click on macOS) on the current-time
display of the Timeline or the Composition panel now toggles it between
Timecode and Frames (Feet + Frames goes to Timecode). It runs
`file.projectSettings {timeDisplay}`, so it is one undo step.
Under the Timeline's display the other style shows now (frames under
timecode, timecode under frames), as in After Effects; it always showed
frames.
Tests: ctrl_click_on_the_time_display_toggles_timecode_and_frames
(ui_inline_edit).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every bounding-box handle in the Composition viewer showed the
north-west / south-east resize cursor, so two corners and all four
edges showed the wrong direction.
Each handle now takes the cursor along the direction it scales, worked
out from the on-screen box: a corner points between its two edges (the
box's diagonal whatever its proportions), an edge handle across its
edge. The cursors turn with the layer's rotation, as in After Effects.
Tests: handle_cursors_follow_each_handle_and_the_layer_rotation
(ui_viewer: a wide box at 0°, 90° and 45°).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Cause: pipeline::Cx rendered with Backend::Auto, which picks the CPU or the
GPU compositor for each top-level frame from measured frame times
(render::AutoPick: warm-up on both sides, a re-probe every 48 frames). The
two compositors differ slightly, so frames came out differently depending
on which side the timings favoured in that run, and two GPU exports of the
same frames differed.
Fix: the job renders every frame on the project's renderer, as After
Effects renders a job with one renderer: Backend::Gpu with Mercury GPU
Acceleration (frames the GPU declines still render on the CPU, as they do
in every GPU render), Backend::Cpu with Mercury Software Only. The viewer
and previews keep Auto. Docs (architecture, parity, web) say so.
Tests: render_settings::a_job_renders_on_one_compositor exports ten frames
with a stand-in GPU compositor that keeps Auto history and draws solid
green: with Mercury GPU Acceleration every frame is green and two runs are
identical (with Auto the warm-up sent frames to the CPU: it fails), with
Software Only the stand-in is never asked. The web worker GPU job test's
comment no longer describes Auto.
Diagnosis by @pat-the-wizard.
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>