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:
2026-05-15 23:46:56 +02:00
parent 3d237039c6
commit 4aa3480b64
155 changed files with 13832 additions and 13035 deletions

456
src/widget/element.rs Normal file
View File

@@ -0,0 +1,456 @@
// 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 crate::render::Canvas;
use super::{
anchored_overlay, button, checkbox, container, external, flex, image,
list_item, pressable, progress_bar, radio, scroll, separator, slider,
spinner, text, text_edit, toggle, viewport, vslider, window_button,
};
use super::handlers::WidgetHandlers;
use super::MapFn;
pub enum Element<Msg: Clone>
{
Button( button::Button<Msg> ),
Container( container::Container<Msg> ),
TextEdit( text_edit::TextEdit<Msg> ),
Image( image::Image ),
Column( crate::layout::column::Column<Msg> ),
Row( crate::layout::row::Row<Msg> ),
Stack( crate::layout::stack::Stack<Msg> ),
Text( text::Text ),
Spacer( crate::layout::spacer::Spacer ),
Scroll( scroll::Scroll<Msg> ),
Viewport( viewport::Viewport<Msg> ),
WrapGrid( crate::layout::wrap_grid::WrapGrid<Msg> ),
Slider( slider::Slider<Msg> ),
VSlider( vslider::VSlider<Msg> ),
Toggle( toggle::Toggle<Msg> ),
Separator( separator::Separator ),
ProgressBar( progress_bar::ProgressBar ),
Checkbox( checkbox::Checkbox<Msg> ),
Radio( radio::Radio<Msg> ),
ListItem( list_item::ListItem<Msg> ),
WindowButton( window_button::WindowButton<Msg> ),
Pressable( pressable::Pressable<Msg> ),
Flex( flex::Flex<Msg> ),
AnchoredOverlay( anchored_overlay::AnchoredOverlay<Msg> ),
Spinner( spinner::Spinner ),
External( external::External ),
}
impl<Msg: Clone> Element<Msg>
{
pub fn preferred_size( &self, max_width: f32, canvas: &Canvas ) -> ( f32, f32 )
{
match self
{
Element::Button( b ) => b.preferred_size( max_width, canvas ),
Element::TextEdit( t ) => t.preferred_size( max_width, canvas ),
Element::Image( i ) => i.preferred_size( max_width ),
Element::Column( c ) => c.preferred_size( max_width, canvas ),
Element::Row( r ) => r.preferred_size( max_width, canvas ),
Element::Stack( s ) => s.preferred_size( max_width, canvas ),
Element::Text( t ) => t.preferred_size( max_width, canvas ),
Element::Spacer( s ) => s.preferred_size(),
Element::Scroll( s ) => s.preferred_size( max_width, canvas ),
Element::Viewport( v ) => v.preferred_size( max_width, canvas ),
Element::WrapGrid( g ) => g.preferred_size( max_width, canvas ),
Element::Slider( s ) => s.preferred_size( max_width, canvas ),
Element::VSlider( s ) => s.preferred_size( max_width, canvas ),
Element::Container( c ) => c.preferred_size( max_width, canvas ),
Element::Toggle( t ) => t.preferred_size( max_width, canvas ),
Element::Separator( s ) => s.preferred_size( max_width ),
Element::ProgressBar( p ) => p.preferred_size( max_width ),
Element::Checkbox( c ) => c.preferred_size( max_width, canvas ),
Element::Radio( r ) => r.preferred_size( max_width, canvas ),
Element::ListItem( l ) => l.preferred_size( max_width, canvas ),
Element::WindowButton( b ) => b.preferred_size( max_width, canvas ),
Element::Pressable( p ) => p.preferred_size( max_width, canvas ),
Element::Flex( f ) => f.preferred_size( max_width, canvas ),
Element::AnchoredOverlay( a ) => a.child.preferred_size( max_width, canvas ),
Element::Spinner( s ) => s.preferred_size( max_width ),
Element::External( e ) => e.preferred_size( max_width ),
}
}
pub fn draw(
&self,
canvas: &mut Canvas,
rect: Rect,
focused: bool,
hovered: bool,
pressed: bool,
cursor_pos: usize,
selection_anchor: usize,
)
{
match self
{
Element::Button( b ) => b.draw( canvas, rect, focused, hovered, pressed ),
Element::TextEdit( t ) => t.draw( canvas, rect, focused, cursor_pos, selection_anchor ),
Element::Image( i ) => i.draw( canvas, rect ),
Element::Column( c ) => c.draw( canvas, rect, focused ),
Element::Row( r ) => r.draw( canvas, rect, focused ),
Element::Stack( s ) => s.draw( canvas, rect, focused ),
Element::Text( t ) => t.draw( canvas, rect, focused ),
Element::Spacer( _ ) => {}
Element::Scroll( _ ) => {}
Element::Viewport( _ ) => {}
Element::WrapGrid( _ ) => {}
Element::Slider( s ) => s.draw( canvas, rect, focused ),
Element::VSlider( s ) => s.draw( canvas, rect, focused ),
Element::Container( _ ) => {}
Element::Toggle( t ) => t.draw( canvas, rect, focused ),
Element::Separator( s ) => s.draw( canvas, rect ),
Element::ProgressBar( p ) => p.draw( canvas, rect ),
Element::Checkbox( c ) => c.draw( canvas, rect, focused ),
Element::Radio( r ) => r.draw( canvas, rect, focused ),
Element::ListItem( l ) => l.draw( canvas, rect, focused, hovered, pressed ),
Element::WindowButton( b ) => b.draw( canvas, rect, focused, hovered, pressed ),
Element::Pressable( _ ) => {}
Element::Flex( _ ) => {}
Element::AnchoredOverlay( _ ) => {}
Element::Spinner( s ) => s.draw( canvas, rect ),
Element::External( e ) => e.draw( canvas, rect ),
}
}
pub fn hit_test( &self, rect: Rect, pos: Point ) -> bool
{
rect.contains( pos )
}
/// Bounding box of every pixel this widget may paint given `rect` as its
/// layout rect. Must enclose the `draw` output in every possible state
/// (hover, focus, press). Widgets that paint outside their layout rect
/// (hover halos, focus rings, drop shadows…) must override this; the
/// default returns `rect` unchanged.
///
/// Used by the partial-redraw path to invalidate exactly the pixels that
/// might change when a widget transitions in/out of a state.
pub( crate ) fn paint_bounds( &self, rect: Rect ) -> Rect
{
match self
{
Element::Button( b ) => b.paint_bounds( rect ),
Element::Toggle( t ) => t.paint_bounds( rect ),
Element::Radio( r ) => r.paint_bounds( rect ),
Element::Checkbox( c ) => c.paint_bounds( rect ),
Element::TextEdit( e ) => e.paint_bounds( rect ),
Element::ListItem( l ) => l.paint_bounds( rect ),
Element::WindowButton( b ) => b.paint_bounds( rect ),
Element::Slider( s ) => s.paint_bounds( rect ),
Element::VSlider( s ) => s.paint_bounds( rect ),
_ => rect,
}
}
/// Whether the widget should be included in the per-surface
/// `widget_rects` list — i.e. whether pointer / touch hit testing must be
/// able to land on it. This is the predicate that gates the layout pass's
/// push to `DrawCtx::widget_rects` in the draw pass.
///
/// Defaults to [`Self::is_focusable`] for every widget that takes keyboard
/// focus (those are interactive by definition). Widgets that are
/// click/touch-only without taking keyboard focus — currently
/// [`Element::WindowButton`] — opt in here without opting in to the Tab
/// cycle.
pub fn is_interactive( &self ) -> bool
{
match self
{
// Always hit-testable regardless of `focusable`. Lets callers
// opt out of keyboard focus (no Tab target, no lingering focus
// ring after a press) while keeping clicks / taps working.
Element::Button( _ ) | Element::WindowButton( _ ) => true,
// Pressable wrappers participate in hit testing only when they
// carry a handler — a no-op pressable is invisible to input.
Element::Pressable( p ) => p.has_handler(),
_ => self.is_focusable(),
}
}
/// Whether the widget participates in the Tab / Shift+Tab focus cycle.
/// Snapshotted onto [`super::LaidOutWidget::keyboard_focusable`] at layout time so
/// `next_focusable_index` can iterate without the [`Element`] tree.
///
/// Hit-testable chrome that should *not* steal keyboard focus from window
/// content (e.g. [`Element::WindowButton`]) returns `false` here while
/// still returning `true` from [`Self::is_interactive`].
pub fn is_focusable( &self ) -> bool
{
match self
{
Element::Button( b ) => b.focusable,
Element::TextEdit( _ ) | Element::Slider( _ ) | Element::VSlider( _ ) => true,
Element::Toggle( _ ) | Element::Checkbox( _ ) | Element::Radio( _ ) | Element::ListItem( _ ) => true,
Element::WindowButton( b ) => b.focusable,
_ => false,
}
}
pub fn is_text_input( &self ) -> bool
{
matches!( self, Element::TextEdit( _ ) )
}
/// Cursor shape to display while the pointer is over this widget.
/// Per-widget defaults match desktop conventions:
///
/// * Text inputs → `Text` (I-beam)
/// * Clickables (Button, Pressable, Toggle, Checkbox, Radio,
/// ListItem, WindowButton) → `Pointer` (hand)
/// * Anything else → `Default`
///
/// Widgets that carry a per-instance override (set via the
/// `.cursor( shape )` builder) return the override; otherwise they
/// return their type-based default. The runtime snapshots this onto
/// [`super::LaidOutWidget::cursor`] at layout time and dispatches the
/// shape via `wp_cursor_shape_v1` on hover transitions.
pub fn cursor_shape( &self ) -> crate::types::CursorShape
{
use crate::types::CursorShape::*;
match self
{
Element::TextEdit( t ) => t.cursor.unwrap_or( Text ),
Element::Button( b ) => b.cursor.unwrap_or( Pointer ),
Element::Pressable( p ) => p.cursor.unwrap_or( Pointer ),
Element::Toggle( _ ) => Pointer,
Element::Checkbox( _ ) => Pointer,
Element::Radio( _ ) => Pointer,
Element::ListItem( _ ) => Pointer,
Element::WindowButton( _ ) => Pointer,
// Sliders read as buttons on hover (`Pointer`) and as
// `Grabbing` during a drag — the runtime swaps to
// `Grabbing` itself when `gesture.dragging_slider` is
// set, so the static value here is just the hover state.
Element::Slider( _ ) => Pointer,
Element::VSlider( _ ) => Pointer,
_ => Default,
}
}
/// Add an arm here to opt a widget kind into the auto-tooltip flow.
pub fn tooltip( &self ) -> Option<&str>
{
match self
{
Element::Button( b ) => b.tooltip.as_deref(),
_ => None,
}
}
/// Snapshot the widget's callbacks/value into a [`WidgetHandlers`] for
/// O(1) dispatch later. Called once per focusable leaf during the layout
/// pass; the handlers are then stored alongside the rect in
/// [`super::LaidOutWidget`] so input handlers don't need the [`Element`] tree.
///
/// Containers and layouts that delegate to children return
/// [`WidgetHandlers::None`] — only leaf widgets actually carry payload.
pub( crate ) fn handlers( &self ) -> WidgetHandlers<Msg>
{
match self
{
Element::Button( b ) => WidgetHandlers::Button
{
on_press: b.on_press.clone(),
on_long_press: b.on_long_press.clone(),
on_drag_start: b.on_drag_start.clone(),
on_escape: None,
repeating: b.repeating,
},
Element::Pressable( p ) => WidgetHandlers::Button
{
on_press: p.on_press.clone(),
on_long_press: p.on_long_press.clone(),
on_drag_start: p.on_drag_start.clone(),
on_escape: p.on_escape.clone(),
repeating: false,
},
Element::Toggle( t ) => WidgetHandlers::Toggle { on_toggle: t.on_toggle.clone() },
Element::Checkbox( c ) => WidgetHandlers::Checkbox { on_toggle: c.on_toggle.clone() },
Element::Radio( r ) => WidgetHandlers::Radio { on_select: r.on_select.clone() },
Element::ListItem( l ) => WidgetHandlers::ListItem { on_press: l.on_press.clone() },
Element::WindowButton( b ) => WidgetHandlers::WindowButton { on_press: b.on_press.clone() },
Element::TextEdit( t ) =>
{
WidgetHandlers::TextEdit
{
value: t.value.clone(),
on_change: t.on_change.clone(),
on_submit: t.on_submit.clone(),
// `secure` here drives memory wipe-on-drop and
// the IME bypass — so any password field (with
// or without a toggle) opts in, regardless of
// the user's current visibility choice.
secure: t.secure || t.password_toggle.is_some(),
multiline: t.is_multiline(),
align: t.align,
font_size: t.font_size,
select_on_focus: t.select_on_focus,
password_toggle_msg: t.password_toggle.as_ref().map( |( _, m )| m.clone() ),
}
}
Element::Slider( s ) =>
{
WidgetHandlers::Slider
{
on_change: s.on_change.clone(),
axis: slider::SliderAxis::Horizontal,
}
}
Element::VSlider( s ) =>
{
WidgetHandlers::Slider
{
on_change: s.on_change.clone(),
axis: slider::SliderAxis::Vertical,
}
}
_ => WidgetHandlers::None,
}
}
}
impl<Msg: Clone + 'static> Element<Msg>
{
/// Re-tag every message a sub-view emits.
///
/// Walks the tree once and rewrites every per-leaf message store —
/// `Button::on_press`, `Slider::on_change`, the children of
/// `Column`/`Row`/`Stack`/`WrapGrid`, and so on — so the returned
/// `Element<U>` no longer references `Msg`. The standard Elm /
/// `iced` shape: a sub-view defined as `fn view( …) -> Element<SubMsg>`
/// can be embedded inside a parent that produces `Element<AppMsg>`
/// by calling `.map( AppMsg::Sub )`.
///
/// Cost: `O( leaves )` allocations for the closure-wrapping in the
/// `Arc<dyn Fn(...)>` callbacks (`text_edit`, `slider`, `vslider`),
/// and the closure itself runs an extra indirect call per emitted
/// message — both per-`map`-layer. Trees built fresh every frame
/// (the typical case) absorb this in the same allocator pressure
/// `view()` already produces, so the overhead is in the noise.
///
/// ```rust,no_run
/// # use ltk::{ button, text, Element };
/// # #[ derive( Clone ) ] enum SubMsg { Save }
/// # #[ derive( Clone ) ] enum AppMsg { Sub( SubMsg ) }
/// # fn sub_view() -> Element<SubMsg> {
/// # button( "Save" ).on_press( SubMsg::Save ).into()
/// # }
/// # fn _ex() -> Element<AppMsg> {
/// sub_view().map( AppMsg::Sub )
/// # }
/// ```
pub fn map<U, F>( self, f: F ) -> Element<U>
where
U: Clone + 'static,
F: Fn( Msg ) -> U + 'static,
{
let f: MapFn<Msg, U> = Arc::new( f );
self.map_arc( &f )
}
pub( crate ) fn map_arc<U>( self, f: &MapFn<Msg, U> ) -> Element<U>
where
U: Clone + 'static,
{
match self
{
Element::Button( b ) => Element::Button( b.map_msg( f ) ),
Element::Container( c ) => Element::Container( c.map_msg( f ) ),
Element::TextEdit( t ) => Element::TextEdit( t.map_msg( f ) ),
Element::Image( i ) => Element::Image( i ),
Element::Column( c ) => Element::Column( c.map_msg( f ) ),
Element::Row( r ) => Element::Row( r.map_msg( f ) ),
Element::Stack( s ) => Element::Stack( s.map_msg( f ) ),
Element::Text( t ) => Element::Text( t ),
Element::Spacer( s ) => Element::Spacer( s ),
Element::Scroll( s ) => Element::Scroll( s.map_msg( f ) ),
Element::Viewport( v ) => Element::Viewport( v.map_msg( f ) ),
Element::WrapGrid( g ) => Element::WrapGrid( g.map_msg( f ) ),
Element::Slider( s ) => Element::Slider( s.map_msg( f ) ),
Element::VSlider( s ) => Element::VSlider( s.map_msg( f ) ),
Element::Toggle( t ) => Element::Toggle( t.map_msg( f ) ),
Element::Separator( s ) => Element::Separator( s ),
Element::ProgressBar( p ) => Element::ProgressBar( p ),
Element::Checkbox( c ) => Element::Checkbox( c.map_msg( f ) ),
Element::Radio( r ) => Element::Radio( r.map_msg( f ) ),
Element::ListItem( l ) => Element::ListItem( l.map_msg( f ) ),
Element::WindowButton( b ) => Element::WindowButton( b.map_msg( f ) ),
Element::Pressable( p ) => Element::Pressable( p.map_msg( f ) ),
Element::Flex( fx ) => Element::Flex( fx.map_msg( f ) ),
Element::AnchoredOverlay( a ) => Element::AnchoredOverlay( a.map_msg( f ) ),
Element::Spinner( s ) => Element::Spinner( s ),
Element::External( e ) => Element::External( e ),
}
}
}
impl<Msg: Clone> From<container::Container<Msg>> for Element<Msg>
{
fn from( c: container::Container<Msg> ) -> Self
{
Element::Container( c )
}
}
impl<Msg: Clone> From<button::Button<Msg>> for Element<Msg>
{
fn from( b: button::Button<Msg> ) -> Self
{
Element::Button( b )
}
}
impl<Msg: Clone> From<text_edit::TextEdit<Msg>> for Element<Msg>
{
fn from( t: text_edit::TextEdit<Msg> ) -> Self
{
Element::TextEdit( t )
}
}
impl<Msg: Clone> From<image::Image> for Element<Msg>
{
fn from( i: image::Image ) -> Self
{
Element::Image( i )
}
}
impl<Msg: Clone> From<external::External> for Element<Msg>
{
fn from( e: external::External ) -> Self
{
Element::External( e )
}
}
impl<Msg: Clone> From<crate::layout::column::Column<Msg>> for Element<Msg>
{
fn from( c: crate::layout::column::Column<Msg> ) -> Self
{
Element::Column( c )
}
}
impl<Msg: Clone> From<crate::layout::row::Row<Msg>> for Element<Msg>
{
fn from( r: crate::layout::row::Row<Msg> ) -> Self
{
Element::Row( r )
}
}
impl<Msg: Clone> From<crate::layout::stack::Stack<Msg>> for Element<Msg>
{
fn from( s: crate::layout::stack::Stack<Msg> ) -> Self
{
Element::Stack( s )
}
}