This repository has been archived on 2026-08-20. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
el-retired/ui/vessels/el-style/src/modifier.rs
T

383 lines
10 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/// 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)));
}
}