//! Homescreen rendering — the wallpaper + paged grid + dock tree.
//!
//! Pure presentation: takes a `&Homescreen` for the wallpaper / logo
//! handles, the persisted [`Config`] for grid + dock contents, the
//! live drag-preview slot from the shell, and emits an
//! `ltk::Element<Message>`. Everything that decides "where would
//! this drop land" lives in [`super::drop`]; everything that resolves
//! "how big is one cell on this surface" lives in [`super::geometry`].
//! This file consumes both.

use rust_i18n::t;

use crate::config::{ Config, GridItem };
use crate::dock;
use crate::launcher::AppEntry;
use crate::launcher::icon_cache::IconCache;
use crate::running::RunningApps;

use super::drop::DragOrigin;
use super::geometry::{
	dock_edge_half, dock_geom_desktop, grid_dims, grid_side_pad, grid_top_reserve,
	page_indicator, GridDims, ICON_FRACTION, LABEL_GAP_FRACTION, LABEL_PX_LANDSCAPE,
	MOBILE_LABEL_FRAC, MOBILE_LABEL_GAP_FRACTION, PageIndicator,
};
use super::Homescreen;

/// Build the homescreen view: wallpaper + paged grid + dock.
///
/// `current_page` picks which mobile page is centred; `drag_dx` offsets
/// every page horizontally in logical pixels so swipe-in-progress and
/// settle animations slide smoothly. Desktop renders a single page and
/// ignores both. `on_long_press` opens the edit-mode context menu;
/// `on_drag_start` arms the drag.
pub( super ) fn build<Message: Clone + 'static>(
	hs:               &Homescreen,
	config:           &Config,
	all_apps:         &[ AppEntry ],
	icon_cache:       &mut IconCache,
	default_icon:     &( std::sync::Arc<Vec<u8>>, u32, u32 ),
	screen_width:     u32,
	screen_height:    u32,
	current_page:     usize,
	drag_dx:          f32,
	pages_on_main:    bool,
	launcher_msg:     Message,
	on_launch:        impl Fn( String ) -> Message + Clone + 'static,
	on_activate:      impl Fn( String ) -> Message + Clone + 'static,
	on_long_press:    impl Fn( DragOrigin, String ) -> Message + Clone + 'static,
	on_drag_start:    impl Fn( DragOrigin, String ) -> Message + Clone + 'static,
	on_page_select:   impl Fn( usize ) -> Message + Clone + 'static,
	dock_preview:     Option<usize>,
	running:          &RunningApps,
) -> ltk::Element<Message>
{
	let is_landscape = screen_width > screen_height;

	let use_desktop = config.display.mode.use_desktop( is_landscape );

	let dims = grid_dims( screen_width, screen_height, is_landscape );

	// Dock icon size and layout. Mobile portrait shares the grid's
	// column footprint and slots each dock icon into a column-aligned
	// cell that mirrors the grid above (5 favourites, no launcher cell).
	// Desktop / landscape keeps its compact pill — a glass card sized to
	// its contents.
	let dock_long_press = on_long_press.clone();
	let dock_long_press = move |index, app_id| dock_long_press( DragOrigin::Dock { index }, app_id );
	let dock_long_press_temp_src = on_long_press.clone();
	let dock_long_press_temp = move |app_id: String| dock_long_press_temp_src( DragOrigin::Running { app_id: app_id.clone() }, app_id );
	let dock_drag_start = on_drag_start.clone();
	let dock_drag_start = move |index, app_id| dock_drag_start( DragOrigin::Dock { index }, app_id );
	let dock_in_overlay = use_desktop;

	let dock_el: ltk::Element<Message> = if dock_in_overlay
	{
		ltk::spacer().height( 0.0 ).into()
	}
	else if use_desktop
	{
		let geom = dock_geom_desktop( screen_width, screen_height, &dims );
		dock::build_dock(
			&config.dock.items, all_apps, icon_cache, hs.logo(), default_icon,
			launcher_msg, &on_launch, &on_activate, dock_long_press, dock_long_press_temp, dock_drag_start,
			geom.icon as u32, geom.spacing, true,
			dock_preview,
			running,
		)
	}
	else
	{
		let side_pad = grid_side_pad( screen_width, screen_height, is_landscape );
		dock::build_dock_aligned(
			&config.dock.items, all_apps, icon_cache,
			&on_launch, &on_activate, dock_long_press, dock_drag_start,
			dims.cell_w as u32, dims.cols as usize, dims.cell_w, dims.spacing_x,
			side_pad,
			running,
		)
	};

	let dock_bottom_margin = dock_edge_half( screen_width, screen_height, is_landscape );
	let topbar_margin      = grid_top_reserve( screen_width, screen_height, is_landscape ) * 0.5;
	let side_pad = grid_side_pad( screen_width, screen_height, is_landscape );

	let palette = ltk::theme_palette();

	// Build the page layer: one page for desktop; every mobile page
	// stacked at its own horizontal offset so swipe-in-progress and
	// settle animations slide correctly. When `!pages_on_main` (a mobile
	// finger-drag), the pages ride sliding subsurfaces instead (see
	// `Crustace::build_subsurfaces`) and the main leaves the page region
	// to them, painting only wallpaper + dock + indicator.
	let mut pages_stack = ltk::stack::<Message>();
	if use_desktop
	{
		pages_stack = pages_stack.push( build_one_page(
			&config.desktop.items, None, all_apps, icon_cache,
			screen_width, screen_height, is_landscape, topbar_margin, side_pad, palette,
			on_launch.clone(), on_long_press.clone(), on_drag_start.clone(),
		) );
	}
	else if pages_on_main
	{
		let sw = screen_width as f32;
		for ( i, page ) in config.mobile.pages.iter().enumerate()
		{
			let offset_x = ( i as f32 - current_page as f32 ) * sw + drag_dx;
			// Pages more than one surface width away from the visible
			// frame cannot contribute any pixel to the screen. Skipping
			// them saves a full subtree layout + draw per off-screen
			// page — a real win once users start authoring five or six
			// pages.
			if offset_x.abs() > sw + 1.0 { continue; }
			let built = build_one_page(
				&page.items, Some( i ), all_apps, icon_cache,
				screen_width, screen_height, is_landscape, topbar_margin, side_pad, palette,
				on_launch.clone(), on_long_press.clone(), on_drag_start.clone(),
			);
			pages_stack = pages_stack.push_translated(
				built,
				ltk::HAlign::Fill,
				ltk::VAlign::Top,
				offset_x,
				0.0,
			);
		}
	}

	// The whole homescreen body: pages fill the top, the dock pins to
	// the bottom. Using a Stack instead of a Column lets the pages
	// render at absolute offsets without the column sequencing
	// interfering with the slide. Mobile portrait inserts a page
	// indicator strip above the dock.
	let pages_n      = config.mobile.pages.len();
	let show_indicator = !use_desktop && !is_landscape && pages_n > 1;
	let mut dock_strip = ltk::column::<Message>()
		.padding( 0.0 )
		.spacing( 0.0 )
		.align_center_x( true );
	if show_indicator
	{
		let ind = page_indicator( screen_width, screen_height );
		dock_strip = dock_strip
			.push( ltk::spacer().height( ind.margin ) )
			.push( build_page_indicator(
				pages_n, current_page, &palette, ind, on_page_select.clone(),
			) )
			.push( ltk::spacer().height( ind.margin ) );
	}
	let dock_strip = if use_desktop
	{
		dock_strip.push( dock_el )
	}
	else
	{
		dock_strip
			.push( dock_el )
			.push( ltk::spacer().height( dock_bottom_margin * 2.0 ) )
	};

	let content = ltk::stack::<Message>()
		.push( pages_stack )
		.push_aligned( dock_strip, ltk::HAlign::Center, ltk::VAlign::Bottom );

	// Background — orientation-aware. Desktop / single-page mobile keep
	// the classic `.cover()` fill against the for_size variant. Mobile
	// paged mode pans the wallpaper horizontally as pages slide: the
	// landscape image is rendered at its native aspect scaled to the
	// surface height, then translated by `-wallpaper_x`. When the image
	// is wider than `pages * screen_width` the pan is 1:1 per pixel; when
	// narrower it is prorated so page 0 reveals the leftmost surface-
	// width strip and the last page reveals the rightmost one.
	let outer = if use_desktop
	{
		let ( bg_rgba, bg_w, bg_h ) = hs.wallpaper().for_size( screen_width, screen_height );
		let bg = ltk::img_widget( bg_rgba, bg_w, bg_h ).cover();
		ltk::stack::<Message>()
			.push( bg )
			.push( content )
	}
	else
	{
		let sw             = screen_width as f32;
		let effective_page = current_page as f32 - drag_dx / sw.max( 1.0 );
		ltk::stack::<Message>()
			.push( mobile_wallpaper_bg::<Message>( hs, config, screen_width, screen_height, effective_page ) )
			.push( content )
	};

	outer.into()
}

/// Build the mobile homescreen wallpaper background, panned to `effective_page`
/// (a possibly-fractional page index). `current_page - drag_dx/sw` for the live
/// on-main render; an integer page index for a sliding page subsurface. Shared
/// so the main and the subsurfaces pan the wallpaper identically — a wide
/// wallpaper pans 1:1 with the pages, so a sliding page carries its slice with
/// it. Returns a screen-filling layer (opaque where the image covers).
fn mobile_wallpaper_bg<Message: Clone + 'static>(
	hs:             &Homescreen,
	config:         &Config,
	screen_width:   u32,
	screen_height:  u32,
	effective_page: f32,
) -> ltk::Element<Message>
{
	let ( rgba, iw, ih ) = hs.wallpaper().landscape();
	let sh      = screen_height as f32;
	let sw      = screen_width as f32;
	let iw_at_h = if ih > 0 { iw as f32 * sh / ih as f32 } else { sw };
	let pages_n = config.mobile.pages.len().max( 1 ) as f32;
	// Rubber-band reserve (see `paging::rubber_band_past`): hold back `m` px of
	// wallpaper each side so an over-pull slides real wallpaper into view.
	let rubber_reserve_max = sw * crate::paging::RUBBER_BAND_FRACTION;
	let headroom_per_side  = ( iw_at_h - pages_n * sw ).max( 0.0 ) / 2.0;
	let m                  = headroom_per_side.min( rubber_reserve_max );
	let usable             = iw_at_h - 2.0 * m;
	let offset_per_page = if usable >= pages_n * sw { sw }
		else { ( usable - sw ).max( 0.0 ) / ( pages_n - 1.0 ).max( 1.0 ) };
	let wallpaper_x = m + effective_page * offset_per_page;
	let bg: ltk::Element<Message> = ltk::img_widget( rgba, iw, ih )
		.size( iw_at_h, sh )
		.into();
	ltk::stack::<Message>()
		.push_translated( bg, ltk::HAlign::Start, ltk::VAlign::Top, -wallpaper_x, 0.0 )
		.into()
}

// =============================================================================
// Single page (shared by the on-main render and the sliding subsurfaces)
// =============================================================================

/// Build one page's grid column (topbar reserve + horizontally-padded grid).
/// Shared between the desktop path (single page, `page_idx = None`), the mobile
/// on-main pages, and the sliding page subsurfaces. `page_idx` decides which
/// [`DragOrigin`] the long-press / drag closures stamp; an empty first / desktop
/// page shows the first-run hint instead of a blank grid.
#[ allow( clippy::too_many_arguments ) ]
fn build_one_page<Message: Clone + 'static>(
	items:         &[ GridItem ],
	page_idx:      Option<usize>,
	all_apps:      &[ AppEntry ],
	icon_cache:    &mut IconCache,
	screen_width:  u32,
	screen_height: u32,
	is_landscape:  bool,
	topbar_margin: f32,
	side_pad:      f32,
	palette:       ltk::Palette,
	on_launch:     impl Fn( String ) -> Message + Clone + 'static,
	on_long_press: impl Fn( DragOrigin, String ) -> Message + Clone + 'static,
	on_drag_start: impl Fn( DragOrigin, String ) -> Message + Clone + 'static,
) -> ltk::Element<Message>
{
	let show_hint = items.is_empty() && matches!( page_idx, None | Some( 0 ) );

	let body: ltk::Element<Message> = if show_hint
	{
		empty_page_hint::<Message>( palette )
	}
	else
	{
		let page_long_press = on_long_press.clone();
		let page_drag_start = on_drag_start.clone();
		build_grid(
			items, all_apps, icon_cache,
			screen_width, screen_height, is_landscape,
			on_launch,
			move |item_index, app_id| match page_idx
			{
				Some( page ) => page_long_press( DragOrigin::GridMobile  { page, index: item_index }, app_id ),
				None         => page_long_press( DragOrigin::GridDesktop { index: item_index }, app_id ),
			},
			move |item_index, app_id| match page_idx
			{
				Some( page ) => page_drag_start( DragOrigin::GridMobile  { page, index: item_index }, app_id ),
				None         => page_drag_start( DragOrigin::GridDesktop { index: item_index }, app_id ),
			},
		)
	};

	ltk::column::<Message>()
		.padding( 0.0 )
		.spacing( 0.0 )
		.align_center_x( true )
		.push( ltk::spacer().height( topbar_margin * 2.0 ) )
		.push(
			ltk::container( body )
				.padding_h( side_pad )
		)
		.into()
}

/// Build one mobile page laid out to fill a screen-sized, transparent surface,
/// for the sliding page subsurface. The page sits in its usual top region; the
/// rest stays transparent so the main's wallpaper and dock show through. The
/// subsurface itself carries the horizontal slide offset via its position, so
/// the page renders at local x = 0 here.
#[ allow( clippy::too_many_arguments ) ]
pub( super ) fn build_page_subsurface<Message: Clone + 'static>(
	hs:            &Homescreen,
	config:        &Config,
	all_apps:      &[ AppEntry ],
	icon_cache:    &mut IconCache,
	screen_width:  u32,
	screen_height: u32,
	page_idx:      usize,
	on_launch:     impl Fn( String ) -> Message + Clone + 'static,
	on_long_press: impl Fn( DragOrigin, String ) -> Message + Clone + 'static,
	on_drag_start: impl Fn( DragOrigin, String ) -> Message + Clone + 'static,
) -> ltk::Element<Message>
{
	let is_landscape  = screen_width > screen_height;
	let topbar_margin = grid_top_reserve( screen_width, screen_height, is_landscape ) * 0.5;
	let side_pad      = grid_side_pad( screen_width, screen_height, is_landscape );
	let palette       = ltk::theme_palette();

	let items: &[ GridItem ] = config.mobile.pages
		.get( page_idx )
		.map( |p| p.items.as_slice() )
		.unwrap_or( &[] );

	let page = build_one_page(
		items, Some( page_idx ), all_apps, icon_cache,
		screen_width, screen_height, is_landscape, topbar_margin, side_pad, palette,
		on_launch, on_long_press, on_drag_start,
	);

	// Opaque: the page carries its own slice of the wallpaper (panned to this
	// page, so a wide wallpaper moves 1:1 with the icons as the subsurface
	// slides) plus the grid on top. Being opaque, the subsurface fully covers
	// the frozen main beneath it during the slide.
	ltk::stack::<Message>()
		.push( mobile_wallpaper_bg::<Message>( hs, config, screen_width, screen_height, page_idx as f32 ) )
		.push_translated( page, ltk::HAlign::Fill, ltk::VAlign::Top, 0.0, 0.0 )
		.into()
}

// =============================================================================
// Page indicator
// =============================================================================

/// Build the row of dashes / dots that signals which mobile homescreen
/// page is active. The active page renders as a 32×8 rounded pill in
/// `text_primary` (white in dark, near-black in light); inactive pages
/// render as 8×8 round dots in `text_secondary` (gray in either mode).
/// Dots and dashes are separated by `ind.gap` horizontally, with the
/// whole row centered horizontally by the dock-strip column it lives
/// in.
///
/// Each pill is wrapped in a [`ltk::Pressable`] sized to a finger-friendly
/// rect — pill height + `2 * ind.tap_v` vertically — so a tap anywhere
/// in that band emits `on_page_select( i )`. The visible pill stays at
/// its 8 px height; the tap padding is invisible.
///
/// Implementation note — each pill is a [`ltk::container`] wrapping a
/// fixed-size [`ltk::spacer`], with `radius = HEIGHT / 2.0` so the dot
/// becomes a circle and the dash becomes a pill. There is no
/// dedicated rounded-rect primitive at this layer; this composition
/// gets exactly the same pixel result through the existing
/// container's rounded-background path.
fn build_page_indicator<Message: Clone + 'static>(
	pages_n:        usize,
	current_page:   usize,
	palette:        &ltk::Palette,
	ind:            PageIndicator,
	on_page_select: impl Fn( usize ) -> Message + Clone + 'static,
) -> ltk::Element<Message>
{
	let mut row = ltk::row::<Message>()
		.padding( 0.0 )
		.spacing( ind.gap );
	for i in 0..pages_n
	{
		let is_active = i == current_page;
		let ( w, color ) = if is_active
		{
			( ind.dash_w, palette.text_primary )
		}
		else
		{
			( ind.dot_w, palette.text_secondary )
		};
		let pill = ltk::container::<Message>(
			ltk::spacer().width( w ).height( ind.height ),
		)
			.background( color )
			.radius( ind.height * 0.5 );
		// Vertical-only padding inflates the tap area without
		// pushing pills horizontally apart — the row spacing already
		// handles inter-pill separation.
		let tap_target = ltk::container( pill )
			.padding_v( ind.tap_v );
		let tap = ltk::pressable( tap_target )
			.on_press( on_page_select( i ) );
		row = row.push( tap );
	}
	row.into()
}

// =============================================================================
// Empty-state hint
// =============================================================================

/// First-run hint used when a homescreen page contains no items. Two short
/// lines centred vertically against `palette.text_secondary`, no scrim — the
/// hint sits over the wallpaper just like a populated grid would. Disappears
/// the moment the user drops a single app onto the page (the page no longer
/// reports `items.is_empty()`).
fn empty_page_hint<Message: Clone + 'static>( palette: ltk::Palette ) -> ltk::Element<Message>
{
	ltk::column::<Message>()
		.padding( 0.0 )
		.spacing( 6.0 )
		.align_center_x( true )
		.push( ltk::spacer().weight( 2 ) )
		.push(
			ltk::text( t!( "homescreen_empty_title" ).to_string() )
				.size( 18.0 )
				.color( palette.text_primary )
				.align_center(),
		)
		.push(
			ltk::text( t!( "homescreen_empty_hint" ).to_string() )
				.size( 14.0 )
				.color( palette.text_secondary )
				.align_center(),
		)
		.push( ltk::spacer().weight( 3 ) )
		.into()
}

// =============================================================================
// Grid rendering
// =============================================================================

/// Render a page of the homescreen grid into a [`ltk::grid`].
///
/// Items are placed by their explicit `[col, row]` cell. Gaps are filled
/// with a zero-width spacer sized to the cell target so row heights stay
/// stable across sparse layouts. Items whose cell falls outside the current
/// `(cols, rows)` bounds are silently dropped for phase 1 — a future pass
/// (or the loader) will reflow them onto a trailing page.
///
/// `on_long_press` and `on_drag_start` both receive the `(vec_index,
/// app_id)` of the pressed item so the caller can stamp the
/// corresponding [`super::DragOrigin`] variant. `on_long_press` opens the
/// edit-mode menu (touch hold / right-click); `on_drag_start` arms the
/// drag (touch hold / mouse left-button drag past threshold).
fn build_grid<Message: Clone + 'static>(
	items:         &[ GridItem ],
	all_apps:      &[ AppEntry ],
	icon_cache:    &mut IconCache,
	screen_width:  u32,
	screen_height: u32,
	is_landscape:  bool,
	on_launch:     impl Fn( String ) -> Message + Clone + 'static,
	on_long_press: impl Fn( usize, String ) -> Message + Clone + 'static,
	on_drag_start: impl Fn( usize, String ) -> Message + Clone + 'static,
) -> ltk::Element<Message>
{
	let GridDims { cols, rows, cell_w, cell_h, spacing_x, spacing_y } =
		grid_dims( screen_width, screen_height, is_landscape );

	// Flat index = row * cols + col. `None` → empty slot, filled with a
	// zero-width, fixed-height spacer so the wrap grid's per-row height
	// doesn't collapse when a row has only empty cells. `Some( (vec_idx,
	// item) )` carries the item's index into `items` so the long-press
	// closure can stamp a `DragOrigin::Grid*` with the precise slot.
	let slot_count = ( cols * rows ) as usize;
	let mut slots: Vec<Option<( usize, &GridItem )>> = vec![ None; slot_count ];
	for ( idx, item ) in items.iter().enumerate()
	{
		let GridItem::App { cell, .. } = item;
		let ( c, r ) = ( cell[ 0 ], cell[ 1 ] );
		if c >= cols || r >= rows { continue; }
		slots[ ( r * cols + c ) as usize ] = Some( ( idx, item ) );
	}

	// Portrait icons fill the full column width (the cell IS the icon
	// footprint, gaps live in `spacing_x`); landscape keeps the
	// `cell * ICON_FRACTION` (≈ 0.72) inset with the label inside the
	// square cell. The label is a width fraction in portrait, fixed dp
	// in landscape.
	let icon_px = if is_landscape
	{
		( cell_w * ICON_FRACTION ).round().max( 40.0 * ltk::density().max( 1.0 ) ) as u32
	}
	else
	{
		cell_w as u32
	};
	let ( label_px, label_gap ) = if is_landscape
	{
		let px = LABEL_PX_LANDSCAPE;
		( px, px * LABEL_GAP_FRACTION * ltk::density().max( 1.0 ) )
	}
	else
	{
		let px = ( screen_width as f32 * MOBILE_LABEL_FRAC ).round();
		( px, px * MOBILE_LABEL_GAP_FRACTION )
	};

	let mut grid = ltk::grid::<Message>( cols as usize )
		.spacing_x( spacing_x )
		.spacing_y( spacing_y )
		.padding( 0.0 );
	for slot in slots
	{
		let cell_el: ltk::Element<Message> = match slot
		{
			Some( ( vec_idx, GridItem::App { app_id, .. } ) ) =>
			{
				let lp_msg = on_long_press.clone()( vec_idx, app_id.clone() );
				let ds_msg = on_drag_start.clone()( vec_idx, app_id.clone() );
				match build_icon_cell(
					app_id, all_apps, icon_cache,
					icon_px, cell_h, label_px, label_gap, on_launch.clone(), Some( lp_msg ), Some( ds_msg ),
				)
				{
					Some( e ) => e,
					// Configured app is not installed on this host — leave
					// the slot visually empty rather than rendering a
					// broken placeholder. The user can tidy it up in edit
					// mode, or the entry will vanish on next save.
					None => ltk::spacer().height( cell_h ).into(),
				}
			}
			None => ltk::spacer().height( cell_h ).into(),
		};
		grid = grid.push( cell_el );
	}
	grid.into()
}

/// Build a single homescreen cell: icon button with the app's icon and a
/// small label beneath it. Returns `None` when the `app_id` does not
/// resolve to an installed entry.
///
/// The cell is wrapped in a [`ltk::stack`] with a `cell_size`-tall spacer
/// so the cell's preferred height is always exactly `cell_size`, matching
/// the empty-slot spacers. Without this, [`ltk::grid`] picks `row_h` as
/// the tallest child per row — mixed rows (icons + empty slots) get
/// `row_h = cell_size` but fully-filled rows collapse to the icon column's
/// natural height (~60% of `cell_size`), breaking
/// [`super::drop::drop_target`]'s uniform row-pitch assumption and causing
/// drops to land in the wrong cell.
fn build_icon_cell<Message: Clone + 'static>(
	app_id:     &str,
	all_apps:   &[ AppEntry ],
	icon_cache: &mut IconCache,
	icon_px:    u32,
	cell_size:  f32,
	label_px:   f32,
	label_gap:  f32,
	on_launch:  impl Fn( String ) -> Message + 'static,
	long_press: Option<Message>,
	drag_start: Option<Message>,
) -> Option<ltk::Element<Message>>
{
	let app    = all_apps.iter().find( |a| a.app_id == app_id )?;
	let palette = ltk::theme_palette();
	let msg    = on_launch( app.app_id.clone() );

	// Layer optional `on_long_press` then optional `on_drag_start` on
	// a builder that already has `on_press` set, then turn it into an
	// element. Closure type is inferred from the first call site.
	let arm_lp_ds = |b: ltk::button::Button<Message>| -> ltk::Element<Message>
	{
		let b = match long_press.clone()
		{
			Some( m ) => b.on_long_press( m ),
			None      => b,
		};
		let b = match drag_start.clone()
		{
			Some( m ) => b.on_drag_start( m ),
			None      => b,
		};
		b.into_element()
	};

	let button: ltk::Element<Message> = if let Some( ref icon_path ) = app.icon
	{
		if let Some( ( arc_rgba, w, h ) ) = icon_cache.get_or_load_sized( icon_path, icon_px )
		{
			arm_lp_ds( ltk::icon_button::<Message>( arc_rgba, w, h )
				.icon_size( icon_px as f32 )
				.variant( ltk::ButtonVariant::Tertiary )
				.focusable( false )
				.on_press( msg ) )
		}
		else
		{
			arm_lp_ds( ltk::button::<Message>( &app.name )
				.variant( ltk::ButtonVariant::Secondary )
				.focusable( false )
				.on_press( msg ) )
		}
	}
	else
	{
		arm_lp_ds( ltk::button::<Message>( &app.name )
			.variant( ltk::ButtonVariant::Secondary )
			.focusable( false )
			.on_press( msg ) )
	};

	let content: ltk::Element<Message> = ltk::column::<Message>()
		.spacing( label_gap )
		.padding( 0.0 )
		.align_center_x( true )
		.push( button )
		.push(
			ltk::text( &app.name )
				.size( label_px )
				.color( palette.text_primary )
				.align_center(),
		)
		.into();

	Some(
		ltk::stack::<Message>()
			.push( ltk::spacer().height( cell_size ) )
			.push_aligned( content, ltk::HAlign::Center, ltk::VAlign::Top )
			.into()
	)
}
