diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d18071..fa81438 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,6 +38,7 @@ All notable changes to `ltk` are documented here. The format is based on [Keep a ### Changed +- **The software backend's ARGB8888 conversion works on whole pixels.** `SoftwareCanvas::write_to_wayland_buf` swapped red and blue with a byte-by-byte loop over the frame; it now reads each pixel as a `u32`, exchanges the two channels with masks and shifts, and lets the compiler vectorise it. Compositors that offer `ABGR8888` were never affected. - **Breaking: `App::app_id`, `App::save_state` and `App::restore_state` are new mandatory trait methods** — every implementor must add them (shell components: constant id, `None`, no-op). `xdg_toplevel.set_app_id` and the AccessKit application name now come from `app_id()` (previously `"ltk"` / `"ltk-app"`); the `app_id` element of the deprecated `window_config` tuple is ignored. Crate version bumped to 0.3.0. - **`Button::icon_size` takes `impl Into`** and resolves through `Canvas::resolve_geom`, joining `font_size` / `height` / `width`. A bare `f32` still means `Length::px` — every existing call keeps its exact size — but a caller can now pass `Length::widget( n )` to have an icon button follow the widget-scaling mode the way stock icons do. Without it an explicit `icon_size` was the one geometry setter that ignored the mode, so a 21 px back arrow sat next to a `list_item` chevron that fluid sizing had grown well past 21 and looked visibly smaller. - **`Length::dp` now applies the density at resolution time, not at construction.** The value carries its design pixels in a new `LengthBase::Dp` variant and `resolve` multiplies by the density in effect when it runs, so a `set_density` change takes effect on the next paint without rebuilding the view's lengths — previously a `dp` value was frozen to the density read when it was constructed. Behaviour is unchanged for code that sets density once at startup. @@ -52,6 +53,7 @@ All notable changes to `ltk` are documented here. The format is based on [Keep a - **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. +- **A tap on a button survives finger motion inside the button.** Any travel past the slop turned the press into a vertical or horizontal drag and its release skipped the button, so a thumb that rolled on a large key lost the tap. While the pressed widget fires on press (`WidgetHandlers::has_press`) and the finger is still inside its rect, motion is ignored; the swipe machinery takes over from the original press point once the finger leaves, and presses inside a scroll viewport are unaffected. - **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). diff --git a/docs/architecture.md b/docs/architecture.md index f0c6123..771fa87 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -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. -**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. +**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`. A press on a widget that fires on tap also stays a tap for as long as the finger is inside that widget, however far it rolls; the swipe machinery only engages, from the original press point, once the finger leaves it. Presses inside a scroll viewport keep the 8 logical px slop, since a list of full-width rows is scrolled by dragging on them. 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. diff --git a/src/input/gesture/mod.rs b/src/input/gesture/mod.rs index 75e99a0..d1ad570 100644 --- a/src/input/gesture/mod.rs +++ b/src/input/gesture/mod.rs @@ -420,6 +420,16 @@ impl GestureState return MoveOutcome::Idle; } + // A press on a button stays a tap while the finger is inside it; + // the swipe only starts once it leaves. + if let Some( w ) = self.pressed_idx.and_then( |idx| find_widget( widget_rects, idx ) ) + { + if w.handlers.has_press() && w.rect.contains( pos ) + { + return MoveOutcome::Idle; + } + } + // Swipe progress — locked to a single axis (see below) so a // gesture only ever drives one of the vertical panels or the // horizontal pager, never both. diff --git a/src/input/gesture/tests.rs b/src/input/gesture/tests.rs index c5f83fc..fd05583 100644 --- a/src/input/gesture/tests.rs +++ b/src/input/gesture/tests.rs @@ -487,6 +487,33 @@ fn release_on_button_emits_press_msg() assert!( matches!( events[ 0 ], ReleaseEvent::PushMsg( Msg::Pressed ) ) ); } +#[ test ] +fn motion_inside_pressed_button_keeps_the_tap() +{ + let widgets = vec![ button( 1, rect( 0.0, 0.0, 100.0, 100.0 ), Some( Msg::Pressed ), None ) ]; + let mut g = GestureState::::new(); + let _ = g.on_press( pt( 50.0, 80.0 ), &widgets, &[] ); + let mut offsets = HashMap::new(); + let out = g.on_move( pt( 55.0, 50.0 ), &widgets, &mut offsets, &cfg_full( 800, 1200 ), false ); + assert!( matches!( out, MoveOutcome::Idle ) ); + assert!( !g.vertical_drag_started ); + let events = g.on_release( pt( 55.0, 50.0 ), &widgets, &cfg_full( 800, 1200 ), false ); + assert!( matches!( events[ 0 ], ReleaseEvent::PushMsg( Msg::Pressed ) ) ); +} + +#[ test ] +fn motion_leaving_pressed_button_arms_the_swipe() +{ + let widgets = vec![ button( 1, rect( 0.0, 0.0, 100.0, 100.0 ), Some( Msg::Pressed ), None ) ]; + let mut g = GestureState::::new(); + let _ = g.on_press( pt( 50.0, 50.0 ), &widgets, &[] ); + let mut offsets = HashMap::new(); + let _ = g.on_move( pt( 50.0, 120.0 ), &widgets, &mut offsets, &cfg_full( 800, 1200 ), false ); + assert!( g.vertical_drag_started ); + let events = g.on_release( pt( 50.0, 120.0 ), &widgets, &cfg_full( 800, 1200 ), false ); + assert!( events.iter().all( |e| !matches!( e, ReleaseEvent::PushMsg( _ ) ) ) ); +} + #[ test ] fn release_on_different_widget_does_not_emit_press_msg() { diff --git a/src/input/keyboard/dispatch.rs b/src/input/keyboard/dispatch.rs index 997c74e..9c986fe 100644 --- a/src/input/keyboard/dispatch.rs +++ b/src/input/keyboard/dispatch.rs @@ -32,7 +32,7 @@ impl AppData Keysym::x | Keysym::X if self.ctrl_pressed => self.handle_key_ctrl_x( focus ), Keysym::v | Keysym::V if self.ctrl_pressed => self.handle_key_ctrl_v( focus ), Keysym::Tab | Keysym::ISO_Left_Tab => self.handle_key_tab( focus, &event, qh ), - Keysym::Escape => self.handle_key_escape( focus, &event, qh ), + Keysym::Escape | Keysym::XF86_Back => self.handle_key_escape( focus, &event, qh ), Keysym::space => self.handle_key_space( focus, &event ), _ => self.handle_key_default( focus, &event ), } diff --git a/src/render/image.rs b/src/render/image.rs index 8289d74..b2d1475 100644 --- a/src/render/image.rs +++ b/src/render/image.rs @@ -181,36 +181,12 @@ impl SoftwareCanvas return; } - let chunks = len / 16; - let remainder = len % 16; - let mut i = 0; - for _ in 0..chunks + let whole = len - len % 4; + for ( dst, px ) in buf[..whole].chunks_exact_mut( 4 ).zip( src[..whole].chunks_exact( 4 ) ) { - buf[i] = src[i + 2]; - buf[i + 1] = src[i + 1]; - buf[i + 2] = src[i]; - buf[i + 3] = src[i + 3]; - buf[i + 4] = src[i + 6]; - buf[i + 5] = src[i + 5]; - buf[i + 6] = src[i + 4]; - buf[i + 7] = src[i + 7]; - buf[i + 8] = src[i + 10]; - buf[i + 9] = src[i + 9]; - buf[i + 10] = src[i + 8]; - buf[i + 11] = src[i + 11]; - buf[i + 12] = src[i + 14]; - buf[i + 13] = src[i + 13]; - buf[i + 14] = src[i + 12]; - buf[i + 15] = src[i + 15]; - i += 16; - } - for _ in 0..(remainder / 4) - { - buf[i] = src[i + 2]; - buf[i + 1] = src[i + 1]; - buf[i + 2] = src[i]; - buf[i + 3] = src[i + 3]; - i += 4; + let v = u32::from_le_bytes( [ px[0], px[1], px[2], px[3] ] ); + let swapped = ( v & 0xff00_ff00 ) | ( ( v >> 16 ) & 0xff ) | ( ( v & 0xff ) << 16 ); + dst.copy_from_slice( &swapped.to_le_bytes() ); } } } diff --git a/src/widget/handlers.rs b/src/widget/handlers.rs index 26410de..ee6cdd6 100644 --- a/src/widget/handlers.rs +++ b/src/widget/handlers.rs @@ -295,6 +295,12 @@ impl WidgetHandlers } } + /// Whether a tap on this widget fires something. + pub fn has_press( &self ) -> bool + { + self.press_msg().is_some() + } + /// Submit message (Enter on a focused TextEdit). `None` for every other /// variant. pub fn submit_msg( &self ) -> Option