/// GridLayout — a responsive column grid. /// /// The auto column mode (GridColumns::Auto) automatically computes how many /// columns fit given a minimum column width. No breakpoints needed. /// Fixed column count (GridColumns::Fixed) puts exactly N columns in a row. use el_style::modifier::{StyleModifier, StyleSet}; /// How to determine the number of grid columns. #[derive(Debug, Clone, PartialEq)] pub enum GridColumns { /// A fixed number of equally-wide columns. Fixed(u32), /// As many columns as fit with each column at least `min_width` dp wide. /// This is how you get responsive grids without breakpoints. Auto { min_width: f32 }, } impl GridColumns { /// Compute the actual column count given a container width. pub fn count_for_width(&self, container_width: f32) -> u32 { match self { GridColumns::Fixed(n) => *n, GridColumns::Auto { min_width } => { if container_width <= 0.0 || *min_width <= 0.0 { return 1; } let cols = (container_width / min_width).floor() as u32; cols.max(1) } } } /// Compute the width of each column given container width and gap. pub fn column_width(&self, container_width: f32, gap: f32) -> f32 { let cols = self.count_for_width(container_width) as f32; let total_gap = gap * (cols - 1.0).max(0.0); ((container_width - total_gap) / cols).max(0.0) } } /// Row height specification. #[derive(Debug, Clone, PartialEq)] pub enum GridRows { /// All rows are the same height (dp). Fixed(f32), /// Rows take the height of their tallest item. Auto, } /// A responsive grid layout. #[derive(Debug, Clone)] pub struct GridLayout { pub columns: GridColumns, pub rows: GridRows, /// Horizontal gap between columns (dp). pub column_gap: u32, /// Vertical gap between rows (dp). pub row_gap: u32, pub style: StyleSet, } impl Default for GridLayout { fn default() -> Self { Self { columns: GridColumns::Auto { min_width: 200.0 }, rows: GridRows::Auto, column_gap: 16, row_gap: 16, style: StyleSet::default(), } } } impl GridLayout { pub fn new() -> Self { Self::default() } /// Set a fixed column count. pub fn columns_fixed(mut self, n: u32) -> Self { self.columns = GridColumns::Fixed(n); self } /// Set auto columns with a minimum column width. pub fn columns_auto(mut self, min_width: f32) -> Self { self.columns = GridColumns::Auto { min_width }; self } /// Set gap (same for both axes). pub fn gap(mut self, dp: u32) -> Self { self.column_gap = dp; self.row_gap = dp; self } /// Set column and row gaps separately. pub fn gap_xy(mut self, column_gap: u32, row_gap: u32) -> Self { self.column_gap = column_gap; self.row_gap = row_gap; self } /// How many columns are active at a given container width? pub fn active_columns(&self, container_width: f32) -> u32 { self.columns.count_for_width(container_width) } } impl StyleModifier for GridLayout { fn style_mut(&mut self) -> &mut StyleSet { &mut self.style } } #[cfg(test)] mod tests { use super::*; #[test] fn fixed_columns_always_returns_n() { let cols = GridColumns::Fixed(3); assert_eq!(cols.count_for_width(100.0), 3); assert_eq!(cols.count_for_width(2000.0), 3); } #[test] fn auto_columns_small_container() { let cols = GridColumns::Auto { min_width: 200.0 }; assert_eq!(cols.count_for_width(300.0), 1); } #[test] fn auto_columns_medium_container() { let cols = GridColumns::Auto { min_width: 200.0 }; assert_eq!(cols.count_for_width(500.0), 2); } #[test] fn auto_columns_wide_container() { let cols = GridColumns::Auto { min_width: 200.0 }; assert_eq!(cols.count_for_width(1200.0), 6); } #[test] fn auto_columns_minimum_one() { let cols = GridColumns::Auto { min_width: 500.0 }; assert_eq!(cols.count_for_width(100.0), 1); } #[test] fn column_width_with_gap() { let cols = GridColumns::Fixed(3); // 300px wide, 3 cols, 16px gap between each = 2 gaps // (300 - 32) / 3 = 268/3 ≈ 89.33 let w = cols.column_width(300.0, 16.0); assert!((w - (300.0 - 32.0) / 3.0).abs() < 0.01); } #[test] fn grid_defaults() { let grid = GridLayout::new(); assert_eq!(grid.column_gap, 16); assert_eq!(grid.row_gap, 16); } #[test] fn grid_active_columns_auto() { let grid = GridLayout::new().columns_auto(200.0); assert_eq!(grid.active_columns(600.0), 3); } #[test] fn grid_active_columns_fixed() { let grid = GridLayout::new().columns_fixed(4); assert_eq!(grid.active_columns(100.0), 4); } #[test] fn grid_gap_xy() { let grid = GridLayout::new().gap_xy(8, 24); assert_eq!(grid.column_gap, 8); assert_eq!(grid.row_gap, 24); } }