// 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() ); }