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

923
src/widget/mod.rs Executable file
View File

@@ -0,0 +1,923 @@
// SPDX-License-Identifier: LGPL-2.1-only
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>
//! Widgets — the interactive and decorative leaves of the [`Element`] tree.
//!
//! Each widget lives in its own submodule and is reached through the
//! crate-root re-exports (`button`, `text`, `text_edit`, `slider`, …) plus
//! the `img_widget` alias for [`image::Image`]. Construct one from its
//! free constructor function, configure it through builder-style methods,
//! and convert it into [`Element<Msg>`] via `.into()` when pushing it
//! into a layout.
//!
//! ```rust,no_run
//! # use ltk::{ button, column, slider, text, Element };
//! # #[ derive( Clone ) ] enum Msg { SetVolume( f32 ), Mute }
//! # struct App { volume: f32 }
//! # impl App { fn _ex( &self ) -> Element<Msg> {
//! column()
//! .push( text( "Volume" ) )
//! .push( slider( self.volume ).on_change( |v| Msg::SetVolume( v ) ) )
//! .push( button( "Mute" ).on_press( Msg::Mute ) )
//! .into()
//! # }}
//! ```
//!
//! ## What lives here
//!
//! * **Buttons / activations**: [`button::Button`],
//! [`pressable::Pressable`], [`window_button::WindowButton`],
//! [`list_item::ListItem`].
//! * **Stateful binary controls**: [`toggle::Toggle`],
//! [`checkbox::Checkbox`], [`radio::Radio`].
//! * **Continuous controls**: [`slider::Slider`], [`vslider::VSlider`],
//! [`progress_bar::ProgressBar`].
//! * **Text**: [`text::Text`], [`text_edit::TextEdit`].
//! * **Images / decoration**: [`image::Image`], [`separator::Separator`],
//! [`container::Container`].
//! * **Clipping wrappers**: [`scroll::Scroll`] (with gesture-driven
//! scrolling), [`viewport::Viewport`] (passive clip / fade), and
//! [`flex::Flex`] (treats a non-spacer child as a row filler).
//! * **Overlays**: [`dialog::Dialog`] (modal / non-modal centered
//! confirmation card with built-in scrim, ESC-to-cancel, and
//! tap-outside-to-dismiss for the non-modal variant).
//!
//! Layouts ([`column`](crate::column), [`row`](crate::row),
//! [`stack`](crate::stack), [`grid`](crate::grid),
//! [`spacer`](crate::spacer)) live in [`crate::layout`]; they share the
//! same [`Element`] tree but are kept separate to make the "what does
//! this paint" / "how is this arranged" distinction explicit.
//!
//! ## Per-leaf handler snapshot
//!
//! [`WidgetHandlers`] is the snapshot the layout pass takes of every
//! interactive widget so the input handlers can dispatch in O(1) without
//! re-walking the [`Element`] tree. It is `pub( crate )` plumbing for the
//! runtime; downstream apps usually never see it. The `test_support`
//! module re-exports it for integration tests that want to assert on the
//! handler shape.
pub mod button;
pub mod container;
pub mod text_edit;
pub mod image;
pub mod text;
pub mod scroll;
pub mod viewport;
pub mod slider;
pub mod vslider;
pub mod toggle;
pub mod separator;
pub mod progress_bar;
pub mod checkbox;
pub mod radio;
pub mod list_item;
pub mod window_button;
pub mod pressable;
pub mod flex;
pub mod combo;
pub mod anchored_overlay;
pub mod spinner;
pub mod tab_bar;
pub mod toast;
pub mod tooltip;
pub mod notebook;
pub mod date_picker;
pub mod time_picker;
pub mod color_picker;
pub mod dialog;
pub mod external;
use std::sync::Arc;
use crate::types::{ Point, Rect, WidgetId };
use crate::render::Canvas;
/// Per-leaf interaction snapshot captured during layout. One variant per
/// interactive widget kind; the layout pass clones the relevant callbacks /
/// values from the [`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 [`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`](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,
}
}
}
/// Result of laying out one *interactive* widget — i.e. a widget that should
/// receive pointer / touch hit-testing. Captures both the *hit rect* (where
/// input lands) and the *paint rect* (the bounding box of everything the
/// widget actually paints, including hover circles, focus rings, shadows…).
/// The paint rect is used by the partial-redraw path to know how much of the
/// canvas must be invalidated when a widget transitions in/out of a given
/// state — it is always `>= rect`.
///
/// `handlers` carries the snapshot of the widget's callbacks/values at layout
/// time so input dispatch is O(1) instead of re-walking the [`Element`] tree.
///
/// `keyboard_focusable` snapshots whether the widget should participate in the
/// Tab / Shift+Tab cycle. Most interactive widgets are also keyboard-focusable
/// (`Button`, `TextEdit`, `Slider`, …) but window-decoration chrome
/// (`WindowButton`) is interactive without taking keyboard focus by default —
/// matching the convention of macOS / GNOME / Windows title bars.
pub struct LaidOutWidget<Msg: Clone>
{
pub rect: Rect,
pub flat_idx: usize,
pub id: Option<WidgetId>,
pub paint_rect: Rect,
pub handlers: WidgetHandlers<Msg>,
pub keyboard_focusable: bool,
/// Cursor shape this widget wants to show on hover. Snapshotted
/// from [`Element::cursor_shape`] at layout time so the input
/// dispatch can pick the right shape without re-walking the
/// element tree.
pub cursor: crate::types::CursorShape,
}
impl<Msg: Clone> Clone for LaidOutWidget<Msg>
{
fn clone( &self ) -> Self
{
Self
{
rect: self.rect,
flat_idx: self.flat_idx,
id: self.id,
paint_rect: self.paint_rect,
handlers: self.handlers.clone(),
keyboard_focusable: self.keyboard_focusable,
cursor: self.cursor,
}
}
}
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 [`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
/// [`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,
}
}
/// 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
/// [`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,
}
}
}
/// Type alias for the message-mapping closure shared across an
/// [`Element::map`] walk. Stored as `Arc<dyn Fn>` so every per-widget
/// `map_msg` can clone and re-share it without copying the closure body
/// — the same closure is invoked once per emitted message, regardless
/// of how many leaves the sub-tree has.
pub( crate ) type MapFn<Msg, U> = Arc<dyn Fn( Msg ) -> U>;
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 )
}
}
pub fn button<Msg: Clone>( label: impl Into<String> ) -> button::Button<Msg>
{
button::Button::new( label.into() )
}
pub fn icon_button<Msg: Clone>( rgba: Arc<Vec<u8>>, img_w: u32, img_h: u32 ) -> button::Button<Msg>
{
button::Button::new_icon( rgba, img_w, img_h )
}
pub fn text_edit<Msg: Clone>(
placeholder: impl Into<String>,
value: impl Into<String>,
) -> text_edit::TextEdit<Msg>
{
text_edit::TextEdit::new( placeholder.into(), value.into() )
}
pub fn image( rgba: Arc<Vec<u8>>, width: u32, height: u32 ) -> image::Image
{
image::Image::new( rgba, width, height )
}
pub fn text( content: impl Into<String> ) -> text::Text
{
text::Text::new( content )
}
pub fn container<Msg: Clone>( child: impl Into<Element<Msg>> ) -> container::Container<Msg>
{
container::Container::new( child )
}
/// Build an [`external::External`] widget that hosts content rendered by
/// a caller-managed GL texture producer.
pub fn external( width: f32, height: f32, source: external::ExternalSource ) -> external::External
{
external::External::new( width, height, source )
}