From 3db5aebb33855df7e661f07e1c64e0bbac911338 Mon Sep 17 00:00:00 2001 From: Marco Arnulfo Date: Tue, 13 Oct 2026 03:47:24 +0200 Subject: [PATCH] Graph editor: speed graph handles keep dragging once their key leaves the plot (fixes #1171) --- crates/ui-egui/src/panels/graph.rs | 5 ++++- crates/ui-egui/tests/ui_keyframes.rs | 28 ++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/crates/ui-egui/src/panels/graph.rs b/crates/ui-egui/src/panels/graph.rs index 77e39c62..0ba5d2d1 100644 --- a/crates/ui-egui/src/panels/graph.rs +++ b/crates/ui-egui/src/panels/graph.rs @@ -376,7 +376,10 @@ pub(crate) fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, p: &egui::Painte let (lid, uid, d) = (c.layer.id, c.prop.uid, c.dim); for k in &c.keys { let kp = pos2(tm.x(k.t), ymap(k.v)); - if !plot.expand(6.0).contains(kp) { + // In the speed graph an end key rises with the handle being dragged; skipping it once it + // leaves the plot would drop the handle's widget and end the drag at the top edge. + let handle_dragged = || ["in", "out"].iter().any(|side| ctx.is_being_dragged(egui::Id::new(("ghandle", uid, d, k.idx, *side)))); + if !plot.expand(6.0).contains(kp) && !handle_dragged() { continue; } let kjson = json!({"layer": lid.0, "prop": uid, "time": k.time.seconds()}); diff --git a/crates/ui-egui/tests/ui_keyframes.rs b/crates/ui-egui/tests/ui_keyframes.rs index 74352824..e5b2cae9 100644 --- a/crates/ui-egui/tests/ui_keyframes.rs +++ b/crates/ui-egui/tests/ui_keyframes.rs @@ -435,3 +435,31 @@ fn ctrl_alt_scrub_in_the_timeline_is_proportional() { assert!(moved[0].abs() < 1e-9, "the first selected layer stays: {moved:?}"); assert!((moved[1] - moved[2] / 2.0).abs() < 1e-6, "the middle one moves by half: {moved:?}"); } + +/// #1171: in the speed graph an end key sits at its one side's speed, so it rises with the handle +/// being dragged; once it left the plot its handles were skipped and the drag stopped at the top +/// edge. A drag that ends above the graph keeps raising the speed as far as the pointer goes. +#[test] +fn speed_graph_handle_drags_upward_past_the_top() { + let (mut h, id, uid) = harness(); + let s = &mut h.state_mut().session; + s.execute("prop.select", json!({"layer": id.0, "prop": uid})).unwrap(); + s.execute("keys.selectAll", json!({})).unwrap(); + s.execute("keys.easyEase", json!({})).unwrap(); + h.state_mut().ui.timeline.graph_editor = true; + h.state_mut().ui.timeline.graph_mode = "speed".into(); + h.run_steps(4); + let out_speed = |h: &Harness<'_, EffectcraftApp>| { + h.state().session.active_comp().unwrap().layer(id).unwrap().props.prop("transform/opacity").unwrap().keys[0].out_ease[0].speed + }; + let e = h.state().auto.find(&format!("timeline.graph.handle.{uid}.0.0.out")).cloned().expect("the first key's out handle shows"); + let at = pos2(e.rect[0] + e.rect[2] / 2.0, e.rect[1] + e.rect[3] / 2.0); + drag(&mut h, at, at + egui::vec2(0.0, -40.0), Default::default()); + let short = out_speed(&h); + h.state_mut().session.execute("edit.undo", json!({})).unwrap(); + h.run_steps(4); + assert_eq!(out_speed(&h), 0.0); + drag(&mut h, at, at + egui::vec2(0.0, -400.0), Default::default()); + let long = out_speed(&h); + assert!(short > 1.0 && long > 9.0 * short, "40 px up gave {short}, 400 px up gave {long}"); +}