The motivating bug was a lockscreen in a downstream app (eydos-loginmanager) where the clock at 87 px overlapped the date at 24 px on a Pinephone but not on a winit dev screen. The root cause split in two: the layout was wired with a single `f32` spacing constant that worked at the dev resolution and broke at the smaller one, and `text::Text::preferred_size` was returning `ascent - descent` for the line height — fontdue's terminology for "the minimum bounding box of an unaccented line", which deliberately drops the `line_gap` that every typographic renderer (Pango, CoreText, DirectWrite) reserves between adjacent rows. At Sora's 200/em line gap, an 87 px row was visually 17 px taller than the rect the column allocated for it; stacked tight against the row above, the descenders bled into the row below. This commit fixes both halves at the toolkit level so every consumer benefits without bolting on a per-screen `Sizing` helper in their own view code. `types::Length` (with the `LengthBase` enum behind it) is the new currency for any "how big" or "how far apart" parameter. Six variants — `Px`, `Vw`, `Vh`, `Vmin`, `Vmax`, `Em` — cover the cases a real UI hits: absolute pixels for fixed-chrome decisions, viewport-relative percentages for sizes that have to survive a portrait/landscape rotation, and root-font-size multiples for typographic hierarchy. Optional `min_px` / `max_px` bounds attach to the same `Length` value via `.clamp( lo, hi )` (both ends), `.at_least( lo )` and `.at_most( hi )` (one-sided); the names are intentionally divergent from `f32::min`/`f32::max` to avoid being read with the opposite semantics (`x.min(24)` in std means "the smaller of x and 24", which is the inverse of what a min bound expresses). The bounds are stored as raw `f32` rather than nested `Length` values, which keeps `Length` `Copy` and avoids a `Box` allocation per widget per frame — the bounded-by-relative case (`Vmin(20).clamp(Vmin(10), Vmin(40))`) is rare enough that the trade is the right one. `From<f32>`, `From<i32>` and `From<u32>` are implemented so every legacy `.size( 24.0 )` / `.padding( 8.0 )` / `.spacing( 4.0 )` call keeps compiling unchanged; the migration is opt-in per call site. The `EM_BASE_DEFAULT = 16.0` constant matches `theme::typography::BODY` so `Length::em( 2.0 )` resolves consistently with the body-text default; a future change can thread a theme-supplied em base through without breaking the resolver shape. The resolver — `Length::resolve( viewport: ( f32, f32 ), em_base: f32 ) -> f32` — runs at layout time against a viewport supplied by the renderer. `Canvas::viewport_logical()` is the new helper that exposes that viewport: it divides the canvas's physical size by `dpi_scale` and falls back to physical size when `dpi_scale <= 0.0`, guarding the misconfigured-canvas path so a Vmin call doesn't poison every downstream measurement with `NaN` or `inf`. The viewport is in **logical** pixels — matching what every wayland `xdg_toplevel.configure` event already hands the client — so `Length::vmin( 18.0 )` on a 360×720-logical Librem 5 portrait surface resolves to 64.8 px and the same expression on a 1600×900 dev screen resolves to 162 px, automatically. Every widget setter that took an `f32` size, padding, spacing, max-width, or fixed dimension now takes `impl Into<Length>` and stores the value as `Length`: - `widget::text::Text::size( impl Into<Length> )`; the `size` field is now `Length`. `Text::resolved_size( &Canvas )` is the internal accessor that every measurement / drawing path routes through, so the field can stay `Length` without churning the call sites. `preferred_size` and `draw` now read `new_line_size = ascent - descent + line_gap` from fontdue's `LineMetrics` (the fix for the original bug) — the baseline placement is unchanged, only the row height grows by the font's declared leading, which is what every stacked layout was implicitly relying on. - `layout::Spacer::height( impl Into<Length> )` / `.width( impl Into<Length> )`; `fixed_height` / `fixed_width` are now `Option<Length>`. New `resolved_height( &Canvas )` / `resolved_width( &Canvas )` helpers replace the direct `s.fixed_height.unwrap_or( 0.0 )` reads in `layout::column`, `layout::row` and `layout::stack`. `Spacer::preferred_size` grows a `&Canvas` parameter for the same reason; `Element::preferred_size` passes the canvas through. - `layout::Column::spacing` / `.padding` / `.max_width`, `layout::Row::spacing` / `.padding` — all take `impl Into<Length>` and store `Length`. Internal `resolved_spacing( &Canvas )`, `resolved_padding( &Canvas )`, `resolved_max_width( &Canvas )` helpers funnel every read, so the layout code paths stay readable. The column's `inner_w` private helper picks up a `&Canvas` argument; the test that used it directly is updated. `theme::typography` keeps its historic `f32` constants (`H0`…`BODY_XS`, plus `LINE_HEIGHT`) so the migration is gradual, and adds a parallel responsive scale exposed as functions returning `Length`: `h0()`, `h1()`, `h2()`, `h3()`, `body()`, `body_s()`, `body_xs()`. Each is a `Length::vmin( pct ).clamp( min_px, max_px )` whose percentage is calibrated against a 1000-px smaller side reproducing the legacy px constant exactly, and whose px clamps protect both ends of the spectrum — a 360-px Pinephone hits the lower clamp on the larger headings, a 4K desktop hits the upper one. The tests in `theme::typography` exercise all three regimes (narrow phone, calibration point, large display) so future drift in the percentages or clamps is caught immediately. `Canvas::viewport_logical` is the only render-surface API touched. None of the existing per-frame paths (`draw_text`, `measure_text`, `font_line_metrics`) change shape, so backends and external embedders aren't disturbed. The `dpi_scale` accessor already existed; this commit only adds the convenience that ratios it against the surface size to return the unit layout actually wants. Test coverage rounds out the addition rather than just smoke-testing the happy path: 22 new tests, broken down as `types::length_tests` (7 — every variant, clamp with relative value, clamp with swapped bounds, `From<f32>`), `render::viewport_tests` (3 — scale 1, scale 2, scale 0 fallback), `theme::typography::tests` (3 — phone-clamped, calibrated, 4K-clamped), `layout::spacer::tests` (4 — px height, vmin height, vw width, flex spacer reports `None`), `layout::column::tests` (3 new — vmin spacing accumulates, vmin padding, vmin max-width caps inner-w), `layout::row::tests` (2 new — vmin padding, vmin spacing produces correct visible gap between non-flex children regardless of the row's centering anchor), and `widget::text::tests` (3 updated/new — defaults compare against `Length::px(16.0)`, `.size( f32 )` and `.size( Length )` both verified). The existing integration test in `tests/layout_stack_spacer.rs` is updated to call `Spacer::preferred_size( &canvas )` and compare `fixed_height` / `fixed_width` against `Some( Length::px( n ) )`. Documentation is updated end-to-end so the new API is discoverable from `cargo doc` without grepping the source: `lib.rs` gets a new entry for `Length` under the **Types** section and a new **Designing for multiple resolutions** section that lists the three patterns (relative `Length` for sizing, responsive typography for hierarchy, `view()`-level branching on surface dimensions only when the structure itself must change). `Canvas::viewport_logical` ships with a runnable `assert_eq!` example covering the scale-2 case. The module-level docstrings for `Spacer`, `Column` and `Row` now show both an `f32` example (legacy, still valid) and a `Length::vmin( ... ).clamp( ... )` example for the responsive variant — `cargo doc` renders both side by side so the upgrade path is obvious. Out of scope for this commit, deliberate: `WrapGrid::spacing_x` / `spacing_y` / `padding`, `widget::text_edit::TextEdit::font_size`, and `widget::image::Image::size` still take `f32`. None of them are on a critical responsive path right now, the `From<f32>` shim means migrating later is a one-line setter signature change per widget, and keeping this commit focused on the widgets the lockscreen actually uses keeps the diff reviewable. The line-gap fix in `text::preferred_size` already benefits `TextEdit` indirectly because its caret/row math reads from the same metrics helpers.
482 lines
18 KiB
Rust
482 lines
18 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, scroll, separator,
|
|
slider, spinner, 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 ),
|
|
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 ),
|
|
Carousel( carousel::Carousel<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 ),
|
|
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<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() ),
|
|
_ => 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(),
|
|
// `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<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 ),
|
|
Element::Carousel( c ) => Element::Carousel( 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 )
|
|
}
|
|
}
|