From 49477f8be5e6fd002a8c71422784c5a96a6caea9 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Mon, 2 Nov 2026 12:04:05 -0800 Subject: [PATCH] Canvas: keep the last move before the button-up when it is elsewhere When the release frame held moves after the button-up, the last move before it was dropped as the pointer's position, although the release event carries its own position. That move's sample was lost from the stroke. It is now kept unless it is at the button-up's position, and its tilt and rotation still give the release's pose. --- crates/ui-egui/src/canvas/drag.rs | 32 +++++++++++++++++++++--------- crates/ui-egui/src/canvas/tests.rs | 21 ++++++++++++++++++++ 2 files changed, 44 insertions(+), 9 deletions(-) diff --git a/crates/ui-egui/src/canvas/drag.rs b/crates/ui-egui/src/canvas/drag.rs index 871af252f..88293880a 100644 --- a/crates/ui-egui/src/canvas/drag.rs +++ b/crates/ui-egui/src/canvas/drag.rs @@ -150,15 +150,20 @@ pub(super) type FrameMove = (Pos2, Option, Option); /// tilt and rotation the feed kept for it come second, if any. A mouse's moves count too, with no /// pressure: a fast mouse stroke otherwise keeps one position a frame, a sample every few pixels, /// too few for a low Fidelity to follow the hand (#3328). With `release`, the moves stop at that -/// button's button-up in this frame, if it came: the moves after it are hover, and the last move -/// before it is the pointer's position. +/// button's button-up in this frame, if it came: the moves after it are hover, and the button-up's +/// position is the pointer's. The last move before it is left out only when it is at that +/// position; its tilt and rotation come second either way. pub(super) fn frame_moves(ui: &Ui, stylus: Option<&StylusFeed>, release: Option) -> (Vec, Option) { let samples = stylus.map(StylusFeed::take_moves).unwrap_or_default(); ui.input(|i| { - // How many of the frame's moves came before the button-up, if it came. + // How many of the frame's moves came before the button-up, and where it came, if it came. let before_up = release.and_then(|b| { - let up = i.events.iter().position(|e| matches!(e, egui::Event::PointerButton { button, pressed: false, .. } if *button == b))?; - Some(i.events.iter().take(up).filter(|e| matches!(e, egui::Event::PointerMoved(_))).count()) + i.events.iter().enumerate().find_map(|(k, e)| match e { + egui::Event::PointerButton { pos, button, pressed: false, .. } if *button == b => { + Some((i.events.iter().take(k).filter(|e| matches!(e, egui::Event::PointerMoved(_))).count(), *pos)) + } + _ => None, + }) }); let mut moves = vec![]; if i.events.iter().any(|e| matches!(e, egui::Event::Touch { .. })) { @@ -181,10 +186,19 @@ pub(super) fn frame_moves(ui: &Ui, stylus: Option<&StylusFeed>, release: Option< moves = i.events.iter().filter_map(|e| if let egui::Event::PointerMoved(p) = e { Some((*p, None, None)) } else { None }).collect(); } // Cut after pairing: the feed's samples pair with every move of the frame. - if let Some(n) = before_up { - moves.truncate(n); - } - let last_pose = moves.pop().and_then(|(_, _, q)| q); + // The pointer's position is the button-up's: a last move before it that is elsewhere + // stays a move. Its tilt and rotation are still the pose. + let last_pose = match before_up { + Some((n, up_at)) => { + moves.truncate(n); + let pose = moves.last().and_then(|&(_, _, q)| q); + if moves.last().is_some_and(|&(p, _, _)| p == up_at) { + moves.pop(); + } + pose + } + None => moves.pop().and_then(|(_, _, q)| q), + }; // A burst of events (a stalled frame) is thinned to a bounded count. const MAX: usize = 64; if moves.len() > MAX { diff --git a/crates/ui-egui/src/canvas/tests.rs b/crates/ui-egui/src/canvas/tests.rs index 79954a6b0..3e184bef4 100644 --- a/crates/ui-egui/src/canvas/tests.rs +++ b/crates/ui-egui/src/canvas/tests.rs @@ -6636,6 +6636,27 @@ fn frame_moves_stop_at_the_button_up() { assert_eq!(moves, [(a, None, None), (b, None, None), (c, None, None)], "without a release every move counts"); } +/// A button-up away from the last move before it keeps that move: the release takes only its own +/// position, and the pose is still the last move's. +#[test] +fn frame_moves_keep_the_last_move_when_the_button_went_up_elsewhere() { + let ctx = egui::Context::default(); + let (a, b, up_at, c) = (pos2(10.0, 20.0), pos2(20.0, 30.0), pos2(25.0, 35.0), pos2(30.0, 40.0)); + let up = egui::Event::PointerButton { pos: up_at, button: egui::PointerButton::Primary, pressed: false, modifiers: Default::default() }; + let events = vec![egui::Event::PointerMoved(a), egui::Event::PointerMoved(b), up, egui::Event::PointerMoved(c)]; + let feed = StylusFeed::default(); + let at_b = PenPose { tilt: Some([20.0, 0.0]), rotation: None }; + for (p, pose) in [(0.4, PenPose::default()), (0.6, at_b), (0.7, PenPose::default())] { + feed.set_move(crate::stylus::PenSample { pressure: p, pose, ..Default::default() }); + } + let (mut moves, mut last) = (vec![], None); + let release = Some(egui::PointerButton::Primary); + let mut out = ctx.run_ui(egui::RawInput { events, ..Default::default() }, |ui| (moves, last) = frame_moves(ui, Some(&feed), release)); + out.textures_delta.clear(); + assert_eq!(moves, [(a, Some(0.4), Some(PenPose::default())), (b, Some(0.6), Some(at_b))], "the move before the button-up is kept"); + assert_eq!(last, Some(at_b), "the pose is the last move's before the button-up"); +} + #[test] fn frame_moves_keeps_touch_feed_and_mouse_samples_with_their_pressure() { let ctx = egui::Context::default();