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