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,
	}
}