From dd515787ec966d2248c80ac5fc72b772c14f49a6 Mon Sep 17 00:00:00 2001 From: Harald Date: Mon, 26 Oct 2026 14:08:13 +0100 Subject: [PATCH] fix: measure the swipe speed with the compositor's touch event times (Issue #28) Pia's log shows the lift arriving 200-300 ms after the last move on every swipe, so the release looked like a resting finger and nothing glided. The vendored winit now keeps the Wayland timestamp of each touch event (take_touch_time), and the fling measures with those times instead of when the app got around to handling the events. The diagnostics log both gaps and any event handling that blocks the app for more than 40 ms. Co-Authored-By: Claude Opus 5.5 --- src/app.rs | 90 +++++++++++++++++-- src/viewport.rs | 27 ++++++ vendor/winit/README.ink-notes.md | 4 + vendor/winit/src/platform/wayland.rs | 30 +++++++ .../linux/wayland/seat/touch/mod.rs | 10 ++- 5 files changed, 152 insertions(+), 9 deletions(-) diff --git a/src/app.rs b/src/app.rs index 578cfef..5e8d943 100644 --- a/src/app.rs +++ b/src/app.rs @@ -466,6 +466,49 @@ fn page_to_focus( (!grabs_selection).then_some(index) } +/// Fling diagnostics (Issue #28): reports events whose handling blocks the app for longer than +/// a few frames, because touch events wait in the queue meanwhile. +struct SlowEventLog { + label: Option<&'static str>, + start: Instant, +} + +impl SlowEventLog { + const LIMIT: Duration = Duration::from_millis(40); + + fn new(enabled: bool, event: &Event<()>) -> Self { + let label = enabled.then_some(match event { + Event::WindowEvent { + event: WindowEvent::RedrawRequested, + .. + } => "Zeichnen", + Event::WindowEvent { + event: WindowEvent::Touch(_), + .. + } => "Finger", + Event::WindowEvent { .. } => "Fensterereignis", + Event::AboutToWait => "Leerlauf", + _ => "Ereignis", + }); + Self { + label, + start: Instant::now(), + } + } +} + +impl Drop for SlowEventLog { + fn drop(&mut self) { + let elapsed = self.start.elapsed(); + if let Some(label) = self.label.filter(|_| elapsed > Self::LIMIT) { + eprintln!( + "[Nachgleiten] {label} hat {} ms gedauert", + elapsed.as_millis() + ); + } + } +} + /// Zwischenablage: interne Objekte (Strg+C in ink-notes), Bilder aus der /// Systemzwischenablage (Screenshots, aus anderen Programmen kopierte Bilder) und Text. struct ClipboardBridge { @@ -1429,8 +1472,12 @@ pub fn run() -> Result<(), Box> { // Langes Drücken mit einem Finger öffnet das Kontextmenü let mut long_press = ui::LongPress::default(); let mut touch_pan_last_pos = (0.0_f32, 0.0_f32); - // Diagnostics for the fling: moves, path length (px) and time of the last move of this swipe - let mut touch_pan_trace: (u32, f32, Option) = (0, 0.0, None); + // Diagnostics for the fling: moves, path length (px), and when the last move of this swipe + // was handled and happened (compositor time) + let mut touch_pan_trace: (u32, f32, Option<(Instant, Instant)>) = (0, 0.0, None); + // Compositor time (ms) of the latest touch Started and when it was handled: maps the event + // times of the following touch events to `Instant`s + let mut touch_clock: Option<(Instant, u32)> = None; // Nachgleiten nach schnellem Wischen mit dem Finger (Issue #28) let mut fling = crate::viewport::Fling::default(); // Diagnosis for Issue #28: log every finger lift and the swipe speed @@ -1550,6 +1597,7 @@ pub fn run() -> Result<(), Box> { } event_loop.run(move |event, target| { + let _slow_event_log = SlowEventLog::new(debug_fling, &event); // Anordnung der Seiten gehört zum offenen Notizbuch (Issue #30) // Endlos ist ein Notizbuch an seiner einzigen, endlosen Seite erkennbar ui_state.endless = notebook.is_endless(); @@ -2915,6 +2963,27 @@ pub fn run() -> Result<(), Box> { WindowEvent::Touch(touch) => { let now = start_time.elapsed().as_secs_f64(); + // When the touch really happened (Wayland reports the time of each + // event): the swipe speed must not depend on how late we handle it + let handled_at = Instant::now(); + let event_time = match touch.phase { + TouchPhase::Cancelled => None, + phase => winit::platform::wayland::take_touch_time(touch.id, phase), + }; + if let (TouchPhase::Started, Some(time)) = (touch.phase, event_time) { + touch_clock = Some((handled_at, time)); + } + let touch_at = match (event_time, touch_clock) { + (Some(time), Some((at, start))) => { + let ms = time.wrapping_sub(start) as i32; + if ms >= 0 { + at + Duration::from_millis(ms as u64) + } else { + handled_at + } + } + _ => handled_at, + }; let tx = touch.location.x as f32; let ty = touch.location.y as f32; let raw_pressure = match touch.force { @@ -3126,10 +3195,10 @@ pub fn run() -> Result<(), Box> { let dy = ty - touch_pan_last_pos.1; touch_pan_last_pos = (tx, ty); canvas.pan(dx, dy); - fling.track(Vec2::new(dx, dy), Instant::now()); + fling.track(Vec2::new(dx, dy), touch_at); touch_pan_trace.0 += 1; touch_pan_trace.1 += Vec2::new(dx, dy).length(); - touch_pan_trace.2 = Some(Instant::now()); + touch_pan_trace.2 = Some((handled_at, touch_at)); window.request_redraw(); } else if is_touch_drawing { current_pressure = pressure; @@ -3188,15 +3257,20 @@ pub fn run() -> Result<(), Box> { // Letztes Stück bis zum Abheben noch mitnehmen let (dx, dy) = (tx - touch_pan_last_pos.0, ty - touch_pan_last_pos.1); canvas.pan(dx, dy); - fling.track(Vec2::new(dx, dy), Instant::now()); - let velocity = fling.release(Instant::now()); + fling.track(Vec2::new(dx, dy), touch_at); + let velocity = fling.release(touch_at); + // The glide itself starts now + fling.restart_clock(Instant::now()); if debug_fling { let (moves, path, last_move) = touch_pan_trace; + let ms = |d: Option| d.map_or(-1, |d| d.as_millis() as i64); eprintln!( - "[Nachgleiten] {:.0} px/s beim Abheben, gleitet: {} ({moves} Bewegungen, {path:.0} px, letzte vor {} ms)", + "[Nachgleiten] {:.0} px/s beim Abheben, gleitet: {} ({moves} Bewegungen, {path:.0} px, letzte vor {} ms, laut System vor {} ms{})", velocity.length(), fling.is_active(), - last_move.map_or(-1, |t| t.elapsed().as_millis() as i64) + ms(last_move.map(|(handled, _)| handled_at.duration_since(handled))), + ms(last_move.map(|(_, at)| touch_at.duration_since(at))), + if event_time.is_some() { "" } else { ", keine Systemzeit" } ); } window.request_redraw(); diff --git a/src/viewport.rs b/src/viewport.rs index 4b9229b..2f00c5b 100644 --- a/src/viewport.rs +++ b/src/viewport.rs @@ -389,6 +389,14 @@ impl Fling { velocity } + /// The glide continues from `now` (after [`Self::release`] with the time the finger was + /// lifted, which may lie a little in the past). + pub fn restart_clock(&mut self, now: std::time::Instant) { + if self.active { + self.last = Some(now); + } + } + /// Hält an (neue Berührung, Mausklick, Mausrad). pub fn stop(&mut self) { *self = Self::default(); @@ -430,6 +438,25 @@ mod tests { PageStack::new(vec![A4; count], current) } + #[test] + fn test_fling_measures_with_event_times_handled_late() { + use std::time::{Duration, Instant}; + let t0 = Instant::now(); + let ms = |n: u64| t0 + Duration::from_millis(n); + let mut fling = Fling::default(); + // The finger moved until 100 ms and was lifted at 105 ms; the app only got to the + // lift 300 ms later, so the glide starts from there + for i in 0..=10 { + fling.track(Vec2::new(0.0, -30.0), ms(i * 10)); + } + let velocity = fling.release(ms(105)); + assert!(velocity.y < -2000.0, "{velocity:?}"); + assert!(fling.is_active()); + fling.restart_clock(ms(405)); + let first = fling.step(ms(421)).unwrap(); + assert!(first.y < -20.0 && first.y > -60.0, "{first:?}"); + } + #[test] fn test_fling_glides_and_slows_down() { use std::time::{Duration, Instant}; diff --git a/vendor/winit/README.ink-notes.md b/vendor/winit/README.ink-notes.md index 58e1762..f002643 100644 --- a/vendor/winit/README.ink-notes.md +++ b/vendor/winit/README.ink-notes.md @@ -14,6 +14,10 @@ docs of the original crate were removed. - `src/platform/wayland.rs`: `ignore_decoration_touches_until` lets ink-notes ignore touches on the title bar while the pen is near the screen, so a resting palm does not move the window (Issue #39). +- `src/platform/wayland.rs` and the touch handler: `take_touch_time` returns the compositor + timestamp of each touch event (Started, Moved, Ended), so ink-notes measures the swipe speed + for gliding with the real event times and not with the time it got around to handling them + (Issue #28). - `Cargo.toml`: `[lints.rust] warnings = "allow"`, because a path dependency is not built with capped lints like a crates.io dependency. diff --git a/vendor/winit/src/platform/wayland.rs b/vendor/winit/src/platform/wayland.rs index 3b71185..3fa4e8e 100644 --- a/vendor/winit/src/platform/wayland.rs +++ b/vendor/winit/src/platform/wayland.rs @@ -28,6 +28,36 @@ pub(crate) fn decoration_touches_ignored() -> bool { .is_some_and(|until| std::time::Instant::now() < until) } +/// ink-notes patch: compositor timestamps (ms) of the touch events not yet taken, in event order. +static TOUCH_TIMES: std::sync::Mutex< + std::collections::VecDeque<(u64, crate::event::TouchPhase, u32)>, +> = std::sync::Mutex::new(std::collections::VecDeque::new()); + +/// ink-notes patch: remembers the compositor time of a touch event that is about to be sent. +pub(crate) fn push_touch_time(id: u64, phase: crate::event::TouchPhase, time: u32) { + let mut times = TOUCH_TIMES.lock().unwrap_or_else(|e| e.into_inner()); + // Nobody takes them (e.g. an app that never asks): keep only the latest + if times.len() >= 256 { + times.pop_front(); + } + times.push_back((id, phase, time)); +} + +/// ink-notes patch: compositor time in milliseconds (arbitrary origin, wraps around) of the +/// touch event with this id and phase (`Started`, `Moved` or `Ended`; `Cancelled` has none), +/// for the event currently being handled. Call it once for every such `WindowEvent::Touch`. +/// `None` on X11 or if the time is unknown. +pub fn take_touch_time(id: u64, phase: crate::event::TouchPhase) -> Option { + let mut times = TOUCH_TIMES.lock().unwrap_or_else(|e| e.into_inner()); + // Older entries that were never taken are skipped + while let Some((i, p, time)) = times.pop_front() { + if i == id && p == phase { + return Some(time); + } + } + None +} + /// Additional methods on [`EventLoopWindowTarget`] that are specific to Wayland. pub trait EventLoopWindowTargetExtWayland { /// True if the [`EventLoopWindowTarget`] uses Wayland. diff --git a/vendor/winit/src/platform_impl/linux/wayland/seat/touch/mod.rs b/vendor/winit/src/platform_impl/linux/wayland/seat/touch/mod.rs index 5c5ccda..f160738 100644 --- a/vendor/winit/src/platform_impl/linux/wayland/seat/touch/mod.rs +++ b/vendor/winit/src/platform_impl/linux/wayland/seat/touch/mod.rs @@ -83,6 +83,9 @@ impl TouchHandler for WinitState { .touch_map .insert(id, TouchPoint { surface, location, frame_of: None }); + // ink-notes patch: keep the event time for `take_touch_time` + crate::platform::wayland::push_touch_time(id as u64, TouchPhase::Started, time); + self.events_sink.push_window_event( WindowEvent::Touch(Touch { device_id: crate::event::DeviceId(crate::platform_impl::DeviceId::Wayland( @@ -138,6 +141,8 @@ impl TouchHandler for WinitState { None => return, }; + // ink-notes patch: keep the event time for `take_touch_time` + crate::platform::wayland::push_touch_time(id as u64, TouchPhase::Ended, time); self.events_sink.push_window_event( WindowEvent::Touch(Touch { device_id: crate::event::DeviceId(crate::platform_impl::DeviceId::Wayland( @@ -157,7 +162,7 @@ impl TouchHandler for WinitState { _: &Connection, _: &QueueHandle, touch: &WlTouch, - _: u32, + time: u32, id: i32, position: (f64, f64), ) { @@ -192,6 +197,9 @@ impl TouchHandler for WinitState { touch_point.location = LogicalPosition::::from(position); + // ink-notes patch: keep the event time for `take_touch_time` + crate::platform::wayland::push_touch_time(id as u64, TouchPhase::Moved, time); + self.events_sink.push_window_event( WindowEvent::Touch(Touch { device_id: crate::event::DeviceId(crate::platform_impl::DeviceId::Wayland(