From 958e9dc2f8b4225965edc83062948d8b62350795 Mon Sep 17 00:00:00 2001 From: Zinadin Zidan Date: Thu, 5 Nov 2026 11:44:34 +0600 Subject: [PATCH 1/5] Timeline: a double-click on the composition marker bin adds a marker at the current time (#1826) The bin at the right end of the time ruler only answered a drag, so a marker had to be dragged out to where the current time was. As in After Effects, a double-click on it now adds a composition marker at the current time, one undo step. A single click still adds nothing, and dragging a marker out of the bin works as before. Co-Authored-By: Claude Opus 5.5 --- crates/ui-egui/src/panels/markers_ui.rs | 9 +++- crates/ui-egui/tests/it/ui_timeline_zoom.rs | 46 +++++++++++++++++++++ 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/crates/ui-egui/src/panels/markers_ui.rs b/crates/ui-egui/src/panels/markers_ui.rs index 54f06d6b6..c24b8a1ca 100644 --- a/crates/ui-egui/src/panels/markers_ui.rs +++ b/crates/ui-egui/src/panels/markers_ui.rs @@ -428,9 +428,16 @@ pub(crate) fn comp_markers(app: &mut EffectcraftApp, ui: &mut egui::Ui, comp: &C /// (#275): drag a marker out of it and it follows the pointer along the marker strip `strip` /// (snapping like a dragged marker); dropping it there adds a composition marker at that time, /// one undo step. Dropped back on the bin or outside the time graph's width, nothing is added. +/// A double-click on the bin adds one at the current time (#1826). pub(crate) fn marker_bin(app: &mut EffectcraftApp, ui: &mut egui::Ui, comp: &Comp, tm: TMap, bin: Rect, strip: Rect) { let t = app.tokens; - let resp = ui.interact(bin, egui::Id::unique("marker-bin"), Sense::drag()).on_hover_text(tr("Composition marker bin: drag a marker to the time ruler")); + let resp = + ui.interact(bin, egui::Id::unique("marker-bin"), Sense::click_and_drag()).on_hover_text(tr("Composition marker bin: drag a marker to the time ruler")); + if resp.double_clicked() + && let Err(e) = app.session.execute("comp.addMarker", json!({})) + { + app.ui.status = e.to_string(); + } app.auto.add("timeline.markerBin", bin, "Composition marker bin"); let hot = resp.hovered() || resp.dragged(); if hot { diff --git a/crates/ui-egui/tests/it/ui_timeline_zoom.rs b/crates/ui-egui/tests/it/ui_timeline_zoom.rs index d05ffae64..8ac40c24c 100644 --- a/crates/ui-egui/tests/it/ui_timeline_zoom.rs +++ b/crates/ui-egui/tests/it/ui_timeline_zoom.rs @@ -376,6 +376,52 @@ fn dragging_out_of_the_marker_bin_adds_a_comp_marker() { assert_eq!(markers(&h), vec![13.0, 20.0]); } +/// #1826: as in After Effects, a double-click on the composition marker bin adds a composition +/// marker at the current time, one undo step; a single click adds none, and a marker dragged +/// out of the bin still lands where it is dropped. +#[test] +fn double_clicking_the_marker_bin_adds_a_comp_marker_at_the_current_time() { + let (mut h, _) = harness(); + h.state_mut().ui.timeline.pps = Some(200.0); + h.state_mut().ui.timeline.start = 10.0; + h.state_mut().session.execute("edit.deselectAll", json!({})).unwrap(); + h.state_mut().session.execute("time.set", json!({"time": 21.5})).unwrap(); + h.run_steps(2); + let markers = |h: &Harness<'_, EffectcraftApp>| -> Vec { h.state().session.active_comp().unwrap().markers.iter().map(|m| m.time.seconds()).collect() }; + let bin = rect(&h, "timeline.markerBin"); + let ruler = rect(&h, "timeline.ruler"); + // Presses and releases in one frame each, so a pair falls inside egui's double-click time. + let clicks = |h: &mut Harness<'_, EffectcraftApp>, n: usize| { + h.event(Event::PointerMoved(bin.center())); + h.step(); + for _ in 0..n { + for pressed in [true, false] { + h.input_mut().events.push(Event::PointerButton { + pos: bin.center(), + button: egui::PointerButton::Primary, + pressed, + modifiers: Modifiers::NONE, + }); + } + } + h.step(); + h.run_steps(3); + }; + clicks(&mut h, 1); + assert!(markers(&h).is_empty(), "a single click: {:?}", markers(&h)); + let undo = h.state().session.history.undo.len(); + clicks(&mut h, 2); + assert_eq!(markers(&h), vec![21.5], "at the current time"); + let s = &h.state().session; + assert_eq!(s.active_comp().unwrap().markers[0].time, s.time(), "exactly the current time"); + let history = &h.state().session.history.undo; + assert_eq!((history.len(), history.last().map(|u| u.0.as_str())), (undo + 1, Some("Add Marker")), "one undo step"); + assert_eq!(h.state().session.time().seconds(), 21.5, "the current time stays"); + // Dragged out 3 s into the visible span (from 10 s at 200 px/s): a marker at 13 s too. + drag(&mut h, bin.center(), pos2(ruler.min.x + 6.0 + 600.0, ruler.center().y)); + assert_eq!(markers(&h), vec![13.0, 21.5]); +} + /// #976: as in After Effects, Numpad * marks the selected layers at the current time; only with /// no layer selected does it add a composition marker. #[test] From 5cb2658e382c5d8aac052b24bae8809da7506272 Mon Sep 17 00:00:00 2001 From: Zinadin Zidan Date: Thu, 5 Nov 2026 17:47:44 +0600 Subject: [PATCH 2/5] Timeline: a marker dragged out of the bin and dropped within one frame is added again (#1826) Sensing clicks on the marker bin as well as drags held the drag back until the pointer had moved, so a drag out that was released in that same frame (a fast drag, or a slow frame) added nothing. The bin senses drags only again, so its drag starts the moment it is pressed, as before #1826, and the double-click is read from the pointer over it. The double-click test now also gives the second press and its release frames of their own, with the bin dragged in between. Co-Authored-By: Claude Opus 5.5 --- crates/ui-egui/src/panels/markers_ui.rs | 11 +++--- crates/ui-egui/tests/it/ui_timeline_zoom.rs | 40 ++++++++++++++++++++- 2 files changed, 45 insertions(+), 6 deletions(-) diff --git a/crates/ui-egui/src/panels/markers_ui.rs b/crates/ui-egui/src/panels/markers_ui.rs index c24b8a1ca..5bdd04ad4 100644 --- a/crates/ui-egui/src/panels/markers_ui.rs +++ b/crates/ui-egui/src/panels/markers_ui.rs @@ -431,11 +431,12 @@ pub(crate) fn comp_markers(app: &mut EffectcraftApp, ui: &mut egui::Ui, comp: &C /// A double-click on the bin adds one at the current time (#1826). pub(crate) fn marker_bin(app: &mut EffectcraftApp, ui: &mut egui::Ui, comp: &Comp, tm: TMap, bin: Rect, strip: Rect) { let t = app.tokens; - let resp = - ui.interact(bin, egui::Id::unique("marker-bin"), Sense::click_and_drag()).on_hover_text(tr("Composition marker bin: drag a marker to the time ruler")); - if resp.double_clicked() - && let Err(e) = app.session.execute("comp.addMarker", json!({})) - { + let resp = ui.interact(bin, egui::Id::unique("marker-bin"), Sense::drag()).on_hover_text(tr("Composition marker bin: drag a marker to the time ruler")); + // The bin senses drags only, so a drag starts as soon as it is pressed: sensing clicks too + // would hold that back until the pointer moved, and a drag out released within that frame + // would add nothing. The double-click is read from the pointer instead. + let double_clicked = resp.contains_pointer() && ui.input(|i| i.pointer.button_double_clicked(egui::PointerButton::Primary)); + if double_clicked && let Err(e) = app.session.execute("comp.addMarker", json!({})) { app.ui.status = e.to_string(); } app.auto.add("timeline.markerBin", bin, "Composition marker bin"); diff --git a/crates/ui-egui/tests/it/ui_timeline_zoom.rs b/crates/ui-egui/tests/it/ui_timeline_zoom.rs index 8ac40c24c..78535e3dd 100644 --- a/crates/ui-egui/tests/it/ui_timeline_zoom.rs +++ b/crates/ui-egui/tests/it/ui_timeline_zoom.rs @@ -417,9 +417,47 @@ fn double_clicking_the_marker_bin_adds_a_comp_marker_at_the_current_time() { let history = &h.state().session.history.undo; assert_eq!((history.len(), history.last().map(|u| u.0.as_str())), (undo + 1, Some("Add Marker")), "one undo step"); assert_eq!(h.state().session.time().seconds(), 21.5, "the current time stays"); + // The second press in one frame and its release in the next (a frame here is 0.25 s, inside + // egui's 0.3 s double-click time): the bin is dragged by then, and still a marker at 22 s. + h.state_mut().session.execute("time.set", json!({"time": 22.0})).unwrap(); + h.event(Event::PointerMoved(bin.center())); + h.step(); + for pressed in [true, false, true] { + h.input_mut().events.push(Event::PointerButton { pos: bin.center(), button: egui::PointerButton::Primary, pressed, modifiers: Modifiers::NONE }); + } + h.step(); + assert!(h.ctx.dragged_id().is_some(), "the second press drags the bin"); + h.event(Event::PointerButton { pos: bin.center(), button: egui::PointerButton::Primary, pressed: false, modifiers: Modifiers::NONE }); + h.step(); + h.run_steps(3); + assert_eq!(markers(&h), vec![21.5, 22.0], "a double-click released while the bin is dragged"); // Dragged out 3 s into the visible span (from 10 s at 200 px/s): a marker at 13 s too. drag(&mut h, bin.center(), pos2(ruler.min.x + 6.0 + 600.0, ruler.center().y)); - assert_eq!(markers(&h), vec![13.0, 21.5]); + assert_eq!(markers(&h), vec![13.0, 21.5, 22.0]); +} + +/// A marker dragged out of the bin and dropped within one frame (a fast drag, or a slow frame) +/// still lands where it is dropped: the bin starts its drag as soon as it is pressed. +#[test] +fn a_marker_dragged_out_of_the_bin_and_dropped_in_one_frame_is_added() { + let (mut h, _) = harness(); + h.state_mut().ui.timeline.pps = Some(200.0); + h.state_mut().ui.timeline.start = 10.0; + h.run_steps(2); + let bin = rect(&h, "timeline.markerBin"); + let ruler = rect(&h, "timeline.ruler"); + let at = pos2(ruler.min.x + 6.0 + 600.0, ruler.center().y); + h.event(Event::PointerMoved(bin.center())); + h.step(); + h.event(Event::PointerButton { pos: bin.center(), button: egui::PointerButton::Primary, pressed: true, modifiers: Modifiers::NONE }); + h.step(); + // Both in one frame (`event` would give each a frame of its own). + h.input_mut().events.push(Event::PointerMoved(at)); + h.input_mut().events.push(Event::PointerButton { pos: at, button: egui::PointerButton::Primary, pressed: false, modifiers: Modifiers::NONE }); + h.step(); + h.run_steps(2); + let markers: Vec = h.state().session.active_comp().unwrap().markers.iter().map(|m| m.time.seconds()).collect(); + assert_eq!(markers, vec![13.0]); } /// #976: as in After Effects, Numpad * marks the selected layers at the current time; only with From 5e96b2a38598d52f1444c59982c4cbb11dd232fc Mon Sep 17 00:00:00 2001 From: Zinadin Zidan Date: Thu, 5 Nov 2026 18:05:43 +0600 Subject: [PATCH 3/5] Timeline: the marker bin's double-click needs both clicks on the bin (#1826) egui takes any click within a few points of the last one for the first click of a double-click, so a click on the ruler right beside the bin and then one on the bin added a marker. The bin now remembers when it was last clicked and adds one only when the first click was on it too (or both came in one frame over it). Co-Authored-By: Claude Opus 5.5 --- crates/ui-egui/src/panels/markers_ui.rs | 37 +++++++++++++++++++-- crates/ui-egui/tests/it/ui_timeline_zoom.rs | 12 +++++++ 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/crates/ui-egui/src/panels/markers_ui.rs b/crates/ui-egui/src/panels/markers_ui.rs index 5bdd04ad4..c23a6605c 100644 --- a/crates/ui-egui/src/panels/markers_ui.rs +++ b/crates/ui-egui/src/panels/markers_ui.rs @@ -435,8 +435,9 @@ pub(crate) fn marker_bin(app: &mut EffectcraftApp, ui: &mut egui::Ui, comp: &Com // The bin senses drags only, so a drag starts as soon as it is pressed: sensing clicks too // would hold that back until the pointer moved, and a drag out released within that frame // would add nothing. The double-click is read from the pointer instead. - let double_clicked = resp.contains_pointer() && ui.input(|i| i.pointer.button_double_clicked(egui::PointerButton::Primary)); - if double_clicked && let Err(e) = app.session.execute("comp.addMarker", json!({})) { + if bin_double_clicked(ui, resp.id, bin, resp.contains_pointer()) + && let Err(e) = app.session.execute("comp.addMarker", json!({})) + { app.ui.status = e.to_string(); } app.auto.add("timeline.markerBin", bin, "Composition marker bin"); @@ -463,6 +464,38 @@ pub(crate) fn marker_bin(app: &mut EffectcraftApp, ui: &mut egui::Ui, comp: &Com } } +/// Whether the marker bin `bin` (widget `id`, under the pointer when `here`) was double-clicked +/// this frame: egui's double-click, with its first click on the bin too. egui takes any click +/// within a few points of the last for the first one, so a click on the ruler right beside the +/// bin and then one on the bin would count. The bin remembers when it was last clicked; two +/// clicks on it within one frame need no memory. +fn bin_double_clicked(ui: &egui::Ui, id: egui::Id, bin: Rect, here: bool) -> bool { + use egui::{Event, PointerButton}; + if !here { + return false; + } + let (time, clicked, double, releases) = ui.input(|i| { + let releases = i + .events + .iter() + .filter(|e| matches!(e, Event::PointerButton { button: PointerButton::Primary, pressed: false, pos, .. } if bin.contains(*pos))) + .count(); + (i.time, i.pointer.button_clicked(PointerButton::Primary), i.pointer.button_double_clicked(PointerButton::Primary), releases) + }); + if !clicked { + return false; + } + let delay = ui.ctx().options(|o| o.input_options.max_double_click_delay); + let id = id.with("last click"); + let last = ui.data_mut(|d| { + let last = d.get_temp::(id); + d.insert_temp(id, time); + last + }); + let last = last.unwrap_or(f64::NEG_INFINITY); + double && (releases >= 2 || time - last < delay) +} + /// Layer markers on a layer's row `r`; on a precomp layer, its comp's markers too (read-only, /// outlined: hover names them, a double-click opens the nested comp at the marker). pub(crate) fn layer_markers(app: &mut EffectcraftApp, ui: &mut egui::Ui, clip: Rect, comp: &Comp, layer: &Layer, tm: TMap, r: Rect) { diff --git a/crates/ui-egui/tests/it/ui_timeline_zoom.rs b/crates/ui-egui/tests/it/ui_timeline_zoom.rs index 78535e3dd..f8a5783f2 100644 --- a/crates/ui-egui/tests/it/ui_timeline_zoom.rs +++ b/crates/ui-egui/tests/it/ui_timeline_zoom.rs @@ -431,6 +431,18 @@ fn double_clicking_the_marker_bin_adds_a_comp_marker_at_the_current_time() { h.step(); h.run_steps(3); assert_eq!(markers(&h), vec![21.5, 22.0], "a double-click released while the bin is dragged"); + // A click on the ruler right beside the bin and then one on the bin, in one frame: egui + // takes them for a double-click (they are a few points apart), but only one was on the bin. + let (beside, on) = (pos2(bin.min.x - 2.0, bin.center().y), pos2(bin.min.x + 2.0, bin.center().y)); + assert!(ruler.contains(beside) && !bin.contains(beside) && bin.contains(on)); + h.event(Event::PointerMoved(beside)); + h.step(); + for (pos, pressed) in [(beside, true), (beside, false), (on, true), (on, false)] { + h.input_mut().events.push(Event::PointerButton { pos, button: egui::PointerButton::Primary, pressed, modifiers: Modifiers::NONE }); + } + h.step(); + h.run_steps(3); + assert_eq!(markers(&h), vec![21.5, 22.0], "one click beside the bin and one on it"); // Dragged out 3 s into the visible span (from 10 s at 200 px/s): a marker at 13 s too. drag(&mut h, bin.center(), pos2(ruler.min.x + 6.0 + 600.0, ruler.center().y)); assert_eq!(markers(&h), vec![13.0, 21.5, 22.0]); From 133dcddf4de8b2718edc44df0dab514454e0faeb Mon Sep 17 00:00:00 2001 From: Zinadin Zidan Date: Thu, 5 Nov 2026 18:26:39 +0600 Subject: [PATCH 4/5] Timeline: the marker bin pairs its own clicks, in the order they come (#1826) The bin counted a double-click from egui's flag for the whole frame and the number of releases over it, so three clicks in one frame, on the bin, beside it and on it again, still added a marker. It now walks the frame's pointer events itself, by egui's rules for a click and a double-click, and adds one only for two clicks in a row that are both on the bin. It also counts while the bin is dragged or its drag has just stopped, not only while the pointer is over it. Co-Authored-By: Claude Opus 5.5 --- crates/ui-egui/src/panels/markers_ui.rs | 81 ++++++++++++++------- crates/ui-egui/tests/it/ui_timeline_zoom.rs | 10 +++ 2 files changed, 63 insertions(+), 28 deletions(-) diff --git a/crates/ui-egui/src/panels/markers_ui.rs b/crates/ui-egui/src/panels/markers_ui.rs index c23a6605c..61a75e72f 100644 --- a/crates/ui-egui/src/panels/markers_ui.rs +++ b/crates/ui-egui/src/panels/markers_ui.rs @@ -435,7 +435,7 @@ pub(crate) fn marker_bin(app: &mut EffectcraftApp, ui: &mut egui::Ui, comp: &Com // The bin senses drags only, so a drag starts as soon as it is pressed: sensing clicks too // would hold that back until the pointer moved, and a drag out released within that frame // would add nothing. The double-click is read from the pointer instead. - if bin_double_clicked(ui, resp.id, bin, resp.contains_pointer()) + if bin_double_clicked(ui, resp.id, bin, resp.contains_pointer() || resp.dragged() || resp.drag_stopped()) && let Err(e) = app.session.execute("comp.addMarker", json!({})) { app.ui.status = e.to_string(); @@ -464,36 +464,61 @@ pub(crate) fn marker_bin(app: &mut EffectcraftApp, ui: &mut egui::Ui, comp: &Com } } -/// Whether the marker bin `bin` (widget `id`, under the pointer when `here`) was double-clicked -/// this frame: egui's double-click, with its first click on the bin too. egui takes any click -/// within a few points of the last for the first one, so a click on the ruler right beside the -/// bin and then one on the bin would count. The bin remembers when it was last clicked; two -/// clicks on it within one frame need no memory. -fn bin_double_clicked(ui: &egui::Ui, id: egui::Id, bin: Rect, here: bool) -> bool { +/// What the marker bin keeps of the primary button between frames: the press being held (when, +/// where, and whether it has moved too far since to be a click), and the last click (when, +/// where, and whether it was on the bin). +#[derive(Clone, Copy, Default)] +struct BinClicks { + press: Option<(f64, Pos2, bool)>, + last: Option<(f64, Pos2, bool)>, +} + +/// Whether the marker bin `bin` (widget `id`; `over` when it, not something above it, has the +/// pointer) was double-clicked this frame: two clicks in a row, both on the bin. egui's own +/// double-click takes any click within a few points of the last for the first one, so a click on +/// the ruler right beside the bin and then one on the bin would count. The bin pairs the clicks +/// itself, in the order they came, by egui's rules: a click is released within `max_click_dist` +/// of where it was pressed and within `max_click_duration`, and the second of a double-click comes +/// within `max_double_click_delay` and `max_click_dist` of the first. A third click starts over. +fn bin_double_clicked(ui: &egui::Ui, id: egui::Id, bin: Rect, over: bool) -> bool { use egui::{Event, PointerButton}; - if !here { - return false; - } - let (time, clicked, double, releases) = ui.input(|i| { - let releases = i - .events - .iter() - .filter(|e| matches!(e, Event::PointerButton { button: PointerButton::Primary, pressed: false, pos, .. } if bin.contains(*pos))) - .count(); - (i.time, i.pointer.button_clicked(PointerButton::Primary), i.pointer.button_double_clicked(PointerButton::Primary), releases) + let (dist, duration, delay) = ui.ctx().options(|o| { + let i = &o.input_options; + (i.max_click_dist, i.max_click_duration, i.max_double_click_delay) }); - if !clicked { - return false; - } - let delay = ui.ctx().options(|o| o.input_options.max_double_click_delay); - let id = id.with("last click"); - let last = ui.data_mut(|d| { - let last = d.get_temp::(id); - d.insert_temp(id, time); - last + let id = id.with("clicks"); + let mut s = ui.data(|d| d.get_temp::(id)).unwrap_or_default(); + let double = ui.input(|i| { + let mut double = false; + for e in &i.events { + match *e { + Event::PointerMoved(p) => { + if let Some((_, at, moved)) = &mut s.press { + *moved |= at.distance(p) > dist; + } + } + Event::PointerButton { button: PointerButton::Primary, pressed: true, pos, .. } => s.press = Some((i.time, pos, false)), + Event::PointerButton { button: PointerButton::Primary, pressed: false, pos, .. } => { + let Some((pressed_at, at, moved)) = s.press.take() else { continue }; + if moved || at.distance(pos) > dist || i.time - pressed_at >= duration { + continue; + } + let on = bin.contains(pos); + let pair = s.last.is_some_and(|(t, p, was_on)| was_on && on && i.time - t < delay && p.distance(pos) < dist); + if pair { + double = true; + s.last = None; + } else { + s.last = Some((i.time, pos, on)); + } + } + _ => {} + } + } + double }); - let last = last.unwrap_or(f64::NEG_INFINITY); - double && (releases >= 2 || time - last < delay) + ui.data_mut(|d| d.insert_temp(id, s)); + double && over } /// Layer markers on a layer's row `r`; on a precomp layer, its comp's markers too (read-only, diff --git a/crates/ui-egui/tests/it/ui_timeline_zoom.rs b/crates/ui-egui/tests/it/ui_timeline_zoom.rs index f8a5783f2..17666e3db 100644 --- a/crates/ui-egui/tests/it/ui_timeline_zoom.rs +++ b/crates/ui-egui/tests/it/ui_timeline_zoom.rs @@ -443,6 +443,16 @@ fn double_clicking_the_marker_bin_adds_a_comp_marker_at_the_current_time() { h.step(); h.run_steps(3); assert_eq!(markers(&h), vec![21.5, 22.0], "one click beside the bin and one on it"); + // On the bin, beside it, on it again: the two in a row are never both on the bin. + h.run_steps(3); + for pos in [on, beside, on] { + for pressed in [true, false] { + h.input_mut().events.push(Event::PointerButton { pos, button: egui::PointerButton::Primary, pressed, modifiers: Modifiers::NONE }); + } + } + h.step(); + h.run_steps(3); + assert_eq!(markers(&h), vec![21.5, 22.0], "on, beside, on"); // Dragged out 3 s into the visible span (from 10 s at 200 px/s): a marker at 13 s too. drag(&mut h, bin.center(), pos2(ruler.min.x + 6.0 + 600.0, ruler.center().y)); assert_eq!(markers(&h), vec![13.0, 21.5, 22.0]); From 0c7cf039b96768d08081f6d7d4fb1f42da5b1fda Mon Sep 17 00:00:00 2001 From: Zinadin Zidan Date: Thu, 5 Nov 2026 18:48:34 +0600 Subject: [PATCH 5/5] Timeline: a bin click is pressed and released on the bin, while the bin has the pointer (#1826) A press on the ruler right beside the marker bin, released a few points away on the bin, is still a click, and it counted as one on the bin; two of those added a marker. A click now counts as the bin's only when both its press and its release are on the bin, in a frame when the bin is enabled and has the pointer (not a popup above it). Co-Authored-By: Claude Opus 5.5 --- crates/ui-egui/src/panels/markers_ui.rs | 13 +++++++------ crates/ui-egui/tests/it/ui_timeline_zoom.rs | 10 ++++++++++ 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/crates/ui-egui/src/panels/markers_ui.rs b/crates/ui-egui/src/panels/markers_ui.rs index 61a75e72f..ee33fc9de 100644 --- a/crates/ui-egui/src/panels/markers_ui.rs +++ b/crates/ui-egui/src/panels/markers_ui.rs @@ -435,7 +435,7 @@ pub(crate) fn marker_bin(app: &mut EffectcraftApp, ui: &mut egui::Ui, comp: &Com // The bin senses drags only, so a drag starts as soon as it is pressed: sensing clicks too // would hold that back until the pointer moved, and a drag out released within that frame // would add nothing. The double-click is read from the pointer instead. - if bin_double_clicked(ui, resp.id, bin, resp.contains_pointer() || resp.dragged() || resp.drag_stopped()) + if bin_double_clicked(ui, resp.id, bin, resp.enabled() && (resp.contains_pointer() || resp.dragged() || resp.drag_stopped())) && let Err(e) = app.session.execute("comp.addMarker", json!({})) { app.ui.status = e.to_string(); @@ -466,15 +466,16 @@ pub(crate) fn marker_bin(app: &mut EffectcraftApp, ui: &mut egui::Ui, comp: &Com /// What the marker bin keeps of the primary button between frames: the press being held (when, /// where, and whether it has moved too far since to be a click), and the last click (when, -/// where, and whether it was on the bin). +/// where, and whether it was on the bin: both pressed and released there). #[derive(Clone, Copy, Default)] struct BinClicks { press: Option<(f64, Pos2, bool)>, last: Option<(f64, Pos2, bool)>, } -/// Whether the marker bin `bin` (widget `id`; `over` when it, not something above it, has the -/// pointer) was double-clicked this frame: two clicks in a row, both on the bin. egui's own +/// Whether the marker bin `bin` (widget `id`; `over` when it is enabled and has the pointer, not +/// something above it) was double-clicked this frame: two clicks in a row, both on the bin, each +/// in a frame when it was `over`. egui's own /// double-click takes any click within a few points of the last for the first one, so a click on /// the ruler right beside the bin and then one on the bin would count. The bin pairs the clicks /// itself, in the order they came, by egui's rules: a click is released within `max_click_dist` @@ -503,7 +504,7 @@ fn bin_double_clicked(ui: &egui::Ui, id: egui::Id, bin: Rect, over: bool) -> boo if moved || at.distance(pos) > dist || i.time - pressed_at >= duration { continue; } - let on = bin.contains(pos); + let on = over && bin.contains(at) && bin.contains(pos); let pair = s.last.is_some_and(|(t, p, was_on)| was_on && on && i.time - t < delay && p.distance(pos) < dist); if pair { double = true; @@ -518,7 +519,7 @@ fn bin_double_clicked(ui: &egui::Ui, id: egui::Id, bin: Rect, over: bool) -> boo double }); ui.data_mut(|d| d.insert_temp(id, s)); - double && over + double } /// Layer markers on a layer's row `r`; on a precomp layer, its comp's markers too (read-only, diff --git a/crates/ui-egui/tests/it/ui_timeline_zoom.rs b/crates/ui-egui/tests/it/ui_timeline_zoom.rs index 17666e3db..76f55740a 100644 --- a/crates/ui-egui/tests/it/ui_timeline_zoom.rs +++ b/crates/ui-egui/tests/it/ui_timeline_zoom.rs @@ -453,6 +453,16 @@ fn double_clicking_the_marker_bin_adds_a_comp_marker_at_the_current_time() { h.step(); h.run_steps(3); assert_eq!(markers(&h), vec![21.5, 22.0], "on, beside, on"); + // Twice pressed beside the bin and released on it, a few points away: clicks, not on the bin. + h.run_steps(3); + for _ in 0..2 { + for (pos, pressed) in [(beside, true), (on, false)] { + h.input_mut().events.push(Event::PointerButton { pos, button: egui::PointerButton::Primary, pressed, modifiers: Modifiers::NONE }); + } + } + h.step(); + h.run_steps(3); + assert_eq!(markers(&h), vec![21.5, 22.0], "pressed beside, released on"); // Dragged out 3 s into the visible span (from 10 s at 200 px/s): a marker at 13 s too. drag(&mut h, bin.center(), pos2(ruler.min.x + 6.0 + 600.0, ruler.center().y)); assert_eq!(markers(&h), vec![13.0, 21.5, 22.0]);