This repository has been archived on 2026-05-05. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
el-ui-retired/vessels/el-layout/src/grid.rs
T
Will Anderson f4abfe6fdc feat: rename crates/ → vessels/ + add El ports per sub-vessel
Belated rename commit for foundation/el-ui — was missed in the
workspace-wide crates→vessels pass earlier today. Same structural
intent as the rename in the other repos: 'crates' is the Rust word,
'vessel' is El's, and the directory rename is the marker that this
slot holds an El buildable unit even if its current contents are
still Rust pending port.

Plus the El ports themselves — manifest.el + src/main.el per sub-
vessel (el-aop, el-auth, el-config, el-i18n, el-identity, el-layout,
el-platform, el-publish, el-secrets, el-services, el-style, el-ui-
compiler). The ui-compiler is a stub: elc only emits C right now;
generating browser-target JS/Wasm is the biggest open language gap
and gets its own project. Until then, el-ui-compiler emits a JS
module that throws elc.backend_missing so callers fail loudly.
Cross-repo path dependencies in Cargo.toml updated to vessels/.
2026-04-30 18:18:39 -05:00

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);
}
}