ltk/widget/mod.rs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153
// 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),
//! [`flex::Flex`] (treats a non-spacer child as a row filler), and
//! [`carousel::Carousel`] (horizontal focused-tile carousel with
//! host-controlled offset).
//! * **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 rich_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;
pub mod carousel;
pub mod element;
pub mod handlers;
pub mod laid_out;
pub mod factory;
pub use element::Element;
pub use handlers::WidgetHandlers;
pub use laid_out::LaidOutWidget;
pub use factory::{ button, icon_button, text_edit, image, text, container, external };
/// 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> = std::sync::Arc<dyn Fn( Msg ) -> U>;
/// Shorten `text` with a trailing ellipsis so it fits `max_w` at `size`.
///
/// The single truncation rule for widgets that paint a line of text into a
/// rect they do not control: a leaf must stay inside what the layout gave
/// it, or a long label runs under its neighbour. Widths accumulate per
/// character, which is what [`text::Text`] does with its own inline copy of
/// this — folding that one in is pending, as it measures through a font
/// override this signature does not carry.
pub( crate ) fn elide(
canvas: &crate::render::Canvas,
text: &str,
size: f32,
max_w: f32,
) -> String
{
if max_w <= 0.0
{
return String::new();
}
// Half-pixel slack: a parent that sized itself from this same
// measurement can hand back a width a few ULP short after its
// padding add-then-subtract round-trip.
if canvas.measure_text( text, size ) <= max_w + 0.5
{
return text.to_string();
}
let ellipsis = "...";
let budget = max_w - canvas.measure_text( ellipsis, size );
if budget <= 0.0
{
return ellipsis.to_string();
}
let mut accum = 0.0_f32;
let kept: String = text.chars().take_while( |ch|
{
accum += canvas.measure_text( &ch.to_string(), size );
accum <= budget
} ).collect();
format!( "{kept}{ellipsis}" )
}