// SPDX-License-Identifier: LGPL-2.1-only
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>

//! `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 view( &self ) -> Element<Message>
	{
		let palette   = ltk::theme_palette();
		let primary   = palette.text_primary;
		let secondary = palette.text_secondary;

		let mut car = carousel::<Message>()
			.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::<Message>(
				column::<Message>()
					.padding( 24.0 )
					.spacing( 12.0 )
					.push( text( format!( "Tile {}", i + 1 ) ).size( 28.0 ).color( Color::WHITE ).align_center() )
					.push( spacer() )
					.push(
						button::<Message>( "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::<Message>()
			.padding( 16.0 )
			.spacing( 12.0 )
			.push( text( "ltk — carousel showcase" ).size( 22.0 ).color( primary ).align_center() )
			.push(
				row::<Message>()
					.spacing( 8.0 )
					.push( button::<Message>( "◀ 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::<Message>( "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<Message>
	{
		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<Message>
	{
		Some( Message::Next )
	}

	fn on_swipe_right( &mut self ) -> Option<Message>
	{
		Some( Message::Prev )
	}
}

fn main()
{
	ltk::run( CarouselApp::new() );
}
