// 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 drives it three ways: Prev / Next buttons and arrow keys //! snap to an index, and a pointer / touch drag pans it live through //! the `App` horizontal-swipe hooks (`on_swipe_horizontal_progress` //! for follow-the-finger, `on_swipe_left` / `on_swipe_right` for the //! commit) — the same pattern crustace's homescreen pager uses. //! //! Esc quits. Arrow keys = Prev / Next. Drag horizontally to pan, or //! step tile by tile with the mouse wheel. //! //! 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, viewport_w: f32, wheel_accum: f32, } const TILE_COUNT: usize = 7; /// Horizontal padding around the carousel (the root column's 16 px per /// side) — subtracted from the surface width to get the carousel's real /// viewport, so the snap / drag math matches what the widget draws. const H_PADDING: f32 = 32.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(), viewport_w: 800.0 - H_PADDING, wheel_accum: 0.0 } } fn snap_offset_for( &self, focused: usize ) -> f32 { let stride = self.viewport_w * FOCUSED_FRAC + GAP; -( focused as f32 ) * stride } } impl App for CarouselApp { type Message = Message; fn app_id( &self ) -> &str { "net.liberux.ltk.example.carousel" } fn save_state( &self ) -> Option> { None } fn restore_state( &mut self, _state: Vec ) {} 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 · drag or wheel to pan · 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; } // Always re-snap: a drag committed at the first / last // tile leaves the strip displaced otherwise. 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 on_resize( &mut self, width: u32, _height: u32 ) { self.viewport_w = width as f32 - H_PADDING; // Keep the focused tile centred through window resizes. self.offset = self.snap_offset_for( self.focused ); } // Wheel / touchpad scroll steps the strip one tile at a time. One // wheel detent arrives as ~100-150 units (the compositor's ~10-15 // per detent times the runtime's wheel multiplier), touchpads as a // continuous stream of small deltas — so accumulate up to a detent // and step at most once per event, resetting the residue so a // coarse wheel cannot burst through several tiles. fn on_pointer_axis( &mut self, _x: f32, _y: f32, dx: f32, dy: f32 ) { const DETENT: f32 = 100.0; // Wheels report one axis at a time; take the dominant one so a // tilt-wheel or horizontal touchpad flick also pans the strip. self.wheel_accum += if dx.abs() > dy.abs() { dx } else { dy }; if self.wheel_accum.abs() < DETENT { return; } let forward = self.wheel_accum > 0.0; self.wheel_accum = 0.0; if forward { if self.focused + 1 < TILE_COUNT { self.focused += 1; } } else if self.focused > 0 { self.focused -= 1; } self.offset = self.snap_offset_for( self.focused ); } // Pointer / touch drag, the same pattern crustace's homescreen pager // uses: live progress pans the strip, a committed swipe steps the // focus, and the cancellation sample (0.0) snaps back. fn swipe_horizontal_threshold( &self ) -> f32 { 0.35 } fn on_swipe_horizontal_progress( &mut self, progress: f32 ) { // `progress` is dx / (threshold × width): ±1.0 marks the commit // threshold. A release without commit delivers one final 0.0. let dx = progress * ( self.viewport_w + H_PADDING ) * self.swipe_horizontal_threshold(); let min = self.snap_offset_for( TILE_COUNT - 1 ); self.offset = ( self.snap_offset_for( self.focused ) + dx ).clamp( min, 0.0 ); } fn on_swipe_left( &mut self ) -> Option { Some( Message::Next ) } fn on_swipe_right( &mut self ) -> Option { Some( Message::Prev ) } } fn main() { ltk::run( CarouselApp::new() ); }