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.
This commit is contained in:
dotCooCoo
2026-11-03 16:03:38 +07:00
committed by bflatastic
parent df61ee2f84
commit 49477f8be5
2 changed files with 44 additions and 9 deletions
+23 -9
View File
@@ -150,15 +150,20 @@ pub(super) type FrameMove = (Pos2, Option<f32>, Option<PenPose>);
/// 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<egui::PointerButton>) -> (Vec<FrameMove>, Option<PenPose>) {
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 {
+21
View File
@@ -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();