// SPDX-License-Identifier: LGPL-2.1-only // Copyright (C) 2026 Liberux Labs, S. L. use crate::render::Canvas; use crate::types::{ Length, Rect }; use crate::widget::Element; /// A grid layout that wraps children into rows of a fixed column count. /// /// All cells in a row share the same height (the tallest item in that row). /// Column widths are equal, dividing the available width minus padding and spacing. /// /// Designed for app-drawer style layouts — combine with [`scroll()`](crate::widget::scroll::scroll) /// for vertically scrollable grids: /// /// ```rust,no_run /// # use std::sync::Arc; /// # use ltk::{ grid, icon_button, scroll, Element }; /// # #[ derive( Clone ) ] enum Msg { Open( usize ) } /// # fn _ex( data: Arc>, w: u32, h: u32 ) -> Element { /// scroll( /// grid( 4 ) /// .padding( 16.0 ) /// .spacing( 12.0 ) /// .push( icon_button( data.clone(), w, h ).on_press( Msg::Open( 0 ) ) ) /// .push( icon_button( data, w, h ).on_press( Msg::Open( 1 ) ) ) /// // ... /// ) /// .into() /// # } /// ``` pub struct WrapGrid { /// Child widgets laid out in row-major order. pub( crate ) children: Vec>, /// Number of columns per row. pub( crate ) columns: usize, /// Horizontal gap between cells. pub( crate ) spacing_x: Length, /// Vertical gap between rows. pub( crate ) spacing_y: Length, /// Padding on all sides. pub( crate ) padding: Length, /// When `true`, a partial last row is centred horizontally within /// the grid's content rect instead of being left-aligned. pub( crate ) centre_last_row: bool, } impl WrapGrid { /// Append a child widget to the grid. pub fn push( mut self, child: impl Into> ) -> Self { self.children.push( child.into() ); self } /// Set both horizontal and vertical gap between cells (default 8.0). pub fn spacing( mut self, s: impl Into ) -> Self { let s = s.into(); self.spacing_x = s; self.spacing_y = s; self } /// Set only the horizontal gap between cells; leaves vertical spacing untouched. pub fn spacing_x( mut self, s: impl Into ) -> Self { self.spacing_x = s.into(); self } /// Set only the vertical gap between rows; leaves horizontal spacing untouched. pub fn spacing_y( mut self, s: impl Into ) -> Self { self.spacing_y = s.into(); self } /// Set the padding on all sides (default 0.0). pub fn padding( mut self, p: impl Into ) -> Self { self.padding = p.into(); self } /// Centre a partial last row horizontally inside the content rect. /// Default is `false` (left-aligned, like other grids). pub fn centre_last_row( mut self, yes: bool ) -> Self { self.centre_last_row = yes; self } fn resolved( &self, canvas: &Canvas ) -> ( f32, f32, f32 ) { let vp = canvas.viewport_layout(); let em = Length::EM_BASE_DEFAULT; ( self.spacing_x.resolve( vp, em ), self.spacing_y.resolve( vp, em ), self.padding.resolve( vp, em ), ) } /// Compute the preferred size given an available width. pub fn preferred_size( &self, max_width: f32, canvas: &Canvas ) -> (f32, f32) { if self.children.is_empty() || self.columns == 0 { return ( max_width, 0.0 ); } let ( sx, sy, pad ) = self.resolved( canvas ); let cols = self.columns; let inner_w = (max_width - pad * 2.0).max( 0.0 ); let cell_w = (inner_w - sx * (cols as f32 - 1.0)).max( 0.0 ) / cols as f32; let row_count = (self.children.len() + cols - 1) / cols; let mut total_h = pad * 2.0; for row in 0..row_count { let start = row * cols; let end = (start + cols).min( self.children.len() ); let row_h = self.children[start..end] .iter() .map( |c| c.preferred_size( cell_w, canvas ).1 ) .fold( 0.0_f32, f32::max ); total_h += row_h; if row + 1 < row_count { total_h += sy; } } ( max_width, total_h ) } /// Compute child rects. Returns `(child_rect, index_in_children)` pairs. pub fn layout( &self, rect: Rect, canvas: &Canvas ) -> Vec<(Rect, usize)> { if self.children.is_empty() || self.columns == 0 { return Vec::new(); } let ( sx, sy, pad ) = self.resolved( canvas ); let cols = self.columns; let inner_w = (rect.width - pad * 2.0).max( 0.0 ); let cell_w = (inner_w - sx * (cols as f32 - 1.0)).max( 0.0 ) / cols as f32; let x0 = rect.x + pad; let mut y = rect.y + pad; let row_count = (self.children.len() + cols - 1) / cols; let mut out = Vec::with_capacity( self.children.len() ); for row in 0..row_count { let start = row * cols; let end = (start + cols).min( self.children.len() ); let row_h = self.children[start..end] .iter() .map( |c| c.preferred_size( cell_w, canvas ).1 ) .fold( 0.0_f32, f32::max ); let items_in_row = end - start; let row_offset = if self.centre_last_row && items_in_row < cols { let missing = (cols - items_in_row) as f32; missing * (cell_w + sx) / 2.0 } else { 0.0 }; for col in 0..items_in_row { let x = x0 + row_offset + col as f32 * (cell_w + sx); let crect = Rect { x, y, width: cell_w, height: row_h }; out.push( ( crect, start + col ) ); } y += row_h + sy; } out } pub( crate ) fn map_msg( self, f: &crate::widget::MapFn ) -> WrapGrid where U: Clone + 'static, Msg: 'static, { WrapGrid { children: self.children.into_iter().map( |c| c.map_arc( f ) ).collect(), columns: self.columns, spacing_x: self.spacing_x, spacing_y: self.spacing_y, padding: self.padding, centre_last_row: self.centre_last_row, } } } impl From> for Element { fn from( g: WrapGrid ) -> Self { Element::WrapGrid( g ) } } #[cfg(test)] mod tests { use super::*; use crate::render::Canvas; use crate::layout::spacer::spacer; fn canvas() -> Canvas { Canvas::new( 1, 1 ) } // Helper: build a grid of N spacer children with the given settings. fn spacer_grid( cols: usize, n: usize, spacing: f32, padding: f32 ) -> WrapGrid<()> { let mut g = grid( cols ).spacing( spacing ).padding( padding ); for _ in 0..n { g = g.push( spacer() ); } g } // --- preferred_size --- #[test] fn empty_grid_height_is_zero() { let g: WrapGrid<()> = grid( 4 ); let ( _, h ) = g.preferred_size( 400.0, &canvas() ); assert_eq!( h, 0.0 ); } #[test] fn preferred_width_equals_max_width() { let g = spacer_grid( 4, 8, 0.0, 0.0 ); let ( w, _ ) = g.preferred_size( 320.0, &canvas() ); assert_eq!( w, 320.0 ); } // --- layout: cell widths --- #[test] fn cell_width_no_spacing_no_padding() { // 400px / 4 cols = 100px each let g = spacer_grid( 4, 4, 0.0, 0.0 ); let c = canvas(); let rect = Rect { x: 0.0, y: 0.0, width: 400.0, height: 200.0 }; let rects = g.layout( rect, &c ); assert_eq!( rects.len(), 4 ); for ( r, _ ) in &rects { assert!( (r.width - 100.0).abs() < 0.01 ); } } #[test] fn cell_width_with_spacing() { // (400 - 3 * 10) / 4 = 370 / 4 = 92.5 let g = spacer_grid( 4, 4, 10.0, 0.0 ); let c = canvas(); let rect = Rect { x: 0.0, y: 0.0, width: 400.0, height: 200.0 }; let rects = g.layout( rect, &c ); for ( r, _ ) in &rects { assert!( (r.width - 92.5).abs() < 0.01 ); } } #[test] fn cell_width_with_padding() { // inner = 400 - 2*20 = 360; 360 / 4 = 90 let g = spacer_grid( 4, 4, 0.0, 20.0 ); let c = canvas(); let rect = Rect { x: 0.0, y: 0.0, width: 400.0, height: 200.0 }; let rects = g.layout( rect, &c ); for ( r, _ ) in &rects { assert!( (r.width - 90.0).abs() < 0.01 ); } } // --- layout: child count and indices --- #[test] fn layout_yields_one_rect_per_child() { let g = spacer_grid( 4, 7, 0.0, 0.0 ); let c = canvas(); let rect = Rect { x: 0.0, y: 0.0, width: 400.0, height: 400.0 }; let rects = g.layout( rect, &c ); assert_eq!( rects.len(), 7 ); } #[test] fn layout_indices_are_sequential() { let g = spacer_grid( 3, 5, 0.0, 0.0 ); let c = canvas(); let rect = Rect { x: 0.0, y: 0.0, width: 300.0, height: 300.0 }; let rects = g.layout( rect, &c ); let indices: Vec = rects.iter().map( |( _, i )| *i ).collect(); assert_eq!( indices, vec![ 0, 1, 2, 3, 4 ] ); } // --- layout: column x-positions --- #[test] fn column_x_positions_no_spacing() { // 300px / 3 cols = 100px each, starting at x=0 let g = spacer_grid( 3, 3, 0.0, 0.0 ); let c = canvas(); let rect = Rect { x: 0.0, y: 0.0, width: 300.0, height: 100.0 }; let rects = g.layout( rect, &c ); let xs: Vec = rects.iter().map( |( r, _ )| r.x ).collect(); assert!( (xs[0] - 0.0).abs() < 0.01 ); assert!( (xs[1] - 100.0).abs() < 0.01 ); assert!( (xs[2] - 200.0).abs() < 0.01 ); } #[test] fn column_x_positions_with_spacing() { // (300 - 2*10) / 3 = 280/3 ≈ 93.33; x[0]=0, x[1]=103.33, x[2]=206.67 let g = spacer_grid( 3, 3, 10.0, 0.0 ); let c = canvas(); let rect = Rect { x: 0.0, y: 0.0, width: 300.0, height: 100.0 }; let rects = g.layout( rect, &c ); let cell_w = 280.0_f32 / 3.0; let xs: Vec = rects.iter().map( |( r, _ )| r.x ).collect(); assert!( (xs[0] - 0.0).abs() < 0.01 ); assert!( (xs[1] - (cell_w + 10.0)).abs() < 0.01 ); assert!( (xs[2] - (2.0 * (cell_w + 10.0))).abs() < 0.01 ); } // --- layout: partial last row --- #[test] fn partial_last_row_has_correct_count() { // 7 children, 4 cols => row 0: 4, row 1: 3. let g = spacer_grid( 4, 7, 0.0, 0.0 ); let c = canvas(); let rect = Rect { x: 0.0, y: 0.0, width: 400.0, height: 400.0 }; let rects = g.layout( rect, &c ); assert_eq!( rects.len(), 7 ); for ( r, _ ) in &rects[..4] { assert!( r.y.abs() < 0.01 ); } } // --- layout: rect origin offset --- #[test] fn layout_respects_rect_origin() { let g = spacer_grid( 2, 2, 0.0, 0.0 ); let c = canvas(); let rect = Rect { x: 50.0, y: 30.0, width: 200.0, height: 100.0 }; let rects = g.layout( rect, &c ); assert!( (rects[0].0.x - 50.0).abs() < 0.01 ); assert!( (rects[0].0.y - 30.0).abs() < 0.01 ); } // --- layout: centre_last_row --- #[test] fn last_row_centred_when_partial() { // 3 children, 2 cols => row 0: 2 items, row 1: 1 item centred. // cell_w = 200/2 = 100; centred-offset = (2-1)*100/2 = 50. let g = spacer_grid( 2, 3, 0.0, 0.0 ).centre_last_row( true ); let c = canvas(); let rect = Rect { x: 0.0, y: 0.0, width: 200.0, height: 400.0 }; let rects = g.layout( rect, &c ); assert!( (rects[2].0.x - 50.0).abs() < 0.01 ); } #[test] fn centre_last_row_noop_on_full_row() { // 4 children, 2 cols => both rows full; nothing to centre. let g = spacer_grid( 2, 4, 0.0, 0.0 ).centre_last_row( true ); let c = canvas(); let rect = Rect { x: 0.0, y: 0.0, width: 200.0, height: 400.0 }; let rects = g.layout( rect, &c ); assert!( rects[2].0.x.abs() < 0.01 ); assert!( (rects[3].0.x - 100.0).abs() < 0.01 ); } #[test] fn centre_last_row_off_by_default() { // Same case as above but without the flag — last item stays at x=0. let g = spacer_grid( 2, 3, 0.0, 0.0 ); let c = canvas(); let rect = Rect { x: 0.0, y: 0.0, width: 200.0, height: 400.0 }; let rects = g.layout( rect, &c ); assert!( rects[2].0.x.abs() < 0.01 ); } } /// Create a grid layout with the given number of columns. /// /// Use [`.push()`](WrapGrid::push), [`.spacing()`](WrapGrid::spacing), and /// [`.padding()`](WrapGrid::padding) to populate and style the grid. /// /// ```rust,no_run /// # use ltk::{ button, grid, WrapGrid }; /// # #[ derive( Clone ) ] enum Msg { A } /// # fn _ex() -> WrapGrid { /// grid( 4 ).padding( 16.0 ).spacing( 8.0 ).push( button( "A" ).on_press( Msg::A ) ) /// # } /// ``` pub fn grid( columns: usize ) -> WrapGrid { WrapGrid { children: Vec::new(), columns, spacing_x: Length::px( 8.0 ), spacing_y: Length::px( 8.0 ), padding: Length::px( 0.0 ), centre_last_row: false, } }