[ltk — src/input/gesture/mod.rs, src/input/gesture/tests.rs, src/widget/handlers.rs, src/input/keyboard/dispatch.rs, src/render/image.rs, docs/architecture.md, CHANGELOG.md] Keep a tap on a button while the finger rolls inside it, treat XF86Back like Escape, and convert ARGB8888 by whole pixels
CI / build + test (push) Has been cancelled
CI / cargo audit (push) Has been cancelled

Any travel past the slop turned a 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. `GestureState::on_move` now stays idle while the pressed widget fires on press (the new `WidgetHandlers::has_press`) and the finger is still inside its rect; the swipe machinery takes over from the original press point once the finger leaves. Presses inside a scroll viewport are unaffected, since a list of full-width rows is scrolled by dragging on them. Two tests cover the tap that survives motion inside the button and the swipe that arms once the finger leaves it; the architecture notes and the changelog describe the rule.
The compositor now sends XF86Back for the Android back gesture, an inward swipe from either side edge. With a text input focused that keysym fell into `handle_key_default` and was swallowed, so an app never saw it. `XF86Back` now dispatches through `handle_key_escape`, so every ltk app gets the back gesture with Escape's layering: open popups, then the context menu, then a selection, then a dialog's cancel, and finally the focus drop and `on_key_with_modifiers` with the keysym, where an app can act on it.
`SoftwareCanvas::write_to_wayland_buf` swapped red and blue with a byte-by-byte loop, unrolled by hand over sixteen bytes. It now reads each pixel as a `u32`, exchanges the two channels with masks and shifts, and leaves the vectorisation to the compiler. Compositors that offer `ABGR8888` never took this path.
This commit is contained in:
2026-09-14 10:57:10 +02:00
parent 75be800390
commit 5b8ae35830
7 changed files with 52 additions and 31 deletions
+10
View File
@@ -420,6 +420,16 @@ impl<Msg: Clone> GestureState<Msg>
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.
+27
View File
@@ -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::<Msg>::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::<Msg>::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()
{
+1 -1
View File
@@ -32,7 +32,7 @@ impl<A: App> AppData<A>
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 ),
}
+5 -29
View File
@@ -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() );
}
}
}
+6
View File
@@ -295,6 +295,12 @@ impl<Msg: Clone> WidgetHandlers<Msg>
}
}
/// 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<Msg>