Forge's app switcher needs two layouts the existing widget set didn't cover. The desktop grid wants a partial last row centred under the rows above (3 tiles → row 1: two, row 2: one centred) so a 7-of-9 leftover band reads balanced rather than left-aligned. The mobile variant wants a horizontal carousel where the focused tile sits centred in the viewport at a configurable fraction of its width and its neighbours peek out on the sides at a fixed gap.
Extend `WrapGrid` with `centre_last_row( bool )`. When set, layout offsets a row that has fewer than `columns` children by `(missing * (cell_w + spacing)) / 2` so it stays centred inside the content rect. Defaults to false; every existing call site continues to land tiles flush-left. Covered by three layout tests (centred partial row, full row no-op, off-by-default).
Add the `Carousel` widget at `src/widget/carousel/`. It is a pure layout primitive: `focused_width_frac` (0.05–1.0, clamped), `gap` and `offset` are owned by the caller, leaving drag / inertia / snap policy to the host so the compositor can plug in its existing touch pipeline. Each child gets a rect at `base_x + idx * (child_w + gap)` and the full viewport height; `snap_offset( viewport_w, idx )` translates index to centring offset and `focused_index( viewport_w )` rounds the current offset back to the nearest tile. Plumbed into `Element::Carousel` with the matching arms in `widget/element.rs` and walker in `draw/layout.rs`; re-exported as `ltk::{ Carousel, carousel }`. Covered by nine unit tests (layout, offset shift, snap / focus round-trip, frac clamp, child height) plus a `cargo run --example carousel` demo with Prev / Next / arrow-key navigation against an external offset state. The example is wired into the `examples` Makefile target.
Updates the widget catalogue and the `widget/mod.rs` landing comment to list the carousel under "Clipping wrappers" and to mention `centre_last_row` in the grid section.
110 lines
3.9 KiB
Rust
Executable File
110 lines
3.9 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 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>;
|