188 lines
5.1 KiB
Rust
188 lines
5.1 KiB
Rust
/// 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);
|
|
}
|
|
}
|