input, render: keep fast touch typing whole, fire pattern nodes once, and make the software image path cheap
Two ways a tap was lost on a phone. The gesture machine serves one finger: a second finger landing while the first was still down went to the auxiliary slot and never reached it, so with two thumbs every overlapping tap on a keypad vanished. A finger landing on a primary that is still a plain press (GestureState::is_plain_press: no drag, scroll, swipe or slider in flight) now commits that press as a tap at its last position and takes the primary slot; the earlier finger's release reports through on_touch_up, and a second finger during a pattern stroke or a slider drag stays out as before. The touch-up body moves to release_primary so both paths share it. The other loss needed one finger: the 8 px travel that turns a press into a scroll, swipe or drag, and the 6 px stray that cancels a touch long-press, were compared against physical coordinates, so at buffer scale 2 they were 4 and 3 logical px and an ordinary thumb tap became a vertical drag whose release skipped the button. SwipeConfig carries the surface's scale and the machine reads the thresholds through slop() and stray(), which are logical. Pattern grids fired on_node on every motion event within a node's snap radius, and the Slider move outcome requested a repaint each time, so drawing a pattern rebuilt and re-rastered the surface at the touchscreen's motion rate. The machine remembers the node under the finger (last_pattern_node) and emits only on change; the handlers split pattern_node_at from pattern_msg, with pattern_node_msg kept on top of them; a move outcome without a message no longer requests a redraw. SoftwareCanvas::draw_image_data premultiplied the whole source in floating point on every call, so a full-surface backdrop paid a pass over every pixel per frame before the blit. Premultiplied copies now live in a 32 MiB least-recently-drawn cache keyed by size, content fingerprint and alpha, the key the GLES texture cache already used; fingerprint_rgba moves to render::helpers and both backends share it. The entry records whether the source was fully opaque, and an opaque image drawn 1:1 with no clip and full alpha is copied row by row into the canvas instead of going through tiny-skia's bilinear pipeline. docs/architecture.md describes the primary-slot hand-over, docs/widgets.md the once-per-node contract, and the CHANGELOG lists the five fixes.
This commit is contained in:
@@ -37,8 +37,8 @@ impl<A: App> AppData<A>
|
||||
if let Some( m ) = msg
|
||||
{
|
||||
self.pending_msgs.push( m );
|
||||
self.surface_mut( focus ).request_redraw();
|
||||
}
|
||||
self.surface_mut( focus ).request_redraw();
|
||||
}
|
||||
MoveOutcome::Scroll =>
|
||||
{
|
||||
|
||||
@@ -144,6 +144,9 @@ pub struct GestureState<Msg: Clone>
|
||||
/// the pointer Press handler right after [`Self::on_press`]; left
|
||||
/// `false` for touch presses.
|
||||
pub mouse_press: bool,
|
||||
/// Pattern-grid node the finger was last over, so `on_node` fires
|
||||
/// once per node rather than once per motion event.
|
||||
pub last_pattern_node: Option<u8>,
|
||||
}
|
||||
|
||||
impl<Msg: Clone> Default for GestureState<Msg>
|
||||
@@ -176,9 +179,22 @@ impl<Msg: Clone> GestureState<Msg>
|
||||
long_press_fired: false,
|
||||
overscroll_y: 0.0,
|
||||
mouse_press: false,
|
||||
last_pattern_node: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// A press that has not become anything else yet: no drag, scroll,
|
||||
/// swipe or slider in flight. Its release would be a tap.
|
||||
pub fn is_plain_press( &self ) -> bool
|
||||
{
|
||||
self.start.is_some()
|
||||
&& self.dragging_slider.is_none()
|
||||
&& !self.scroll_drag_started
|
||||
&& !self.horizontal_drag_started
|
||||
&& !self.vertical_drag_started
|
||||
&& !self.long_press_fired
|
||||
}
|
||||
|
||||
/// Press / touch-down: record the gesture origin, capture long-press
|
||||
/// handler if the hit widget has one, identify slider / scroll
|
||||
/// targets. Returns the hit index (for keyboard focus update) and
|
||||
@@ -256,8 +272,10 @@ impl<Msg: Clone> GestureState<Msg>
|
||||
let value = w.handlers.slider_value_from_pos( w.rect, pos );
|
||||
initial_slider_msg = w.handlers.slider_change_msg( value );
|
||||
} else if w.handlers.is_drag_target() {
|
||||
self.dragging_slider = Some( idx );
|
||||
initial_slider_msg = w.handlers.pattern_node_msg( w.rect, pos );
|
||||
self.dragging_slider = Some( idx );
|
||||
let node = w.handlers.pattern_node_at( w.rect, pos );
|
||||
self.last_pattern_node = node;
|
||||
initial_slider_msg = node.and_then( |n| w.handlers.pattern_msg( n ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -303,7 +321,7 @@ impl<Msg: Clone> GestureState<Msg>
|
||||
if let Some( origin ) = self.long_press_origin
|
||||
{
|
||||
let d = ( pos.x - origin.x ).hypot( pos.y - origin.y );
|
||||
if d > 6.0
|
||||
if d > swipe.stray()
|
||||
{
|
||||
self.long_press_start = None;
|
||||
self.long_press_origin = None;
|
||||
@@ -324,7 +342,10 @@ impl<Msg: Clone> GestureState<Msg>
|
||||
let value = w.handlers.slider_value_from_pos( w.rect, pos );
|
||||
w.handlers.slider_change_msg( value )
|
||||
} else {
|
||||
w.handlers.pattern_node_msg( w.rect, pos )
|
||||
let node = w.handlers.pattern_node_at( w.rect, pos );
|
||||
if node == self.last_pattern_node { return MoveOutcome::Idle; }
|
||||
self.last_pattern_node = node;
|
||||
node.and_then( |n| w.handlers.pattern_msg( n ) )
|
||||
};
|
||||
return MoveOutcome::Slider { msg };
|
||||
}
|
||||
@@ -340,7 +361,7 @@ impl<Msg: Clone> GestureState<Msg>
|
||||
|
||||
if !self.scroll_locked
|
||||
{
|
||||
if dx.abs() <= 8.0 && dy.abs() <= 8.0
|
||||
if dx.abs() <= swipe.slop() && dy.abs() <= swipe.slop()
|
||||
{
|
||||
return MoveOutcome::Idle;
|
||||
}
|
||||
@@ -381,7 +402,7 @@ impl<Msg: Clone> GestureState<Msg>
|
||||
let moved = if axis.allows_x() && axis.allows_y() { dx.hypot( dy ) }
|
||||
else if axis.allows_x() { dx.abs() }
|
||||
else { dy.abs() };
|
||||
if moved > 8.0 { self.scroll_drag_started = true; }
|
||||
if moved > swipe.slop() { self.scroll_drag_started = true; }
|
||||
self.start = Some( pos );
|
||||
// Gate the dismiss on the same 8 px arm as a real scroll drag so
|
||||
// tap jitter at the top can't engage a follow-the-finger close.
|
||||
@@ -409,7 +430,7 @@ impl<Msg: Clone> GestureState<Msg>
|
||||
|
||||
// Lock onto the dominant axis on the first 8 px of travel so
|
||||
// vertical and horizontal swipes stay mutually exclusive.
|
||||
if self.swipe_axis.is_none() && dx.abs().max( dy.abs() ) > 8.0
|
||||
if self.swipe_axis.is_none() && dx.abs().max( dy.abs() ) > swipe.slop()
|
||||
{
|
||||
self.swipe_axis = Some( if dy.abs() >= dx.abs() { SwipeAxis::Vertical } else { SwipeAxis::Horizontal } );
|
||||
}
|
||||
@@ -421,7 +442,7 @@ impl<Msg: Clone> GestureState<Msg>
|
||||
if dy > 0.0
|
||||
{
|
||||
let threshold = h * swipe.up_thresh;
|
||||
if dy.abs() > 8.0 { self.vertical_drag_started = true; }
|
||||
if dy.abs() > swipe.slop() { self.vertical_drag_started = true; }
|
||||
// Unclamped on purpose — lets follow-the-finger
|
||||
// panels track past the commit threshold.
|
||||
( Some( ( dy / threshold ).max( 0.0 ) ), None )
|
||||
@@ -429,7 +450,7 @@ impl<Msg: Clone> GestureState<Msg>
|
||||
else if start.y <= h * swipe.down_edge
|
||||
{
|
||||
let threshold = h * swipe.down_thresh;
|
||||
if dy.abs() > 8.0 { self.vertical_drag_started = true; }
|
||||
if dy.abs() > swipe.slop() { self.vertical_drag_started = true; }
|
||||
// Unclamped on purpose — lets follow-the-finger
|
||||
// panels track past the commit threshold.
|
||||
( None, Some( ( -dy / threshold ).max( 0.0 ) ) )
|
||||
@@ -501,6 +522,7 @@ impl<Msg: Clone> GestureState<Msg>
|
||||
self.mouse_press = false;
|
||||
self.horizontal_drag_started = false;
|
||||
self.vertical_drag_started = false;
|
||||
self.last_pattern_node = None;
|
||||
|
||||
let mut events = Vec::new();
|
||||
|
||||
|
||||
@@ -92,4 +92,22 @@ pub struct SwipeConfig
|
||||
pub overscroll_thresh: f32,
|
||||
pub surface_width: u32,
|
||||
pub surface_height: u32,
|
||||
/// Buffer scale of the surface (physical px per logical px), so the
|
||||
/// motion thresholds below stay a logical size on a HiDPI panel.
|
||||
pub scale: f32,
|
||||
}
|
||||
|
||||
impl SwipeConfig
|
||||
{
|
||||
/// Travel past which a press becomes a scroll, swipe or drag: 8 logical px.
|
||||
pub fn slop( &self ) -> f32
|
||||
{
|
||||
8.0 * self.scale.max( 1.0 )
|
||||
}
|
||||
|
||||
/// Travel past which a touch hold stops being a long-press candidate: 6 logical px.
|
||||
pub fn stray( &self ) -> f32
|
||||
{
|
||||
6.0 * self.scale.max( 1.0 )
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,6 +87,7 @@ fn cfg_full( w: u32, h: u32 ) -> SwipeConfig
|
||||
overscroll_thresh: 0.25,
|
||||
surface_width: w,
|
||||
surface_height: h,
|
||||
scale: 1.0,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ impl<A: App> AppData<A>
|
||||
overscroll_thresh: self.app.overscroll_dismiss_threshold(),
|
||||
surface_width: ss.physical_width(),
|
||||
surface_height: ss.physical_height(),
|
||||
scale: ss.scale_factor.max( 1 ) as f32,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+36
-14
@@ -63,6 +63,23 @@ impl<A: App> TouchHandler for AppData<A>
|
||||
if self.is_secondary_surface( &surface ) { return; }
|
||||
let focus = self.focus_for_surface( &surface ).unwrap_or( SurfaceFocus::Main );
|
||||
self.touch_focus.insert( id, focus );
|
||||
|
||||
// A second finger landing on a plain press means the first was a
|
||||
// tap: commit it at its last position and hand the slot over, so
|
||||
// alternating thumbs never lose a key.
|
||||
if !self.app.claims_raw_touch()
|
||||
{
|
||||
let takeover =
|
||||
{
|
||||
let ss = self.surface( focus );
|
||||
ss.primary_touch_id.is_some_and( |primary| primary != id ) && ss.gesture.is_plain_press()
|
||||
};
|
||||
if takeover
|
||||
{
|
||||
self.release_primary( focus );
|
||||
}
|
||||
}
|
||||
|
||||
let pos = self.surface( focus ).to_physical( position.0, position.1 );
|
||||
self.pointer_pos = pos;
|
||||
|
||||
@@ -226,20 +243,7 @@ impl<A: App> TouchHandler for AppData<A>
|
||||
return;
|
||||
}
|
||||
|
||||
// Touch-up does not carry a position in wl_touch — the last
|
||||
// motion's position is the release point.
|
||||
let pos = self.pointer_pos;
|
||||
let global_drag = self.has_active_long_press_drag();
|
||||
let swipe = self.swipe_config( focus );
|
||||
let events_out =
|
||||
{
|
||||
let ss = self.surface_mut( focus );
|
||||
ss.needs_redraw = true;
|
||||
ss.primary_touch_id = None;
|
||||
ss.gesture.on_release( pos, &ss.frame.widget_rects, &swipe, global_drag )
|
||||
};
|
||||
self.apply_release_events( focus, events_out );
|
||||
self.stop_button_repeat();
|
||||
self.release_primary( focus );
|
||||
}
|
||||
|
||||
fn motion(
|
||||
@@ -334,6 +338,24 @@ impl<A: App> TouchHandler for AppData<A>
|
||||
|
||||
impl<A: App> AppData<A>
|
||||
{
|
||||
/// Release the primary slot's gesture. Touch-up carries no position in
|
||||
/// wl_touch, so the last motion's position is the release point.
|
||||
fn release_primary( &mut self, focus: SurfaceFocus )
|
||||
{
|
||||
let pos = self.pointer_pos;
|
||||
let global_drag = self.has_active_long_press_drag();
|
||||
let swipe = self.swipe_config( focus );
|
||||
let events_out =
|
||||
{
|
||||
let ss = self.surface_mut( focus );
|
||||
ss.needs_redraw = true;
|
||||
ss.primary_touch_id = None;
|
||||
ss.gesture.on_release( pos, &ss.frame.widget_rects, &swipe, global_drag )
|
||||
};
|
||||
self.apply_release_events( focus, events_out );
|
||||
self.stop_button_repeat();
|
||||
}
|
||||
|
||||
/// Drop every in-flight touch gesture across all surfaces, notifying
|
||||
/// the app of any slot still down that it was tracking raw (auxiliary
|
||||
/// fingers, or every finger under `claims_raw_touch`). Shared by the
|
||||
|
||||
Reference in New Issue
Block a user