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(