Remove all .rs bootstrap files from el-ui vessels
El SDK CI - dev / build-and-test (pull_request) Successful in 3m33s
El SDK CI - dev / build-and-test (pull_request) Successful in 3m33s
el-ui vessels are El. The Rust bootstrap implementations were added as a stopgap but don't belong here — everything should be El source. Each vessel's src/main.el and manifest.el are the source of truth.
This commit is contained in:
@@ -1,98 +0,0 @@
|
||||
/// Breakpoints — named viewport width thresholds.
|
||||
///
|
||||
/// These are provided for the rare cases where you need explicit breakpoint
|
||||
/// logic. For most cases, use the automatic layout in VStack/HStack/Grid.
|
||||
|
||||
/// Named breakpoint sizes (in dp/logical pixels).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum Breakpoint {
|
||||
/// Default — any width (the base / mobile-first value).
|
||||
Base,
|
||||
/// Small — 640dp+ (large phones, landscape phones).
|
||||
Sm,
|
||||
/// Medium — 768dp+ (tablets, small laptops).
|
||||
Md,
|
||||
/// Large — 1024dp+ (laptops, most desktops).
|
||||
Lg,
|
||||
/// Extra large — 1280dp+ (large desktops, wide monitors).
|
||||
Xl,
|
||||
}
|
||||
|
||||
impl Breakpoint {
|
||||
/// The minimum width (dp) at which this breakpoint activates.
|
||||
pub fn min_width(&self) -> f32 {
|
||||
match self {
|
||||
Breakpoint::Base => 0.0,
|
||||
Breakpoint::Sm => 640.0,
|
||||
Breakpoint::Md => 768.0,
|
||||
Breakpoint::Lg => 1024.0,
|
||||
Breakpoint::Xl => 1280.0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Classify a container width into its active breakpoint.
|
||||
pub fn for_width(width: f32) -> Self {
|
||||
if width >= 1280.0 {
|
||||
Breakpoint::Xl
|
||||
} else if width >= 1024.0 {
|
||||
Breakpoint::Lg
|
||||
} else if width >= 768.0 {
|
||||
Breakpoint::Md
|
||||
} else if width >= 640.0 {
|
||||
Breakpoint::Sm
|
||||
} else {
|
||||
Breakpoint::Base
|
||||
}
|
||||
}
|
||||
|
||||
/// True if this breakpoint is at least as wide as `other`.
|
||||
pub fn at_least(&self, other: Breakpoint) -> bool {
|
||||
self >= &other
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn breakpoint_for_small_width() {
|
||||
assert_eq!(Breakpoint::for_width(320.0), Breakpoint::Base);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn breakpoint_for_tablet_width() {
|
||||
assert_eq!(Breakpoint::for_width(800.0), Breakpoint::Md);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn breakpoint_for_desktop_width() {
|
||||
assert_eq!(Breakpoint::for_width(1440.0), Breakpoint::Xl);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn breakpoint_ordering() {
|
||||
assert!(Breakpoint::Xl > Breakpoint::Base);
|
||||
assert!(Breakpoint::Lg > Breakpoint::Sm);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn at_least() {
|
||||
assert!(Breakpoint::Lg.at_least(Breakpoint::Md));
|
||||
assert!(!Breakpoint::Sm.at_least(Breakpoint::Md));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn min_widths_ascending() {
|
||||
let bps = [
|
||||
Breakpoint::Base,
|
||||
Breakpoint::Sm,
|
||||
Breakpoint::Md,
|
||||
Breakpoint::Lg,
|
||||
Breakpoint::Xl,
|
||||
];
|
||||
for i in 1..bps.len() {
|
||||
assert!(bps[i].min_width() > bps[i - 1].min_width());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,162 +0,0 @@
|
||||
/// Layout constraints — what the parent is offering the child.
|
||||
///
|
||||
/// A parent passes a LayoutConstraints to each child during layout.
|
||||
/// The child must produce a size within these constraints.
|
||||
/// Constraints flow down; sizes flow back up.
|
||||
|
||||
/// Constraints passed from a parent to a child during layout.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct LayoutConstraints {
|
||||
/// Minimum width the child must be (dp).
|
||||
pub min_width: f32,
|
||||
/// Maximum width available to the child (dp). f32::INFINITY = unbounded.
|
||||
pub max_width: f32,
|
||||
/// Minimum height the child must be (dp).
|
||||
pub min_height: f32,
|
||||
/// Maximum height available to the child (dp). f32::INFINITY = unbounded.
|
||||
pub max_height: f32,
|
||||
}
|
||||
|
||||
impl LayoutConstraints {
|
||||
/// Unconstrained — the child can be any size.
|
||||
pub fn unbounded() -> Self {
|
||||
Self {
|
||||
min_width: 0.0,
|
||||
max_width: f32::INFINITY,
|
||||
min_height: 0.0,
|
||||
max_height: f32::INFINITY,
|
||||
}
|
||||
}
|
||||
|
||||
/// Constrained to a specific width, unconstrained height.
|
||||
pub fn with_max_width(max_width: f32) -> Self {
|
||||
Self {
|
||||
min_width: 0.0,
|
||||
max_width,
|
||||
min_height: 0.0,
|
||||
max_height: f32::INFINITY,
|
||||
}
|
||||
}
|
||||
|
||||
/// Constrained to an exact width and height.
|
||||
pub fn tight(width: f32, height: f32) -> Self {
|
||||
Self {
|
||||
min_width: width,
|
||||
max_width: width,
|
||||
min_height: height,
|
||||
max_height: height,
|
||||
}
|
||||
}
|
||||
|
||||
/// Return constraints loosened to allow any size up to the maximums.
|
||||
pub fn loosen(&self) -> Self {
|
||||
Self {
|
||||
min_width: 0.0,
|
||||
max_width: self.max_width,
|
||||
min_height: 0.0,
|
||||
max_height: self.max_height,
|
||||
}
|
||||
}
|
||||
|
||||
/// Deflate the constraints by padding amounts.
|
||||
/// Useful when a parent applies its own padding before offering space to a child.
|
||||
pub fn deflate(&self, horizontal: f32, vertical: f32) -> Self {
|
||||
Self {
|
||||
min_width: (self.min_width - horizontal).max(0.0),
|
||||
max_width: (self.max_width - horizontal).max(0.0),
|
||||
min_height: (self.min_height - vertical).max(0.0),
|
||||
max_height: (self.max_height - vertical).max(0.0),
|
||||
}
|
||||
}
|
||||
|
||||
/// Is the width dimension bounded?
|
||||
pub fn has_bounded_width(&self) -> bool {
|
||||
self.max_width.is_finite()
|
||||
}
|
||||
|
||||
/// Is the height dimension bounded?
|
||||
pub fn has_bounded_height(&self) -> bool {
|
||||
self.max_height.is_finite()
|
||||
}
|
||||
|
||||
/// Clamp a proposed size to fit within these constraints.
|
||||
pub fn clamp_size(&self, width: f32, height: f32) -> (f32, f32) {
|
||||
(
|
||||
width.clamp(self.min_width, self.max_width),
|
||||
height.clamp(self.min_height, self.max_height),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// The size a child reports back to its parent after layout.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct Size {
|
||||
pub width: f32,
|
||||
pub height: f32,
|
||||
}
|
||||
|
||||
impl Size {
|
||||
pub fn new(width: f32, height: f32) -> Self {
|
||||
Self { width, height }
|
||||
}
|
||||
|
||||
pub fn zero() -> Self {
|
||||
Self { width: 0.0, height: 0.0 }
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn unbounded_has_no_max() {
|
||||
let c = LayoutConstraints::unbounded();
|
||||
assert!(c.max_width.is_infinite());
|
||||
assert!(c.max_height.is_infinite());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tight_constraints() {
|
||||
let c = LayoutConstraints::tight(100.0, 50.0);
|
||||
assert_eq!(c.min_width, 100.0);
|
||||
assert_eq!(c.max_width, 100.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deflate_reduces_available_space() {
|
||||
let c = LayoutConstraints::tight(200.0, 100.0);
|
||||
let deflated = c.deflate(16.0, 8.0);
|
||||
assert_eq!(deflated.max_width, 184.0);
|
||||
assert_eq!(deflated.max_height, 92.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deflate_does_not_go_negative() {
|
||||
let c = LayoutConstraints::tight(10.0, 10.0);
|
||||
let deflated = c.deflate(20.0, 20.0);
|
||||
assert_eq!(deflated.max_width, 0.0);
|
||||
assert_eq!(deflated.max_height, 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clamp_size() {
|
||||
let c = LayoutConstraints {
|
||||
min_width: 50.0,
|
||||
max_width: 200.0,
|
||||
min_height: 30.0,
|
||||
max_height: 100.0,
|
||||
};
|
||||
let (w, h) = c.clamp_size(250.0, 20.0);
|
||||
assert_eq!(w, 200.0);
|
||||
assert_eq!(h, 30.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bounded_width_detection() {
|
||||
let bounded = LayoutConstraints::with_max_width(400.0);
|
||||
let unbounded = LayoutConstraints::unbounded();
|
||||
assert!(bounded.has_bounded_width());
|
||||
assert!(!unbounded.has_bounded_width());
|
||||
}
|
||||
}
|
||||
@@ -1,178 +0,0 @@
|
||||
/// FlexLayout — the underlying flex engine for VStack, HStack.
|
||||
///
|
||||
/// This is the power behind the stacks. Most developers use VStack/HStack
|
||||
/// directly; FlexLayout is available when you need full control.
|
||||
|
||||
/// Direction of the flex axis.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum FlexDirection {
|
||||
/// Children arranged top-to-bottom (VStack).
|
||||
Column,
|
||||
/// Children arranged left-to-right (HStack) — respects RTL automatically.
|
||||
Row,
|
||||
/// Like Column, but children wrap to new columns when they overflow.
|
||||
ColumnWrap,
|
||||
/// Like Row, but children wrap to new rows when they overflow.
|
||||
RowWrap,
|
||||
}
|
||||
|
||||
impl FlexDirection {
|
||||
pub fn is_horizontal(&self) -> bool {
|
||||
matches!(self, FlexDirection::Row | FlexDirection::RowWrap)
|
||||
}
|
||||
|
||||
pub fn is_vertical(&self) -> bool {
|
||||
matches!(self, FlexDirection::Column | FlexDirection::ColumnWrap)
|
||||
}
|
||||
|
||||
pub fn wraps(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
FlexDirection::ColumnWrap | FlexDirection::RowWrap
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Alignment along the cross axis.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum CrossAxisAlignment {
|
||||
/// Stretch children to fill the cross axis.
|
||||
Stretch,
|
||||
/// Align children to the start of the cross axis.
|
||||
Start,
|
||||
/// Center children on the cross axis.
|
||||
Center,
|
||||
/// Align children to the end of the cross axis.
|
||||
End,
|
||||
/// Align text baselines (horizontal stacks only).
|
||||
Baseline,
|
||||
}
|
||||
|
||||
/// Alignment along the main axis.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum MainAxisAlignment {
|
||||
/// Pack children toward the start.
|
||||
Start,
|
||||
/// Center children.
|
||||
Center,
|
||||
/// Pack children toward the end.
|
||||
End,
|
||||
/// Distribute space between children.
|
||||
SpaceBetween,
|
||||
/// Distribute space around children.
|
||||
SpaceAround,
|
||||
/// Distribute space evenly around children.
|
||||
SpaceEvenly,
|
||||
}
|
||||
|
||||
/// How much space the main axis should occupy.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum MainAxisSize {
|
||||
/// Shrink to minimum size needed.
|
||||
Min,
|
||||
/// Expand to fill all available space.
|
||||
Max,
|
||||
}
|
||||
|
||||
/// Logical horizontal alignment (RTL-aware).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum HAlign {
|
||||
/// Toward the reading-start (left in LTR, right in RTL).
|
||||
Leading,
|
||||
Center,
|
||||
/// Toward the reading-end (right in LTR, left in RTL).
|
||||
Trailing,
|
||||
}
|
||||
|
||||
/// Vertical alignment.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum VAlign {
|
||||
Top,
|
||||
Center,
|
||||
Bottom,
|
||||
Baseline,
|
||||
}
|
||||
|
||||
/// Full flex layout specification.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct FlexLayout {
|
||||
pub direction: FlexDirection,
|
||||
pub main_axis_alignment: MainAxisAlignment,
|
||||
pub cross_axis_alignment: CrossAxisAlignment,
|
||||
pub main_axis_size: MainAxisSize,
|
||||
/// Gap between children in dp.
|
||||
pub gap: u32,
|
||||
}
|
||||
|
||||
impl FlexLayout {
|
||||
/// VStack defaults (column, wrapping, leading-aligned).
|
||||
pub fn vstack(spacing: u32, wrap: bool) -> Self {
|
||||
Self {
|
||||
direction: if wrap {
|
||||
FlexDirection::ColumnWrap
|
||||
} else {
|
||||
FlexDirection::Column
|
||||
},
|
||||
main_axis_alignment: MainAxisAlignment::Start,
|
||||
cross_axis_alignment: CrossAxisAlignment::Stretch,
|
||||
main_axis_size: MainAxisSize::Max,
|
||||
gap: spacing,
|
||||
}
|
||||
}
|
||||
|
||||
/// HStack defaults (row, wrapping, center-aligned vertically).
|
||||
pub fn hstack(spacing: u32, wrap: bool) -> Self {
|
||||
Self {
|
||||
direction: if wrap {
|
||||
FlexDirection::RowWrap
|
||||
} else {
|
||||
FlexDirection::Row
|
||||
},
|
||||
main_axis_alignment: MainAxisAlignment::Start,
|
||||
cross_axis_alignment: CrossAxisAlignment::Center,
|
||||
main_axis_size: MainAxisSize::Max,
|
||||
gap: spacing,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn flex_direction_horizontal() {
|
||||
assert!(FlexDirection::Row.is_horizontal());
|
||||
assert!(FlexDirection::RowWrap.is_horizontal());
|
||||
assert!(!FlexDirection::Column.is_horizontal());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn flex_direction_vertical() {
|
||||
assert!(FlexDirection::Column.is_vertical());
|
||||
assert!(!FlexDirection::Row.is_vertical());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn flex_direction_wraps() {
|
||||
assert!(FlexDirection::RowWrap.wraps());
|
||||
assert!(!FlexDirection::Row.wraps());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vstack_layout_defaults() {
|
||||
let layout = FlexLayout::vstack(8, true);
|
||||
assert!(layout.direction.is_vertical());
|
||||
assert!(layout.direction.wraps());
|
||||
assert_eq!(layout.gap, 8);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hstack_layout_defaults() {
|
||||
let layout = FlexLayout::hstack(16, false);
|
||||
assert!(layout.direction.is_horizontal());
|
||||
assert!(!layout.direction.wraps());
|
||||
assert_eq!(layout.gap, 16);
|
||||
assert_eq!(layout.cross_axis_alignment, CrossAxisAlignment::Center);
|
||||
}
|
||||
}
|
||||
@@ -1,187 +0,0 @@
|
||||
/// 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);
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
//! el-layout — Responsive layout engine for el-ui.
|
||||
//!
|
||||
//! **Responsive by default.** VStack and HStack wrap automatically.
|
||||
//! Grid uses auto columns. You don't write breakpoints for basic layouts.
|
||||
//!
|
||||
//! ## Layout primitives
|
||||
//!
|
||||
//! - [`VStack`] — vertical stack, wraps by default
|
||||
//! - [`HStack`] — horizontal stack, wraps by default, RTL-aware
|
||||
//! - [`ZStack`] — depth stack, children overlap
|
||||
//! - [`GridLayout`] — responsive grid, auto or fixed columns
|
||||
//! - [`ScrollView`] — scrollable container
|
||||
//!
|
||||
//! ## Responsive values
|
||||
//!
|
||||
//! For the rare case where you need a value to change at a specific breakpoint:
|
||||
//! ```
|
||||
//! use el_layout::prelude::*;
|
||||
//!
|
||||
//! // Most specific value that applies cascades down to less specific
|
||||
//! let cols: Responsive<u32> = Responsive::fixed(1).md(2).lg(3);
|
||||
//! assert_eq!(*cols.resolve(Breakpoint::Sm), 1);
|
||||
//! assert_eq!(*cols.resolve(Breakpoint::Md), 2);
|
||||
//! assert_eq!(*cols.resolve(Breakpoint::Lg), 3);
|
||||
//! assert_eq!(*cols.resolve(Breakpoint::Xl), 3); // cascades from lg
|
||||
//! ```
|
||||
|
||||
#![deny(warnings)]
|
||||
|
||||
pub mod breakpoint;
|
||||
pub mod constraints;
|
||||
pub mod flex;
|
||||
pub mod grid;
|
||||
pub mod platform;
|
||||
pub mod responsive;
|
||||
pub mod scroll;
|
||||
pub mod stack;
|
||||
|
||||
pub mod prelude {
|
||||
pub use crate::breakpoint::Breakpoint;
|
||||
pub use crate::constraints::{LayoutConstraints, Size};
|
||||
pub use crate::flex::{CrossAxisAlignment, FlexDirection, FlexLayout, HAlign, MainAxisAlignment, MainAxisSize, VAlign};
|
||||
pub use crate::grid::{GridColumns, GridLayout, GridRows};
|
||||
pub use crate::platform::{PlatformFamily, PlatformSizing, SafeAreaInsets};
|
||||
pub use crate::responsive::Responsive;
|
||||
pub use crate::scroll::{ScrollAxis, ScrollIndicator, ScrollView};
|
||||
pub use crate::stack::{HStack, Spacer, VStack, ZStack};
|
||||
}
|
||||
|
||||
pub use prelude::*;
|
||||
@@ -1,197 +0,0 @@
|
||||
/// Platform-aware sizing constants.
|
||||
///
|
||||
/// Platform HIG minimum touch targets, safe area handling, and
|
||||
/// density-independent pixel conventions.
|
||||
|
||||
/// Which platform family the app is running on.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum PlatformFamily {
|
||||
/// iOS / iPadOS
|
||||
Ios,
|
||||
/// Android
|
||||
Android,
|
||||
/// macOS
|
||||
Macos,
|
||||
/// Windows
|
||||
Windows,
|
||||
/// Linux desktop
|
||||
Linux,
|
||||
/// Web browser
|
||||
Web,
|
||||
}
|
||||
|
||||
/// Platform-specific sizing constants.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PlatformSizing {
|
||||
/// Minimum touch target dimension in dp (height and width).
|
||||
pub min_touch_target: f32,
|
||||
/// Standard icon size in dp.
|
||||
pub icon_size: f32,
|
||||
/// Standard small icon size in dp.
|
||||
pub icon_size_sm: f32,
|
||||
/// Standard navigation bar height in dp.
|
||||
pub nav_bar_height: f32,
|
||||
/// Standard tab bar height in dp.
|
||||
pub tab_bar_height: f32,
|
||||
/// Standard status bar height in dp (approximate; actual is platform-provided).
|
||||
pub status_bar_height: f32,
|
||||
}
|
||||
|
||||
impl PlatformSizing {
|
||||
/// Sizing constants for a given platform.
|
||||
pub fn for_platform(platform: PlatformFamily) -> Self {
|
||||
match platform {
|
||||
PlatformFamily::Ios => Self {
|
||||
min_touch_target: 44.0, // Apple HIG
|
||||
icon_size: 24.0,
|
||||
icon_size_sm: 16.0,
|
||||
nav_bar_height: 44.0,
|
||||
tab_bar_height: 49.0,
|
||||
status_bar_height: 44.0, // approximate; varies with notch
|
||||
},
|
||||
PlatformFamily::Android => Self {
|
||||
min_touch_target: 48.0, // Material Design
|
||||
icon_size: 24.0,
|
||||
icon_size_sm: 18.0,
|
||||
nav_bar_height: 56.0,
|
||||
tab_bar_height: 56.0,
|
||||
status_bar_height: 24.0,
|
||||
},
|
||||
PlatformFamily::Macos => Self {
|
||||
min_touch_target: 44.0, // macOS HIG
|
||||
icon_size: 16.0,
|
||||
icon_size_sm: 12.0,
|
||||
nav_bar_height: 28.0,
|
||||
tab_bar_height: 36.0,
|
||||
status_bar_height: 0.0, // macOS status bar is system chrome
|
||||
},
|
||||
PlatformFamily::Windows => Self {
|
||||
min_touch_target: 44.0,
|
||||
icon_size: 16.0,
|
||||
icon_size_sm: 12.0,
|
||||
nav_bar_height: 40.0,
|
||||
tab_bar_height: 40.0,
|
||||
status_bar_height: 0.0,
|
||||
},
|
||||
PlatformFamily::Linux => Self {
|
||||
min_touch_target: 44.0,
|
||||
icon_size: 16.0,
|
||||
icon_size_sm: 12.0,
|
||||
nav_bar_height: 36.0,
|
||||
tab_bar_height: 36.0,
|
||||
status_bar_height: 0.0,
|
||||
},
|
||||
PlatformFamily::Web => Self {
|
||||
min_touch_target: 44.0, // WCAG 2.5.5 recommended
|
||||
icon_size: 20.0,
|
||||
icon_size_sm: 16.0,
|
||||
nav_bar_height: 64.0,
|
||||
tab_bar_height: 48.0,
|
||||
status_bar_height: 0.0,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Is the given width adequate for a touch target?
|
||||
pub fn is_touch_adequate(&self, width: f32, height: f32) -> bool {
|
||||
width >= self.min_touch_target && height >= self.min_touch_target
|
||||
}
|
||||
}
|
||||
|
||||
/// Safe area insets — space reserved by system chrome.
|
||||
///
|
||||
/// These are provided at runtime by the platform. The values here
|
||||
/// are conservative defaults for simulation.
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
pub struct SafeAreaInsets {
|
||||
pub top: f32,
|
||||
pub right: f32,
|
||||
pub bottom: f32,
|
||||
pub left: f32,
|
||||
}
|
||||
|
||||
impl SafeAreaInsets {
|
||||
/// No safe area insets (desktop platforms).
|
||||
pub fn none() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Typical iPhone safe area (notch at top, home indicator at bottom).
|
||||
pub fn iphone_notch() -> Self {
|
||||
Self {
|
||||
top: 44.0,
|
||||
right: 0.0,
|
||||
bottom: 34.0,
|
||||
left: 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Dynamic island (iPhone 14 Pro+).
|
||||
pub fn iphone_dynamic_island() -> Self {
|
||||
Self {
|
||||
top: 59.0,
|
||||
right: 0.0,
|
||||
bottom: 34.0,
|
||||
left: 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Total horizontal safe area.
|
||||
pub fn horizontal(&self) -> f32 {
|
||||
self.left + self.right
|
||||
}
|
||||
|
||||
/// Total vertical safe area.
|
||||
pub fn vertical(&self) -> f32 {
|
||||
self.top + self.bottom
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn ios_min_touch_target() {
|
||||
let sizing = PlatformSizing::for_platform(PlatformFamily::Ios);
|
||||
assert_eq!(sizing.min_touch_target, 44.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn android_min_touch_target() {
|
||||
let sizing = PlatformSizing::for_platform(PlatformFamily::Android);
|
||||
assert_eq!(sizing.min_touch_target, 48.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn touch_adequate_check() {
|
||||
let sizing = PlatformSizing::for_platform(PlatformFamily::Ios);
|
||||
assert!(sizing.is_touch_adequate(44.0, 44.0));
|
||||
assert!(!sizing.is_touch_adequate(40.0, 44.0));
|
||||
assert!(!sizing.is_touch_adequate(44.0, 40.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn safe_area_horizontal() {
|
||||
let sa = SafeAreaInsets {
|
||||
top: 44.0,
|
||||
right: 16.0,
|
||||
bottom: 34.0,
|
||||
left: 16.0,
|
||||
};
|
||||
assert_eq!(sa.horizontal(), 32.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn safe_area_vertical() {
|
||||
let sa = SafeAreaInsets::iphone_notch();
|
||||
assert_eq!(sa.vertical(), 78.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_safe_area_is_zero() {
|
||||
let sa = SafeAreaInsets::none();
|
||||
assert_eq!(sa.horizontal(), 0.0);
|
||||
assert_eq!(sa.vertical(), 0.0);
|
||||
}
|
||||
}
|
||||
@@ -1,154 +0,0 @@
|
||||
/// Responsive<T> — a value that varies by breakpoint.
|
||||
///
|
||||
/// The base value is always required (mobile-first). `sm`, `md`, `lg`, `xl`
|
||||
/// are all optional — if not set, the nearest smaller value is used.
|
||||
///
|
||||
/// You generally don't need Responsive<T> for layout — VStack and Grid handle
|
||||
/// reflow automatically. Use Responsive<T> when you need to vary a non-layout
|
||||
/// value (e.g. font size, column count, visibility) at specific breakpoints.
|
||||
|
||||
use crate::breakpoint::Breakpoint;
|
||||
|
||||
/// A value that varies by viewport breakpoint.
|
||||
///
|
||||
/// Follows mobile-first cascade: base < sm < md < lg < xl.
|
||||
/// If a breakpoint value is not set, it inherits from the next smaller one.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct Responsive<T: Clone> {
|
||||
/// Mobile-first base value. Always required.
|
||||
pub base: T,
|
||||
/// 640dp+. If None, uses `base`.
|
||||
pub sm: Option<T>,
|
||||
/// 768dp+. If None, uses `sm` or `base`.
|
||||
pub md: Option<T>,
|
||||
/// 1024dp+. If None, uses `md`, `sm`, or `base`.
|
||||
pub lg: Option<T>,
|
||||
/// 1280dp+. If None, uses `lg`, `md`, `sm`, or `base`.
|
||||
pub xl: Option<T>,
|
||||
}
|
||||
|
||||
impl<T: Clone> Responsive<T> {
|
||||
/// Create a responsive value with only the base (same on all screen sizes).
|
||||
pub fn fixed(value: T) -> Self {
|
||||
Self {
|
||||
base: value,
|
||||
sm: None,
|
||||
md: None,
|
||||
lg: None,
|
||||
xl: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a fully-specified responsive value.
|
||||
pub fn new(
|
||||
base: T,
|
||||
sm: Option<T>,
|
||||
md: Option<T>,
|
||||
lg: Option<T>,
|
||||
xl: Option<T>,
|
||||
) -> Self {
|
||||
Self { base, sm, md, lg, xl }
|
||||
}
|
||||
|
||||
/// Resolve to the most-specific value that applies at the given breakpoint.
|
||||
///
|
||||
/// Cascades downward: xl → lg → md → sm → base.
|
||||
pub fn resolve(&self, breakpoint: Breakpoint) -> &T {
|
||||
match breakpoint {
|
||||
Breakpoint::Xl => {
|
||||
self.xl.as_ref()
|
||||
.or(self.lg.as_ref())
|
||||
.or(self.md.as_ref())
|
||||
.or(self.sm.as_ref())
|
||||
.unwrap_or(&self.base)
|
||||
}
|
||||
Breakpoint::Lg => {
|
||||
self.lg.as_ref()
|
||||
.or(self.md.as_ref())
|
||||
.or(self.sm.as_ref())
|
||||
.unwrap_or(&self.base)
|
||||
}
|
||||
Breakpoint::Md => {
|
||||
self.md.as_ref()
|
||||
.or(self.sm.as_ref())
|
||||
.unwrap_or(&self.base)
|
||||
}
|
||||
Breakpoint::Sm => {
|
||||
self.sm.as_ref().unwrap_or(&self.base)
|
||||
}
|
||||
Breakpoint::Base => &self.base,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the sm value (builder pattern).
|
||||
pub fn sm(mut self, value: T) -> Self {
|
||||
self.sm = Some(value);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the md value.
|
||||
pub fn md(mut self, value: T) -> Self {
|
||||
self.md = Some(value);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the lg value.
|
||||
pub fn lg(mut self, value: T) -> Self {
|
||||
self.lg = Some(value);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the xl value.
|
||||
pub fn xl(mut self, value: T) -> Self {
|
||||
self.xl = Some(value);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn fixed_always_returns_base() {
|
||||
let r = Responsive::fixed(42u32);
|
||||
assert_eq!(*r.resolve(Breakpoint::Base), 42);
|
||||
assert_eq!(*r.resolve(Breakpoint::Xl), 42);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolves_most_specific() {
|
||||
let r = Responsive::fixed(1u32).sm(2).md(3).lg(4).xl(5);
|
||||
assert_eq!(*r.resolve(Breakpoint::Base), 1);
|
||||
assert_eq!(*r.resolve(Breakpoint::Sm), 2);
|
||||
assert_eq!(*r.resolve(Breakpoint::Md), 3);
|
||||
assert_eq!(*r.resolve(Breakpoint::Lg), 4);
|
||||
assert_eq!(*r.resolve(Breakpoint::Xl), 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cascades_down_when_specific_missing() {
|
||||
let r = Responsive::fixed(1u32).md(3);
|
||||
// sm not set → falls back to base
|
||||
assert_eq!(*r.resolve(Breakpoint::Sm), 1);
|
||||
// lg not set → falls back to md
|
||||
assert_eq!(*r.resolve(Breakpoint::Lg), 3);
|
||||
// xl not set → falls back to md (lg not set either)
|
||||
assert_eq!(*r.resolve(Breakpoint::Xl), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cascade_xl_to_lg_to_sm_to_base() {
|
||||
let r = Responsive::fixed("base").sm("sm");
|
||||
assert_eq!(*r.resolve(Breakpoint::Md), "sm");
|
||||
assert_eq!(*r.resolve(Breakpoint::Lg), "sm");
|
||||
assert_eq!(*r.resolve(Breakpoint::Xl), "sm");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn responsive_with_strings() {
|
||||
let r = Responsive::fixed("mobile").lg("desktop");
|
||||
assert_eq!(*r.resolve(Breakpoint::Base), "mobile");
|
||||
assert_eq!(*r.resolve(Breakpoint::Lg), "desktop");
|
||||
}
|
||||
}
|
||||
@@ -1,141 +0,0 @@
|
||||
/// ScrollView — scrollable container.
|
||||
///
|
||||
/// Wraps content that may exceed the available space. Scrolling axis can be
|
||||
/// vertical (default), horizontal, or both. On platforms with native scroll
|
||||
/// behaviors (iOS, Android), the backend translates this to a native scroll
|
||||
/// container — you get momentum, overscroll, pull-to-refresh for free.
|
||||
|
||||
use el_style::modifier::{StyleModifier, StyleSet};
|
||||
|
||||
/// Which axis (or axes) can scroll.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ScrollAxis {
|
||||
/// Vertical scrolling only (default). Most content views.
|
||||
Vertical,
|
||||
/// Horizontal scrolling only. Carousels, horizontal lists.
|
||||
Horizontal,
|
||||
/// Free scrolling in both directions. Maps, canvases.
|
||||
Both,
|
||||
}
|
||||
|
||||
/// Scroll indicator visibility.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ScrollIndicator {
|
||||
/// Show when scrolling, hide otherwise (platform default).
|
||||
Automatic,
|
||||
/// Always show.
|
||||
Always,
|
||||
/// Never show.
|
||||
Never,
|
||||
}
|
||||
|
||||
/// A scrollable container.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ScrollView {
|
||||
pub axis: ScrollAxis,
|
||||
pub indicator: ScrollIndicator,
|
||||
/// Whether the user can zoom (pinch-to-zoom). Default: false.
|
||||
pub zoomable: bool,
|
||||
/// Minimum zoom scale (only relevant when zoomable = true).
|
||||
pub min_zoom: f32,
|
||||
/// Maximum zoom scale (only relevant when zoomable = true).
|
||||
pub max_zoom: f32,
|
||||
/// Whether to clip content to the scroll view bounds. Default: true.
|
||||
pub clips_to_bounds: bool,
|
||||
pub style: StyleSet,
|
||||
}
|
||||
|
||||
impl Default for ScrollView {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
axis: ScrollAxis::Vertical,
|
||||
indicator: ScrollIndicator::Automatic,
|
||||
zoomable: false,
|
||||
min_zoom: 1.0,
|
||||
max_zoom: 3.0,
|
||||
clips_to_bounds: true,
|
||||
style: StyleSet::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ScrollView {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub fn horizontal() -> Self {
|
||||
Self {
|
||||
axis: ScrollAxis::Horizontal,
|
||||
..Self::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn both_axes() -> Self {
|
||||
Self {
|
||||
axis: ScrollAxis::Both,
|
||||
..Self::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn indicator(mut self, indicator: ScrollIndicator) -> Self {
|
||||
self.indicator = indicator;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn zoomable(mut self, min: f32, max: f32) -> Self {
|
||||
self.zoomable = true;
|
||||
self.min_zoom = min;
|
||||
self.max_zoom = max;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl StyleModifier for ScrollView {
|
||||
fn style_mut(&mut self) -> &mut StyleSet {
|
||||
&mut self.style
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn scroll_default_is_vertical() {
|
||||
let sv = ScrollView::new();
|
||||
assert_eq!(sv.axis, ScrollAxis::Vertical);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scroll_horizontal_constructor() {
|
||||
let sv = ScrollView::horizontal();
|
||||
assert_eq!(sv.axis, ScrollAxis::Horizontal);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scroll_both_axes() {
|
||||
let sv = ScrollView::both_axes();
|
||||
assert_eq!(sv.axis, ScrollAxis::Both);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scroll_zoomable() {
|
||||
let sv = ScrollView::new().zoomable(0.5, 4.0);
|
||||
assert!(sv.zoomable);
|
||||
assert_eq!(sv.min_zoom, 0.5);
|
||||
assert_eq!(sv.max_zoom, 4.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scroll_clips_by_default() {
|
||||
let sv = ScrollView::new();
|
||||
assert!(sv.clips_to_bounds);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scroll_indicator_setting() {
|
||||
let sv = ScrollView::new().indicator(ScrollIndicator::Never);
|
||||
assert_eq!(sv.indicator, ScrollIndicator::Never);
|
||||
}
|
||||
}
|
||||
@@ -1,286 +0,0 @@
|
||||
/// VStack, HStack, ZStack — the primary layout building blocks.
|
||||
///
|
||||
/// These are the component API. FlexLayout is the engine underneath.
|
||||
/// VStack and HStack wrap automatically by default — that's what makes
|
||||
/// the layout responsive without writing breakpoints.
|
||||
|
||||
use crate::flex::{CrossAxisAlignment, FlexLayout, HAlign, VAlign};
|
||||
use el_style::modifier::{StyleModifier, StyleSet};
|
||||
|
||||
/// A vertical stack — children arranged from top to bottom.
|
||||
///
|
||||
/// Wraps automatically when height is constrained (mobile-first).
|
||||
/// The default gap is 8dp. Cross-axis fills the container width.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct VStack {
|
||||
/// Space between children in dp (default: 8).
|
||||
pub spacing: u32,
|
||||
/// Horizontal alignment of children (default: Leading).
|
||||
pub alignment: HAlign,
|
||||
/// Whether to wrap to a new column when out of vertical space (default: true).
|
||||
pub wrap: bool,
|
||||
pub style: StyleSet,
|
||||
}
|
||||
|
||||
impl Default for VStack {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
spacing: 8,
|
||||
alignment: HAlign::Leading,
|
||||
wrap: true,
|
||||
style: StyleSet::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl VStack {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub fn spacing(mut self, dp: u32) -> Self {
|
||||
self.spacing = dp;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn alignment(mut self, align: HAlign) -> Self {
|
||||
self.alignment = align;
|
||||
self
|
||||
}
|
||||
|
||||
/// Enable or disable wrapping.
|
||||
pub fn wrap(mut self, wrap: bool) -> Self {
|
||||
self.wrap = wrap;
|
||||
self
|
||||
}
|
||||
|
||||
/// Convert to FlexLayout spec.
|
||||
pub fn to_flex(&self) -> FlexLayout {
|
||||
let mut flex = FlexLayout::vstack(self.spacing, self.wrap);
|
||||
flex.cross_axis_alignment = match self.alignment {
|
||||
HAlign::Leading => CrossAxisAlignment::Start,
|
||||
HAlign::Center => CrossAxisAlignment::Center,
|
||||
HAlign::Trailing => CrossAxisAlignment::End,
|
||||
};
|
||||
flex
|
||||
}
|
||||
}
|
||||
|
||||
impl StyleModifier for VStack {
|
||||
fn style_mut(&mut self) -> &mut StyleSet {
|
||||
&mut self.style
|
||||
}
|
||||
}
|
||||
|
||||
/// A horizontal stack — children arranged from leading to trailing.
|
||||
///
|
||||
/// Respects reading direction (RTL reverses automatically).
|
||||
/// Wraps to new rows by default when width is limited (mobile-first).
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct HStack {
|
||||
/// Space between children in dp (default: 8).
|
||||
pub spacing: u32,
|
||||
/// Vertical alignment of children (default: Center).
|
||||
pub alignment: VAlign,
|
||||
/// Whether to wrap to a new row when out of horizontal space (default: true).
|
||||
pub wrap: bool,
|
||||
pub style: StyleSet,
|
||||
}
|
||||
|
||||
impl Default for HStack {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
spacing: 8,
|
||||
alignment: VAlign::Center,
|
||||
wrap: true,
|
||||
style: StyleSet::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl HStack {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub fn spacing(mut self, dp: u32) -> Self {
|
||||
self.spacing = dp;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn alignment(mut self, align: VAlign) -> Self {
|
||||
self.alignment = align;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn wrap(mut self, wrap: bool) -> Self {
|
||||
self.wrap = wrap;
|
||||
self
|
||||
}
|
||||
|
||||
/// Convert to FlexLayout spec.
|
||||
pub fn to_flex(&self) -> FlexLayout {
|
||||
let mut flex = FlexLayout::hstack(self.spacing, self.wrap);
|
||||
flex.cross_axis_alignment = match self.alignment {
|
||||
VAlign::Top => CrossAxisAlignment::Start,
|
||||
VAlign::Center => CrossAxisAlignment::Center,
|
||||
VAlign::Bottom => CrossAxisAlignment::End,
|
||||
VAlign::Baseline => CrossAxisAlignment::Baseline,
|
||||
};
|
||||
flex
|
||||
}
|
||||
}
|
||||
|
||||
impl StyleModifier for HStack {
|
||||
fn style_mut(&mut self) -> &mut StyleSet {
|
||||
&mut self.style
|
||||
}
|
||||
}
|
||||
|
||||
/// A depth stack — children layered on top of each other.
|
||||
///
|
||||
/// ZStack places all children at the same position, overlapping.
|
||||
/// Later children appear on top of earlier children.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ZStack {
|
||||
pub h_align: HAlign,
|
||||
pub v_align: VAlign,
|
||||
pub style: StyleSet,
|
||||
}
|
||||
|
||||
impl Default for ZStack {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
h_align: HAlign::Center,
|
||||
v_align: VAlign::Center,
|
||||
style: StyleSet::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ZStack {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub fn h_align(mut self, align: HAlign) -> Self {
|
||||
self.h_align = align;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn v_align(mut self, align: VAlign) -> Self {
|
||||
self.v_align = align;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl StyleModifier for ZStack {
|
||||
fn style_mut(&mut self) -> &mut StyleSet {
|
||||
&mut self.style
|
||||
}
|
||||
}
|
||||
|
||||
/// A spacer that expands to fill available space in a stack.
|
||||
///
|
||||
/// In HStack: expands horizontally. In VStack: expands vertically.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct Spacer {
|
||||
/// Minimum size in dp (0 = truly flexible).
|
||||
pub min_size: u32,
|
||||
}
|
||||
|
||||
impl Spacer {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub fn min(mut self, dp: u32) -> Self {
|
||||
self.min_size = dp;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::flex::CrossAxisAlignment;
|
||||
use el_style::modifier::StyleModifier;
|
||||
use el_style::color::Color;
|
||||
|
||||
#[test]
|
||||
fn vstack_default_spacing() {
|
||||
let v = VStack::new();
|
||||
assert_eq!(v.spacing, 8);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vstack_default_wraps() {
|
||||
let v = VStack::new();
|
||||
assert!(v.wrap);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vstack_to_flex_direction() {
|
||||
let v = VStack::new();
|
||||
let flex = v.to_flex();
|
||||
assert!(flex.direction.is_vertical());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vstack_center_alignment() {
|
||||
let v = VStack::new().alignment(HAlign::Center);
|
||||
let flex = v.to_flex();
|
||||
assert_eq!(flex.cross_axis_alignment, CrossAxisAlignment::Center);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hstack_default_alignment() {
|
||||
let h = HStack::new();
|
||||
assert_eq!(h.alignment, VAlign::Center);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hstack_default_wraps() {
|
||||
let h = HStack::new();
|
||||
assert!(h.wrap);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hstack_to_flex_direction() {
|
||||
let h = HStack::new();
|
||||
let flex = h.to_flex();
|
||||
assert!(flex.direction.is_horizontal());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hstack_no_wrap() {
|
||||
let h = HStack::new().wrap(false);
|
||||
let flex = h.to_flex();
|
||||
assert!(!flex.direction.wraps());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zstack_defaults() {
|
||||
let z = ZStack::new();
|
||||
assert_eq!(z.h_align, HAlign::Center);
|
||||
assert_eq!(z.v_align, VAlign::Center);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vstack_style_modifier() {
|
||||
let v = VStack::new().background(Color::Surface);
|
||||
assert_eq!(v.style.background, Some(Color::Surface));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spacer_min_size() {
|
||||
let s = Spacer::new().min(16);
|
||||
assert_eq!(s.min_size, 16);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spacer_default_zero() {
|
||||
let s = Spacer::new();
|
||||
assert_eq!(s.min_size, 0);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user