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}" )
}