// SPDX-License-Identifier: LGPL-2.1-only // Copyright (C) 2026 Liberux Labs, S. L. use std::sync::Arc; use crate::types::{ Point, Rect }; use crate::render::Canvas; use super::{ anchored_overlay, button, carousel, 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 { 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 ), Carousel( carousel::Carousel ), } 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( canvas ), 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 ), Element::Carousel( c ) => c.preferred_size( max_width, canvas ), } } 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 ), Element::Carousel( _ ) => {} } } 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 accessible_label( &self ) -> Option { match self { Element::Button( b ) => match &b.content { super::button::ButtonContent::Text( s ) => Some( s.clone() ), super::button::ButtonContent::Icon { .. } => b.tooltip.clone(), }, Element::Toggle( t ) => t.label.clone(), Element::Checkbox( c ) => c.label.clone(), Element::Radio( r ) => r.label.clone(), Element::ListItem( l ) => Some( l.label.clone() ).filter( |s| !s.is_empty() ), Element::WindowButton( _ ) => None, Element::Text( t ) => Some( t.content.clone() ).filter( |s| !s.is_empty() ), _ => None, } } 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(), value: t.value }, Element::Checkbox( c ) => WidgetHandlers::Checkbox { on_toggle: c.on_toggle.clone(), value: c.checked }, Element::Radio( r ) => WidgetHandlers::Radio { on_select: r.on_select.clone(), selected: r.selected }, 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, value: s.value, } } Element::VSlider( s ) => { WidgetHandlers::Slider { on_change: s.on_change.clone(), axis: slider::SliderAxis::Vertical, value: s.value, } } _ => WidgetHandlers::None, } } } 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 ), Element::Carousel( c ) => Element::Carousel( c.map_msg( f ) ), } } } 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 ) } }