feat: port el-ui vessels — rename crates→vessels, add El source + manifests
This commit is contained in:
@@ -0,0 +1,286 @@
|
||||
/// 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