287 lines
6.9 KiB
Rust
287 lines
6.9 KiB
Rust
/// 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);
|
|
}
|
|
}
|