From 18a0e02249eb6c7d1f6d0cd19a9c0f206839cb2d Mon Sep 17 00:00:00 2001 From: "Pedro M. de Echanove Pasquin" Date: Sun, 25 Oct 2026 17:33:32 +0100 Subject: [PATCH] A pull from the top that is unwound back to where it started still reports its release, so an app that followed the pull hears that it was not committed A scroll view at its top turns a downward drag into overscroll, which `on_move` reports as `MoveOutcome::OverscrollDown { progress }`, and on release `ReleaseEvent::OverscrollDown { committed }` tells the app whether the pull went past its threshold, as pull-from-top-to-dismiss needs. The release was reported only while `overscroll_y` was above zero. A pull dragged back up until it was unwound to zero therefore ended without any release event, and an app that had moved its view with the pull's progress was left waiting for one, its view where the last progress put it. `GestureState::overscroll_engaged` is set once a gesture has reported overscroll progress and cleared with the rest of the gesture's state. On release, a gesture that reported progress now always gets its `OverscrollDown`, uncommitted when nothing is left of the pull. A gesture that never reached overscroll still reports nothing. Tests cover a pull unwound to zero, which reports an uncommitted release, and a drag that only scrolled, which reports none. `CHANGELOG.md` describes the change. --- CHANGELOG.md | 1 + src/input/gesture/mod.rs | 11 +++++++++-- src/input/gesture/tests.rs | 29 +++++++++++++++++++++++++++++ 3 files changed, 39 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3df286d..3f3ecdd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -90,6 +90,7 @@ All notable changes to `ltk` are documented here. The format is based on [Keep a ### Fixed +- **A pull from the top unwound back to where it started still reports its release.** `ReleaseEvent::OverscrollDown` was sent only while some overscroll was left, so a pull dragged back up to zero ended without one, and an app that had moved its view with the pull's progress was left waiting for it. `GestureState::overscroll_engaged` remembers that the gesture reported progress, and its release now always reports `OverscrollDown`, uncommitted when nothing is left of the pull; a drag that never reached overscroll still reports nothing. - **Pasting what another application copied.** The Wayland backend bound its `wl_data_device` and its primary-selection device in `SeatHandler::new_seat`, which smithay-client-toolkit calls only for seats announced after start-up, not for the seat already there when the app connects. An app therefore never had either device, the compositor never offered it another client's selection, and neither Ctrl+V nor the middle button pasted anything copied outside the app, nor did what it copied reach other applications through the compositor. Both devices are now bound on the seat's first capability, which comes for every seat. - **The middle button pastes the selection just made in another application.** A compositor offers the primary selection to the window it gives the keyboard, and gives it on the press, so a middle click on a window that did not have the keyboard reaches it together with the offer. ltk reads an offer on a thread of its own and pasted at once what it had read before, so after selecting in another application an ltk app pasted the selection before that. A middle-button paste, into the text field under the pointer or one an app asks for with `SelectionKind::Primary`, now waits while a selection is being read (`Platform::primary_selection_pending`, which the Wayland backend counts) and is made once it has been, or after 500 ms with what there is. - **A held button is not also tapped.** A button with `on_long_press` and no `on_drag_start` stayed a tap after its hold fired, so releasing it sent its `on_press` as well; it no longer does. diff --git a/src/input/gesture/mod.rs b/src/input/gesture/mod.rs index d21835a..7457cf4 100644 --- a/src/input/gesture/mod.rs +++ b/src/input/gesture/mod.rs @@ -138,6 +138,9 @@ pub struct GestureState /// drained on release into an `OverscrollDown` event. Drives /// pull-from-top-to-dismiss. pub overscroll_y: f32, + /// `true` once this gesture has reported overscroll progress, so the + /// release still reports it when the pull was unwound back to zero. + pub overscroll_engaged: bool, /// `true` when the press came from a mouse (`wl_pointer`) rather /// than touch. The 6 px stray-cancel in [`Self::on_move`] is gated /// on this: touch needs the cancel so a hold that turns into a @@ -185,6 +188,7 @@ impl GestureState long_press_fired: false, long_press_msg_fired: false, overscroll_y: 0.0, + overscroll_engaged: false, mouse_press: false, last_pattern_node: None, fling_x: FlingTracker::default(), @@ -236,6 +240,7 @@ impl GestureState self.scroll_locked = false; self.scroll_drag_started = false; self.overscroll_y = 0.0; + self.overscroll_engaged = false; self.horizontal_drag_started = false; self.vertical_drag_started = false; self.swipe_axis = None; @@ -423,6 +428,7 @@ impl GestureState self.overscroll_y / swipe.surface_height as f32 } else { 0.0 }; + self.overscroll_engaged = true; return MoveOutcome::OverscrollDown { progress }; } return MoveOutcome::Scroll; @@ -591,10 +597,11 @@ impl GestureState // the app decides whether the accumulated pull committed. let over = self.overscroll_y; self.overscroll_y = 0.0; - if over > 0.0 + let engaged = std::mem::take( &mut self.overscroll_engaged ); + if over > 0.0 || engaged { let threshold = swipe.surface_height as f32 * swipe.overscroll_thresh; - events.push( ReleaseEvent::OverscrollDown { committed: over >= threshold } ); + events.push( ReleaseEvent::OverscrollDown { committed: over > 0.0 && over >= threshold } ); } return events; } diff --git a/src/input/gesture/tests.rs b/src/input/gesture/tests.rs index 49f321e..7d55c4c 100644 --- a/src/input/gesture/tests.rs +++ b/src/input/gesture/tests.rs @@ -341,6 +341,35 @@ fn upward_drag_unwinds_overscroll_before_scrolling_content() assert_eq!( offsets.get( &4 ).map( |o| o.1 ), Some( 50.0 ) ); } +#[ test ] +fn release_after_overscroll_unwound_to_zero_still_reports_the_pull() +{ + let widgets: Vec> = vec![]; + let scrolls: [( Rect, usize, ScrollAxis ); 1] = [ ( rect( 0.0, 0.0, 400.0, 800.0 ), 7, ScrollAxis::Vertical ) ]; + let mut g = GestureState::::new(); + let _ = g.on_press( pt( 100.0, 100.0 ), &widgets, &scrolls ); + let mut offsets = HashMap::new(); + let _ = g.on_move( pt( 100.0, 200.0 ), &widgets, &mut offsets, &cfg_full( 800, 1200 ), false ); + let _ = g.on_move( pt( 100.0, 50.0 ), &widgets, &mut offsets, &cfg_full( 800, 1200 ), false ); + assert_eq!( g.overscroll_y, 0.0 ); + let events = g.on_release( pt( 100.0, 50.0 ), &widgets, &cfg_full( 800, 1200 ), false ); + assert!( events.iter().any( |e| matches!( e, ReleaseEvent::OverscrollDown { committed: false } ) ) ); +} + +#[ test ] +fn release_without_any_overscroll_reports_nothing() +{ + let widgets: Vec> = vec![]; + let scrolls: [( Rect, usize, ScrollAxis ); 1] = [ ( rect( 0.0, 0.0, 400.0, 800.0 ), 8, ScrollAxis::Vertical ) ]; + let mut g = GestureState::::new(); + let _ = g.on_press( pt( 100.0, 300.0 ), &widgets, &scrolls ); + let mut offsets = HashMap::new(); + offsets.insert( 8, ( 0.0, 200.0 ) ); + let _ = g.on_move( pt( 100.0, 350.0 ), &widgets, &mut offsets, &cfg_full( 800, 1200 ), false ); + let events = g.on_release( pt( 100.0, 350.0 ), &widgets, &cfg_full( 800, 1200 ), false ); + assert!( !events.iter().any( |e| matches!( e, ReleaseEvent::OverscrollDown { .. } ) ) ); +} + #[ test ] fn release_after_overscroll_past_threshold_commits_dismiss() {