// SPDX-License-Identifier: LGPL-2.1-only // Copyright (C) 2026 Liberux Labs, S. L. use crate::types::Rect; use crate::render::Canvas; use crate::widget::Element; /// Horizontal alignment of a child within a [`Stack`] rect. #[ derive( Debug, Clone, Copy, PartialEq ) ] pub enum HAlign { /// Align to the left edge. Start, /// Center horizontally. Center, /// Align to the right edge. End, /// Stretch to fill the full width. Fill, } /// Vertical alignment of a child within a [`Stack`] rect. #[ derive( Debug, Clone, Copy, PartialEq ) ] pub enum VAlign { /// Align to the top edge. Top, /// Center vertically. Center, /// Align to the bottom edge. Bottom, /// Stretch to fill the full height. Fill, } /// A layout that draws all its children stacked on top of each other. /// Each child can be positioned within the Stack rect via [`HAlign`]/[`VAlign`]. /// /// Useful for overlaying a foreground widget on top of a background image: /// /// ```rust,no_run /// # use std::sync::Arc; /// # use ltk::{ column, img_widget, stack, text, Element, HAlign, VAlign }; /// # #[ derive( Clone ) ] enum Msg {} /// # fn _ex( bg_rgba: Arc>, w: u32, h: u32 ) -> Element { /// stack() /// .push( img_widget( bg_rgba, w, h ) ) /// .push_aligned( column().push( text( "Bottom right" ) ), HAlign::End, VAlign::Bottom ) /// .into() /// # } /// ``` pub struct Stack { /// Children with their alignment, margin, and extra `(x, y)` translation /// applied after alignment. Drawn in order — last child is on top. The 7th /// `Option` overrides alignment/sizing with an exact rect (see /// [`push_placed`](Self::push_placed)); the 8th clips the child's draw to a /// rect (Android clipChildren — see [`push_placed_clipped`](Self::push_placed_clipped)). pub( crate ) children: Vec<( Element, HAlign, VAlign, f32, f32, f32, Option, Option )>, /// When `true`, [`preferred_size`](Self::preferred_size) reports the max of /// children's intrinsic widths and heights instead of `(max_width, tallest)`. pub( crate ) fit_content: bool, } impl Stack { /// Create an empty stack. pub fn new() -> Self { Self { children: Vec::new(), fit_content: false } } /// Enable [`Self::fit_content`]. pub fn fit_content( mut self ) -> Self { self.fit_content = true; self } /// Append a child that fills the entire Stack rect (Android FrameLayout default). pub fn push( self, e: impl Into> ) -> Self { self.push_aligned_margin( e, HAlign::Fill, VAlign::Fill, 0.0 ) } /// Append a child with explicit horizontal and vertical alignment. pub fn push_aligned( self, e: impl Into>, h_align: HAlign, v_align: VAlign, ) -> Self { self.push_aligned_margin( e, h_align, v_align, 0.0 ) } /// Append a child with alignment and a uniform margin (inset from the Stack edges). pub fn push_aligned_margin( mut self, e: impl Into>, h_align: HAlign, v_align: VAlign, margin: f32, ) -> Self { self.children.push( ( e.into(), h_align, v_align, margin, 0.0, 0.0, None, None ) ); self } /// Append a child with alignment plus an extra `(x, y)` translation in /// logical pixels. Useful when a child needs to shift outside the normal /// alignment grid without giving up the margin or alignment shorthand. /// Positive `x` / `y` move the child right / down. pub fn push_translated( mut self, e: impl Into>, h_align: HAlign, v_align: VAlign, offset_x: f32, offset_y: f32, ) -> Self { self.children.push( ( e.into(), h_align, v_align, 0.0, offset_x, offset_y, None, None ) ); self } /// Append a child placed at an exact `rect` (in the Stack's coordinate space), /// bypassing alignment and intrinsic sizing. This is what hosts a view tree /// whose geometry is computed elsewhere — e.g. Android's measure/layout pass, /// which yields absolute rects per view. pub fn push_placed( mut self, e: impl Into>, rect: Rect, ) -> Self { self.children.push( ( e.into(), HAlign::Start, VAlign::Top, 0.0, 0.0, 0.0, Some( rect ), None ) ); self } /// Like [`push_placed`](Self::push_placed) but clips the child's drawing to /// `clip` (in the Stack's coordinate space). Mirrors Android's clipChildren: /// content that overflows the clip — e.g. a scrolled list row reaching above /// the list, or an inner card past a rounded bubble — is not painted. pub fn push_placed_clipped( mut self, e: impl Into>, rect: Rect, clip: Rect, ) -> Self { self.children.push( ( e.into(), HAlign::Start, VAlign::Top, 0.0, 0.0, 0.0, Some( rect ), Some( clip ) ) ); self } pub fn preferred_size( &self, max_width: f32, canvas: &Canvas ) -> (f32, f32) { if self.fit_content { let content_w = self.children.iter() .map( |( c, .. )| match c { Element::Spacer( s ) => s.resolved_width( canvas ).unwrap_or( 0.0 ), Element::Separator( _ ) => 0.0, Element::Scroll( _ ) => 0.0, Element::ProgressBar( _ ) => 0.0, Element::Slider( _ ) => 0.0, Element::TextEdit( t ) => if t.fixed_width.is_some() { t.preferred_size( max_width, canvas ).0 } else { 0.0 }, other => other.preferred_size( max_width, canvas ).0, } ) .fold( 0.0_f32, f32::max ); let max_h = self.children.iter() .map( |( c, .. )| c.preferred_size( max_width, canvas ).1 ) .fold( 0.0_f32, f32::max ); return ( content_w.min( max_width ), max_h ); } let max_h = self.children.iter() .map( |( c, .. )| c.preferred_size( max_width, canvas ).1 ) .fold( 0.0_f32, f32::max ); ( max_width, max_h ) } /// Return `(rect, child_index)` pairs, computing each child's rect from its alignment. pub fn layout( &self, rect: Rect, canvas: &Canvas ) -> Vec<(Rect, usize)> { self.children.iter().enumerate().map( |( i, ( child, h_align, v_align, margin, ox, oy, placed, .. ) )| { if let Some( p ) = placed { return ( Rect { x: rect.x + p.x, y: rect.y + p.y, width: p.width, height: p.height }, i ); } let inner_w = ( rect.width - margin * 2.0 ).max( 0.0 ); let inner_h = ( rect.height - margin * 2.0 ).max( 0.0 ); let ( pref_w, pref_h ) = child.preferred_size( inner_w, canvas ); let ( x, width ) = match h_align { HAlign::Start => ( rect.x + margin, pref_w ), HAlign::Center => ( rect.x + ( rect.width - pref_w ) / 2.0, pref_w ), HAlign::End => ( rect.x + rect.width - pref_w - margin, pref_w ), HAlign::Fill => ( rect.x + margin, inner_w ), }; let ( y, height ) = match v_align { VAlign::Top => ( rect.y + margin, pref_h ), VAlign::Center => ( rect.y + ( rect.height - pref_h ) / 2.0, pref_h ), VAlign::Bottom => ( rect.y + rect.height - pref_h - margin, pref_h ), VAlign::Fill => ( rect.y + margin, inner_h ), }; ( Rect { x: x + ox, y: y + oy, width, height }, i ) } ).collect() } /// No-op — children are drawn directly by the event loop during layout. pub fn draw( &self, _canvas: &mut Canvas, _rect: Rect, _focused: bool ) {} pub( crate ) fn map_msg( self, f: &crate::widget::MapFn ) -> Stack where U: Clone + 'static, Msg: 'static, { Stack { children: self.children.into_iter() .map( |( child, ha, va, margin, ox, oy, placed, clip )| ( child.map_arc( f ), ha, va, margin, ox, oy, placed, clip ) ) .collect(), fit_content: self.fit_content, } } } impl Default for Stack { fn default() -> Self { Self::new() } } /// Create an empty [`Stack`]. pub fn stack() -> Stack { Stack::new() }