refactor: split every monolithic module into focused submodules
Each source file that had grown beyond a single concern is replaced by an identically-named directory containing focused submodules. `src/event_loop/mod.rs` (878 lines) becomes a directory with clipboard, context_menu, cursor_shape, drag, focus, handlers, invalidation, overlays_reconcile, repeat, run, surface, text_editing, and tooltip. Every widget, input handler, and theme component follows the same split. Public interfaces are unchanged — only the internal file layout moves. image bumped from 0.25.2 to 0.25.9.
This commit is contained in:
@@ -0,0 +1,309 @@
|
||||
// SPDX-License-Identifier: LGPL-2.1-only
|
||||
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>
|
||||
|
||||
use std::sync::Arc;
|
||||
use crate::types::{ Point, Rect };
|
||||
use super::{ slider, text };
|
||||
|
||||
/// Per-leaf interaction snapshot captured during layout. One variant per
|
||||
/// interactive widget kind; the layout pass clones the relevant callbacks /
|
||||
/// values from the [`Element`](super::Element) tree into here so input handlers can dispatch
|
||||
/// in O(1) without re-walking the tree.
|
||||
///
|
||||
/// `None` is used for focusable widgets that emit no message (e.g. a disabled
|
||||
/// button or a focusable container) — the entry still appears in
|
||||
/// `widget_rects` for hit testing and focus traversal.
|
||||
pub enum WidgetHandlers<Msg: Clone>
|
||||
{
|
||||
None,
|
||||
Button
|
||||
{
|
||||
on_press: Option<Msg>,
|
||||
on_long_press: Option<Msg>,
|
||||
on_drag_start: Option<Msg>,
|
||||
/// Keyboard `Escape`-key message — the runtime scans every laid-out
|
||||
/// `Button` snapshot in reverse and fires the first non-`None`
|
||||
/// `on_escape` it finds before the default ESC fallthrough chain.
|
||||
/// Currently sourced from
|
||||
/// [`crate::widget::pressable::Pressable::on_escape`]; native
|
||||
/// [`crate::widget::button::Button`] always sets this to `None`.
|
||||
on_escape: Option<Msg>,
|
||||
repeating: bool,
|
||||
},
|
||||
Toggle { on_toggle: Option<Msg> },
|
||||
Checkbox { on_toggle: Option<Msg> },
|
||||
Radio { on_select: Option<Msg> },
|
||||
ListItem { on_press: Option<Msg> },
|
||||
WindowButton { on_press: Option<Msg> },
|
||||
TextEdit
|
||||
{
|
||||
value: String,
|
||||
on_change: Option<Arc<dyn Fn( String ) -> Msg>>,
|
||||
on_submit: Option<Msg>,
|
||||
/// `true` when the source `TextEdit` was built with
|
||||
/// `.secure( true )`. Propagates the wipe-on-drop behaviour to
|
||||
/// every per-frame handler snapshot so credential text does not
|
||||
/// linger across multiple cloned `WidgetHandlers` allocations.
|
||||
secure: bool,
|
||||
/// `true` when the source `TextEdit` was built with
|
||||
/// `.multiline( true )`. The keyboard dispatch reads this so
|
||||
/// pressing Enter inserts a `\n` instead of firing
|
||||
/// [`Self::submit_msg`]. Mutually exclusive with `secure`.
|
||||
multiline: bool,
|
||||
/// Horizontal alignment snapshot — needed by the runtime's
|
||||
/// hit-testing path so a click on a centred / right-aligned
|
||||
/// field lands on the correct glyph.
|
||||
align: text::TextAlign,
|
||||
/// Font size snapshot — needed by the hit-testing path so
|
||||
/// the runtime measures glyphs at the same size the renderer
|
||||
/// drew them. Always the default `theme::FONT_SIZE` for
|
||||
/// fields that do not call `.font_size( … )`.
|
||||
font_size: f32,
|
||||
/// `true` when the source field opted into select-all-on-
|
||||
/// focus. The runtime reads this in `set_focus` to decide
|
||||
/// whether the new selection should anchor at `0` (replace
|
||||
/// on next keystroke) or at the cursor (insert).
|
||||
select_on_focus: bool,
|
||||
/// Snapshot of the
|
||||
/// [`crate::widget::text_edit::TextEdit::password_toggle`]
|
||||
/// callback. When `Some`, pointer / touch dispatch checks
|
||||
/// the eye-icon hit zone (via
|
||||
/// [`crate::widget::text_edit::password_toggle_hit_zone`])
|
||||
/// before falling through to cursor placement and fires
|
||||
/// this message instead.
|
||||
password_toggle_msg: Option<Msg>,
|
||||
},
|
||||
Slider
|
||||
{
|
||||
on_change: Option<Arc<dyn Fn( f32 ) -> Msg>>,
|
||||
axis: slider::SliderAxis,
|
||||
},
|
||||
}
|
||||
|
||||
impl<Msg: Clone> Drop for WidgetHandlers<Msg>
|
||||
{
|
||||
/// Mirror the wipe-on-drop behaviour of [`super::text_edit::TextEdit`] for the
|
||||
/// per-frame handler snapshots the runtime keeps. When the snapshot
|
||||
/// was built from a secure text edit we scrub the value bytes here so
|
||||
/// the heap allocation that backs the cloned `String` is overwritten
|
||||
/// before it is returned to the allocator. Non-secure variants run an
|
||||
/// inert path; the field-level drops that fire after this fn handle
|
||||
/// the rest of the data.
|
||||
fn drop( &mut self )
|
||||
{
|
||||
if let WidgetHandlers::TextEdit { value, secure: true, .. } = self
|
||||
{
|
||||
// SAFETY: identical reasoning to `TextEdit::drop` — we only
|
||||
// write zeros into the underlying byte buffer, leaving valid
|
||||
// UTF-8 (NUL bytes) in place until the auto-drop frees it.
|
||||
let bytes = unsafe { value.as_mut_vec() };
|
||||
crate::secure_mem::secure_zero( bytes );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<Msg: Clone> Clone for WidgetHandlers<Msg>
|
||||
{
|
||||
fn clone( &self ) -> Self
|
||||
{
|
||||
match self
|
||||
{
|
||||
WidgetHandlers::None => WidgetHandlers::None,
|
||||
WidgetHandlers::Button { on_press, on_long_press, on_drag_start, on_escape, repeating } => WidgetHandlers::Button
|
||||
{
|
||||
on_press: on_press.clone(),
|
||||
on_long_press: on_long_press.clone(),
|
||||
on_drag_start: on_drag_start.clone(),
|
||||
on_escape: on_escape.clone(),
|
||||
repeating: *repeating,
|
||||
},
|
||||
WidgetHandlers::Toggle { on_toggle } => WidgetHandlers::Toggle { on_toggle: on_toggle.clone() },
|
||||
WidgetHandlers::Checkbox { on_toggle } => WidgetHandlers::Checkbox { on_toggle: on_toggle.clone() },
|
||||
WidgetHandlers::Radio { on_select } => WidgetHandlers::Radio { on_select: on_select.clone() },
|
||||
WidgetHandlers::ListItem { on_press } => WidgetHandlers::ListItem { on_press: on_press.clone() },
|
||||
WidgetHandlers::WindowButton { on_press } => WidgetHandlers::WindowButton { on_press: on_press.clone() },
|
||||
WidgetHandlers::TextEdit { value, on_change, on_submit, secure, multiline, align, font_size, select_on_focus, password_toggle_msg } =>
|
||||
{
|
||||
WidgetHandlers::TextEdit
|
||||
{
|
||||
value: value.clone(),
|
||||
on_change: on_change.clone(),
|
||||
on_submit: on_submit.clone(),
|
||||
secure: *secure,
|
||||
multiline: *multiline,
|
||||
align: *align,
|
||||
font_size: *font_size,
|
||||
select_on_focus: *select_on_focus,
|
||||
password_toggle_msg: password_toggle_msg.clone(),
|
||||
}
|
||||
}
|
||||
WidgetHandlers::Slider { on_change, axis } =>
|
||||
{
|
||||
WidgetHandlers::Slider { on_change: on_change.clone(), axis: *axis }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<Msg: Clone> WidgetHandlers<Msg>
|
||||
{
|
||||
pub fn is_text_input( &self ) -> bool
|
||||
{
|
||||
matches!( self, WidgetHandlers::TextEdit { .. } )
|
||||
}
|
||||
|
||||
/// `true` when this is a [`WidgetHandlers::TextEdit`] whose source
|
||||
/// widget was built with `.multiline( true )`. The keyboard
|
||||
/// dispatch reads this so pressing Enter inserts a `\n` instead of
|
||||
/// submitting.
|
||||
pub fn is_multiline_text_input( &self ) -> bool
|
||||
{
|
||||
matches!( self, WidgetHandlers::TextEdit { multiline: true, .. } )
|
||||
}
|
||||
|
||||
pub fn is_slider( &self ) -> bool
|
||||
{
|
||||
matches!( self, WidgetHandlers::Slider { .. } )
|
||||
}
|
||||
|
||||
/// `true` when this widget is a row inside a scrollable list that
|
||||
/// keyboard arrow navigation should treat as a stepping point.
|
||||
/// Currently restricted to [`ListItem`](crate::ListItem); buttons,
|
||||
/// toggles, sliders, etc. are not stepped over by Arrow Up/Down so
|
||||
/// they do not interfere with the row-by-row navigation pattern in
|
||||
/// combo popups, settings menus and similar lists.
|
||||
pub fn is_navigable_list_item( &self ) -> bool
|
||||
{
|
||||
matches!( self, WidgetHandlers::ListItem { .. } )
|
||||
}
|
||||
|
||||
/// Convenience: extract the press / activation message for the variants
|
||||
/// that have one (Button, Toggle, Checkbox, Radio, ListItem). Returns
|
||||
/// `None` for sliders / text edits / `None` / disabled widgets.
|
||||
pub fn press_msg( &self ) -> Option<Msg>
|
||||
{
|
||||
match self
|
||||
{
|
||||
WidgetHandlers::Button { on_press, .. } => on_press.clone(),
|
||||
WidgetHandlers::Toggle { on_toggle } => on_toggle.clone(),
|
||||
WidgetHandlers::Checkbox { on_toggle } => on_toggle.clone(),
|
||||
WidgetHandlers::Radio { on_select } => on_select.clone(),
|
||||
WidgetHandlers::ListItem { on_press } => on_press.clone(),
|
||||
WidgetHandlers::WindowButton { on_press } => on_press.clone(),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Submit message (Enter on a focused TextEdit). `None` for every other
|
||||
/// variant.
|
||||
pub fn submit_msg( &self ) -> Option<Msg>
|
||||
{
|
||||
match self
|
||||
{
|
||||
WidgetHandlers::TextEdit { on_submit, .. } => on_submit.clone(),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Build the `on_change` message for a TextEdit given the new value.
|
||||
pub fn text_change_msg( &self, new_value: &str ) -> Option<Msg>
|
||||
{
|
||||
match self
|
||||
{
|
||||
WidgetHandlers::TextEdit { on_change: Some( f ), .. } => Some( f( new_value.to_string() ) ),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Build the `on_change` message for a Slider given a value in `[0,1]`.
|
||||
pub fn slider_change_msg( &self, value: f32 ) -> Option<Msg>
|
||||
{
|
||||
match self
|
||||
{
|
||||
WidgetHandlers::Slider { on_change: Some( f ), .. } => Some( f( value ) ),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Compute the `[0.0, 1.0]` value for the slider this handler belongs to,
|
||||
/// given a pointer position inside its layout rect. Dispatches on the
|
||||
/// stored [`slider::SliderAxis`] so the same call site in `input.rs` drives
|
||||
/// both horizontal [`Slider`](slider::Slider) and vertical
|
||||
/// [`VSlider`](super::vslider::VSlider).
|
||||
///
|
||||
/// Returns `0.0` for non-slider variants — callers combine this with
|
||||
/// [`Self::slider_change_msg`], which also gates on the variant, so the
|
||||
/// zero is never consumed in practice.
|
||||
pub fn slider_value_from_pos( &self, rect: Rect, pos: Point ) -> f32
|
||||
{
|
||||
match self
|
||||
{
|
||||
WidgetHandlers::Slider { axis, .. } =>
|
||||
{
|
||||
slider::value_from_pos_in_rect( rect, pos, *axis )
|
||||
}
|
||||
_ => 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
/// `true` when this is a [`WidgetHandlers::Button`] whose source
|
||||
/// widget opted into press-and-hold repeat. The runtime reads
|
||||
/// this on press to decide whether to fire `press_msg`
|
||||
/// immediately + arm a calloop repeat timer, and on release to
|
||||
/// suppress the regular tap-on-release fire.
|
||||
pub fn is_repeating( &self ) -> bool
|
||||
{
|
||||
matches!( self, WidgetHandlers::Button { repeating: true, .. } )
|
||||
}
|
||||
|
||||
/// Long-press / right-click message for this widget, or `None` if
|
||||
/// none configured. Currently only [`WidgetHandlers::Button`]
|
||||
/// carries one. Firing this does not by itself put the press into
|
||||
/// drag mode — see [`Self::drag_start_msg`] for that.
|
||||
pub fn long_press_msg( &self ) -> Option<Msg>
|
||||
{
|
||||
match self
|
||||
{
|
||||
WidgetHandlers::Button { on_long_press, .. } => on_long_press.clone(),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Drag-arm message for this widget, or `None` if none configured.
|
||||
/// Fired by the touch hold-timer alongside `long_press_msg`, and
|
||||
/// by mouse left-button motion past the drag-promotion threshold
|
||||
/// (without firing the menu). Promotes the gesture into drag mode.
|
||||
pub fn drag_start_msg( &self ) -> Option<Msg>
|
||||
{
|
||||
match self
|
||||
{
|
||||
WidgetHandlers::Button { on_drag_start, .. } => on_drag_start.clone(),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Keyboard `Escape` message for this widget, or `None` if none
|
||||
/// configured. Used by the keyboard ESC handler to scan
|
||||
/// `widget_rects` for a [`crate::widget::dialog::Dialog`] (or other
|
||||
/// `Pressable::on_escape`-bearing wrapper) and fire its cancel
|
||||
/// message before the default ESC fallthrough chain.
|
||||
pub fn escape_msg( &self ) -> Option<Msg>
|
||||
{
|
||||
match self
|
||||
{
|
||||
WidgetHandlers::Button { on_escape, .. } => on_escape.clone(),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Current text-edit value (for cursor placement on focus, backspace
|
||||
/// rebuild, etc.). `None` for non-text-edit variants.
|
||||
pub fn current_value( &self ) -> Option<&str>
|
||||
{
|
||||
match self
|
||||
{
|
||||
WidgetHandlers::TextEdit { value, .. } => Some( value.as_str() ),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user