Timeline: the pick whip scrolls the layers at their edges (#362)
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>
This commit is contained in:
parent
0b2ef8e584
commit
639342a5e7
|
|
@ -132,6 +132,20 @@ fn pick_whip_id() -> egui::Id {
|
|||
egui::Id::new("tl-pickwhip")
|
||||
}
|
||||
|
||||
/// Autoscroll speed (points per second, up negative) of a drag held at height `y` within
|
||||
/// `zone` of the top or bottom edge of `rows`, or past it: faster the deeper.
|
||||
fn edge_scroll_speed(rows: Rect, y: f32, zone: f32) -> f32 {
|
||||
let zone = zone.min(rows.height() / 2.0).max(1.0);
|
||||
let depth = if y < rows.min.y + zone {
|
||||
y - (rows.min.y + zone)
|
||||
} else if y > rows.max.y - zone {
|
||||
y - (rows.max.y - zone)
|
||||
} else {
|
||||
return 0.0;
|
||||
};
|
||||
(depth / zone).clamp(-3.0, 3.0) * 400.0
|
||||
}
|
||||
|
||||
/// The text of an expression being edited when its pick whip was pressed, and the selection in
|
||||
/// it (characters): the pick whip's reference goes in there.
|
||||
type WhipInto = (String, [usize; 2]);
|
||||
|
|
@ -1552,7 +1566,18 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
|||
}
|
||||
}
|
||||
}
|
||||
app.ui.timeline.scroll_y = app.ui.timeline.scroll_y.min(max_scroll);
|
||||
// A pick whip held near the top or bottom of the layers (or past it) scrolls them, as in
|
||||
// After Effects.
|
||||
if ctx.data(|d| d.get_temp::<PickWhip>(pick_whip_id())).is_some()
|
||||
&& let Some(ptr) = ctx.input(|i| i.pointer.latest_pos()).filter(|p| rect.x_range().contains(p.x))
|
||||
{
|
||||
let speed = edge_scroll_speed(rows_rect, ptr.y, rh);
|
||||
if speed != 0.0 {
|
||||
app.ui.timeline.scroll_y += speed * ctx.input(|i| i.stable_dt).min(0.1);
|
||||
ctx.request_repaint();
|
||||
}
|
||||
}
|
||||
app.ui.timeline.scroll_y = app.ui.timeline.scroll_y.clamp(0.0, max_scroll);
|
||||
let snap_project = app.session.project.clone();
|
||||
let snap_expr = app.session.expr.clone();
|
||||
let ectx = EvalCtx { project: &snap_project, comp_id: cid, comp: &comp, time, expr: snap_expr.as_deref(), footage: None };
|
||||
|
|
|
|||
|
|
@ -128,6 +128,51 @@ fn the_expression_pick_whip_inserts_at_the_cursor_while_editing() {
|
|||
assert_eq!(expr(&h), "transform.rotation");
|
||||
}
|
||||
|
||||
/// #362: an expression pick whip held near the bottom or top of the Timeline's layers scrolls
|
||||
/// them (After Effects), so a property out of view can be picked.
|
||||
#[test]
|
||||
fn the_pick_whip_scrolls_the_timeline_at_its_edges() {
|
||||
let (mut h, _) = harness();
|
||||
let (top, opacity, transform) = {
|
||||
let s = &mut h.state_mut().session;
|
||||
for i in 0..40 {
|
||||
s.execute("layer.newSolid", json!({"name": format!("Layer {i}"), "color": "#406080"})).unwrap();
|
||||
}
|
||||
let l = s.execute("layer.newSolid", json!({"name": "Top", "color": "#406080"})).unwrap()["layer"].as_u64().unwrap();
|
||||
s.execute("prop.setExpression", json!({"layer": l, "path": "transform/opacity", "expression": "50"})).unwrap();
|
||||
let tr = s.active_comp().unwrap().layer(effectcraft_engine::project::LayerId(l)).unwrap().props.sub("transform").unwrap().clone();
|
||||
(l, tr.get("opacity").unwrap().uid, tr.uid)
|
||||
};
|
||||
h.state_mut().ui.timeline.open_layers.insert(top);
|
||||
h.state_mut().ui.timeline.open_groups.insert(transform);
|
||||
h.run_steps(3);
|
||||
let scroll = |h: &Harness<'_, EffectcraftApp>| h.state().ui.timeline.scroll_y;
|
||||
let rows = h.state().auto.find("timeline.scroll").unwrap().rect;
|
||||
let from = center(&h, &format!("timeline.prop.{opacity}.pickWhip"));
|
||||
let (bottom, upper) = (pos2(from.x, rows[1] + rows[3] - 2.0), pos2(from.x, rows[1] + 2.0));
|
||||
h.input_mut().events.push(Event::PointerMoved(from));
|
||||
h.step();
|
||||
h.input_mut().events.push(Event::PointerButton { pos: from, button: PointerButton::Primary, pressed: true, modifiers: Modifiers::NONE });
|
||||
h.step();
|
||||
for k in 1..=8 {
|
||||
h.input_mut().events.push(Event::PointerMoved(from + (bottom - from) * (k as f32 / 8.0)));
|
||||
h.step();
|
||||
}
|
||||
h.run_steps(20);
|
||||
let down = scroll(&h);
|
||||
assert!(down > 50.0, "held at the bottom edge the layers scroll up: {down}");
|
||||
h.input_mut().events.push(Event::PointerMoved(upper));
|
||||
h.run_steps(10);
|
||||
assert!(scroll(&h) < down, "and at the top edge back down: {} after {down}", scroll(&h));
|
||||
h.input_mut().events.push(Event::PointerMoved(pos2(from.x, rows[1] + rows[3] / 2.0)));
|
||||
h.run_steps(2);
|
||||
let still = scroll(&h);
|
||||
h.run_steps(10);
|
||||
assert_eq!(scroll(&h), still, "away from the edges it stays");
|
||||
h.input_mut().events.push(Event::PointerButton { pos: upper, button: PointerButton::Primary, pressed: false, modifiers: Modifiers::NONE });
|
||||
h.run_steps(3);
|
||||
}
|
||||
|
||||
/// A click at `p` with `modifiers` held.
|
||||
fn click_with(h: &mut Harness<'_, EffectcraftApp>, p: Pos2, modifiers: Modifiers) {
|
||||
h.input_mut().events.push(Event::PointerMoved(p));
|
||||
|
|
|
|||
|
|
@ -166,7 +166,7 @@ the disk cache in the formats wave; the 3D Camera Tracker in M12.6.)
|
|||
| Automation | ≈ 99% | 0.1 | `.jsxbin` (AUT-2: concave / self-intersecting `onDraw` fills, real images in `drawImage`, image controls and icon buttons landed in M13.11; ScriptUI resource strings, `onDraw`/ScriptUIGraphics, live `onChanging`, `Socket`, Essential Graphics hooks — `addToMotionGraphicsTemplate(As)`, `canAddToMotionGraphicsTemplate`, `exportAsMotionGraphicsTemplate`, `motionGraphicsTemplateName`, controller count/names — and Watch Folder landed in M13.7; the core object model landed in M14.4; ScriptUI windows/dialogs/dockable panels with `scriptui.*` agent commands, File ▸ Scripts install + sample scripts, and the effect plug-in API (EFF-6, WebAssembly) landed in M13.1) |
|
||||
| Shapes | ≈ 80% | 1.5 | Lottie can't carry stroke taper/wave (stroke Taper and Wave, Dash 2/Gap 2/Dash 3/Gap 3 and radial-gradient Highlight Length/Angle landed in M13.5; pen tool for shape paths and vertex editing in M6.5) |
|
||||
| Compositions | ≈ 81% | 3.3 | Mocha-style planar tracks for templates; nested comp markers on the precomp layer bar (Preserve frame rate / resolution when nested and nested comps showing nothing past their end landed in M4.9–M4.10; Pre-compose keeping the comp's settings and New Comp from Selection's dialog in M3.12; CMP-7: Essential Graphics mirrored and linked properties landed in M13.11; Font and uniform Scale controls, Composition ▸ Open in Essential Graphics, Save Frame As ▸ Photoshop Layers / ProEXR and the VR Comp Editor landed in M13.7; the marker dialog, Composition Flowchart, Essential Graphics with master properties, `.ectemplate` templates and Responsive Design — Time landed: CMP-6, CMP-7) |
|
||||
| Animation | 70% | 9.0 | puppet depth beyond pins, recording, rigging and follow-through (G2: an angle's revolutions scrub and take typing in Effect Controls, the Timeline and Properties, #93; puppet pin recording with Record Options landed in M13.1; pin selection, rotate/scale handles, nulls for pins and Follow-Through in M13.14–M13.16), Wiggler/Smoother/Motion Sketch (motion-path handles and the graph editor transform box landed in M5.8; keyframe colour labels and Select Keyframe Label Group, Graph Editor snapping to markers / layer ends in M13.5) |
|
||||
| Animation | 70% | 9.0 | puppet depth beyond pins, recording, rigging and follow-through (G2: the expression pick whip scrolls the Timeline when held at its top or bottom, #362; an angle's revolutions scrub and take typing in Effect Controls, the Timeline and Properties, #93; puppet pin recording with Record Options landed in M13.1; pin selection, rotate/scale handles, nulls for pins and Follow-Through in M13.14–M13.16), Wiggler/Smoother/Motion Sketch (motion-path handles and the graph editor transform box landed in M5.8; keyframe colour labels and Select Keyframe Label Group, Graph Editor snapping to markers / layer ends in M13.5) |
|
||||
| Text | ≈ 96% | 0.3 | no extruded strokes (G2: the font menus list every installed family with its own styles — before, only a project asking for a font read the system fonts — and `text.fonts` / MCP `list_fonts` list them for agents; M13.12: the Variable Font Axes animator re-spaces the text — advances follow the animated axes; M13.6: variable font axes in the character style — `layer.setText variations`, the Character panel's Variable Font Axes fields — shape with HVAR / gvar advances and draw at that design-space position; OpenType features — stylistic sets, discretionary ligatures, contextual / stylistic alternates, swash, titling, ordinals, fractions, figure styles, true small caps / all small caps and superior / inferior glyphs with faux fallback — per character with the Character panel's OpenType popup and `text.fontFeatures` landed in M13.2; vertical Roman / Tate-Chu-Yoko, forced LTR paragraphs, caret on animated and path text, Variable Font Axes and Lottie style runs landed in M13.5; extruded, bevelled text in M7.6; per-character styles, paragraph settings, on-canvas editing and the `sourceText` style API in M9.9–M9.10) |
|
||||
| Web | 98% | 0.1 | No shared-memory threads inside one engine instance (the decided design is one engine instance per worker; a threaded build needs nightly `build-std`) (M13.30: Render Queue, analyses and Content-Aware Fill render on each job worker's own WebGPU device in passes, particles and Advanced 3D read back under keys, layer buffers in the browser's disk cache; browser storage, Web Audio, Web Worker renders/analyses, WebGPU viewer and offline install landed in M15.2; viewer frames in frame workers fed by project diffs, Roto Brush propagation in a worker, non-blocking `wait: true` jobs and a browser Media Browser (File System Access folders, browser storage) in M13.10; WEB-1 in M13.24: GPU effects and the GPU compositor in the frame workers on their own WebGPU devices with deferred readbacks (frames render in passes; Backend Auto per comp), the disk cache in the Origin Private File System (written by the workers with sync access handles, LRU under the settings' limit, served after a reload), the storage manager (Settings ▸ Disk ▸ Browser Storage, `storage.info` / `storage.persist` / `storage.clear`), and a WGSL constant Chrome rejected (it disabled every GPU kernel in Chrome) fixed) |
|
||||
| 3D | 88% | 5.5 | reflections of other layers (Advanced 3D surfaces reflect only the Environment light; Material Options ▸ Appears in Reflections is stored but has no visible effect yet, #261), multi-view layouts, the Extended Viewer for Advanced 3D comps (Classic 3D Extended Viewer landed in M13.5 UI completion); collapsed precomps of another size seen through the parent's camera render (fixed in M13.2); stereo rigs, orbit nulls, lights controlled by the camera, cameras/lights from glTF models, environment backgrounds, Advanced 3D motion blur, blend modes and track mattes landed in M7.7; Classic 3D iris-shaped bokeh with highlights, progressive depth of field on tilted layers and the focus-link commands landed in M13.5; Advanced 3D depth of field with the iris and highlight options, collapsed precomps as real Advanced 3D geometry and extruded text/shape strokes landed in M13.8; Advanced 3D (glTF/OBJ models, primitives, extruded text and shapes, PBR, image-based light, shadow maps, GPU rasteriser) in M7.4–M7.6; Advanced 3D end to end on the GPU (motion blur, iris depth of field, compositing) in M13.11 |
|
||||
|
|
|
|||
Loading…
Reference in New Issue