feat: port el-ui vessels — rename crates→vessels, add El source + manifests

This commit is contained in:
Will Anderson
2026-05-05 04:19:22 -05:00
parent b580a63540
commit faee6fdb25
145 changed files with 4050 additions and 12 deletions
+382
View File
@@ -0,0 +1,382 @@
/// StyleModifier trait — fluent, zero-cost style chaining.
///
/// Every el-ui component implements StyleModifier so callers can chain style
/// adjustments in a natural builder pattern. The trait is compile-time only;
/// there is no runtime allocation or dynamic dispatch.
use crate::color::Color;
use crate::radius::Radius;
use crate::shadow::Shadow;
use crate::typography::TextStyle;
/// A dimension — how wide or tall something should be.
#[derive(Debug, Clone, PartialEq)]
pub enum Dimension {
/// Exact density-independent pixels.
Fixed(u32),
/// Fill all available space from the parent.
Fill,
/// Shrink to fit the content.
Wrap,
/// Fraction of the parent's dimension (0.01.0).
Fraction(f32),
/// Minimum of two dimensions.
Min(Box<Dimension>, Box<Dimension>),
/// Maximum of two dimensions.
Max(Box<Dimension>, Box<Dimension>),
}
impl Dimension {
pub fn fraction(f: f32) -> Self {
Dimension::Fraction(f.clamp(0.0, 1.0))
}
pub fn half() -> Self {
Dimension::Fraction(0.5)
}
pub fn full() -> Self {
Dimension::Fill
}
}
/// A complete set of style properties that can be applied to a component.
///
/// All fields are optional — only the ones set by the caller are applied.
/// The platform backend reads these when rendering and maps them to native
/// style properties.
#[derive(Debug, Clone, Default)]
pub struct StyleSet {
pub padding: Option<[u32; 4]>, // top, right, bottom, left
pub margin: Option<[u32; 4]>,
pub background: Option<Color>,
pub foreground: Option<Color>,
pub font: Option<TextStyle>,
pub radius: Option<[u32; 4]>, // top-left, top-right, bottom-right, bottom-left
pub shadow: Option<Shadow>,
pub opacity: Option<f32>,
pub border_width: Option<u32>,
pub border_color: Option<Color>,
pub width: Option<Dimension>,
pub height: Option<Dimension>,
pub max_width: Option<Dimension>,
pub max_height: Option<Dimension>,
pub min_width: Option<Dimension>,
pub min_height: Option<Dimension>,
pub flex_grow: Option<f32>,
pub flex_shrink: Option<f32>,
pub z_index: Option<i32>,
pub hidden: bool,
pub clip: bool,
}
/// The core trait every styled el-ui component implements.
///
/// Returns `Self` — the methods consume and return the component for
/// fluent chaining without allocation.
pub trait StyleModifier: Sized {
/// Access the mutable StyleSet for this component.
fn style_mut(&mut self) -> &mut StyleSet;
/// Apply uniform padding on all four sides.
fn padding(mut self, value: u32) -> Self {
self.style_mut().padding = Some([value; 4]);
self
}
/// Apply horizontal (x) and vertical (y) padding.
fn padding_xy(mut self, x: u32, y: u32) -> Self {
self.style_mut().padding = Some([y, x, y, x]);
self
}
/// Apply padding individually: top, right, bottom, left.
fn padding_sides(mut self, top: u32, right: u32, bottom: u32, left: u32) -> Self {
self.style_mut().padding = Some([top, right, bottom, left]);
self
}
/// Apply uniform margin on all four sides.
fn margin(mut self, value: u32) -> Self {
self.style_mut().margin = Some([value; 4]);
self
}
/// Apply horizontal and vertical margin.
fn margin_xy(mut self, x: u32, y: u32) -> Self {
self.style_mut().margin = Some([y, x, y, x]);
self
}
/// Set the background color.
fn background(mut self, color: Color) -> Self {
self.style_mut().background = Some(color);
self
}
/// Set the foreground (text/icon) color.
fn foreground(mut self, color: Color) -> Self {
self.style_mut().foreground = Some(color);
self
}
/// Set the text/font style.
fn font(mut self, style: TextStyle) -> Self {
self.style_mut().font = Some(style);
self
}
/// Set a uniform border radius on all four corners.
fn radius(mut self, radius: Radius) -> Self {
let r = radius.dp();
self.style_mut().radius = Some([r; 4]);
self
}
/// Set the shadow/elevation.
fn shadow(mut self, elevation: Shadow) -> Self {
self.style_mut().shadow = Some(elevation);
self
}
/// Set the opacity (0.0 = invisible, 1.0 = fully visible).
fn opacity(mut self, value: f32) -> Self {
self.style_mut().opacity = Some(value.clamp(0.0, 1.0));
self
}
/// Set a border with width and color.
fn border(mut self, width: u32, color: Color) -> Self {
let s = self.style_mut();
s.border_width = Some(width);
s.border_color = Some(color);
self
}
/// Set the width.
fn width(mut self, value: Dimension) -> Self {
self.style_mut().width = Some(value);
self
}
/// Set the height.
fn height(mut self, value: Dimension) -> Self {
self.style_mut().height = Some(value);
self
}
/// Set the maximum width.
fn max_width(mut self, value: Dimension) -> Self {
self.style_mut().max_width = Some(value);
self
}
/// Set the maximum height.
fn max_height(mut self, value: Dimension) -> Self {
self.style_mut().max_height = Some(value);
self
}
/// Set the minimum width.
fn min_width(mut self, value: Dimension) -> Self {
self.style_mut().min_width = Some(value);
self
}
/// Set the minimum height.
fn min_height(mut self, value: Dimension) -> Self {
self.style_mut().min_height = Some(value);
self
}
/// How much this component grows to fill available space (flex-grow).
fn grow(mut self, factor: f32) -> Self {
self.style_mut().flex_grow = Some(factor);
self
}
/// Z-index for layering.
fn z_index(mut self, z: i32) -> Self {
self.style_mut().z_index = Some(z);
self
}
/// Clip content that overflows the component's bounds.
fn clip(mut self) -> Self {
self.style_mut().clip = true;
self
}
/// Hide this component (still occupies space in layout).
fn hidden(mut self, value: bool) -> Self {
self.style_mut().hidden = value;
self
}
}
/// Pre-composed style variants for common component types.
/// Button style variants.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ButtonVariant {
/// Filled primary-color button (the main CTA).
Primary,
/// Filled secondary-color button.
Secondary,
/// Destructive action (red).
Destructive,
/// Text-only, no fill or border.
Ghost,
/// Inline hyperlink style.
Link,
}
/// Card style variants.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CardStyle {
/// Card with a drop shadow.
Elevated,
/// Card with an outline border, no shadow.
Outlined,
/// Card with a filled background color, no shadow.
Filled,
}
/// Text input style variants.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum InputStyle {
/// Standard input (platform default).
Default,
/// Outlined/bordered input.
Outlined,
/// Filled background input.
Filled,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::color::Color;
use crate::radius::Radius;
use crate::shadow::Shadow;
use crate::typography::TextStyle;
/// A minimal test component that implements StyleModifier.
#[derive(Default)]
struct TestWidget {
style: StyleSet,
}
impl StyleModifier for TestWidget {
fn style_mut(&mut self) -> &mut StyleSet {
&mut self.style
}
}
#[test]
fn padding_sets_all_sides() {
let w = TestWidget::default().padding(16);
assert_eq!(w.style.padding, Some([16; 4]));
}
#[test]
fn padding_xy_sets_correctly() {
let w = TestWidget::default().padding_xy(8, 16);
// [top, right, bottom, left] = [y, x, y, x]
assert_eq!(w.style.padding, Some([16, 8, 16, 8]));
}
#[test]
fn background_stored() {
let w = TestWidget::default().background(Color::Primary);
assert_eq!(w.style.background, Some(Color::Primary));
}
#[test]
fn foreground_stored() {
let w = TestWidget::default().foreground(Color::OnPrimary);
assert_eq!(w.style.foreground, Some(Color::OnPrimary));
}
#[test]
fn font_stored() {
let w = TestWidget::default().font(TextStyle::Body);
assert_eq!(w.style.font, Some(TextStyle::Body));
}
#[test]
fn radius_stored() {
let w = TestWidget::default().radius(Radius::Md);
assert_eq!(w.style.radius, Some([8; 4]));
}
#[test]
fn shadow_stored() {
let w = TestWidget::default().shadow(Shadow::Md);
assert_eq!(w.style.shadow, Some(Shadow::Md));
}
#[test]
fn opacity_clamped() {
let w = TestWidget::default().opacity(2.5);
assert_eq!(w.style.opacity, Some(1.0));
}
#[test]
fn opacity_valid() {
let w = TestWidget::default().opacity(0.5);
assert!((w.style.opacity.unwrap() - 0.5).abs() < 0.001);
}
#[test]
fn border_stored() {
let w = TestWidget::default().border(2, Color::Outline);
assert_eq!(w.style.border_width, Some(2));
assert_eq!(w.style.border_color, Some(Color::Outline));
}
#[test]
fn chain_multiple_modifiers() {
let w = TestWidget::default()
.padding(16)
.background(Color::Surface)
.radius(Radius::Lg)
.shadow(Shadow::Sm)
.opacity(0.9);
assert!(w.style.padding.is_some());
assert!(w.style.background.is_some());
assert!(w.style.radius.is_some());
assert!(w.style.shadow.is_some());
assert!(w.style.opacity.is_some());
}
#[test]
fn hidden_flag() {
let w = TestWidget::default().hidden(true);
assert!(w.style.hidden);
}
#[test]
fn clip_flag() {
let w = TestWidget::default().clip();
assert!(w.style.clip);
}
#[test]
fn dimension_fraction_clamped() {
let d = Dimension::fraction(1.5);
assert_eq!(d, Dimension::Fraction(1.0));
}
#[test]
fn width_stored() {
let w = TestWidget::default().width(Dimension::Fill);
assert_eq!(w.style.width, Some(Dimension::Fill));
}
#[test]
fn max_width_stored() {
let w = TestWidget::default().max_width(Dimension::Fixed(600));
assert_eq!(w.style.max_width, Some(Dimension::Fixed(600)));
}
}