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]);