Skip to main content

esoc_gfx/
style.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2//! Stroke, fill, and text styling.
3
4use crate::color::Color;
5
6/// A dash pattern for stroked paths.
7#[derive(Clone, Debug, PartialEq)]
8pub struct DashPattern {
9    /// Alternating dash/gap lengths.
10    pub dashes: Vec<f64>,
11    /// Offset into the pattern.
12    pub offset: f64,
13}
14
15impl DashPattern {
16    /// Create a new dash pattern.
17    pub fn new(dashes: &[f64]) -> Self {
18        Self {
19            dashes: dashes.to_vec(),
20            offset: 0.0,
21        }
22    }
23
24    /// Format as an SVG `stroke-dasharray` attribute value.
25    pub fn to_svg_string(&self) -> String {
26        self.dashes
27            .iter()
28            .map(|d| format!("{d}"))
29            .collect::<Vec<_>>()
30            .join(",")
31    }
32}
33
34/// Stroke style for lines and shapes.
35#[derive(Clone, Debug)]
36pub struct Stroke {
37    /// Stroke color.
38    pub color: Color,
39    /// Stroke width in pixels.
40    pub width: f64,
41    /// Optional dash pattern.
42    pub dash: Option<DashPattern>,
43    /// Line cap style.
44    pub line_cap: LineCap,
45    /// Line join style.
46    pub line_join: LineJoin,
47}
48
49impl Stroke {
50    /// Create a solid stroke.
51    pub fn solid(color: Color, width: f64) -> Self {
52        Self {
53            color,
54            width,
55            dash: None,
56            line_cap: LineCap::Butt,
57            line_join: LineJoin::Miter,
58        }
59    }
60
61    /// Create a dashed stroke.
62    pub fn dashed(color: Color, width: f64, dashes: &[f64]) -> Self {
63        Self {
64            color,
65            width,
66            dash: Some(DashPattern::new(dashes)),
67            line_cap: LineCap::Butt,
68            line_join: LineJoin::Miter,
69        }
70    }
71}
72
73impl Default for Stroke {
74    fn default() -> Self {
75        Self::solid(Color::BLACK, 1.0)
76    }
77}
78
79/// Line cap style.
80#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
81pub enum LineCap {
82    /// Flat end (default).
83    #[default]
84    Butt,
85    /// Rounded end.
86    Round,
87    /// Square end (extends past endpoint).
88    Square,
89}
90
91impl LineCap {
92    /// SVG attribute value.
93    pub fn as_svg_str(self) -> &'static str {
94        match self {
95            Self::Butt => "butt",
96            Self::Round => "round",
97            Self::Square => "square",
98        }
99    }
100}
101
102/// Line join style.
103#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
104pub enum LineJoin {
105    /// Miter join (default).
106    #[default]
107    Miter,
108    /// Rounded join.
109    Round,
110    /// Beveled join.
111    Bevel,
112}
113
114impl LineJoin {
115    /// SVG attribute value.
116    pub fn as_svg_str(self) -> &'static str {
117        match self {
118            Self::Miter => "miter",
119            Self::Round => "round",
120            Self::Bevel => "bevel",
121        }
122    }
123}
124
125/// Fill style.
126#[derive(Clone, Debug, Default)]
127pub enum Fill {
128    /// No fill.
129    #[default]
130    None,
131    /// Solid color fill.
132    Solid(Color),
133    /// Reference a gradient by ID.
134    Gradient(String),
135}
136
137impl Fill {
138    /// Format as an SVG `fill` attribute value.
139    pub fn to_svg_string(&self) -> String {
140        match self {
141            Self::None => "none".to_string(),
142            Self::Solid(c) => c.to_svg_string(),
143            Self::Gradient(id) => format!("url(#{id})"),
144        }
145    }
146}
147
148/// Text anchor position.
149#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
150pub enum TextAnchor {
151    /// Left-aligned (default).
152    #[default]
153    Start,
154    /// Center-aligned.
155    Middle,
156    /// Right-aligned.
157    End,
158}
159
160impl TextAnchor {
161    /// SVG attribute value.
162    pub fn as_svg_str(self) -> &'static str {
163        match self {
164            Self::Start => "start",
165            Self::Middle => "middle",
166            Self::End => "end",
167        }
168    }
169}
170
171/// Font style for text elements.
172#[derive(Clone, Debug)]
173pub struct FontStyle {
174    /// Font family.
175    pub family: String,
176    /// Font size in pixels.
177    pub size: f64,
178    /// Font weight (400 = normal, 700 = bold).
179    pub weight: u16,
180    /// Fill color.
181    pub color: Color,
182    /// Text anchor.
183    pub anchor: TextAnchor,
184}
185
186impl FontStyle {
187    /// Create a font style with sensible defaults.
188    pub fn new(size: f64) -> Self {
189        Self {
190            family: "sans-serif".to_string(),
191            size,
192            weight: 400,
193            color: Color::BLACK,
194            anchor: TextAnchor::Start,
195        }
196    }
197}
198
199impl Default for FontStyle {
200    fn default() -> Self {
201        Self::new(12.0)
202    }
203}