924 lines
34 KiB
Rust
Executable File
924 lines
34 KiB
Rust
Executable File
// 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 )
|
|
}
|