ltk/widget/carousel/mod.rs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166
// SPDX-License-Identifier: LGPL-2.1-only
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>
//! 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<Msg> {
//! 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<Msg: Clone>
{
/// Child widgets in display order, left-to-right.
pub children: Vec<Element<Msg>>,
/// Optional stable identifier — used by the host as the key for its
/// drag / inertia / snap state.
pub id: Option<WidgetId>,
/// 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<Msg: Clone> Carousel<Msg>
{
pub fn push( mut self, child: impl Into<Element<Msg>> ) -> 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<U>( self, f: &super::MapFn<Msg, U> ) -> Carousel<U>
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<Msg: Clone + 'static> From<Carousel<Msg>> for Element<Msg>
{
fn from( c: Carousel<Msg> ) -> Self
{
Element::Carousel( c )
}
}
pub fn carousel<Msg: Clone>() -> Carousel<Msg>
{
Carousel
{
children: Vec::new(),
id: None,
focused_width_frac: 0.8,
gap: 16.0,
offset: 0.0,
}
}