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:
@@ -50,6 +50,11 @@ All notable changes to `ltk` are documented here. The format is based on [Keep a
|
|||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
|
- **Fast two-thumb typing no longer drops taps.** A finger landing while another was still down went to the auxiliary slot and never reached the gesture machine, so on a touch keypad every overlapping tap was lost. 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 `App::on_touch_up`. Presses that have become a drag keep the old behaviour, so a second finger during a pattern stroke or a slider drag still stays out of the gesture machine.
|
||||||
|
- **`PatternGrid::on_node` fires once per node, not once per motion event.** Every touch motion within a node's snap radius re-emitted the node's message and requested a repaint, so drawing a pattern rebuilt and re-rastered the surface at the touchscreen's motion rate. The gesture machine now remembers the node the finger is over (`last_pattern_node`) and emits only on change; a `Slider` move outcome without a message no longer requests a redraw.
|
||||||
|
- **Gesture thresholds are logical pixels.** 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 on a scale-2 panel they were 4 and 3 logical px and an ordinary thumb tap became a vertical drag whose release skipped the button. `SwipeConfig` now carries the surface's buffer `scale` and the machine reads the thresholds through `SwipeConfig::slop` / `stray`.
|
||||||
|
- **A 1:1 opaque image is a row copy in the software renderer.** With the premultiplied copy cached, drawing it unscaled with no clip and full alpha now copies rows into the canvas instead of running tiny-skia's bilinear pipeline over every pixel, which is what a full-surface backdrop cost on every frame.
|
||||||
|
- **The software renderer premultiplies an image once, not on every draw.** `SoftwareCanvas::draw_image_data` converted the whole source to premultiplied alpha in floating point on each call, so a full-surface backdrop cost a pass over every pixel per frame on top of the blit. Premultiplied copies are now kept in a 32 MiB least-recently-drawn cache keyed by size, content fingerprint and alpha, the same key the GLES texture cache uses (`fingerprint_rgba` moves to `render::helpers` to be shared).
|
||||||
- **Rows no longer spill their children out of the layout.** `Row` distributed leftover width but had no notion of a shortfall: `leftover` floored at zero, so a cluster wider than its rect kept every preferred width and overflowed — symmetrically, since the no-spacer branch centres it, which is why both end labels of a segmented control were clipped at once. The shortfall now comes off the widest children first (water filling), so a long title gives up its width while the icon beside it keeps its size, and equal siblings — a segmented control — share it evenly; pinned spacers keep their width, and `Row::no_shrink()` opts out for strips deliberately wider than their viewport. `Button` elides its label into the rect it is granted, so shrinking degrades to a truncated label rather than clipped glyphs; the truncation rule moves to one crate-internal helper shared with `ListItem`.
|
- **Rows no longer spill their children out of the layout.** `Row` distributed leftover width but had no notion of a shortfall: `leftover` floored at zero, so a cluster wider than its rect kept every preferred width and overflowed — symmetrically, since the no-spacer branch centres it, which is why both end labels of a segmented control were clipped at once. The shortfall now comes off the widest children first (water filling), so a long title gives up its width while the icon beside it keeps its size, and equal siblings — a segmented control — share it evenly; pinned spacers keep their width, and `Row::no_shrink()` opts out for strips deliberately wider than their viewport. `Button` elides its label into the rect it is granted, so shrinking degrades to a truncated label rather than clipped glyphs; the truncation rule moves to one crate-internal helper shared with `ListItem`.
|
||||||
- **`ListItem` labels no longer run under the trailing slot.** The label and subtitle were painted with no width limit, so a title longer than the row overlapped the trailing text or the disclosure icon instead of truncating. The trailing slots are now laid out first and both lines are elided with an ellipsis against the space they leave, matching what `Text` already did.
|
- **`ListItem` labels no longer run under the trailing slot.** The label and subtitle were painted with no width limit, so a title longer than the row overlapped the trailing text or the disclosure icon instead of truncating. The trailing slots are now laid out first and both lines are elided with an ellipsis against the space they leave, matching what `Text` already did.
|
||||||
- **Text inputs no longer insert mid-string when the value grows after focus.** Focusing a text input pinned the cursor to a snapshot of `value.len()`; if the value kept growing without the widget seeing keystrokes (a field fed over IPC), the first normally-delivered key inserted at the stale position. The focus-time cursor is now an end-of-value sentinel that every consumer clamps to the current value length, collapsing to a concrete position on the first real keystroke or click.
|
- **Text inputs no longer insert mid-string when the value grows after focus.** Focusing a text input pinned the cursor to a snapshot of `value.len()`; if the value kept growing without the widget seeing keystrokes (a field fed over IPC), the first normally-delivered key inserted at the stale position. The focus-time cursor is now an end-of-value sentinel that every consumer clamps to the current value length, collapsing to a concrete position on the first real keystroke or click.
|
||||||
|
|||||||
@@ -320,7 +320,7 @@ Downstream consumers shipping into regulated environments (EN 301 549, WCAG 2.1
|
|||||||
|
|
||||||
**Cross-application drag-and-drop — deferred.** The clipboard now bridges to the Wayland selection via `wl_data_device_manager` (see `event_loop/data_device.rs`), so Ctrl+C / Ctrl+V crosses application boundaries when the compositor advertises the global. Middle-click primary selection (`zwp_primary_selection_v1`) and inter-app drag-and-drop targets (drop-zone widgets that accept text / URI lists from outside the process) are still pending — they share most of the offer / source plumbing but need widget-level drop-target wiring on top.
|
**Cross-application drag-and-drop — deferred.** The clipboard now bridges to the Wayland selection via `wl_data_device_manager` (see `event_loop/data_device.rs`), so Ctrl+C / Ctrl+V crosses application boundaries when the compositor advertises the global. Middle-click primary selection (`zwp_primary_selection_v1`) and inter-app drag-and-drop targets (drop-zone widgets that accept text / URI lists from outside the process) are still pending — they share most of the offer / source plumbing but need widget-level drop-target wiring on top.
|
||||||
|
|
||||||
**Multi-touch — primary slot plus raw auxiliary fingers.** The first finger to land on a surface becomes its primary slot and drives the built-in gesture machine (swipe, scroll, long-press, drag). Additional fingers bypass the gesture machine and surface directly through `App::on_touch_down` / `on_touch_move` / `on_touch_up`, so apps can implement pinch-zoom or two-finger pan without losing the built-in gestures. Apps that want the whole stream raw — an embedded web view, a drawing canvas — return `true` from `App::claims_raw_touch`: every finger, primary included, then reports through `on_touch_*` and widget presses, taps and swipes never fire. What ltk itself does not provide is *recognition* of multi-finger gestures — pinch and rotate detection is the app's job on top of the raw stream.
|
**Multi-touch — primary slot plus raw auxiliary fingers.** The first finger to land on a surface becomes its primary slot and drives the built-in gesture machine (swipe, scroll, long-press, drag). Additional fingers bypass the gesture machine and surface directly through `App::on_touch_down` / `on_touch_move` / `on_touch_up`, so apps can implement pinch-zoom or two-finger pan without losing the built-in gestures. One exception keeps fast two-thumb typing whole: a finger landing while the primary is still a plain press (no drag, scroll, swipe or slider in flight) commits that press as a tap at its last position and takes the primary slot itself, so alternating taps whose touches overlap all register; the earlier finger's release then reports through `on_touch_up`. Apps that want the whole stream raw — an embedded web view, a drawing canvas — return `true` from `App::claims_raw_touch`: every finger, primary included, then reports through `on_touch_*` and widget presses, taps and swipes never fire. What ltk itself does not provide is *recognition* of multi-finger gestures — pinch and rotate detection is the app's job on top of the raw stream.
|
||||||
|
|
||||||
**HarfBuzz shaping — wired in.** `src/text_shaping.rs::shape_line` now drives both renderers: the line is BiDi-reordered, split into per-font sub-runs and shaped through rustybuzz. The glyph cache is keyed on `(glyph_id, size_bits, font_id)` and each glyph is rasterised by index via `fontdue::Font::rasterize_indexed`, so Arabic connected forms, Devanagari clusters and CJK shaped glyphs render correctly.
|
**HarfBuzz shaping — wired in.** `src/text_shaping.rs::shape_line` now drives both renderers: the line is BiDi-reordered, split into per-font sub-runs and shaped through rustybuzz. The glyph cache is keyed on `(glyph_id, size_bits, font_id)` and each glyph is rasterised by index via `fontdue::Font::rasterize_indexed`, so Arabic connected forms, Devanagari clusters and CJK shaped glyphs render correctly.
|
||||||
|
|
||||||
|
|||||||
+4
-4
@@ -362,10 +362,10 @@ pattern_grid( self.path.clone() )
|
|||||||
# }}
|
# }}
|
||||||
```
|
```
|
||||||
|
|
||||||
`on_node( f )` fires with a node index whenever a press or a drag reaches
|
`on_node( f )` fires with a node index when a press or a drag enters a
|
||||||
a node, including nodes already in the path — appending, skipping a
|
node, once per entry rather than once per motion event, including nodes
|
||||||
repeat, or taking in the node a straight stroke crosses is the app's
|
already in the path — appending, skipping a repeat, or taking in the node
|
||||||
rule, not the widget's. `on_release( msg )` fires when the finger lifts.
|
a straight stroke crosses is the app's rule, not the widget's. `on_release( msg )` fires when the finger lifts.
|
||||||
`hide_stroke( true )` draws only the dots, against shoulder surfing;
|
`hide_stroke( true )` draws only the dots, against shoulder surfing;
|
||||||
`error( true )` paints the current path in the palette's `danger` colour.
|
`error( true )` paints the current path in the palette's `danger` colour.
|
||||||
`size( len )` sets the side of the square grid (any `Length`); without it the
|
`size( len )` sets the side of the square grid (any `Length`); without it the
|
||||||
|
|||||||
@@ -24,11 +24,9 @@
|
|||||||
//! single texture larger than the whole budget still draws — the cache
|
//! single texture larger than the whole budget still draws — the cache
|
||||||
//! then holds that one entry until something replaces it.
|
//! then holds that one entry until something replaces it.
|
||||||
|
|
||||||
use std::collections::hash_map::DefaultHasher;
|
|
||||||
use std::hash::{ Hash, Hasher };
|
|
||||||
|
|
||||||
use glow::HasContext;
|
use glow::HasContext;
|
||||||
|
|
||||||
|
use crate::render::helpers::fingerprint_rgba;
|
||||||
use crate::types::Rect;
|
use crate::types::Rect;
|
||||||
|
|
||||||
use super::helpers::{ ortho_rect, upload_rgba_texture };
|
use super::helpers::{ ortho_rect, upload_rgba_texture };
|
||||||
@@ -40,37 +38,6 @@ use super::GlesCanvas;
|
|||||||
/// the compositor.
|
/// the compositor.
|
||||||
pub const IMAGE_CACHE_MAX_BYTES: usize = 32 * 1024 * 1024;
|
pub const IMAGE_CACHE_MAX_BYTES: usize = 32 * 1024 * 1024;
|
||||||
|
|
||||||
/// Compute a 64-bit fingerprint of an RGBA buffer for the texture
|
|
||||||
/// cache. Hashes the full byte slice for small buffers (icons,
|
|
||||||
/// thumbnails — below 16 KB ≈ 64×64 RGBA), and falls back to a
|
|
||||||
/// strided 8 × 512-byte sample for anything larger so a wallpaper
|
|
||||||
/// blit does not pay an 8 MB hash on every frame. Both modes
|
|
||||||
/// distinguish the chevron-icon-style cases that motivated the move
|
|
||||||
/// to content-keying — the SVG-rasterised buffers differ across
|
|
||||||
/// most of their interior bytes, not just at the corners.
|
|
||||||
fn fingerprint_rgba( bytes: &[u8] ) -> u64
|
|
||||||
{
|
|
||||||
const FULL_HASH_THRESHOLD: usize = 16 * 1024;
|
|
||||||
const SAMPLE_CHUNKS: usize = 8;
|
|
||||||
const SAMPLE_CHUNK_BYTES: usize = 512;
|
|
||||||
|
|
||||||
let mut h = DefaultHasher::new();
|
|
||||||
let n = bytes.len();
|
|
||||||
n.hash( &mut h );
|
|
||||||
if n <= FULL_HASH_THRESHOLD
|
|
||||||
{
|
|
||||||
bytes.hash( &mut h );
|
|
||||||
} else {
|
|
||||||
let stride = n / SAMPLE_CHUNKS;
|
|
||||||
for i in 0..SAMPLE_CHUNKS
|
|
||||||
{
|
|
||||||
let pos = ( i * stride ).min( n - SAMPLE_CHUNK_BYTES );
|
|
||||||
bytes[ pos..pos + SAMPLE_CHUNK_BYTES ].hash( &mut h );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
h.finish()
|
|
||||||
}
|
|
||||||
|
|
||||||
impl GlesCanvas
|
impl GlesCanvas
|
||||||
{
|
{
|
||||||
/// Blit RGBA image data scaled to dest rect with opacity.
|
/// Blit RGBA image data scaled to dest rect with opacity.
|
||||||
|
|||||||
@@ -37,9 +37,9 @@ impl<A: App> AppData<A>
|
|||||||
if let Some( m ) = msg
|
if let Some( m ) = msg
|
||||||
{
|
{
|
||||||
self.pending_msgs.push( m );
|
self.pending_msgs.push( m );
|
||||||
}
|
|
||||||
self.surface_mut( focus ).request_redraw();
|
self.surface_mut( focus ).request_redraw();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
MoveOutcome::Scroll =>
|
MoveOutcome::Scroll =>
|
||||||
{
|
{
|
||||||
self.surface_mut( focus ).request_redraw();
|
self.surface_mut( focus ).request_redraw();
|
||||||
|
|||||||
@@ -144,6 +144,9 @@ pub struct GestureState<Msg: Clone>
|
|||||||
/// the pointer Press handler right after [`Self::on_press`]; left
|
/// the pointer Press handler right after [`Self::on_press`]; left
|
||||||
/// `false` for touch presses.
|
/// `false` for touch presses.
|
||||||
pub mouse_press: bool,
|
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>
|
impl<Msg: Clone> Default for GestureState<Msg>
|
||||||
@@ -176,9 +179,22 @@ impl<Msg: Clone> GestureState<Msg>
|
|||||||
long_press_fired: false,
|
long_press_fired: false,
|
||||||
overscroll_y: 0.0,
|
overscroll_y: 0.0,
|
||||||
mouse_press: false,
|
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
|
/// Press / touch-down: record the gesture origin, capture long-press
|
||||||
/// handler if the hit widget has one, identify slider / scroll
|
/// handler if the hit widget has one, identify slider / scroll
|
||||||
/// targets. Returns the hit index (for keyboard focus update) and
|
/// targets. Returns the hit index (for keyboard focus update) and
|
||||||
@@ -257,7 +273,9 @@ impl<Msg: Clone> GestureState<Msg>
|
|||||||
initial_slider_msg = w.handlers.slider_change_msg( value );
|
initial_slider_msg = w.handlers.slider_change_msg( value );
|
||||||
} else if w.handlers.is_drag_target() {
|
} else if w.handlers.is_drag_target() {
|
||||||
self.dragging_slider = Some( idx );
|
self.dragging_slider = Some( idx );
|
||||||
initial_slider_msg = w.handlers.pattern_node_msg( w.rect, pos );
|
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
|
if let Some( origin ) = self.long_press_origin
|
||||||
{
|
{
|
||||||
let d = ( pos.x - origin.x ).hypot( pos.y - origin.y );
|
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_start = None;
|
||||||
self.long_press_origin = 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 );
|
let value = w.handlers.slider_value_from_pos( w.rect, pos );
|
||||||
w.handlers.slider_change_msg( value )
|
w.handlers.slider_change_msg( value )
|
||||||
} else {
|
} 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 };
|
return MoveOutcome::Slider { msg };
|
||||||
}
|
}
|
||||||
@@ -340,7 +361,7 @@ impl<Msg: Clone> GestureState<Msg>
|
|||||||
|
|
||||||
if !self.scroll_locked
|
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;
|
return MoveOutcome::Idle;
|
||||||
}
|
}
|
||||||
@@ -381,7 +402,7 @@ impl<Msg: Clone> GestureState<Msg>
|
|||||||
let moved = if axis.allows_x() && axis.allows_y() { dx.hypot( dy ) }
|
let moved = if axis.allows_x() && axis.allows_y() { dx.hypot( dy ) }
|
||||||
else if axis.allows_x() { dx.abs() }
|
else if axis.allows_x() { dx.abs() }
|
||||||
else { dy.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 );
|
self.start = Some( pos );
|
||||||
// Gate the dismiss on the same 8 px arm as a real scroll drag so
|
// 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.
|
// 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
|
// Lock onto the dominant axis on the first 8 px of travel so
|
||||||
// vertical and horizontal swipes stay mutually exclusive.
|
// 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 } );
|
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
|
if dy > 0.0
|
||||||
{
|
{
|
||||||
let threshold = h * swipe.up_thresh;
|
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
|
// Unclamped on purpose — lets follow-the-finger
|
||||||
// panels track past the commit threshold.
|
// panels track past the commit threshold.
|
||||||
( Some( ( dy / threshold ).max( 0.0 ) ), None )
|
( Some( ( dy / threshold ).max( 0.0 ) ), None )
|
||||||
@@ -429,7 +450,7 @@ impl<Msg: Clone> GestureState<Msg>
|
|||||||
else if start.y <= h * swipe.down_edge
|
else if start.y <= h * swipe.down_edge
|
||||||
{
|
{
|
||||||
let threshold = h * swipe.down_thresh;
|
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
|
// Unclamped on purpose — lets follow-the-finger
|
||||||
// panels track past the commit threshold.
|
// panels track past the commit threshold.
|
||||||
( None, Some( ( -dy / threshold ).max( 0.0 ) ) )
|
( None, Some( ( -dy / threshold ).max( 0.0 ) ) )
|
||||||
@@ -501,6 +522,7 @@ impl<Msg: Clone> GestureState<Msg>
|
|||||||
self.mouse_press = false;
|
self.mouse_press = false;
|
||||||
self.horizontal_drag_started = false;
|
self.horizontal_drag_started = false;
|
||||||
self.vertical_drag_started = false;
|
self.vertical_drag_started = false;
|
||||||
|
self.last_pattern_node = None;
|
||||||
|
|
||||||
let mut events = Vec::new();
|
let mut events = Vec::new();
|
||||||
|
|
||||||
|
|||||||
@@ -92,4 +92,22 @@ pub struct SwipeConfig
|
|||||||
pub overscroll_thresh: f32,
|
pub overscroll_thresh: f32,
|
||||||
pub surface_width: u32,
|
pub surface_width: u32,
|
||||||
pub surface_height: 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,
|
overscroll_thresh: 0.25,
|
||||||
surface_width: w,
|
surface_width: w,
|
||||||
surface_height: h,
|
surface_height: h,
|
||||||
|
scale: 1.0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ impl<A: App> AppData<A>
|
|||||||
overscroll_thresh: self.app.overscroll_dismiss_threshold(),
|
overscroll_thresh: self.app.overscroll_dismiss_threshold(),
|
||||||
surface_width: ss.physical_width(),
|
surface_width: ss.physical_width(),
|
||||||
surface_height: ss.physical_height(),
|
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; }
|
if self.is_secondary_surface( &surface ) { return; }
|
||||||
let focus = self.focus_for_surface( &surface ).unwrap_or( SurfaceFocus::Main );
|
let focus = self.focus_for_surface( &surface ).unwrap_or( SurfaceFocus::Main );
|
||||||
self.touch_focus.insert( id, focus );
|
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 );
|
let pos = self.surface( focus ).to_physical( position.0, position.1 );
|
||||||
self.pointer_pos = pos;
|
self.pointer_pos = pos;
|
||||||
|
|
||||||
@@ -226,20 +243,7 @@ impl<A: App> TouchHandler for AppData<A>
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Touch-up does not carry a position in wl_touch — the last
|
self.release_primary( focus );
|
||||||
// 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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn motion(
|
fn motion(
|
||||||
@@ -334,6 +338,24 @@ impl<A: App> TouchHandler for AppData<A>
|
|||||||
|
|
||||||
impl<A: App> 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
|
/// Drop every in-flight touch gesture across all surfaces, notifying
|
||||||
/// the app of any slot still down that it was tracking raw (auxiliary
|
/// the app of any slot still down that it was tracking raw (auxiliary
|
||||||
/// fingers, or every finger under `claims_raw_touch`). Shared by the
|
/// fingers, or every finger under `claims_raw_touch`). Shared by the
|
||||||
|
|||||||
@@ -5,10 +5,40 @@
|
|||||||
//! rounded-rect construction. System-font resolution lives in
|
//! rounded-rect construction. System-font resolution lives in
|
||||||
//! `crate::system_fonts`.
|
//! `crate::system_fonts`.
|
||||||
|
|
||||||
|
use std::collections::hash_map::DefaultHasher;
|
||||||
|
use std::hash::{ Hash, Hasher };
|
||||||
|
|
||||||
use tiny_skia::{ Path, PathBuilder };
|
use tiny_skia::{ Path, PathBuilder };
|
||||||
|
|
||||||
use crate::types::{ Corners, PathCmd };
|
use crate::types::{ Corners, PathCmd };
|
||||||
|
|
||||||
|
/// A 64-bit fingerprint of an RGBA buffer, the key of both backends' image
|
||||||
|
/// caches. Hashes the full slice below 16 KB (icons, thumbnails) and a
|
||||||
|
/// strided 8 × 512-byte sample above it, so a wallpaper does not pay a
|
||||||
|
/// full hash on every frame.
|
||||||
|
pub( crate ) fn fingerprint_rgba( bytes: &[u8] ) -> u64
|
||||||
|
{
|
||||||
|
const FULL_HASH_THRESHOLD: usize = 16 * 1024;
|
||||||
|
const SAMPLE_CHUNKS: usize = 8;
|
||||||
|
const SAMPLE_CHUNK_BYTES: usize = 512;
|
||||||
|
|
||||||
|
let mut h = DefaultHasher::new();
|
||||||
|
let n = bytes.len();
|
||||||
|
n.hash( &mut h );
|
||||||
|
if n <= FULL_HASH_THRESHOLD
|
||||||
|
{
|
||||||
|
bytes.hash( &mut h );
|
||||||
|
} else {
|
||||||
|
let stride = n / SAMPLE_CHUNKS;
|
||||||
|
for i in 0..SAMPLE_CHUNKS
|
||||||
|
{
|
||||||
|
let pos = ( i * stride ).min( n - SAMPLE_CHUNK_BYTES );
|
||||||
|
bytes[ pos..pos + SAMPLE_CHUNK_BYTES ].hash( &mut h );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
h.finish()
|
||||||
|
}
|
||||||
|
|
||||||
/// Cubic bezier control-point factor for a quarter-circle approximation
|
/// Cubic bezier control-point factor for a quarter-circle approximation
|
||||||
/// (`(4/3) * (sqrt(2) - 1) ≈ 0.5523`).
|
/// (`(4/3) * (sqrt(2) - 1) ≈ 0.5523`).
|
||||||
const KAPPA: f32 = 0.5523_f32;
|
const KAPPA: f32 = 0.5523_f32;
|
||||||
|
|||||||
+117
-22
@@ -13,12 +13,110 @@
|
|||||||
//! (Abgr8888 matches tiny-skia's byte order) or swaps R/B in blocks
|
//! (Abgr8888 matches tiny-skia's byte order) or swaps R/B in blocks
|
||||||
//! of four pixels (Argb8888 fallback).
|
//! of four pixels (Argb8888 fallback).
|
||||||
|
|
||||||
use tiny_skia::{ Pixmap, PixmapPaint, Transform };
|
use std::cell::RefCell;
|
||||||
|
use std::collections::{ HashMap, VecDeque };
|
||||||
|
|
||||||
|
use tiny_skia::{ IntSize, Pixmap, PixmapPaint, Transform };
|
||||||
|
|
||||||
use crate::types::Rect;
|
use crate::types::Rect;
|
||||||
|
|
||||||
|
use super::helpers::fingerprint_rgba;
|
||||||
use super::SoftwareCanvas;
|
use super::SoftwareCanvas;
|
||||||
|
|
||||||
|
/// Byte budget of the premultiplied-image cache, matching the GLES texture
|
||||||
|
/// cache: a phone wallpaper plus a working set of icons.
|
||||||
|
const PREMUL_CACHE_MAX_BYTES: usize = 32 * 1024 * 1024;
|
||||||
|
|
||||||
|
type PremulKey = ( u32, u32, u64, u32 );
|
||||||
|
|
||||||
|
/// A premultiplied source and whether every pixel was fully opaque, which
|
||||||
|
/// lets a 1:1 draw be a row copy.
|
||||||
|
struct Premul
|
||||||
|
{
|
||||||
|
pixmap: Pixmap,
|
||||||
|
opaque: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Premultiplied copies of the images drawn recently, keyed by size,
|
||||||
|
/// content fingerprint and alpha, so a backdrop or a logo is converted once
|
||||||
|
/// rather than on every frame. Least-recently-drawn eviction; the entry
|
||||||
|
/// being drawn is never evicted.
|
||||||
|
#[ derive( Default ) ]
|
||||||
|
struct PremulCache
|
||||||
|
{
|
||||||
|
entries: HashMap<PremulKey, Premul>,
|
||||||
|
lru: VecDeque<PremulKey>,
|
||||||
|
bytes: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
thread_local! {
|
||||||
|
static PREMUL_CACHE: RefCell<PremulCache> = RefCell::new( PremulCache::default() );
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PremulCache
|
||||||
|
{
|
||||||
|
/// Mark `key` as just used. `false` when it is not cached.
|
||||||
|
fn touch( &mut self, key: &PremulKey ) -> bool
|
||||||
|
{
|
||||||
|
let Some( pos ) = self.lru.iter().position( |k| k == key ) else { return false };
|
||||||
|
self.lru.remove( pos );
|
||||||
|
self.lru.push_back( *key );
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn insert( &mut self, key: PremulKey, premul: Premul )
|
||||||
|
{
|
||||||
|
self.bytes += premul.pixmap.data().len();
|
||||||
|
self.entries.insert( key, premul );
|
||||||
|
self.lru.push_back( key );
|
||||||
|
while self.bytes > PREMUL_CACHE_MAX_BYTES && self.lru.len() > 1
|
||||||
|
{
|
||||||
|
let Some( old ) = self.lru.pop_front() else { break };
|
||||||
|
if let Some( p ) = self.entries.remove( &old )
|
||||||
|
{
|
||||||
|
self.bytes = self.bytes.saturating_sub( p.pixmap.data().len() );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn premultiply( rgba: &[u8], size: IntSize, alpha: f32 ) -> Option<Premul>
|
||||||
|
{
|
||||||
|
let mut out = vec![ 0u8; rgba.len() ];
|
||||||
|
let mut opaque = alpha >= 1.0;
|
||||||
|
for ( dst, src ) in out.chunks_exact_mut( 4 ).zip( rgba.chunks_exact( 4 ) )
|
||||||
|
{
|
||||||
|
opaque &= src[3] == 255;
|
||||||
|
let a = ( src[3] as f32 / 255.0 ) * alpha;
|
||||||
|
dst[0] = ( src[0] as f32 * a ) as u8;
|
||||||
|
dst[1] = ( src[1] as f32 * a ) as u8;
|
||||||
|
dst[2] = ( src[2] as f32 * a ) as u8;
|
||||||
|
dst[3] = ( a * 255.0 ) as u8;
|
||||||
|
}
|
||||||
|
Pixmap::from_vec( out, size ).map( |pixmap| Premul { pixmap, opaque } )
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Copy `src` onto `dst` at (`x`, `y`) row by row, clipped to `dst`.
|
||||||
|
fn copy_rows( dst: &mut Pixmap, src: &Pixmap, x: i32, y: i32 )
|
||||||
|
{
|
||||||
|
let ( dw, dh ) = ( dst.width() as i32, dst.height() as i32 );
|
||||||
|
let ( sw, sh ) = ( src.width() as i32, src.height() as i32 );
|
||||||
|
let x0 = x.max( 0 );
|
||||||
|
let x1 = ( x + sw ).min( dw );
|
||||||
|
let y0 = y.max( 0 );
|
||||||
|
let y1 = ( y + sh ).min( dh );
|
||||||
|
if x0 >= x1 || y0 >= y1 { return; }
|
||||||
|
let row_bytes = ( ( x1 - x0 ) * 4 ) as usize;
|
||||||
|
let src_data = src.data();
|
||||||
|
let dst_data = dst.data_mut();
|
||||||
|
for row in y0..y1
|
||||||
|
{
|
||||||
|
let s = ( ( ( row - y ) * sw + ( x0 - x ) ) * 4 ) as usize;
|
||||||
|
let d = ( ( row * dw + x0 ) * 4 ) as usize;
|
||||||
|
dst_data[ d..d + row_bytes ].copy_from_slice( &src_data[ s..s + row_bytes ] );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl SoftwareCanvas
|
impl SoftwareCanvas
|
||||||
{
|
{
|
||||||
pub fn draw_image_data( &mut self, rgba_data: &[u8], img_w: u32, img_h: u32, dest: Rect, opacity: f32 )
|
pub fn draw_image_data( &mut self, rgba_data: &[u8], img_w: u32, img_h: u32, dest: Rect, opacity: f32 )
|
||||||
@@ -41,26 +139,25 @@ impl SoftwareCanvas
|
|||||||
|
|
||||||
let Some( int_size ) = tiny_skia::IntSize::from_wh( img_w, img_h ) else { return };
|
let Some( int_size ) = tiny_skia::IntSize::from_wh( img_w, img_h ) else { return };
|
||||||
|
|
||||||
thread_local! {
|
let alpha = opacity * self.global_alpha;
|
||||||
static PREMUL_BUF: std::cell::RefCell<Vec<u8>> = std::cell::RefCell::new( Vec::new() );
|
let key = ( img_w, img_h, fingerprint_rgba( rgba_data ), alpha.to_bits() );
|
||||||
|
PREMUL_CACHE.with( |cell|
|
||||||
|
{
|
||||||
|
let mut cache = cell.borrow_mut();
|
||||||
|
if !cache.touch( &key )
|
||||||
|
{
|
||||||
|
let Some( premul ) = premultiply( rgba_data, int_size, alpha ) else { return };
|
||||||
|
cache.insert( key, premul );
|
||||||
|
}
|
||||||
|
let Some( src ) = cache.entries.get( &key ) else { return };
|
||||||
|
|
||||||
|
let unscaled = dest.width as u32 == img_w && dest.height as u32 == img_h;
|
||||||
|
if src.opaque && unscaled && self.clip_mask.is_none()
|
||||||
|
{
|
||||||
|
copy_rows( &mut self.pixmap, &src.pixmap, dest.x as i32, dest.y as i32 );
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
PREMUL_BUF.with( |cell|
|
|
||||||
{
|
|
||||||
let mut premul = cell.borrow_mut();
|
|
||||||
let needed = rgba_data.len();
|
|
||||||
premul.resize( needed, 0 );
|
|
||||||
for ( dst, src ) in premul.chunks_exact_mut( 4 ).zip( rgba_data.chunks_exact( 4 ) )
|
|
||||||
{
|
|
||||||
let a = (src[3] as f32 / 255.0) * opacity * self.global_alpha;
|
|
||||||
dst[0] = (src[0] as f32 * a) as u8;
|
|
||||||
dst[1] = (src[1] as f32 * a) as u8;
|
|
||||||
dst[2] = (src[2] as f32 * a) as u8;
|
|
||||||
dst[3] = (a * 255.0) as u8;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some( src_pixmap ) = Pixmap::from_vec( std::mem::take( &mut *premul ), int_size )
|
|
||||||
{
|
|
||||||
let sx = dest.width / img_w as f32;
|
let sx = dest.width / img_w as f32;
|
||||||
let sy = dest.height / img_h as f32;
|
let sy = dest.height / img_h as f32;
|
||||||
let t = Transform::from_scale( sx, sy ).post_translate( dest.x, dest.y );
|
let t = Transform::from_scale( sx, sy ).post_translate( dest.x, dest.y );
|
||||||
@@ -69,9 +166,7 @@ impl SoftwareCanvas
|
|||||||
quality: tiny_skia::FilterQuality::Bilinear,
|
quality: tiny_skia::FilterQuality::Bilinear,
|
||||||
..PixmapPaint::default()
|
..PixmapPaint::default()
|
||||||
};
|
};
|
||||||
self.pixmap.draw_pixmap( 0, 0, src_pixmap.as_ref(), &paint, t, self.clip_mask.as_ref() );
|
self.pixmap.draw_pixmap( 0, 0, src.pixmap.as_ref(), &paint, t, self.clip_mask.as_ref() );
|
||||||
*premul = src_pixmap.take();
|
|
||||||
}
|
|
||||||
} );
|
} );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+19
-4
@@ -210,17 +210,32 @@ impl<Msg: Clone> WidgetHandlers<Msg>
|
|||||||
matches!( self, WidgetHandlers::Slider { .. } | WidgetHandlers::PatternGrid { .. } )
|
matches!( self, WidgetHandlers::Slider { .. } | WidgetHandlers::PatternGrid { .. } )
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The message a pattern grid fires for the node under `pos`, if any.
|
/// The pattern-grid node under `pos`, within the snap radius.
|
||||||
pub fn pattern_node_msg( &self, rect: Rect, pos: Point ) -> Option<Msg>
|
pub fn pattern_node_at( &self, rect: Rect, pos: Point ) -> Option<u8>
|
||||||
{
|
{
|
||||||
match self
|
match self
|
||||||
{
|
{
|
||||||
WidgetHandlers::PatternGrid { on_node: Some( f ), snap_px, .. } =>
|
WidgetHandlers::PatternGrid { snap_px, .. } => super::pattern_grid::node_at( rect, pos, *snap_px ),
|
||||||
super::pattern_grid::node_at( rect, pos, *snap_px ).map( |node| f( node ) ),
|
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The message a pattern grid fires for `node`, if it has an `on_node`.
|
||||||
|
pub fn pattern_msg( &self, node: u8 ) -> Option<Msg>
|
||||||
|
{
|
||||||
|
match self
|
||||||
|
{
|
||||||
|
WidgetHandlers::PatternGrid { on_node: Some( f ), .. } => Some( f( node ) ),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The message a pattern grid fires for the node under `pos`, if any.
|
||||||
|
pub fn pattern_node_msg( &self, rect: Rect, pos: Point ) -> Option<Msg>
|
||||||
|
{
|
||||||
|
self.pattern_node_at( rect, pos ).and_then( |node| self.pattern_msg( node ) )
|
||||||
|
}
|
||||||
|
|
||||||
pub fn pattern_release_msg( &self ) -> Option<Msg>
|
pub fn pattern_release_msg( &self ) -> Option<Msg>
|
||||||
{
|
{
|
||||||
match self
|
match self
|
||||||
|
|||||||
Reference in New Issue
Block a user