First commit. Version 0.1.0

This commit is contained in:
2026-05-10 09:58:23 +02:00
parent af105b7f7d
commit bbab5e238d
635 changed files with 53627 additions and 175 deletions

251
src/input/dispatch.rs Normal file
View File

@@ -0,0 +1,251 @@
// SPDX-License-Identifier: LGPL-2.1-only
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>
//! Dispatch helpers shared by the pointer and touch handlers.
//!
//! The gesture state machine returns abstract outcomes; this file is
//! where those outcomes turn into concrete side-effects on `AppData`:
//! pending-message pushes, app-level callbacks (`on_swipe_*`,
//! `on_drag_move`, `on_drop`, `on_tap`, `overlay_dismiss_msg`), and
//! the redraw / cache invalidation flags that keep the loop moving
//! after a non-Message-producing gesture.
//!
//! Splitting these into a dedicated file means pointer.rs and touch.rs
//! only carry the Wayland-event translation. A future input source
//! (stylus, gamepad) can reuse the same dispatcher by feeding the
//! state machine the same way.
use crate::app::App;
use crate::event_loop::{ AppData, SurfaceFocus };
use crate::types::Point;
use crate::widget::WidgetHandlers;
use super::gesture::{ MoveOutcome, ReleaseEvent };
impl<A: App> AppData<A>
{
/// If the press at `pos` lands on a password-toggle icon zone of
/// the widget at `idx`, push the toggle message and return
/// `true` so the caller can skip the rest of the cursor /
/// selection placement that would otherwise consume the press.
/// Returns `false` when there is no toggle on that widget or
/// the press fell outside the icon's hit area, leaving the
/// caller to dispatch the press normally.
pub( super ) fn handle_password_toggle_press
(
&mut self,
focus: SurfaceFocus,
idx: usize,
pos: Point,
) -> bool
{
let toggle_msg = self.surface( focus ).widget_rects.iter()
.find( |w| w.flat_idx == idx )
.and_then( |w| match &w.handlers
{
WidgetHandlers::TextEdit { password_toggle_msg: Some( msg ), .. } =>
{
let zone = crate::widget::text_edit::password_toggle_hit_zone( w.rect );
if zone.contains( pos ) { Some( msg.clone() ) } else { None }
}
_ => None,
} );
if let Some( msg ) = toggle_msg
{
self.pending_msgs.push( msg );
self.surface_mut( focus ).request_redraw();
true
}
else
{
false
}
}
}
impl<A: App> AppData<A>
{
/// Apply the outcome of a motion event. Non-blocking side-effects
/// only: push messages, call app callbacks, set redraw flags.
pub( super ) fn apply_move_outcome
(
&mut self,
focus: SurfaceFocus,
outcome: MoveOutcome<A::Message>,
)
{
match outcome
{
MoveOutcome::Idle => {}
MoveOutcome::Drag { pos } =>
{
self.app.on_drag_move( pos.x, pos.y );
self.dirty_caches();
self.surface_mut( focus ).request_redraw();
self.main.request_redraw();
}
MoveOutcome::Slider { msg } =>
{
if let Some( m ) = msg
{
self.pending_msgs.push( m );
}
self.surface_mut( focus ).request_redraw();
}
MoveOutcome::Scroll =>
{
self.surface_mut( focus ).request_redraw();
}
MoveOutcome::Swipe { up, down, horizontal } =>
{
if let Some( v ) = up { self.app.on_swipe_progress( v ); }
if let Some( v ) = down { self.app.on_swipe_down_progress( v ); }
if let Some( v ) = horizontal { self.app.on_swipe_horizontal_progress( v ); }
// Swipe-progress callbacks mutate app state outside
// `update`, so the cached view tree is stale.
self.dirty_caches();
self.surface_mut( focus ).request_redraw();
for ss in self.overlays.values_mut()
{
ss.request_redraw();
}
}
}
}
/// Apply the ordered list of events from a release. A single
/// release can emit more than one event (e.g. horizontal
/// fall-through followed by a vertical commit or fall-through), so
/// we walk the list and run each event's side-effects in order.
pub( super ) fn apply_release_events
(
&mut self,
focus: SurfaceFocus,
events: Vec<ReleaseEvent<A::Message>>,
)
{
for event in events
{
self.apply_release_event( focus, event );
}
}
fn apply_release_event
(
&mut self,
focus: SurfaceFocus,
event: ReleaseEvent<A::Message>,
)
{
match event
{
ReleaseEvent::Drop { pos } =>
{
if let Some( msg ) = self.app.on_drop( pos.x, pos.y )
{
self.pending_msgs.push( msg );
}
self.clear_long_press_drag();
self.dirty_caches();
self.main.request_redraw();
self.main.frame_pending = false;
}
ReleaseEvent::SwipeLeft =>
{
if let Some( msg ) = self.app.on_swipe_left()
{
self.pending_msgs.push( msg );
}
// When the app handles the release internally (settle
// animation, state-only mutation) it returns no
// Message, so the update-driven redraw never fires.
// Kick the main surface here so `is_animating()` has a
// frame to latch onto.
self.dirty_caches();
self.main.request_redraw();
self.main.frame_pending = false;
}
ReleaseEvent::SwipeRight =>
{
if let Some( msg ) = self.app.on_swipe_right()
{
self.pending_msgs.push( msg );
}
self.dirty_caches();
self.main.request_redraw();
self.main.frame_pending = false;
}
ReleaseEvent::SwipeUp =>
{
if let Some( msg ) = self.app.on_swipe_up()
{
self.pending_msgs.push( msg );
}
self.dirty_caches();
self.main.request_redraw();
self.main.frame_pending = false;
for ss in self.overlays.values_mut()
{
ss.request_redraw();
ss.frame_pending = false;
}
}
ReleaseEvent::SwipeDown =>
{
if let Some( msg ) = self.app.on_swipe_down()
{
self.pending_msgs.push( msg );
}
self.dirty_caches();
self.main.request_redraw();
self.main.frame_pending = false;
for ss in self.overlays.values_mut()
{
ss.request_redraw();
ss.frame_pending = false;
}
}
ReleaseEvent::HorizontalFellThrough =>
{
// Below threshold: pulse horizontal 0 so a
// vertical-dominant gesture that drifted laterally
// does not leave the app stuck on a stale horizontal
// progress.
self.app.on_swipe_horizontal_progress( 0.0 );
self.dirty_caches();
self.main.request_redraw();
self.main.frame_pending = false;
}
ReleaseEvent::VerticalFellThrough =>
{
self.app.on_swipe_progress( 0.0 );
self.app.on_swipe_down_progress( 0.0 );
self.dirty_caches();
}
ReleaseEvent::PushMsg( msg ) =>
{
self.pending_msgs.push( msg );
}
ReleaseEvent::EmptyRelease =>
{
match focus
{
SurfaceFocus::Main =>
{
if let Some( msg ) = self.app.on_tap()
{
self.pending_msgs.push( msg );
}
}
SurfaceFocus::Overlay( id ) =>
{
if let Some( msg ) = self.overlay_dismiss_msg( id )
{
self.pending_msgs.push( msg );
}
}
}
}
}
}
}

1070
src/input/gesture.rs Normal file

File diff suppressed because it is too large Load Diff

637
src/input/keyboard.rs Normal file
View File

@@ -0,0 +1,637 @@
// SPDX-License-Identifier: LGPL-2.1-only
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>
//! Wayland keyboard → ltk dispatch.
//!
//! Translates `wl_keyboard` events into focus / text-insertion /
//! widget-submit actions. Does not share state with the gesture state
//! machine — keyboard tracks its own focus (`AppData::keyboard_focus`)
//! and modifier flags (`shift_pressed`, `ctrl_pressed`).
//!
//! The only cross-cutting state it reads is `SurfaceState::focused_idx`
//! (set by pointer / touch focus updates) so keyboard navigation can
//! branch on "is a widget focused?" — Return submits / Space presses /
//! typing inserts all funnel through that check.
use std::time::Duration;
use smithay_client_toolkit::seat::keyboard::
{
KeyboardHandler, KeyEvent, Keysym, Modifiers, RawModifiers, RepeatInfo,
};
use smithay_client_toolkit::reexports::client::
{
protocol::{ wl_keyboard::WlKeyboard, wl_surface::WlSurface },
Connection, QueueHandle,
};
use calloop::timer::{ Timer, TimeoutAction };
use crate::app::App;
use crate::event_loop::{ AppData, SurfaceFocus };
use crate::event_loop::app_data::KeyRepeatState;
use crate::tree::{ find_handlers, next_focusable_index };
/// Built-in fallback for the initial key-repeat delay when the
/// compositor does not advertise one and the app does not override
/// [`crate::App::key_repeat_delay`].
const DEFAULT_REPEAT_DELAY: Duration = Duration::from_millis( 500 );
/// Built-in fallback for the inter-repeat interval when the compositor
/// does not advertise a rate. ~30 Hz, matching the GNOME / KDE default
/// for "fast" without straying into uncomfortably-twitchy territory.
const DEFAULT_REPEAT_INTERVAL: Duration = Duration::from_millis( 33 );
impl<A: App> KeyboardHandler for AppData<A>
{
fn enter(
&mut self,
_conn: &Connection,
_qh: &QueueHandle<Self>,
_keyboard: &WlKeyboard,
surface: &WlSurface,
_serial: u32,
_raw: &[u32],
_keysyms: &[Keysym],
)
{
let focus = self.focus_for_surface( surface ).unwrap_or( SurfaceFocus::Main );
self.keyboard_focus = focus;
self.surface_mut( focus ).request_redraw();
}
fn leave(
&mut self,
_conn: &Connection,
_qh: &QueueHandle<Self>,
_keyboard: &WlKeyboard,
_surface: &WlSurface,
_serial: u32,
)
{
self.stop_key_repeat();
self.keyboard_focus = SurfaceFocus::Main;
}
fn press_key(
&mut self,
_conn: &Connection,
_qh: &QueueHandle<Self>,
_keyboard: &WlKeyboard,
_serial: u32,
event: KeyEvent,
)
{
let focus = self.keyboard_focus;
// Raw observer hook (e.g. forwarding to an embedded WPE view).
// Fires before the focus-aware dispatch.
self.app.on_raw_key( event.keysym, event.raw_code, true, self.ctrl_pressed, self.shift_pressed );
// A new press always cancels any in-flight repeat — the user
// has either released the previous key or is pressing a
// different one. Either way, the prior timer's keysym should
// not keep firing.
self.stop_key_repeat();
self.dispatch_key( focus, event.clone() );
self.start_key_repeat( focus, event );
}
fn release_key(
&mut self,
_conn: &Connection,
_qh: &QueueHandle<Self>,
_keyboard: &WlKeyboard,
_serial: u32,
event: KeyEvent,
)
{
self.app.on_raw_key( event.keysym, event.raw_code, false, self.ctrl_pressed, self.shift_pressed );
// Only stop if the released key matches the one currently
// repeating; releasing a non-repeating key (e.g. a shift that
// snuck through, or any key we never armed) leaves the timer
// untouched.
if let Some( ref state ) = self.key_repeat
{
if state.event.keysym == event.keysym
{
self.stop_key_repeat();
}
}
}
fn update_modifiers(
&mut self,
_conn: &Connection,
_qh: &QueueHandle<Self>,
_keyboard: &WlKeyboard,
_serial: u32,
modifiers: Modifiers,
_raw_modifiers: RawModifiers,
_layout: u32,
)
{
self.shift_pressed = modifiers.shift;
self.ctrl_pressed = modifiers.ctrl;
}
fn update_repeat_info(
&mut self,
_conn: &Connection,
_qh: &QueueHandle<Self>,
_keyboard: &WlKeyboard,
info: RepeatInfo,
)
{
match info
{
RepeatInfo::Repeat { rate, delay } =>
{
self.compositor_repeat_rate = rate.get();
self.compositor_repeat_delay = delay;
}
RepeatInfo::Disable =>
{
self.compositor_repeat_rate = 0;
self.compositor_repeat_delay = 0;
self.stop_key_repeat();
}
}
}
fn repeat_key(
&mut self,
_conn: &Connection,
_qh: &QueueHandle<Self>,
_keyboard: &WlKeyboard,
_serial: u32,
event: KeyEvent,
)
{
// Compositor-driven repeat (wl_keyboard v10). When the
// compositor takes the repeat job we just re-dispatch — and
// suppress our internal timer to avoid double-firing.
self.stop_key_repeat();
let focus = self.keyboard_focus;
self.dispatch_key( focus, event );
}
}
impl<A: App> AppData<A>
{
/// Run the same dispatch logic that the Wayland press-key handler
/// runs, but without the trait-callback signature — used both by
/// the trait method and by the key-repeat timer.
fn dispatch_key( &mut self, focus: SurfaceFocus, event: KeyEvent )
{
// `set_focus` (Tab handling) needs a `QueueHandle`. Cloning is
// cheap (an `Arc`-equivalent under the hood) and avoids
// threading the qh through every helper.
let qh = self.qh.clone();
let qh = &qh;
let focused = self.surface( focus ).focused_idx;
match event.keysym
{
Keysym::BackSpace =>
{
if focused.is_some() { self.handle_backspace( focus ); }
else if let Some( msg ) = self.app.on_key_with_modifiers( event.keysym, self.ctrl_pressed, self.shift_pressed )
{
self.pending_msgs.push( msg );
}
}
Keysym::Delete =>
{
// Supr / Forward-Delete: same shape as Backspace but
// removes the char *after* the cursor. When no widget
// is focused, the keysym bubbles to the app.
if focused.is_some() { self.handle_delete_forward( focus ); }
else if let Some( msg ) = self.app.on_key_with_modifiers( event.keysym, self.ctrl_pressed, self.shift_pressed )
{
self.pending_msgs.push( msg );
}
}
Keysym::Return | Keysym::KP_Enter =>
{
// Enter targets, in order: (a) the focused widget's submit
// message (TextEdit), (b) its press message (Button etc),
// (c) the press message of the keyboard-hovered list item
// in the topmost scroll. The hovered fallback lets users
// confirm a combo / list choice with the keyboard after
// arrow-navigating to it without ever taking widget focus.
//
// Multiline text-input override: when the focused widget
// is a `text_edit().multiline( true )`, Enter inserts a
// literal `\n` into the buffer instead of submitting, so
// the user can compose paragraphs.
let is_multi = focused.and_then( |idx|
find_handlers( &self.surface( focus ).widget_rects, idx )
.map( |h| h.is_multiline_text_input() ) ).unwrap_or( false );
if is_multi
{
self.handle_text_insert( focus, "\n" );
} else {
let target = focused.or( self.surface( focus ).hovered_idx );
if let Some( idx ) = target
{
let msg = find_handlers( &self.surface( focus ).widget_rects, idx )
.and_then( |h| h.submit_msg().or_else( || h.press_msg() ) );
if let Some( m ) = msg
{
self.pending_msgs.push( m );
}
} else if let Some( msg ) = self.app.on_key_with_modifiers( event.keysym, self.ctrl_pressed, self.shift_pressed )
{
self.pending_msgs.push( msg );
}
}
}
Keysym::Down | Keysym::Up =>
{
// When a text input is focused, Up/Down move the cursor
// between lines first. They only fall through to list
// hover-navigation (combo / scrollable list) when the
// cursor was already on the topmost / bottommost line —
// that lets a user keyboard-step out of a multiline
// `text_edit` into surrounding navigable items without
// changing focus first.
let is_text = focused.and_then( |idx|
find_handlers( &self.surface( focus ).widget_rects, idx )
.map( |h| h.is_text_input() ) ).unwrap_or( false );
let reverse = event.keysym == Keysym::Up;
let extend = self.shift_pressed;
let consumed = if is_text
{
if reverse { self.handle_cursor_up( focus, extend ) }
else { self.handle_cursor_down( focus, extend ) }
} else { false };
if !consumed && !self.move_keyboard_hover( focus, reverse )
{
if let Some( msg ) = self.app.on_key_with_modifiers( event.keysym, self.ctrl_pressed, self.shift_pressed )
{
self.pending_msgs.push( msg );
}
}
}
Keysym::Left | Keysym::Right =>
{
let is_text = focused.and_then( |idx|
find_handlers( &self.surface( focus ).widget_rects, idx )
.map( |h| h.is_text_input() ) ).unwrap_or( false );
let extend = self.shift_pressed;
if is_text
{
if event.keysym == Keysym::Left
{
self.handle_cursor_left( focus, extend );
} else {
self.handle_cursor_right( focus, extend );
}
} else if let Some( msg ) = self.app.on_key_with_modifiers( event.keysym, self.ctrl_pressed, self.shift_pressed )
{
self.pending_msgs.push( msg );
}
}
Keysym::Home =>
{
let is_text = focused.and_then( |idx|
find_handlers( &self.surface( focus ).widget_rects, idx )
.map( |h| h.is_text_input() ) ).unwrap_or( false );
if is_text { self.handle_cursor_home( focus, self.shift_pressed ); }
}
Keysym::End =>
{
let is_text = focused.and_then( |idx|
find_handlers( &self.surface( focus ).widget_rects, idx )
.map( |h| h.is_text_input() ) ).unwrap_or( false );
if is_text { self.handle_cursor_end( focus, self.shift_pressed ); }
}
Keysym::a | Keysym::A if self.ctrl_pressed =>
{
let is_text = focused.and_then( |idx|
find_handlers( &self.surface( focus ).widget_rects, idx )
.map( |h| h.is_text_input() ) ).unwrap_or( false );
if is_text { self.handle_select_all( focus ); }
}
Keysym::c | Keysym::C if self.ctrl_pressed =>
{
let is_text = focused.and_then( |idx|
find_handlers( &self.surface( focus ).widget_rects, idx )
.map( |h| h.is_text_input() ) ).unwrap_or( false );
if is_text { self.handle_copy( focus ); }
}
Keysym::x | Keysym::X if self.ctrl_pressed =>
{
let is_text = focused.and_then( |idx|
find_handlers( &self.surface( focus ).widget_rects, idx )
.map( |h| h.is_text_input() ) ).unwrap_or( false );
if is_text { self.handle_cut( focus ); }
}
Keysym::v | Keysym::V if self.ctrl_pressed =>
{
let is_text = focused.and_then( |idx|
find_handlers( &self.surface( focus ).widget_rects, idx )
.map( |h| h.is_text_input() ) ).unwrap_or( false );
if is_text { self.handle_paste( focus ); }
}
Keysym::Tab | Keysym::ISO_Left_Tab =>
{
let reverse = event.keysym == Keysym::ISO_Left_Tab || self.shift_pressed;
let ss = self.surface( focus );
let next_idx = next_focusable_index( &ss.widget_rects, ss.focused_idx, reverse );
if let Some( next_idx ) = next_idx
{
self.set_focus( focus, Some( next_idx ), qh );
}
}
Keysym::Escape =>
{
// Esc peels off transient UI one layer at a time.
// Order: (1) open xdg-popup overlays → dismiss them;
// (2) context menu → close it; (3) active selection in
// a focused text input → collapse to cursor; (4) any
// laid-out pressable carrying an `on_escape` message
// (typically the topmost `dialog`'s cancel) → fire
// that; (5) otherwise → drop focus and let the app see
// Esc.
if !self.overlays.is_empty() && self.app.overlays().iter().any( | s | s.anchor_widget_id.is_some() )
{
self.dismiss_all_popups();
} else if self.surface( focus ).context_menu.is_some()
{
self.hide_context_menu( focus );
} else if self.collapse_selection_if_any( focus )
{
// Selection collapsed — keep focus, no app msg.
} else if let Some( msg ) = self.surface( focus ).widget_rects.iter()
.rev()
.find_map( |w| w.handlers.escape_msg() )
{
self.pending_msgs.push( msg );
} else {
self.set_focus( focus, None, qh );
if let Some( msg ) = self.app.on_key_with_modifiers( event.keysym, self.ctrl_pressed, self.shift_pressed )
{
self.pending_msgs.push( msg );
}
}
}
Keysym::space =>
{
if let Some( idx ) = focused
{
let press = find_handlers( &self.surface( focus ).widget_rects, idx )
.and_then( |h| h.press_msg() );
if let Some( msg ) = press
{
self.pending_msgs.push( msg );
} else {
// Focused widget is a TextEdit (or has no on_press) — insert space as text
self.handle_text_insert( focus, " " );
}
} else if let Some( msg ) = self.app.on_key_with_modifiers( event.keysym, self.ctrl_pressed, self.shift_pressed )
{
self.pending_msgs.push( msg );
}
}
_ =>
{
if self.ctrl_pressed
{
if let Some( msg ) = self.app.on_key_with_modifiers( event.keysym, true, self.shift_pressed )
{
self.pending_msgs.push( msg );
}
} else if focused.is_some()
{
if let Some( txt ) = event.utf8
{
if !txt.is_empty() && txt.chars().all( |c| !c.is_control() )
{
self.handle_text_insert( focus, &txt );
}
}
} else if let Some( msg ) = self.app.on_key_with_modifiers( event.keysym, false, self.shift_pressed )
{
self.pending_msgs.push( msg );
}
}
}
self.surface_mut( focus ).request_redraw();
}
/// Compute the effective initial repeat delay — app override wins,
/// then compositor info, then a built-in default.
pub( crate ) fn effective_repeat_delay( &self ) -> Duration
{
if let Some( d ) = self.app.key_repeat_delay() { return d; }
if self.compositor_repeat_delay > 0
{
Duration::from_millis( self.compositor_repeat_delay as u64 )
} else {
DEFAULT_REPEAT_DELAY
}
}
/// Compute the effective inter-repeat interval. Same precedence as
/// [`Self::effective_repeat_delay`]: app override → compositor →
/// built-in default. Returns `None` when repeat is disabled by the
/// active source (compositor `RepeatInfo::Disable` with no app
/// override, or an app override of `Some(Duration::ZERO)`).
pub( crate ) fn effective_repeat_interval( &self ) -> Option<Duration>
{
if let Some( d ) = self.app.key_repeat_interval()
{
if d.is_zero() { return None; }
return Some( d );
}
if self.compositor_repeat_rate == 0
{
// Compositor explicitly disabled repeat, app did not
// override — fall back to a built-in default rather than
// disabling, because most environments where the
// compositor reports 0 also fail to send the event in
// the first place. The default keeps the feel close to
// what people expect from a desktop toolkit.
return Some( DEFAULT_REPEAT_INTERVAL );
}
let ms = ( 1000 / self.compositor_repeat_rate ).max( 1 );
Some( Duration::from_millis( ms as u64 ) )
}
/// Schedule a key-repeat timer for the given event. No-op when the
/// app's [`crate::App::key_repeats`] gate returns `false` for this
/// keysym, when repeat is disabled, or when the calloop timer
/// insertion fails (in which case held keys simply do not repeat).
pub( crate ) fn start_key_repeat( &mut self, focus: SurfaceFocus, event: KeyEvent )
{
if !self.app.key_repeats( event.keysym ) { return; }
let interval = match self.effective_repeat_interval()
{
Some( i ) => i,
None => return,
};
let delay = self.effective_repeat_delay();
let event_for_timer = event.clone();
let timer = Timer::from_duration( delay );
let token = self.loop_handle.insert_source( timer, move |_, _, data: &mut AppData<A>|
{
data.dispatch_key( focus, event_for_timer.clone() );
TimeoutAction::ToDuration( interval )
} );
match token
{
Ok( token ) => { self.key_repeat = Some( KeyRepeatState { event, token } ); }
Err( _ ) => {}
}
}
/// Cancel any active key-repeat timer.
pub( crate ) fn stop_key_repeat( &mut self )
{
if let Some( state ) = self.key_repeat.take()
{
self.loop_handle.remove( state.token );
}
}
/// Schedule a button-press repeat timer. The runtime fires the
/// `on_press` message *immediately* on press too — this fn only
/// arms the held-down repeat path; the first fire happens at
/// the call site so a quick tap still registers as a single
/// press.
///
/// Each timer tick re-reads the live `on_press` from the
/// current widget tree (via the snapshotted `flat_idx`) rather
/// than replaying a captured message. This is what makes
/// stepper-style buttons work: a stepper builds an `on_press`
/// like `"go to value + 5"` at view-build time, so replaying
/// the press-time snapshot after the first fire would re-issue
/// the same target and the value would freeze. By reading
/// `on_press` afresh each tick we pick up the new target the
/// view rebuilt with the updated value.
///
/// No-op when [`Self::effective_repeat_interval`] reports
/// repeat disabled (zero rate, app override of
/// `Duration::ZERO`). Self-cancels on the first tick where the
/// widget at `idx` no longer exists or no longer carries a
/// press message.
pub( crate ) fn start_button_repeat( &mut self, focus: SurfaceFocus, idx: usize )
{
// Cancel any pre-existing repeat first — only one button can
// be in repeat mode at a time, and a fresh press should
// supersede a previous one.
self.stop_button_repeat();
// Button repeat ticks at a fixed 120 ms (~8 Hz). The keyboard
// repeat interval is ~33 ms (30 Hz), which suits cursor /
// character entry but is too aggressive for pointer steppers
// — a date / time picker would walk a full minute in under
// two seconds. 120 ms is fast enough to ramp through values,
// slow enough that the user can still release on the value
// they want.
if matches!( self.effective_repeat_interval(), None ) { return; }
let interval = std::time::Duration::from_millis( 120 );
let delay = self.effective_repeat_delay();
let timer = Timer::from_duration( delay );
let token = self.loop_handle.insert_source( timer, move |_, _, data: &mut AppData<A>|
{
let live_msg = crate::tree::find_handlers(
&data.surface( focus ).widget_rects,
idx,
)
.and_then( |h| h.press_msg() );
match live_msg
{
Some( m ) =>
{
data.pending_msgs.push( m );
TimeoutAction::ToDuration( interval )
}
None =>
{
// Widget gone (view restructured) or no longer
// has an on_press → drop ourselves so the timer
// does not fire forever against a stale slot.
data.button_repeat = None;
TimeoutAction::Drop
}
}
} );
if let Ok( token ) = token
{
self.button_repeat = Some( crate::event_loop::app_data::ButtonRepeatState { token } );
}
}
/// Cancel any active button-press repeat timer.
pub( crate ) fn stop_button_repeat( &mut self )
{
if let Some( state ) = self.button_repeat.take()
{
self.loop_handle.remove( state.token );
}
}
/// Step the topmost scroll's `hovered_idx` one item up or down with
/// the keyboard. Returns `true` when the move was applied (a scroll
/// with at least one navigable item was on screen and the new item
/// is different from the current hover) so the caller can fall
/// through to the application's own `on_key` only on a no-op.
///
/// The "topmost scroll" is the last entry in `scroll_rects`, which
/// matches Stack-overlay layout order: a popup pushed after the
/// main content sits above it. Auto-scrolls the new item into view
/// by adjusting `scroll_offsets[scroll_idx]`.
pub( crate ) fn move_keyboard_hover( &mut self, focus: SurfaceFocus, reverse: bool ) -> bool
{
// Find the topmost scroll that has a navigable item list.
let scroll_meta = {
let ss = self.surface( focus );
ss.scroll_rects.iter().rev()
.find_map( |( rect, idx )|
{
ss.scroll_navigable_items.get( idx )
.filter( |list| !list.is_empty() )
.map( |list| ( *rect, *idx, list.clone() ) )
} )
};
let Some( ( scroll_rect, scroll_idx, items ) ) = scroll_meta else { return false; };
let current = self.surface( focus ).hovered_idx;
let pos = current.and_then( |h| items.iter().position( |( i, _, _ )| *i == h ) );
let next_pos = match ( reverse, pos )
{
( false, None ) => 0,
( false, Some( p ) ) => ( p + 1 ).min( items.len() - 1 ),
( true, None ) => items.len() - 1,
( true, Some( p ) ) => p.saturating_sub( 1 ),
};
let ( new_idx, content_y, content_h ) = items[ next_pos ];
// Auto-scroll to bring the new item fully into view. Item Y is
// in pre-offset content coordinates, so the offset that places
// the item flush with the top of the viewport is `content_y`,
// and flush with the bottom is `content_y + content_h - viewport_h`.
let viewport_h = scroll_rect.height;
let current_offset = self.surface( focus )
.scroll_offsets.get( &scroll_idx )
.copied().unwrap_or( 0.0 );
let new_offset = if content_y < current_offset
{
content_y
}
else if content_y + content_h > current_offset + viewport_h
{
( content_y + content_h - viewport_h ).max( 0.0 )
}
else
{
current_offset
};
let ss = self.surface_mut( focus );
ss.hovered_idx = Some( new_idx );
ss.scroll_offsets.insert( scroll_idx, new_offset );
ss.request_redraw();
true
}
}

36
src/input/mod.rs Normal file
View File

@@ -0,0 +1,36 @@
// SPDX-License-Identifier: LGPL-2.1-only
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>
//! Input-event layer.
//!
//! Three Wayland input sources — `wl_keyboard`, `wl_pointer`,
//! `wl_touch` — land in their respective submodules and translate
//! into ltk-level actions. Keyboard is standalone (it drives focus
//! and text insertion, no gesture lifecycle); pointer and touch share
//! the [`gesture::GestureState`] state machine so press → move →
//! release logic (long-press, slider drag, scroll, swipe commit, tap,
//! drop) is written once and fed by both sources.
//!
//! The [`dispatch`] submodule turns gesture outcomes into concrete
//! side-effects on [`AppData`](crate::event_loop::AppData): pending
//! messages, app callbacks, surface redraws, cache invalidation. It
//! is `pub( super )`-scoped so pointer.rs and touch.rs can call it
//! without exposing the helpers at the crate root.
//!
//! With [`GestureState`] owning the press / move / release lifecycle
//! and [`dispatch`] owning the side-effects, the Wayland-facing
//! handlers stay small and a hypothetical new input source (stylus,
//! gamepad) can plug in by feeding the state machine the same way.
pub( crate ) mod gesture;
pub( crate ) mod keyboard;
pub( crate ) mod pointer;
pub( crate ) mod touch;
pub( crate ) mod dispatch;
// `GestureState` is the only type consumers outside `input/` need —
// `AppData` stores one per `SurfaceState` and the long-press deadline
// poller reaches through it. The rest (`MoveOutcome`, `PressOutcome`,
// `ReleaseEvent`, `SwipeConfig`) stay at `super::gesture::*` because
// only the sibling modules in `input/` touch them.
pub use gesture::GestureState;

495
src/input/pointer.rs Normal file
View File

@@ -0,0 +1,495 @@
// SPDX-License-Identifier: LGPL-2.1-only
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>
//! Wayland pointer → ltk dispatch.
//!
//! Each `wl_pointer` event (motion / press / release / axis) translates
//! into a call on the surface's [`GestureState`](super::gesture::GestureState)
//! plus the follow-up side-effects that can only live at the
//! [`AppData`] level (pending-message push, surface redraw, dirty-cache
//! invalidation, window move). The pointer handler adds three things
//! touch does not have:
//!
//! * **Hover tracking** — motion updates `SurfaceState::hovered_idx`
//! before delegating the motion to the gesture machine, since the
//! hovered widget drives visual state independent of whether a press
//! is active.
//! * **Title-bar interaction** — a press inside the client-side title
//! bar either closes the window (close button hit) or initiates a
//! compositor-driven window move. Neither path goes through the
//! gesture machine.
//! * **Wheel / axis scroll** — routed directly into the per-viewport
//! `scroll_offsets` map; axis events do not have a press/release
//! lifecycle so they bypass the state machine entirely.
use smithay_client_toolkit::seat::pointer::{ PointerEvent, PointerEventKind, PointerHandler };
use smithay_client_toolkit::reexports::client::
{
protocol::{ wl_pointer, wl_pointer::WlPointer },
Connection, QueueHandle,
};
use crate::app::App;
use crate::event_loop::{ AppData, SurfaceFocus };
use crate::tree::{ find_handlers, find_widget_at };
use crate::widget::WidgetHandlers;
use super::gesture::SwipeConfig;
impl<A: App> PointerHandler for AppData<A>
{
fn pointer_frame(
&mut self,
_conn: &Connection,
qh: &QueueHandle<Self>,
_pointer: &WlPointer,
events: &[PointerEvent],
)
{
for event in events
{
let focus = self.focus_for_surface( &event.surface )
.unwrap_or( SurfaceFocus::Main );
self.pointer_focus = focus;
match event.kind
{
PointerEventKind::Enter { serial } =>
{
// Pointer just entered our surface — capture the
// serial that wp_cursor_shape_device_v1::set_shape
// requires, then push the initial cursor shape
// *unconditionally*. Resetting `current_cursor_shape`
// to `None` is what forces the dispatch: the
// compositor was showing whatever the previous
// client asked for (e.g. a text I-beam from a
// terminal under our window), and we must claim
// the cursor for our surface even when the target
// equals what we last pushed.
self.last_pointer_enter_serial = serial;
self.current_cursor_shape = None;
self.dispatch_cursor_shape( focus );
}
PointerEventKind::Motion { .. } =>
{
let pp = self.surface( focus ).to_physical( event.position.0, event.position.1 );
self.pointer_pos = pp;
self.app.on_pointer_move( pp.x, pp.y );
// Hover tracking — pointer-only (touch has no hover).
// Runs before the gesture motion so the cache-dirty
// below picks up any hover-dependent redraw request.
let new_hover = find_widget_at( &self.surface( focus ).widget_rects, pp );
let old_hover = self.surface( focus ).hovered_idx;
if new_hover != old_hover
{
let redraw = hover_affects_paint( &self.surface( focus ).widget_rects, old_hover )
|| hover_affects_paint( &self.surface( focus ).widget_rects, new_hover );
let ss = self.surface_mut( focus );
ss.hovered_idx = new_hover;
if redraw { ss.needs_redraw = true; }
}
// Mouse drag-promotion: a left-button press whose hit
// widget carries `on_drag_start` should arm a drag as
// soon as the cursor moves past the threshold, without
// waiting for the touch hold timer. Touch keeps its
// hold-then-drag path because scroll / swipe gestures
// need the in-between motion budget; mouse has a
// dedicated right-click for the menu so left-button
// can be drag-only.
//
// Threshold of 24 px (logical) sits comfortably above
// the gesture machine's 6 px long-press cancel
// tolerance and above crustace's 16 px drag-commit
// threshold, so by the time we promote the next
// motion sample is already past the app's commit
// distance and drag mode latches without flashing
// any half-state.
//
// Promotion is synchronous (`self.app.update(...)`
// directly) so the app's drag state is armed BEFORE
// the `on_drag_move` call below runs — otherwise the
// seed coords land on a `dragging_item = None`
// shell and get lost. We pay the cost of bypassing
// `invalidate_after` for this one msg, but the next
// frame will repaint everything anyway because the
// drag is in flight.
let promote = {
let ss = self.surface( focus );
match ( ss.gesture.long_press_origin, ss.gesture.drag_start_msg.is_some() )
{
( Some( o ), true ) => ( pp.x - o.x ).hypot( pp.y - o.y ) > 24.0,
_ => false,
}
};
if promote
{
let ( ds_msg, origin ) = {
let ss = self.surface_mut( focus );
let m = ss.gesture.drag_start_msg.take().expect( "promote checked is_some" );
let o = ss.gesture.long_press_origin.expect( "promote checked Some(origin)" );
ss.gesture.long_press_start = None;
ss.gesture.long_press_origin = None;
ss.gesture.long_press_msg = None;
ss.gesture.long_press_text_idx = None;
ss.gesture.long_press_fired = true;
ss.request_redraw();
( m, o )
};
self.app.update( ds_msg );
self.app.on_drag_move( origin.x, origin.y );
self.dirty_caches();
self.stop_button_repeat();
}
// `global_drag` must be sampled AFTER the promotion
// above — promotion flips `long_press_fired` and we
// want the gesture machine to take the drag branch
// for the same motion event that triggered the
// promotion.
let global_drag = self.has_active_long_press_drag();
let swipe = self.swipe_config( focus );
let outcome =
{
let ss = self.surface_mut( focus );
ss.gesture.on_move( pp, &ss.widget_rects, &mut ss.scroll_offsets, &swipe, global_drag )
};
self.apply_move_outcome( focus, outcome );
// Drag-to-select inside a TextEdit. Runs after the
// gesture machine so the gesture's "did we leave
// the press's hit rect?" reasoning still applies
// to the press itself; for text fields the answer
// is "fine, keep selecting" because we only widen
// the selection while the pointer is still inside
// the same widget rect.
let pressed_text = self.surface( focus ).gesture.pressed_idx
.and_then( |idx|
{
let is_text = find_handlers( &self.surface( focus ).widget_rects, idx )
.map( |h| h.is_text_input() ).unwrap_or( false );
if is_text { Some( idx ) } else { None }
} );
if let Some( idx ) = pressed_text
{
self.handle_text_pointer_drag( focus, idx, pp );
}
// Cursor shape: hover may have changed → push the
// new shape to the compositor (no-op when
// unchanged).
self.dispatch_cursor_shape( focus );
}
PointerEventKind::Press { button: 0x111, .. } =>
{
// Right-click: the desktop equivalent of a touch
// long-press. Three cases on the press target:
//
// * Widget with `on_long_press` (Button or
// Pressable that opted in) — fire the message.
// The drag-arm slot (`on_drag_start`) is NOT
// consumed: right-click never enters drag mode,
// so an icon's context menu opens but no drag
// is armed.
// * TextEdit with no user `on_long_press` — open
// the built-in Copy / Cut / Paste menu near the
// click. Selection is preserved (we deliberately
// skip `handle_text_pointer_down`) so the common
// "select text → right-click → Copy" flow works.
// * Anywhere else — dismiss any already-open menu.
let pos = self.surface( focus ).to_physical( event.position.0, event.position.1 );
self.pointer_pos = pos;
self.app.on_pointer_move( pos.x, pos.y );
let hit_idx = find_widget_at( &self.surface( focus ).widget_rects, pos );
let lp_msg = hit_idx.and_then( |idx|
find_handlers( &self.surface( focus ).widget_rects, idx )
.and_then( |h| h.long_press_msg() ) );
if let Some( msg ) = lp_msg
{
self.pending_msgs.push( msg );
self.surface_mut( focus ).request_redraw();
} else {
let is_text = hit_idx.and_then( |idx|
find_handlers( &self.surface( focus ).widget_rects, idx )
.map( |h| h.is_text_input() ) ).unwrap_or( false );
if is_text
{
let idx = hit_idx.unwrap();
if self.surface( focus ).focused_idx != Some( idx )
{
self.set_focus( focus, hit_idx, qh );
}
self.show_context_menu( focus, idx, pos );
} else {
self.hide_context_menu( focus );
}
}
}
PointerEventKind::Press { button: 0x110, serial, .. } =>
{
self.last_pointer_serial = serial;
self.last_input_serial = serial;
let pos = self.surface( focus ).to_physical( event.position.0, event.position.1 );
self.pointer_pos = pos;
self.app.on_pointer_move( pos.x, pos.y );
if matches!( focus, SurfaceFocus::Main ) && !self.overlays.is_empty()
{
self.dismiss_main_outside_popups( pos );
}
// Built-in context menu intercepts the press
// before the regular gesture machine. Either an
// item activates (Copy / Cut / Paste) or the
// click is outside the menu and dismisses it —
// in both cases we consume the event.
if self.surface( focus ).context_menu.is_some()
{
if self.handle_context_menu_press( focus, pos )
{
continue;
}
}
// Client-side title bar interaction — pointer-only
// (touch never hits a titlebar; layer-shell surfaces
// have titlebar_height == 0).
let sf = self.surface( focus ).scale_factor.max( 1 ) as f32;
let tb_h = self.surface( focus ).titlebar_height * sf;
if tb_h > 0.0 && pos.y < tb_h
{
let close_rect = self.surface( focus ).titlebar_close_rect;
if close_rect.contains( pos )
{
if self.app.on_close_requested()
{
self.exit_requested = true;
}
return;
}
}
// Resize-edge interception. Wins over the titlebar
// drag-move (top-left / top-right corners overlap
// the titlebar) and over the gesture machine.
if let Some( edge ) = self.resize_edge_under_pointer( focus )
{
if let crate::event_loop::SurfaceKind::Window( ref window ) = self.main.surface
{
let seats: Vec<_> = self.seat_state.seats().collect();
if let Some( seat ) = seats.into_iter().next()
{
window.resize( &seat, serial, edge );
}
}
continue;
}
if tb_h > 0.0 && pos.y < tb_h
{
// Drag to move the window.
if matches!( focus, SurfaceFocus::Main )
{
if let crate::event_loop::SurfaceKind::Window( ref window ) = self.main.surface
{
let seats: Vec<_> = self.seat_state.seats().collect();
if let Some( seat ) = seats.into_iter().next()
{
window.move_( &seat, serial );
}
}
}
continue;
}
// Past every chrome interception — surface the
// press to the app so embeddings (e.g. an
// embedded WPE WebView) see real button-down
// events that were not consumed by the window
// frame.
self.app.on_pointer_button( pos.x, pos.y, true );
let outcome =
{
let ss = self.surface_mut( focus );
let result = ss.gesture.on_press( pos, &ss.widget_rects, &ss.scroll_rects );
// Mark this gesture as mouse-driven so the
// gesture machine's 6 px stray-cancel skips
// the drag-start / long-press slots — mouse
// motion is intentional and should never
// drop the candidate before the pointer-side
// promotion at 24 px gets to fire.
ss.gesture.mouse_press = true;
ss.needs_redraw = true;
result
};
self.set_focus( focus, outcome.hit_idx, qh );
if let Some( msg ) = outcome.initial_slider_msg
{
self.pending_msgs.push( msg );
}
// Press-and-hold repeat: when the hit is a button
// that opted into `.repeating( true )`, fire the
// `on_press` message immediately and arm the
// repeat timer. The release handler in
// `gesture.rs` knows to suppress the regular tap-
// on-release fire for repeating buttons so a
// quick click still counts as exactly one press.
// The timer re-reads `on_press` from the live
// widget tree on every tick — see
// `start_button_repeat` for why.
if let Some( idx ) = outcome.hit_idx
{
let immediate = {
let handlers = find_handlers( &self.surface( focus ).widget_rects, idx );
if matches!( handlers, Some( WidgetHandlers::Button { repeating: true, .. } ) )
{
handlers.and_then( |h| h.press_msg() )
} else { None }
};
if let Some( msg ) = immediate
{
self.pending_msgs.push( msg );
self.start_button_repeat( focus, idx );
}
}
// Click-to-position the text cursor when the press
// landed on a TextEdit. `set_focus` above moves the
// cursor to the end of the value; this overrides
// it with the byte offset under the pointer and
// collapses the selection there so a subsequent
// drag widens the selection from the click point.
//
// Double-click on a TextEdit selects the word
// under the cursor instead of just positioning.
if let Some( idx ) = outcome.hit_idx
{
let is_text = find_handlers( &self.surface( focus ).widget_rects, idx )
.map( |h| h.is_text_input() ).unwrap_or( false );
if is_text
{
// Eye icon hit on a password field
// short-circuits the text-edit
// dispatch — fire the toggle msg and
// skip cursor placement.
if self.handle_password_toggle_press( focus, idx, pos )
{
let _ = self.note_press_for_double_click( pos );
}
else
{
let is_double = self.note_press_for_double_click( pos );
if is_double
{
self.handle_text_select_word( focus, idx, pos );
} else {
self.handle_text_pointer_down( focus, idx, pos );
}
}
} else {
let _ = self.note_press_for_double_click( pos );
}
} else {
let _ = self.note_press_for_double_click( pos );
}
// Slider press → drag may have started; refresh
// the cursor shape so it switches to `Grabbing`.
self.dispatch_cursor_shape( focus );
}
PointerEventKind::Release { button: 0x110, .. } =>
{
let pos = self.surface( focus ).to_physical( event.position.0, event.position.1 );
self.pointer_pos = pos;
self.app.on_pointer_move( pos.x, pos.y );
self.app.on_pointer_button( pos.x, pos.y, false );
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.gesture.on_release( pos, &ss.widget_rects, &swipe, global_drag )
};
self.apply_release_events( focus, events_out );
// Cancel any held-button repeat — the press is
// over, so the timer no longer has anything to
// fire against.
self.stop_button_repeat();
// Slider drag (if any) just ended — cursor reverts
// from `Grabbing` to whatever the hovered widget
// asks for.
self.dispatch_cursor_shape( focus );
}
PointerEventKind::Axis { horizontal, vertical, source, .. } =>
{
let pos = self.surface( focus ).to_physical( event.position.0, event.position.1 );
let scroll_idx_opt =
{
let ss = self.surface( focus );
ss.scroll_rects.iter().rev()
.find( |( r, _ )| r.contains( pos ) )
.map( |( _, idx )| *idx )
};
if let Some( scroll_idx ) = scroll_idx_opt
{
let multiplier = match source
{
Some( wl_pointer::AxisSource::Wheel ) => 10.0,
_ => 1.0,
};
let step = vertical.absolute as f32 * multiplier;
let ss = self.surface_mut( focus );
let entry = ss.scroll_offsets.entry( scroll_idx ).or_insert( 0.0 );
*entry = ( *entry + step ).max( 0.0 );
ss.request_redraw();
} else {
// No LTK scroll viewport under the cursor —
// surface the raw axis to the app so embedded
// content (e.g. a WPE view) can handle scrolling
// itself.
let multiplier = match source
{
Some( wl_pointer::AxisSource::Wheel ) => 10.0,
_ => 1.0,
};
let dx = horizontal.absolute as f32 * multiplier;
let dy = vertical.absolute as f32 * multiplier;
self.app.on_pointer_axis( pos.x, pos.y, dx, dy );
}
}
_ => {}
}
}
}
}
fn hover_affects_paint<Msg: Clone>(
widget_rects: &[crate::widget::LaidOutWidget<Msg>],
idx: Option<usize>,
) -> bool
{
idx.and_then( |i| find_handlers( widget_rects, i ) )
.map( |h| !h.is_slider() )
.unwrap_or( false )
}
/// Pointer-side helpers on `AppData`. Split out so touch can call the
/// same swipe-config factory; `apply_*` helpers live in `dispatch.rs`
/// because they are shared.
impl<A: App> AppData<A>
{
/// Snapshot the swipe thresholds + surface dimensions into a
/// [`SwipeConfig`] for the gesture machine. Called once per
/// motion / release event.
pub( super ) fn swipe_config( &self, focus: SurfaceFocus ) -> SwipeConfig
{
let ss = self.surface( focus );
SwipeConfig
{
up_thresh: self.app.swipe_threshold(),
down_thresh: self.app.swipe_down_threshold(),
down_edge: self.app.swipe_down_edge(),
horizontal_thresh: self.app.swipe_horizontal_threshold(),
surface_width: ss.physical_width(),
surface_height: ss.physical_height(),
}
}
}

223
src/input/touch.rs Normal file
View File

@@ -0,0 +1,223 @@
// SPDX-License-Identifier: LGPL-2.1-only
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>
//! Wayland touch → ltk dispatch.
//!
//! `wl_touch` down / up / motion map directly onto the three
//! lifecycle methods of [`GestureState`](super::gesture::GestureState),
//! same as pointer press / release / motion. Touch has no hover and
//! no scroll-axis event, so this file is shorter than `pointer.rs`;
//! the two handlers share `apply_move_outcome` /
//! `apply_release_events` in `dispatch.rs`.
//!
//! The only touch-specific state is `AppData::touch_focus`, a
//! `HashMap<touch_id, SurfaceFocus>` so a finger that landed on an
//! overlay continues to route to that overlay even if it drifts over
//! the main surface. Multi-finger tracking is not yet modelled — the
//! gesture machine is single-gesture; a second finger arriving while
//! the first is pressed overwrites the same slot. Good enough for
//! sliders, swipes and taps; a proper multi-touch rewrite is a
//! separate refactor.
use smithay_client_toolkit::seat::touch::TouchHandler;
use smithay_client_toolkit::reexports::client::
{
protocol::{ wl_surface::WlSurface, wl_touch::WlTouch },
Connection, QueueHandle,
};
use crate::app::App;
use crate::event_loop::{ AppData, SurfaceFocus, SurfaceState };
use crate::tree::find_handlers;
impl<A: App> TouchHandler for AppData<A>
{
fn down(
&mut self,
_conn: &Connection,
qh: &QueueHandle<Self>,
_touch: &WlTouch,
serial: u32,
_time: u32,
surface: WlSurface,
id: i32,
position: ( f64, f64 ),
)
{
self.last_input_serial = serial;
let focus = self.focus_for_surface( &surface ).unwrap_or( SurfaceFocus::Main );
self.touch_focus.insert( id, focus );
let pos = self.surface( focus ).to_physical( position.0, position.1 );
self.pointer_pos = pos;
if matches!( focus, SurfaceFocus::Main ) && !self.overlays.is_empty()
{
self.dismiss_main_outside_popups( pos );
}
// Built-in context menu intercepts the touch before the
// regular gesture machine — same logic as the pointer path.
if self.surface( focus ).context_menu.is_some()
{
if self.handle_context_menu_press( focus, pos )
{
return;
}
}
let outcome =
{
let ss = self.surface_mut( focus );
let result = ss.gesture.on_press( pos, &ss.widget_rects, &ss.scroll_rects );
ss.needs_redraw = true;
result
};
self.set_focus( focus, outcome.hit_idx, qh );
if let Some( msg ) = outcome.initial_slider_msg
{
self.pending_msgs.push( msg );
}
// Press-and-hold repeat — same wiring as the pointer path.
if let Some( idx ) = outcome.hit_idx
{
let immediate = {
let handlers = find_handlers( &self.surface( focus ).widget_rects, idx );
if matches!( handlers, Some( crate::widget::WidgetHandlers::Button { repeating: true, .. } ) )
{
handlers.and_then( |h| h.press_msg() )
} else { None }
};
if let Some( msg ) = immediate
{
self.pending_msgs.push( msg );
self.start_button_repeat( focus, idx );
}
}
// Click-to-position the text cursor for touch presses too,
// and double-tap selects the word under the press.
if let Some( idx ) = outcome.hit_idx
{
let is_text = find_handlers( &self.surface( focus ).widget_rects, idx )
.map( |h| h.is_text_input() ).unwrap_or( false );
if is_text
{
// Eye icon hit on a password field short-circuits
// the text-edit dispatch — fire the toggle msg and
// skip cursor placement.
if self.handle_password_toggle_press( focus, idx, pos )
{
let _ = self.note_press_for_double_click( pos );
}
else
{
let is_double = self.note_press_for_double_click( pos );
if is_double
{
self.handle_text_select_word( focus, idx, pos );
} else {
self.handle_text_pointer_down( focus, idx, pos );
}
}
} else {
let _ = self.note_press_for_double_click( pos );
}
} else {
let _ = self.note_press_for_double_click( pos );
}
}
fn up(
&mut self,
_conn: &Connection,
_qh: &QueueHandle<Self>,
_touch: &WlTouch,
_serial: u32,
_time: u32,
id: i32,
)
{
let focus = self.touch_focus.remove( &id ).unwrap_or( SurfaceFocus::Main );
// 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.gesture.on_release( pos, &ss.widget_rects, &swipe, global_drag )
};
self.apply_release_events( focus, events_out );
self.stop_button_repeat();
}
fn motion(
&mut self,
_conn: &Connection,
_qh: &QueueHandle<Self>,
_touch: &WlTouch,
_time: u32,
id: i32,
position: ( f64, f64 ),
)
{
let focus = *self.touch_focus.get( &id ).unwrap_or( &SurfaceFocus::Main );
let pp = self.surface( focus ).to_physical( position.0, position.1 );
self.pointer_pos = pp;
let global_drag = self.has_active_long_press_drag();
let swipe = self.swipe_config( focus );
let outcome =
{
let ss = self.surface_mut( focus );
ss.gesture.on_move( pp, &ss.widget_rects, &mut ss.scroll_offsets, &swipe, global_drag )
};
self.apply_move_outcome( focus, outcome );
// Drag-to-select inside a TextEdit (touch path).
let pressed_text = self.surface( focus ).gesture.pressed_idx
.and_then( |idx|
{
let is_text = find_handlers( &self.surface( focus ).widget_rects, idx )
.map( |h| h.is_text_input() ).unwrap_or( false );
if is_text { Some( idx ) } else { None }
} );
if let Some( idx ) = pressed_text
{
self.handle_text_pointer_drag( focus, idx, pp );
}
}
fn shape(
&mut self,
_conn: &Connection,
_qh: &QueueHandle<Self>,
_touch: &WlTouch,
_id: i32,
_major: f64,
_minor: f64,
) {}
fn orientation(
&mut self,
_conn: &Connection,
_qh: &QueueHandle<Self>,
_touch: &WlTouch,
_id: i32,
_orientation: f64,
) {}
fn cancel( &mut self, _conn: &Connection, _qh: &QueueHandle<Self>, _touch: &WlTouch )
{
self.touch_focus.clear();
// The compositor is stealing every active touch — drop all
// in-flight gesture state across every surface.
let clear = |ss: &mut SurfaceState<A::Message>| { ss.gesture.on_cancel(); };
clear( &mut self.main );
for ss in self.overlays.values_mut()
{
clear( ss );
}
self.stop_button_repeat();
}
}