PatternGrid is a 3×3 drag widget for the unlock pattern. It is stateless: the app owns the visited nodes and passes them back in for drawing; a press or a drag that reaches a node within the snap radius fires on_node with the index, 0 to 8 row-major, and release fires on_release, so the crossing and minimum-length rules stay in the app. Geometry lives in widget/pattern_grid/theme.rs (size, dot and node radii, stroke width, snap radius, ring alpha), and the snap radius is resolved through the widget-scaling mode at layout time like the slider thumb (set_pattern_snap_px). The gesture machine reuses the slider's drag ownership: WidgetHandlers::is_drag_target covers both, so hover painting, pointer capture and release behave the same, and hover_affects_paint no longer special-cases sliders. The a11y tree maps it to GenericContainer, since a drawn gesture has no assistive equivalent and the app must offer another method. Exported as pattern_grid / PatternGrid, documented in widgets.md, theming.md and architecture.md, with tests for node centres, a wide rect, snapping, the builders, the handler snapshot and map_msg, and a Security tab in examples/widgets.rs that shows the drawn path. Icon buttons given a width or height are framed keys: preferred_size honours the explicit size instead of the icon size, and drawing paints the variant's background, border and focus ring, shared with text buttons through paint_frame, then centres the icon. An icon button without a size keeps the circular hover overlay it had. This is what lets the PIN keypad's backspace and OK keys sit in the same circles as the digits. Keyboard routing now matches what App::on_key documents. handle_key_default, Backspace, Delete and Space checked focused_idx.is_some() and sent the key into text insertion whenever any widget had focus, which is a no-op on a button, so a focused button or pressable swallowed typed characters. focused_text_input replaces the five copies of that check; characters, Backspace and Delete stay in a focused text input and reach the app otherwise, Return and Space still go to the focused widget's submit or press message when it has one, and the arrow, Home and End handlers use the same helper. The lockscreen relies on this to take PIN digits with its dots row focused. debian/changelog: 0.3.0-2.
155 lines
5.2 KiB
Rust
Executable File
155 lines
5.2 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`], [`pattern_grid::PatternGrid`].
|
|
//! * **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 pattern_grid;
|
|
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}" )
|
|
}
|