// SPDX-License-Identifier: LGPL-2.1-only // Copyright (C) 2026 Liberux Labs, S. L. //! 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`] 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 { //! 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 { None, Button { on_press: Option, on_long_press: Option, on_drag_start: Option, /// 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, repeating: bool, }, Toggle { on_toggle: Option }, Checkbox { on_toggle: Option }, Radio { on_select: Option }, ListItem { on_press: Option }, WindowButton { on_press: Option }, TextEdit { value: String, on_change: Option Msg>>, on_submit: Option, /// `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, }, Slider { on_change: Option Msg>>, axis: slider::SliderAxis, }, } impl Drop for WidgetHandlers { /// 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 Clone for WidgetHandlers { 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 WidgetHandlers { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { pub rect: Rect, pub flat_idx: usize, pub id: Option, pub paint_rect: Rect, pub handlers: WidgetHandlers, 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 Clone for LaidOutWidget { 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 { Button( button::Button ), Container( container::Container ), TextEdit( text_edit::TextEdit ), Image( image::Image ), Column( crate::layout::column::Column ), Row( crate::layout::row::Row ), Stack( crate::layout::stack::Stack ), Text( text::Text ), Spacer( crate::layout::spacer::Spacer ), Scroll( scroll::Scroll ), Viewport( viewport::Viewport ), WrapGrid( crate::layout::wrap_grid::WrapGrid ), Slider( slider::Slider ), VSlider( vslider::VSlider ), Toggle( toggle::Toggle ), Separator( separator::Separator ), ProgressBar( progress_bar::ProgressBar ), Checkbox( checkbox::Checkbox ), Radio( radio::Radio ), ListItem( list_item::ListItem ), WindowButton( window_button::WindowButton ), Pressable( pressable::Pressable ), Flex( flex::Flex ), AnchoredOverlay( anchored_overlay::AnchoredOverlay ), Spinner( spinner::Spinner ), External( external::External ), } impl Element { 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 { 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` 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 = Arc U>; impl Element { /// 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` no longer references `Msg`. The standard Elm / /// `iced` shape: a sub-view defined as `fn view( …) -> Element` /// can be embedded inside a parent that produces `Element` /// by calling `.map( AppMsg::Sub )`. /// /// Cost: `O( leaves )` allocations for the closure-wrapping in the /// `Arc` 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 { /// # button( "Save" ).on_press( SubMsg::Save ).into() /// # } /// # fn _ex() -> Element { /// sub_view().map( AppMsg::Sub ) /// # } /// ``` pub fn map( self, f: F ) -> Element where U: Clone + 'static, F: Fn( Msg ) -> U + 'static, { let f: MapFn = Arc::new( f ); self.map_arc( &f ) } pub( crate ) fn map_arc( self, f: &MapFn ) -> Element 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 From> for Element { fn from( c: container::Container ) -> Self { Element::Container( c ) } } impl From> for Element { fn from( b: button::Button ) -> Self { Element::Button( b ) } } impl From> for Element { fn from( t: text_edit::TextEdit ) -> Self { Element::TextEdit( t ) } } impl From for Element { fn from( i: image::Image ) -> Self { Element::Image( i ) } } impl From for Element { fn from( e: external::External ) -> Self { Element::External( e ) } } impl From> for Element { fn from( c: crate::layout::column::Column ) -> Self { Element::Column( c ) } } impl From> for Element { fn from( r: crate::layout::row::Row ) -> Self { Element::Row( r ) } } impl From> for Element { fn from( s: crate::layout::stack::Stack ) -> Self { Element::Stack( s ) } } pub fn button( label: impl Into ) -> button::Button { button::Button::new( label.into() ) } pub fn icon_button( rgba: Arc>, img_w: u32, img_h: u32 ) -> button::Button { button::Button::new_icon( rgba, img_w, img_h ) } pub fn text_edit( placeholder: impl Into, value: impl Into, ) -> text_edit::TextEdit { text_edit::TextEdit::new( placeholder.into(), value.into() ) } pub fn image( rgba: Arc>, width: u32, height: u32 ) -> image::Image { image::Image::new( rgba, width, height ) } pub fn text( content: impl Into ) -> text::Text { text::Text::new( content ) } pub fn container( child: impl Into> ) -> container::Container { 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 ) }