Trust and prompt each drew their own strip of tabs, their own phone bar with a counter and their own grid of cards, and neither animated anything nor let a tab move. `tab_strip` does all of it once, in colours the application chooses, so the same widget serves a black terminal and a browser that follows the theme. Two things the menus of those applications needed come with it: a way to place a menu under the button that opens it, and list items that are not drawn in the theme's colours. The widget. `tab_strip( tabs )` takes `TabItem`s, made with `tab( key, label )`, where the key stays the same while the tab is open, plus `.image( ImageData )` for a page's icon, `.symbol( name )` for a theme icon drawn in the tab's ink, and `.closable( bool )`. The builder sets the `selected` tab, `on_select`, `on_close`, `on_overview`, the buttons before the tabs (`leading`), right after the last one (`after_tabs`) and at the far right (`trailing`), the phone bar's `mobile_center`, the `TabStripColors`, the `TabLayout`, `reorderable` and the `TabMotion`. `TabStripColors` sets the strip's background and ink and the fill, edge and ink of the active tab and of the others; the default follows the theme. On a desktop. `[leading] [tabs…][after_tabs] clear [trailing]`. The tabs share the width between 90 widget pixels and a fifth of the screen's width and scroll sideways when even the minimum does not fit, and 7.5 % of the width between the leading and trailing buttons is always left clear, also while they scroll. A horizontal scroll measures its child with no width limit, so the tabs inside it cannot see how much room there is; two new elements solve that. `TabRow`, outside the scroll, works the tab width out of its own rect, less the clear part and the buttons after the tabs, and hands it to the `TabMotion`; `TabCells`, inside the scroll, places the tabs, the gaps of closing ones and the buttons where the motion says. Each tab is clipped by a `viewport`, so it can grow from nothing and shrink away. On a phone. `TabLayout::Mobile` is a bar with the leading buttons, the active tab's icon and title or the application's `mobile_center`, a counter that sends `on_overview` and the trailing buttons. `tab_overview( tabs )` is the grid of cards it opens, with a `title`, a `back` message and header `action`s: `build` gives the whole page, header and scrolling cards, and `cards` the grid alone for an application that lays its page out itself. The cards are placed by a third new element, `TabCards`, every one as tall as the tallest, so a dragged card can follow the finger. The motion. `TabMotion` is the only state, kept by the application, handed to every view and reported from `App::is_animating`; it is an `Arc<Mutex>` because `view` only gets `&self`, and clones share it. A tab that opens grows from nothing while the others narrow, and one that closes leaves a gap that shrinks away while the others keep their width for a second, so the close button of the tab that slides in lands under the pointer; another close starts the second again. Changes take 180 ms, ease-out. The first layout and a resize alone place the tabs without moving them. Reordering. With `reorderable( true )` each tab, and each card of the overview, carries its select message as `on_drag_start`, so it is dragged after 24 px of mouse motion or a long press, and dragging a tab selects it. The drag reaches the application, which forwards `App::on_drag_move` to `TabMotion::drag_move` and answers `App::on_drop` with the `( from, to )` that `TabMotion::drag_end` reports. The motion remembers where it drew every tab: the first move takes the tab under it and returns `false` when there is none, the tab then follows the pointer within the tabs and is drawn on top, and the others slide out of its way as its centre passes theirs, or as it enters another cell of the overview. On release it slides into its place. A strip and its overview are never shown at once and may share one motion. Anchored. `AnchoredOverlay`, which only the combo used, is public as `anchored( child, id )`: it draws its child under the widget built with that `WidgetId`, as it was laid out on the previous frame. `align` lines it up with the anchor's left edge, its right edge or its middle, `gap` sets the space under it, and the child is now kept within the rect its parent gives it, which also keeps a combo's list inside the window. List items and icons. `ListItem::ink( label, secondary )` draws the label, the subtitle and the trailing text in colours of the application's own and tints hover and press with the label's, for rows on a surface that is not the theme's. A button's icon is now fitted into its square without being stretched: a wide icon such as `actions/remove`, a 25 by 4 minus, was drawn as a block as tall as it was wide. Documentation, example and tests. `docs/widgets.md` describes `tab_strip` and `anchored` and the new `ink`. `cargo run --example tab_strip` opens three documents that can be added, closed and dragged, on a desktop strip or a phone bar with its overview. The tests cover the share of the width, its limits and the clear part, the buttons after the tabs, the drawing order of a dragged tab, the builder's defaults, the overview's rows, and in `motion.rs` the growth, the gap, the wait and its restart, a resize, and dragging a tab and a card; `anchored` and the icon fit have tests of their own. Not done. A strip that scrolls does not scroll to the active tab or follow a tab dragged towards its edge, and the overview does not scroll while a card is dragged to the edge of the screen.
526 lines
20 KiB
Rust
526 lines
20 KiB
Rust
// SPDX-License-Identifier: LGPL-2.1-only
|
|
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>
|
|
|
|
use std::sync::Arc;
|
|
use crate::types::{ Point, Rect };
|
|
use crate::render::Canvas;
|
|
use super::{
|
|
anchored_overlay, button, carousel, checkbox, container, external, flex,
|
|
image, list_item, pressable, progress_bar, radio, rich_text, scroll, separator,
|
|
pattern_grid, slider, spinner, tab_strip, text, text_edit, toggle, viewport, vslider, window_button,
|
|
};
|
|
use super::handlers::WidgetHandlers;
|
|
use super::MapFn;
|
|
|
|
pub enum Element<Msg: Clone>
|
|
{
|
|
Button( button::Button<Msg> ),
|
|
Container( container::Container<Msg> ),
|
|
TextEdit( text_edit::TextEdit<Msg> ),
|
|
Image( image::Image ),
|
|
Column( crate::layout::column::Column<Msg> ),
|
|
Row( crate::layout::row::Row<Msg> ),
|
|
Stack( crate::layout::stack::Stack<Msg> ),
|
|
Text( text::Text ),
|
|
RichText( rich_text::RichText<Msg> ),
|
|
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> ),
|
|
PatternGrid( pattern_grid::PatternGrid<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 ),
|
|
Carousel( carousel::Carousel<Msg> ),
|
|
TabRow( tab_strip::TabRow<Msg> ),
|
|
TabCells( tab_strip::TabCells<Msg> ),
|
|
TabCards( tab_strip::TabCards<Msg> ),
|
|
}
|
|
|
|
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, canvas ),
|
|
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::RichText( r ) => r.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::PatternGrid( g ) => g.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, canvas ),
|
|
Element::ProgressBar( p ) => p.preferred_size( max_width, canvas ),
|
|
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, canvas ),
|
|
Element::External( e ) => e.preferred_size( max_width ),
|
|
Element::Carousel( c ) => c.preferred_size( max_width, canvas ),
|
|
Element::TabRow( r ) => r.preferred_size( max_width, canvas ),
|
|
Element::TabCells( c ) => c.preferred_size( max_width, canvas ),
|
|
Element::TabCards( 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,
|
|
text_scroll: text_edit::TextScroll,
|
|
)
|
|
{
|
|
match self
|
|
{
|
|
Element::Button( b ) => b.draw( canvas, rect, focused, hovered, pressed ),
|
|
Element::TextEdit( t ) => t.draw( canvas, rect, focused, cursor_pos, selection_anchor, text_scroll ),
|
|
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::RichText( r ) => r.draw( canvas, rect ),
|
|
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::PatternGrid( g ) => g.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( _ ) => {}
|
|
Element::TabRow( _ ) => {}
|
|
Element::TabCells( _ ) => {}
|
|
Element::TabCards( _ ) => {}
|
|
}
|
|
}
|
|
|
|
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( t ) => !t.read_only,
|
|
Element::Slider( _ ) | Element::VSlider( _ ) | Element::PatternGrid( _ ) => 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( t ) if !t.read_only )
|
|
}
|
|
|
|
/// 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,
|
|
Element::PatternGrid( _ ) => 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<String>
|
|
{
|
|
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() ),
|
|
Element::RichText( r ) => Some( r.content.clone() ).filter( |s| !s.is_empty() ),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
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(), 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(),
|
|
on_focus: t.on_focus.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(),
|
|
purpose: t.purpose,
|
|
multiline: t.is_multiline(),
|
|
align: t.align,
|
|
font_size: t.font_size,
|
|
pad_h: t.pad_h,
|
|
select_on_focus: t.select_on_focus,
|
|
password_toggle_msg: t.password_toggle.as_ref().map( |( _, m )| m.clone() ),
|
|
}
|
|
}
|
|
Element::PatternGrid( g ) =>
|
|
{
|
|
WidgetHandlers::PatternGrid
|
|
{
|
|
on_node: g.on_node.clone(),
|
|
on_release: g.on_release.clone(),
|
|
snap_px: pattern_grid::snap_design_px(),
|
|
}
|
|
}
|
|
Element::Slider( s ) =>
|
|
{
|
|
WidgetHandlers::Slider
|
|
{
|
|
on_change: s.on_change.clone(),
|
|
on_release: s.on_release.clone(),
|
|
axis: slider::SliderAxis::Horizontal,
|
|
value: s.value,
|
|
// Design-px fallback; the layout pass overwrites this with
|
|
// the widget-scaling-resolved size via `set_slider_thumb_px`.
|
|
thumb_px: slider::thumb_design_px(),
|
|
}
|
|
}
|
|
Element::VSlider( s ) =>
|
|
{
|
|
WidgetHandlers::Slider
|
|
{
|
|
on_change: s.on_change.clone(),
|
|
on_release: s.on_release.clone(),
|
|
axis: slider::SliderAxis::Vertical,
|
|
value: s.value,
|
|
// The vertical axis maps against the full rect height and
|
|
// takes no thumb inset, so this is unused.
|
|
thumb_px: 0.0,
|
|
}
|
|
}
|
|
_ => WidgetHandlers::None,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<Msg: Clone + 'static> Element<Msg>
|
|
{
|
|
/// Re-tag every message a sub-view emits.
|
|
///
|
|
/// Walks the tree once and rewrites every per-leaf message store —
|
|
/// `Button::on_press`, `Slider::on_change`, the children of
|
|
/// `Column`/`Row`/`Stack`/`WrapGrid`, and so on — so the returned
|
|
/// `Element<U>` no longer references `Msg`. The standard Elm /
|
|
/// `iced` shape: a sub-view defined as `fn view( …) -> Element<SubMsg>`
|
|
/// can be embedded inside a parent that produces `Element<AppMsg>`
|
|
/// by calling `.map( AppMsg::Sub )`.
|
|
///
|
|
/// Cost: `O( leaves )` allocations for the closure-wrapping in the
|
|
/// `Arc<dyn Fn(...)>` callbacks (`text_edit`, `slider`, `vslider`),
|
|
/// and the closure itself runs an extra indirect call per emitted
|
|
/// message — both per-`map`-layer. Trees built fresh every frame
|
|
/// (the typical case) absorb this in the same allocator pressure
|
|
/// `view()` already produces, so the overhead is in the noise.
|
|
///
|
|
/// ```rust,no_run
|
|
/// # use ltk::{ button, text, Element };
|
|
/// # #[ derive( Clone ) ] enum SubMsg { Save }
|
|
/// # #[ derive( Clone ) ] enum AppMsg { Sub( SubMsg ) }
|
|
/// # fn sub_view() -> Element<SubMsg> {
|
|
/// # button( "Save" ).on_press( SubMsg::Save ).into()
|
|
/// # }
|
|
/// # fn _ex() -> Element<AppMsg> {
|
|
/// sub_view().map( AppMsg::Sub )
|
|
/// # }
|
|
/// ```
|
|
pub fn map<U, F>( self, f: F ) -> Element<U>
|
|
where
|
|
U: Clone + 'static,
|
|
F: Fn( Msg ) -> U + 'static,
|
|
{
|
|
let f: MapFn<Msg, U> = Arc::new( f );
|
|
self.map_arc( &f )
|
|
}
|
|
|
|
pub( crate ) fn map_arc<U>( self, f: &MapFn<Msg, U> ) -> Element<U>
|
|
where
|
|
U: Clone + 'static,
|
|
{
|
|
match self
|
|
{
|
|
Element::Button( b ) => Element::Button( b.map_msg( f ) ),
|
|
Element::Container( c ) => Element::Container( c.map_msg( f ) ),
|
|
Element::TextEdit( t ) => Element::TextEdit( t.map_msg( f ) ),
|
|
Element::Image( i ) => Element::Image( i ),
|
|
Element::Column( c ) => Element::Column( c.map_msg( f ) ),
|
|
Element::Row( r ) => Element::Row( r.map_msg( f ) ),
|
|
Element::Stack( s ) => Element::Stack( s.map_msg( f ) ),
|
|
Element::Text( t ) => Element::Text( t ),
|
|
Element::RichText( r ) => Element::RichText( r.map_msg( f ) ),
|
|
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::PatternGrid( g ) => Element::PatternGrid( g.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 ) ),
|
|
Element::TabRow( r ) => Element::TabRow( r.map_msg( f ) ),
|
|
Element::TabCells( c ) => Element::TabCells( c.map_msg( f ) ),
|
|
Element::TabCards( c ) => Element::TabCards( c.map_msg( f ) ),
|
|
}
|
|
}
|
|
}
|
|
|
|
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 )
|
|
}
|
|
}
|