Files
ltk/src/widget/mod.rs
Pedro M. de Echanove Pasquin 477ef13ff4
Some checks failed
CI / build + test (push) Has been cancelled
CI / cargo audit (push) Has been cancelled
toggle, widget: scale the pill with an explicit row height, and give elide a half-pixel tolerance
Toggle::height used to adjust only the row: the resolved value was floored at the theme track height and the pill kept its theme size, so a toggle capped below the fluid row height still rendered a full-size pill — on large surfaces, visibly out of scale next to controls that honour their cap. The floor is gone; when the resolved height falls below the theme row height, track and thumb now scale down proportionally (never up — the factor is capped at 1), and preferred_size reports the scaled track width so layout, focus ring and centring stay consistent. A toggle without an explicit height is untouched.
elide compared measure( text ) <= max_w strictly, which breaks when the caller sized itself from the same measurement: a button reports text + 2×pad as its preferred width, the layout grants exactly that, and draw hands elide back rect.width − 2×pad. In f32 the add-then-subtract round-trip can land a few ULP under the original measurement, the strict comparison fails, and the truncation branch then costs the full width of the ellipsis — a sub-pixel deficit turned "Empezar" into "Empez...". The fit check now allows half a pixel of slack, which absorbs any mismatch of this class while leaving genuine overflows to truncate as before.
2026-08-09 12:07:35 +02:00

154 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`].
//! * **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}" )
}