Archived
feat: port el-ui vessels — rename crates→vessels, add El source + manifests
This commit is contained in:
@@ -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.0–1.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)));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user