// 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; /// A horizontal layout container. /// /// Children are arranged left-to-right. Use [`Row::align_right`] to /// push the content block to the right edge of the available width. /// /// ```rust,no_run /// # use std::sync::Arc; /// # use ltk::{ icon_button, row, Element }; /// # #[ derive( Clone ) ] enum Msg { A, B } /// # fn _ex( a_rgba: Arc>, b_rgba: Arc>, w: u32, h: u32 ) -> Element { /// row() /// .spacing( 16.0 ) /// .align_right() /// .push( icon_button( a_rgba, w, h ).on_press( Msg::A ) ) /// .push( icon_button( b_rgba, w, h ).on_press( Msg::B ) ) /// .into() /// # } /// ``` pub struct Row { pub children: Vec>, pub spacing: f32, pub padding: f32, pub align_right: bool, } impl Row { pub fn new() -> Self { Self { children: Vec::new(), spacing: 8.0, padding: 0.0, align_right: false } } /// Append a child widget or layout. pub fn push( mut self, e: impl Into> ) -> Self { self.children.push( e.into() ); self } /// Set the horizontal gap between children in pixels. Default: `8.0`. pub fn spacing( mut self, s: f32 ) -> Self { self.spacing = s; self } /// Set the padding (all sides) in pixels. Default: `0.0`. pub fn padding( mut self, p: f32 ) -> Self { self.padding = p; self } /// Push the content block to the right edge of the available width. pub fn align_right( mut self ) -> Self { self.align_right = true; self } /// Return the preferred `(width, height)` given available `max_width`. pub fn preferred_size( &self, max_width: f32, canvas: &Canvas ) -> (f32, f32) { // Width contribution of every fixed (non-flex, non-flex-spacer) child. // Used to compute the residual width that wrap-style children will // actually render in, so their reported height matches the layout. let inner_w = ( max_width - self.padding * 2.0 ).max( 0.0 ); let gaps = self.spacing * self.children.len().saturating_sub( 1 ) as f32; let fixed_w: f32 = self.children.iter() .filter( |c| match c { Element::Flex( _ ) => false, Element::Spacer( s ) => s.fixed_width.is_some(), _ => true, } ) .map( |c| c.preferred_size( max_width, canvas ).0 ) .sum(); let residual = ( inner_w - fixed_w - gaps ).max( 0.0 ); let max_h: f32 = self.children.iter() .map( |c| match c { Element::Flex( _ ) => c.preferred_size( residual, canvas ).1, Element::Spacer( _ ) => c.preferred_size( max_width, canvas ).1, _ => c.preferred_size( max_width, canvas ).1, } ) .fold( 0.0_f32, f32::max ); // `align_right` and any flex / weight-only spacer child make the row // claim the full `max_width`: in those cases the row's rendered width // comes from leftover distribution (or the right-edge anchor), not // from the sum of children's preferred widths. Reporting only the // natural sum would leave the parent allocating a too-narrow rect and // the flex children would collapse to 0. let has_flex = self.children.iter().any( |c| match c { Element::Flex( _ ) => true, Element::Spacer( s ) => s.fixed_width.is_none(), _ => false, } ); let w = if self.align_right || has_flex { max_width } else { let total_w: f32 = self.children.iter() .map( |c| c.preferred_size( max_width, canvas ).0 ) .sum::() + gaps + self.padding * 2.0; total_w.min( max_width ) }; ( w, max_h + self.padding * 2.0 ) } pub fn draw( &self, _canvas: &mut Canvas, _rect: Rect, _focused: bool ) {} /// Layout children within rect and return `(rect, child_index)` pairs. /// /// Flexible [`Spacer`](crate::layout::spacer::Spacer) children claim the /// leftover horizontal space: non-spacer widgets keep their preferred /// width, the remaining width (after subtracting spacing + padding) is /// distributed between spacers in proportion to their `weight`. When no /// spacers are present the cluster is centered (or right-aligned via /// [`Row::align_right`]). pub fn layout( &self, rect: Rect, canvas: &Canvas ) -> Vec<(Rect, usize)> { let inner_h = rect.height - self.padding * 2.0; // Spacers and `Flex` wrappers report 0 width here; their real width // comes from the flex distribution below. let sizes: Vec<(f32, f32)> = self.children.iter() .map( |c| c.preferred_size( rect.width, canvas ) ) .collect(); let gaps = self.spacing * self.children.len().saturating_sub( 1 ) as f32; let fixed_w: f32 = self.children.iter().zip( sizes.iter() ) .filter( |( c, _ )| match c { // Pure flex children (`Flex` and weight-only `Spacer`) take // width from the leftover pool; everything else, including // `Spacer::width(...)`-pinned spacers, contributes to the // fixed-width tally. Element::Flex( _ ) => false, Element::Spacer( s ) => s.fixed_width.is_some(), _ => true, } ) .map( |( _, ( w, _ ) )| *w ) .sum(); let total_weight: u32 = self.children.iter() .filter_map( |c| match c { Element::Spacer( s ) if s.fixed_width.is_none() => Some( s.weight ), Element::Flex( f ) => Some( f.weight ), _ => None, } ) .sum(); let inner_w = ( rect.width - self.padding * 2.0 ).max( 0.0 ); let leftover = ( inner_w - fixed_w - gaps ).max( 0.0 ); let has_spacers = total_weight > 0; let ( start_x, flex_unit ) = if has_spacers { // Spacers and `Flex` wrappers claim the leftover; the cluster // sits flush to the left edge of the inner rect. ( rect.x + self.padding, leftover / total_weight as f32 ) } else if self.align_right { ( rect.x + rect.width - (fixed_w + gaps) - self.padding, 0.0 ) } else { ( rect.x + (rect.width - fixed_w - gaps) / 2.0, 0.0 ) }; let mut x = start_x; let mut result = Vec::with_capacity( self.children.len() ); for ( i, ( (w, h), child) ) in sizes.into_iter().zip( self.children.iter() ).enumerate() { let width = match child { Element::Spacer( s ) => match s.fixed_width { Some( fw ) => fw, None => flex_unit * s.weight as f32, }, Element::Flex( f ) => flex_unit * f.weight as f32, _ => w, }; let y = rect.y + self.padding + (inner_h - h) / 2.0; result.push( ( Rect { x, y, width, height: h }, i ) ); x += width + self.spacing; } result } pub( crate ) fn map_msg( self, f: &crate::widget::MapFn ) -> Row where U: Clone + 'static, Msg: 'static, { Row { children: self.children.into_iter().map( |c| c.map_arc( f ) ).collect(), spacing: self.spacing, padding: self.padding, align_right: self.align_right, } } } /// Create an empty row layout. pub fn row() -> Row { Row::new() } impl Default for Row { fn default() -> Self { Self::new() } } #[ cfg( test ) ] mod tests { use super::*; use crate::render::Canvas; use crate::types::Rect; fn make_canvas() -> Canvas { Canvas::new( 800, 600 ) } #[ test ] fn align_right_returns_full_max_width() { let canvas = make_canvas(); let r = row::<()>().align_right(); let ( w, _ ) = r.preferred_size( 500.0, &canvas ); assert_eq!( w, 500.0 ); } #[ test ] fn align_right_true_regardless_of_children() { let canvas = make_canvas(); let r = row::<()>().align_right().spacing( 999.0 ); let ( w, _ ) = r.preferred_size( 300.0, &canvas ); assert_eq!( w, 300.0 ); } #[ test ] fn centered_empty_row_returns_zero_width() { let canvas = make_canvas(); let r = row::<()>(); let ( w, _ ) = r.preferred_size( 500.0, &canvas ); assert_eq!( w, 0.0 ); } #[ test ] fn centered_empty_row_returns_zero_height() { let canvas = make_canvas(); let r = row::<()>().padding( 0.0 ); let ( _, h ) = r.preferred_size( 500.0, &canvas ); assert_eq!( h, 0.0 ); } #[ test ] fn padding_adds_to_height() { let canvas = make_canvas(); let r = row::<()>().padding( 8.0 ); let ( _, h ) = r.preferred_size( 500.0, &canvas ); assert_eq!( h, 16.0 ); } #[ test ] fn layout_of_empty_row_is_empty() { let canvas = make_canvas(); let r = row::<()>().align_right(); let rect = Rect { x: 0., y: 0., width: 400., height: 48. }; assert!( r.layout( rect, &canvas ).is_empty() ); } }