Trust and prompt each drew their own strip of tabs, their own phone bar with a counter and their own grid of cards, and neither animated anything nor let a tab move. `tab_strip` does all of it once, in colours the application chooses, so the same widget serves a black terminal and a browser that follows the theme. Two things the menus of those applications needed come with it: a way to place a menu under the button that opens it, and list items that are not drawn in the theme's colours. The widget. `tab_strip( tabs )` takes `TabItem`s, made with `tab( key, label )`, where the key stays the same while the tab is open, plus `.image( ImageData )` for a page's icon, `.symbol( name )` for a theme icon drawn in the tab's ink, and `.closable( bool )`. The builder sets the `selected` tab, `on_select`, `on_close`, `on_overview`, the buttons before the tabs (`leading`), right after the last one (`after_tabs`) and at the far right (`trailing`), the phone bar's `mobile_center`, the `TabStripColors`, the `TabLayout`, `reorderable` and the `TabMotion`. `TabStripColors` sets the strip's background and ink and the fill, edge and ink of the active tab and of the others; the default follows the theme. On a desktop. `[leading] [tabs…][after_tabs] clear [trailing]`. The tabs share the width between 90 widget pixels and a fifth of the screen's width and scroll sideways when even the minimum does not fit, and 7.5 % of the width between the leading and trailing buttons is always left clear, also while they scroll. A horizontal scroll measures its child with no width limit, so the tabs inside it cannot see how much room there is; two new elements solve that. `TabRow`, outside the scroll, works the tab width out of its own rect, less the clear part and the buttons after the tabs, and hands it to the `TabMotion`; `TabCells`, inside the scroll, places the tabs, the gaps of closing ones and the buttons where the motion says. Each tab is clipped by a `viewport`, so it can grow from nothing and shrink away. On a phone. `TabLayout::Mobile` is a bar with the leading buttons, the active tab's icon and title or the application's `mobile_center`, a counter that sends `on_overview` and the trailing buttons. `tab_overview( tabs )` is the grid of cards it opens, with a `title`, a `back` message and header `action`s: `build` gives the whole page, header and scrolling cards, and `cards` the grid alone for an application that lays its page out itself. The cards are placed by a third new element, `TabCards`, every one as tall as the tallest, so a dragged card can follow the finger. The motion. `TabMotion` is the only state, kept by the application, handed to every view and reported from `App::is_animating`; it is an `Arc<Mutex>` because `view` only gets `&self`, and clones share it. A tab that opens grows from nothing while the others narrow, and one that closes leaves a gap that shrinks away while the others keep their width for a second, so the close button of the tab that slides in lands under the pointer; another close starts the second again. Changes take 180 ms, ease-out. The first layout and a resize alone place the tabs without moving them. Reordering. With `reorderable( true )` each tab, and each card of the overview, carries its select message as `on_drag_start`, so it is dragged after 24 px of mouse motion or a long press, and dragging a tab selects it. The drag reaches the application, which forwards `App::on_drag_move` to `TabMotion::drag_move` and answers `App::on_drop` with the `( from, to )` that `TabMotion::drag_end` reports. The motion remembers where it drew every tab: the first move takes the tab under it and returns `false` when there is none, the tab then follows the pointer within the tabs and is drawn on top, and the others slide out of its way as its centre passes theirs, or as it enters another cell of the overview. On release it slides into its place. A strip and its overview are never shown at once and may share one motion. Anchored. `AnchoredOverlay`, which only the combo used, is public as `anchored( child, id )`: it draws its child under the widget built with that `WidgetId`, as it was laid out on the previous frame. `align` lines it up with the anchor's left edge, its right edge or its middle, `gap` sets the space under it, and the child is now kept within the rect its parent gives it, which also keeps a combo's list inside the window. List items and icons. `ListItem::ink( label, secondary )` draws the label, the subtitle and the trailing text in colours of the application's own and tints hover and press with the label's, for rows on a surface that is not the theme's. A button's icon is now fitted into its square without being stretched: a wide icon such as `actions/remove`, a 25 by 4 minus, was drawn as a block as tall as it was wide. Documentation, example and tests. `docs/widgets.md` describes `tab_strip` and `anchored` and the new `ink`. `cargo run --example tab_strip` opens three documents that can be added, closed and dragged, on a desktop strip or a phone bar with its overview. The tests cover the share of the width, its limits and the clear part, the buttons after the tabs, the drawing order of a dragged tab, the builder's defaults, the overview's rows, and in `motion.rs` the growth, the gap, the wait and its restart, a resize, and dragging a tab and a card; `anchored` and the icon fit have tests of their own. Not done. A strip that scrolls does not scroll to the active tab or follow a tab dragged towards its edge, and the overview does not scroll while a card is dragged to the edge of the screen.
156 lines
5.2 KiB
Rust
Executable File
156 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 tab_strip;
|
|
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}" )
|
|
}
|