Archived
383 lines
10 KiB
Rust
383 lines
10 KiB
Rust
/// 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)));
|
||
}
|
||
}
|