diff --git a/Makefile b/Makefile index a9ab503..adcace4 100644 --- a/Makefile +++ b/Makefile @@ -44,6 +44,7 @@ examples: LTK_THEMES_DIR=themes cargo run --release --example widgets LTK_THEMES_DIR=themes cargo run --release --example pickers LTK_THEMES_DIR=themes cargo run --release --example dialog + LTK_THEMES_DIR=themes cargo run --release --example carousel clean: dh_clean diff --git a/docs/widgets.md b/docs/widgets.md index c248fc8..96cb452 100644 --- a/docs/widgets.md +++ b/docs/widgets.md @@ -22,7 +22,7 @@ patterns built from these widgets, see [`docs/cookbook.md`](./cookbook.md). - [Decoration and chrome](#decoration-and-chrome) - [`container`](#container) · [`separator`](#separator) · [`img_widget`](#img_widget) - [Clipping wrappers](#clipping-wrappers) - - [`scroll`](#scroll) · [`viewport`](#viewport) · [`flex`](#flex) + - [`scroll`](#scroll) · [`viewport`](#viewport) · [`flex`](#flex) · [`carousel`](#carousel) - [Overlays and feedback](#overlays-and-feedback) - [`spinner`](#spinner) · [`toast`](#toast) · [`tooltip`](#tooltip) · [`combo`](#combo) · [`tabs`](#tabs) · [`notebook`](#notebook) · [`dialog`](#dialog) - [Pickers](#pickers) @@ -522,6 +522,47 @@ spacer siblings. **See also**: [`spacer`](#spacer) for invisible fillers, [`column`](#column) and [`row`](#row). +### `carousel` + +Horizontal carousel: the focused child sits centred in the viewport at +`focused_width_frac` of the viewport width and its neighbours peek out +on the left / right at `gap` separation. The widget is a pure layout +primitive — the `offset` (positive shifts content right) is owned by +the caller, so drag / inertia / snap-ease live in the host (compositor, +gesture recogniser, etc.). + +**When**: mobile-style app switchers, image / story strips, any +single-focus horizontal navigation where neighbours hint at the next / +previous tile. + +```rust,no_run +# use ltk::{ button, carousel, container, Element, Color, Corners }; +# #[ derive( Clone ) ] enum Msg { Open( usize ) } +# fn _ex( offset: f32 ) -> Element { +carousel() + .focused_width_frac( 0.8 ) + .gap( 16.0 ) + .offset( offset ) + .push( container( button::( "Tile 1" ).on_press( Msg::Open( 0 ) ) ) + .background( Color::rgb( 0.95, 0.4, 0.4 ) ) + .radius( Corners::all( 12.0 ) ) ) + .push( container( button::( "Tile 2" ).on_press( Msg::Open( 1 ) ) ) + .background( Color::rgb( 0.95, 0.85, 0.3 ) ) + .radius( Corners::all( 12.0 ) ) ) +.into() +# } +``` + +Helpers on the widget translate between offsets and indices: +`snap_offset( viewport_w, idx )` returns the offset that centres tile +`idx`; `focused_index( viewport_w )` rounds the current offset to the +nearest tile. The runnable demo at +[`examples/carousel.rs`](../examples/carousel.rs) shows Prev / Next +buttons and arrow-key navigation against an external `offset` state. + +**See also**: [`scroll`](#scroll) for free vertical / horizontal panning +of arbitrary content; [`tabs`](#tabs) for a non-touch alternative. + --- ## Overlays and feedback @@ -834,6 +875,11 @@ grid( 4 ) Wrap inside [`scroll`](#scroll) when the grid may overflow. +`centre_last_row( true )` shifts a partial last row so its tiles sit +centred under the full rows above instead of left-aligned — useful for +app switchers and gallery layouts where a 7-of-9 leftover band reads +better balanced. + ### `spacer` An invisible flexible filler. Inside a column / row, absorbs leftover diff --git a/examples/carousel.rs b/examples/carousel.rs new file mode 100644 index 0000000..b1bd0a4 --- /dev/null +++ b/examples/carousel.rs @@ -0,0 +1,182 @@ +// SPDX-License-Identifier: LGPL-2.1-only +// Copyright (C) 2026 Liberux Labs, S. L. + +//! `cargo run --example carousel` +//! +//! Demonstrates the `carousel()` widget: the focused tile sits centred +//! in the viewport at `focused_width_frac` of its width, and its +//! neighbours peek out on the left / right at `gap` separation. +//! +//! The carousel widget itself is a stateless layout primitive — the +//! `offset` (positive shifts content right) is owned by the host. The +//! example mutates that offset directly when Prev / Next / arrow keys +//! change the focused index; in a real touch-driven app the host would +//! drive it from drag / inertia / snap-ease. +//! +//! Esc quits. Arrow keys = Prev / Next. +//! +//! NOTE: ltk is a Wayland layer-shell toolkit. This example needs a +//! running Wayland compositor. + +use ltk::{ + App, ButtonVariant, Color, Corners, Element, Keysym, + button, carousel, column, container, row, spacer, text, +}; + +#[ derive( Clone ) ] +enum Message +{ + Prev, + Next, + Tile( usize ), +} + +struct CarouselApp +{ + focused: usize, + offset: f32, + last_msg: String, +} + +const TILE_COUNT: usize = 7; + +/// Approximate viewport width used to translate "focused index → offset". +/// Real apps would derive this from the laid-out viewport rect; for an +/// example the constant keeps the focus / step math obvious. +const VIEWPORT_W: f32 = 800.0; +const FOCUSED_FRAC: f32 = 0.7; +const GAP: f32 = 16.0; + +const COLORS: [( f32, f32, f32 ); TILE_COUNT] = [ + ( 0.95, 0.40, 0.40 ), + ( 0.95, 0.65, 0.30 ), + ( 0.95, 0.85, 0.30 ), + ( 0.50, 0.85, 0.35 ), + ( 0.35, 0.80, 0.85 ), + ( 0.45, 0.55, 0.95 ), + ( 0.75, 0.45, 0.90 ), +]; + +impl CarouselApp +{ + fn new() -> Self + { + Self { focused: 0, offset: 0.0, last_msg: String::new() } + } + + fn snap_offset_for( &self, focused: usize ) -> f32 + { + let stride = VIEWPORT_W * FOCUSED_FRAC + GAP; + -( focused as f32 ) * stride + } +} + +impl App for CarouselApp +{ + type Message = Message; + + fn view( &self ) -> Element + { + let palette = ltk::theme_palette(); + let primary = palette.text_primary; + let secondary = palette.text_secondary; + + let mut car = carousel::() + .focused_width_frac( FOCUSED_FRAC ) + .gap( GAP ) + .offset( self.offset ); + + for i in 0..TILE_COUNT + { + let ( r, g, b ) = COLORS[i]; + let tile = container::( + column::() + .padding( 24.0 ) + .spacing( 12.0 ) + .push( text( format!( "Tile {}", i + 1 ) ).size( 28.0 ).color( Color::WHITE ).align_center() ) + .push( spacer() ) + .push( + button::( "Activate" ) + .variant( ButtonVariant::Primary ) + .on_press( Message::Tile( i ) ), + ), + ) + .background( Color::rgb( r, g, b ) ) + .radius( Corners::all( 12.0 ) ); + car = car.push( tile ); + } + + let status_line = if self.last_msg.is_empty() + { + text( "← / → cycle · click Activate to fire Message::Tile · Esc quits" ) + .size( 12.0 ) + .color( secondary ) + .align_center() + } else { + text( &self.last_msg ) + .size( 12.0 ) + .color( secondary ) + .align_center() + }; + + column::() + .padding( 16.0 ) + .spacing( 12.0 ) + .push( text( "ltk — carousel showcase" ).size( 22.0 ).color( primary ).align_center() ) + .push( + row::() + .spacing( 8.0 ) + .push( button::( "◀ Prev" ).on_press( Message::Prev ) ) + .push( spacer() ) + .push( text( format!( "Focused: {} / {}", self.focused + 1, TILE_COUNT ) ).size( 14.0 ).color( secondary ) ) + .push( spacer() ) + .push( button::( "Next ▶" ).on_press( Message::Next ) ), + ) + .push( car ) + .push( status_line ) + .into() + } + + fn update( &mut self, msg: Message ) + { + match msg + { + Message::Prev => + { + if self.focused > 0 + { + self.focused -= 1; + self.offset = self.snap_offset_for( self.focused ); + } + } + Message::Next => + { + if self.focused + 1 < TILE_COUNT + { + self.focused += 1; + self.offset = self.snap_offset_for( self.focused ); + } + } + Message::Tile( i ) => + { + self.last_msg = format!( "Pressed tile {}", i + 1 ); + } + } + } + + fn on_key( &mut self, keysym: Keysym ) -> Option + { + match keysym + { + Keysym::Escape => { std::process::exit( 0 ); } + Keysym::Left => Some( Message::Prev ), + Keysym::Right => Some( Message::Next ), + _ => None, + } + } +} + +fn main() +{ + ltk::run( CarouselApp::new() ); +} diff --git a/src/draw/layout.rs b/src/draw/layout.rs index a625efd..12360d8 100644 --- a/src/draw/layout.rs +++ b/src/draw/layout.rs @@ -67,6 +67,16 @@ pub( crate ) fn layout_and_draw( } idx } + Element::Carousel( car ) => + { + let child_rects = car.layout( rect, canvas ); + let mut idx = flat_idx; + for ( child_rect, child_i ) in child_rects + { + idx = layout_and_draw::( &car.children[child_i], canvas, child_rect, ctx, idx ); + } + idx + } Element::Flex( f ) => { // `Flex` is invisible chrome: it claimed leftover width up at diff --git a/src/layout/wrap_grid.rs b/src/layout/wrap_grid.rs index e3e20d4..ea6e41e 100644 --- a/src/layout/wrap_grid.rs +++ b/src/layout/wrap_grid.rs @@ -32,15 +32,18 @@ use crate::widget::Element; pub struct WrapGrid { /// Child widgets laid out in row-major order. - pub children: Vec>, + pub children: Vec>, /// Number of columns per row. - pub columns: usize, + pub columns: usize, /// Horizontal gap between cells (pixels). - pub spacing_x: f32, + pub spacing_x: f32, /// Vertical gap between rows (pixels). - pub spacing_y: f32, + pub spacing_y: f32, /// Padding on all sides (pixels). - pub padding: f32, + pub padding: f32, + /// When `true`, a partial last row is centred horizontally within + /// the grid's content rect instead of being left-aligned. + pub centre_last_row: bool, } impl WrapGrid @@ -81,6 +84,14 @@ impl WrapGrid self } + /// Centre a partial last row horizontally inside the content rect. + /// Default is `false` (left-aligned, like other grids). + pub fn centre_last_row( mut self, yes: bool ) -> Self + { + self.centre_last_row = yes; + self + } + /// Compute the preferred size given an available width. pub fn preferred_size( &self, max_width: f32, canvas: &Canvas ) -> (f32, f32) { @@ -133,9 +144,16 @@ impl WrapGrid .map( |c| c.preferred_size( cell_w, canvas ).1 ) .fold( 0.0_f32, f32::max ); - for col in 0..(end - start) + let items_in_row = end - start; + let row_offset = if self.centre_last_row && items_in_row < cols { - let x = x0 + col as f32 * (cell_w + self.spacing_x); + let missing = (cols - items_in_row) as f32; + missing * (cell_w + self.spacing_x) / 2.0 + } else { 0.0 }; + + for col in 0..items_in_row + { + let x = x0 + row_offset + col as f32 * (cell_w + self.spacing_x); let crect = Rect { x, y, width: cell_w, height: row_h }; out.push( ( crect, start + col ) ); } @@ -151,11 +169,12 @@ impl WrapGrid { WrapGrid { - children: self.children.into_iter().map( |c| c.map_arc( f ) ).collect(), - columns: self.columns, - spacing_x: self.spacing_x, - spacing_y: self.spacing_y, - padding: self.padding, + children: self.children.into_iter().map( |c| c.map_arc( f ) ).collect(), + columns: self.columns, + spacing_x: self.spacing_x, + spacing_y: self.spacing_y, + padding: self.padding, + centre_last_row: self.centre_last_row, } } } @@ -320,6 +339,43 @@ mod tests assert!( (rects[0].0.x - 50.0).abs() < 0.01 ); assert!( (rects[0].0.y - 30.0).abs() < 0.01 ); } + + // --- layout: centre_last_row --- + + #[test] + fn last_row_centred_when_partial() + { + // 3 children, 2 cols => row 0: 2 items, row 1: 1 item centred. + // cell_w = 200/2 = 100; centred-offset = (2-1)*100/2 = 50. + let g = spacer_grid( 2, 3, 0.0, 0.0 ).centre_last_row( true ); + let c = canvas(); + let rect = Rect { x: 0.0, y: 0.0, width: 200.0, height: 400.0 }; + let rects = g.layout( rect, &c ); + assert!( (rects[2].0.x - 50.0).abs() < 0.01 ); + } + + #[test] + fn centre_last_row_noop_on_full_row() + { + // 4 children, 2 cols => both rows full; nothing to centre. + let g = spacer_grid( 2, 4, 0.0, 0.0 ).centre_last_row( true ); + let c = canvas(); + let rect = Rect { x: 0.0, y: 0.0, width: 200.0, height: 400.0 }; + let rects = g.layout( rect, &c ); + assert!( rects[2].0.x.abs() < 0.01 ); + assert!( (rects[3].0.x - 100.0).abs() < 0.01 ); + } + + #[test] + fn centre_last_row_off_by_default() + { + // Same case as above but without the flag — last item stays at x=0. + let g = spacer_grid( 2, 3, 0.0, 0.0 ); + let c = canvas(); + let rect = Rect { x: 0.0, y: 0.0, width: 200.0, height: 400.0 }; + let rects = g.layout( rect, &c ); + assert!( rects[2].0.x.abs() < 0.01 ); + } } /// Create a grid layout with the given number of columns. @@ -338,10 +394,11 @@ pub fn grid( columns: usize ) -> WrapGrid { WrapGrid { - children: Vec::new(), + children: Vec::new(), columns, - spacing_x: 8.0, - spacing_y: 8.0, - padding: 0.0, + spacing_x: 8.0, + spacing_y: 8.0, + padding: 0.0, + centre_last_row: false, } } diff --git a/src/lib.rs b/src/lib.rs index ee3eb1b..fd6d3b1 100755 --- a/src/lib.rs +++ b/src/lib.rs @@ -244,6 +244,7 @@ pub use layout::stack::{ Stack, stack, HAlign, VAlign }; pub use layout::wrap_grid::{ WrapGrid, grid }; pub use widget::scroll::{ scroll, ScrollAxis }; pub use widget::viewport::{ Viewport, viewport }; +pub use widget::carousel::{ Carousel, carousel }; pub use app::run; pub use app::{ try_run, RunError }; pub use smithay_client_toolkit::seat::keyboard::Keysym; diff --git a/src/widget/carousel/mod.rs b/src/widget/carousel/mod.rs new file mode 100644 index 0000000..e38481d --- /dev/null +++ b/src/widget/carousel/mod.rs @@ -0,0 +1,166 @@ +// SPDX-License-Identifier: LGPL-2.1-only +// Copyright (C) 2026 Liberux Labs, S. L. + +//! Horizontal carousel: each child occupies `focused_width_frac` of the +//! viewport width, neighbours peek out to the sides. The carousel itself +//! is a pure layout primitive — the caller owns the scroll offset and +//! drives drag / inertia / snap externally. This keeps the widget side +//! stateless and lets the host compositor reuse its existing touch +//! pipeline. +//! +//! ```rust,no_run +//! # use ltk::{ carousel, container, spacer, Element }; +//! # #[ derive( Clone ) ] enum Msg { Select( usize ) } +//! # fn _ex( offset: f32 ) -> Element { +//! carousel() +//! .focused_width_frac( 0.8 ) +//! .gap( 16.0 ) +//! .offset( offset ) +//! .push( container( spacer() ) ) +//! .push( container( spacer() ) ) +//! .into() +//! # } +//! ``` + +use crate::render::Canvas; +use crate::types::{ Rect, WidgetId }; +use crate::widget::Element; + +#[cfg(test)] +mod tests; + +pub struct Carousel +{ + /// Child widgets in display order, left-to-right. + pub children: Vec>, + /// Optional stable identifier — used by the host as the key for its + /// drag / inertia / snap state. + pub id: Option, + /// Each child's width as a fraction of the viewport width. 0.8 leaves + /// 10% on each side for the neighbours to peek out. + pub focused_width_frac: f32, + /// Horizontal gap between adjacent children, in logical pixels. + pub gap: f32, + /// Logical-pixel offset applied to every child. Positive values shift + /// children to the right (revealing later tiles). The host clamps, + /// snaps and animates this value. + pub offset: f32, +} + +impl Carousel +{ + pub fn push( mut self, child: impl Into> ) -> Self + { + self.children.push( child.into() ); + self + } + + pub fn id( mut self, id: WidgetId ) -> Self + { + self.id = Some( id ); + self + } + + pub fn focused_width_frac( mut self, f: f32 ) -> Self + { + self.focused_width_frac = f.clamp( 0.05, 1.0 ); + self + } + + pub fn gap( mut self, g: f32 ) -> Self + { + self.gap = g.max( 0.0 ); + self + } + + pub fn offset( mut self, o: f32 ) -> Self + { + self.offset = o; + self + } + + pub fn preferred_size( &self, max_width: f32, canvas: &Canvas ) -> ( f32, f32 ) + { + if self.children.is_empty() { return ( max_width, 0.0 ); } + let child_w = ( max_width * self.focused_width_frac ).max( 1.0 ); + let max_h = self.children.iter() + .map( |c| c.preferred_size( child_w, canvas ).1 ) + .fold( 0.0_f32, f32::max ); + ( max_width, max_h ) + } + + /// Snap-target offset that centres `idx` in the viewport. Positive + /// values shift the carousel content right — i.e. the negation of + /// the natural "scroll x" so callers can pass it straight back into + /// [`offset()`](Self::offset). + pub fn snap_offset( &self, viewport_w: f32, idx: usize ) -> f32 + { + if self.children.is_empty() { return 0.0; } + let child_w = ( viewport_w * self.focused_width_frac ).max( 1.0 ); + let stride = child_w + self.gap; + -( idx as f32 ) * stride + } + + /// Index of the child whose centre is closest to the viewport centre, + /// given the current `offset`. Used by the host to decide which tile a + /// release should snap to and to compute the keyboard-navigation target. + pub fn focused_index( &self, viewport_w: f32 ) -> usize + { + if self.children.is_empty() { return 0; } + let child_w = ( viewport_w * self.focused_width_frac ).max( 1.0 ); + let stride = child_w + self.gap; + let raw = -self.offset / stride; + raw.round().clamp( 0.0, ( self.children.len() - 1 ) as f32 ) as usize + } + + pub fn layout( &self, rect: Rect, _canvas: &Canvas ) -> Vec<( Rect, usize )> + { + if self.children.is_empty() { return Vec::new(); } + let child_w = ( rect.width * self.focused_width_frac ).max( 1.0 ); + let base_x = rect.x + ( rect.width - child_w ) / 2.0 + self.offset; + let stride = child_w + self.gap; + + self.children.iter().enumerate().map( |( i, _ )| + { + let x = base_x + ( i as f32 ) * stride; + ( Rect { x, y: rect.y, width: child_w, height: rect.height }, i ) + }).collect() + } + + pub fn draw( &self ) {} + + pub( crate ) fn map_msg( self, f: &super::MapFn ) -> Carousel + where + U: Clone + 'static, + Msg: 'static, + { + Carousel + { + children: self.children.into_iter().map( |c| c.map_arc( f ) ).collect(), + id: self.id, + focused_width_frac: self.focused_width_frac, + gap: self.gap, + offset: self.offset, + } + } +} + +impl From> for Element +{ + fn from( c: Carousel ) -> Self + { + Element::Carousel( c ) + } +} + +pub fn carousel() -> Carousel +{ + Carousel + { + children: Vec::new(), + id: None, + focused_width_frac: 0.8, + gap: 16.0, + offset: 0.0, + } +} diff --git a/src/widget/carousel/tests.rs b/src/widget/carousel/tests.rs new file mode 100644 index 0000000..19d2893 --- /dev/null +++ b/src/widget/carousel/tests.rs @@ -0,0 +1,112 @@ +// SPDX-License-Identifier: LGPL-2.1-only +// Copyright (C) 2026 Liberux Labs, S. L. + +use super::*; +use crate::layout::spacer::spacer; +use crate::render::Canvas; + +fn canvas() -> Canvas { Canvas::new( 1, 1 ) } + +fn three_spacer_carousel() -> Carousel<()> +{ + carousel() + .focused_width_frac( 0.8 ) + .gap( 0.0 ) + .push( spacer() ) + .push( spacer() ) + .push( spacer() ) +} + +#[test] +fn empty_layout_is_empty() +{ + let c: Carousel<()> = carousel(); + let rect = Rect { x: 0.0, y: 0.0, width: 100.0, height: 100.0 }; + assert!( c.layout( rect, &canvas() ).is_empty() ); +} + +#[test] +fn first_child_centred_at_offset_zero() +{ + let c = three_spacer_carousel(); + let rect = Rect { x: 0.0, y: 0.0, width: 100.0, height: 200.0 }; + let rects = c.layout( rect, &canvas() ); + // child_w = 80, base_x = (100 - 80)/2 = 10 + assert!( ( rects[0].0.x - 10.0 ).abs() < 0.01 ); + assert!( ( rects[0].0.width - 80.0 ).abs() < 0.01 ); +} + +#[test] +fn children_strided_by_child_width_plus_gap() +{ + let c = carousel::<()>() + .focused_width_frac( 0.5 ) + .gap( 8.0 ) + .push( spacer() ) + .push( spacer() ); + let rect = Rect { x: 0.0, y: 0.0, width: 200.0, height: 100.0 }; + let rects = c.layout( rect, &canvas() ); + // child_w = 100; stride = 108; base_x = (200 - 100)/2 = 50 + assert!( ( rects[1].0.x - ( rects[0].0.x + 108.0 ) ).abs() < 0.01 ); +} + +#[test] +fn snap_offset_centres_target_index() +{ + let c = three_spacer_carousel(); + // child_w = 80, stride = 80; index 2 → offset -160 to land it centred. + assert!( ( c.snap_offset( 100.0, 2 ) - ( -160.0 ) ).abs() < 0.01 ); +} + +#[test] +fn focused_index_rounds_to_nearest_tile() +{ + let mut c = three_spacer_carousel(); + c.offset = -120.0; // halfway between index 1 (-80) and index 2 (-160) → rounds away from 0 + assert_eq!( c.focused_index( 100.0 ), 2 ); + c.offset = -100.0; // closer to index 1 + assert_eq!( c.focused_index( 100.0 ), 1 ); +} + +#[test] +fn focused_index_clamps_to_valid_range() +{ + let mut c = three_spacer_carousel(); + c.offset = 500.0; + assert_eq!( c.focused_index( 100.0 ), 0 ); + c.offset = -10_000.0; + assert_eq!( c.focused_index( 100.0 ), 2 ); +} + +#[test] +fn offset_shifts_all_children_horizontally() +{ + let c = three_spacer_carousel().offset( -25.0 ); + let rect = Rect { x: 0.0, y: 0.0, width: 100.0, height: 200.0 }; + let rects = c.layout( rect, &canvas() ); + // child_w=80, base_x_at_offset_0 = 10. With offset = -25, base_x = -15. + assert!( ( rects[0].0.x - ( -15.0 ) ).abs() < 0.01 ); + // Stride = 80, so each subsequent child is +80 of the previous. + assert!( ( rects[1].0.x - rects[0].0.x - 80.0 ).abs() < 0.01 ); +} + +#[test] +fn focused_width_frac_is_clamped_to_unit_range() +{ + let c: Carousel<()> = carousel().focused_width_frac( 5.0 ).push( spacer() ); + assert!( c.focused_width_frac <= 1.0 ); + let c2: Carousel<()> = carousel().focused_width_frac( -3.0 ).push( spacer() ); + assert!( c2.focused_width_frac > 0.0 ); +} + +#[test] +fn children_use_full_rect_height() +{ + let c = three_spacer_carousel(); + let rect = Rect { x: 0.0, y: 0.0, width: 100.0, height: 200.0 }; + let rects = c.layout( rect, &canvas() ); + for ( r, _ ) in &rects { + assert!( ( r.height - 200.0 ).abs() < 0.01 ); + assert!( r.y.abs() < 0.01 ); + } +} diff --git a/src/widget/element.rs b/src/widget/element.rs index 8587c1c..9317cbf 100644 --- a/src/widget/element.rs +++ b/src/widget/element.rs @@ -5,9 +5,9 @@ use std::sync::Arc; use crate::types::{ Point, Rect }; use crate::render::Canvas; use super::{ - anchored_overlay, button, checkbox, container, external, flex, image, - list_item, pressable, progress_bar, radio, scroll, separator, slider, - spinner, text, text_edit, toggle, viewport, vslider, window_button, + anchored_overlay, button, carousel, checkbox, container, external, flex, + image, list_item, pressable, progress_bar, radio, scroll, separator, + slider, spinner, text, text_edit, toggle, viewport, vslider, window_button, }; use super::handlers::WidgetHandlers; use super::MapFn; @@ -40,6 +40,7 @@ pub enum Element AnchoredOverlay( anchored_overlay::AnchoredOverlay ), Spinner( spinner::Spinner ), External( external::External ), + Carousel( carousel::Carousel ), } impl Element @@ -74,6 +75,7 @@ impl Element Element::AnchoredOverlay( a ) => a.child.preferred_size( max_width, canvas ), Element::Spinner( s ) => s.preferred_size( max_width ), Element::External( e ) => e.preferred_size( max_width ), + Element::Carousel( c ) => c.preferred_size( max_width, canvas ), } } @@ -116,6 +118,7 @@ impl Element Element::AnchoredOverlay( _ ) => {} Element::Spinner( s ) => s.draw( canvas, rect ), Element::External( e ) => e.draw( canvas, rect ), + Element::Carousel( _ ) => {} } } @@ -408,6 +411,7 @@ impl Element Element::AnchoredOverlay( a ) => Element::AnchoredOverlay( a.map_msg( f ) ), Element::Spinner( s ) => Element::Spinner( s ), Element::External( e ) => Element::External( e ), + Element::Carousel( c ) => Element::Carousel( c.map_msg( f ) ), } } } diff --git a/src/widget/mod.rs b/src/widget/mod.rs index c83982d..b3ef7c5 100755 --- a/src/widget/mod.rs +++ b/src/widget/mod.rs @@ -36,8 +36,10 @@ //! * **Images / decoration**: [`image::Image`], [`separator::Separator`], //! [`container::Container`]. //! * **Clipping wrappers**: [`scroll::Scroll`] (with gesture-driven -//! scrolling), [`viewport::Viewport`] (passive clip / fade), and -//! [`flex::Flex`] (treats a non-spacer child as a row filler). +//! 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). @@ -87,6 +89,7 @@ pub mod time_picker; pub mod color_picker; pub mod dialog; pub mod external; +pub mod carousel; pub mod element; pub mod handlers;