Skip to main content

azul_css/
shape.rs

1//! CSS Shape data structures for shape-inside, shape-outside, and clip-path
2//!
3//! These types are C-compatible (repr(C)) for use across FFI boundaries.
4
5use alloc::string::String;
6use crate::corety::{AzString, OptionF32};
7
8/// Compares two f32 values for ordering, treating NaN as equal.
9fn cmp_f32(a: f32, b: f32) -> core::cmp::Ordering {
10    a.partial_cmp(&b).unwrap_or(core::cmp::Ordering::Equal)
11}
12
13/// A 2D point for shape coordinates (using f32 for precision)
14#[derive(Debug, Copy, Clone, PartialEq)]
15#[repr(C)]
16pub struct ShapePoint {
17    pub x: f32,
18    pub y: f32,
19}
20
21impl_option!(
22    ShapePoint,
23    OptionShapePoint,
24    [Debug, Copy, Clone, PartialEq, Eq, PartialOrd]
25);
26
27impl ShapePoint {
28    #[must_use] pub const fn new(x: f32, y: f32) -> Self {
29        Self { x, y }
30    }
31
32    #[must_use] pub const fn zero() -> Self {
33        Self { x: 0.0, y: 0.0 }
34    }
35}
36
37impl Eq for ShapePoint {}
38
39// PartialOrd delegates to Ord (NaN-as-equal) so the two stay consistent.
40impl PartialOrd for ShapePoint {
41    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
42        Some(self.cmp(other))
43    }
44}
45
46impl Ord for ShapePoint {
47    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
48        match self.x.partial_cmp(&other.x) {
49            Some(core::cmp::Ordering::Equal) => self
50                .y
51                .partial_cmp(&other.y)
52                .unwrap_or(core::cmp::Ordering::Equal),
53            other => other.unwrap_or(core::cmp::Ordering::Equal),
54        }
55    }
56}
57
58impl core::hash::Hash for ShapePoint {
59    fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
60        self.x.to_bits().hash(state);
61        self.y.to_bits().hash(state);
62    }
63}
64
65impl_vec!(ShapePoint, ShapePointVec, ShapePointVecDestructor, ShapePointVecDestructorType, ShapePointVecSlice, OptionShapePoint);
66impl_vec_debug!(ShapePoint, ShapePointVec);
67impl_vec_partialord!(ShapePoint, ShapePointVec);
68impl_vec_ord!(ShapePoint, ShapePointVec);
69impl_vec_clone!(ShapePoint, ShapePointVec, ShapePointVecDestructor);
70impl_vec_partialeq!(ShapePoint, ShapePointVec);
71impl_vec_eq!(ShapePoint, ShapePointVec);
72impl_vec_hash!(ShapePoint, ShapePointVec);
73
74/// A circle shape defined by center point and radius
75#[derive(Debug, Copy, Clone, PartialEq)]
76#[repr(C)]
77pub struct ShapeCircle {
78    pub center: ShapePoint,
79    pub radius: f32,
80}
81
82impl Eq for ShapeCircle {}
83impl core::hash::Hash for ShapeCircle {
84    fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
85        self.center.hash(state);
86        self.radius.to_bits().hash(state);
87    }
88}
89impl PartialOrd for ShapeCircle {
90    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
91        Some(self.cmp(other))
92    }
93}
94impl Ord for ShapeCircle {
95    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
96        match self.center.cmp(&other.center) {
97            core::cmp::Ordering::Equal => self
98                .radius
99                .partial_cmp(&other.radius)
100                .unwrap_or(core::cmp::Ordering::Equal),
101            other => other,
102        }
103    }
104}
105
106/// An ellipse shape defined by center point and two radii
107#[derive(Debug, Copy, Clone, PartialEq)]
108#[repr(C)]
109pub struct ShapeEllipse {
110    pub center: ShapePoint,
111    pub radius_x: f32,
112    pub radius_y: f32,
113}
114
115impl Eq for ShapeEllipse {}
116impl core::hash::Hash for ShapeEllipse {
117    fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
118        self.center.hash(state);
119        self.radius_x.to_bits().hash(state);
120        self.radius_y.to_bits().hash(state);
121    }
122}
123impl PartialOrd for ShapeEllipse {
124    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
125        Some(self.cmp(other))
126    }
127}
128impl Ord for ShapeEllipse {
129    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
130        match self.center.cmp(&other.center) {
131            core::cmp::Ordering::Equal => match self.radius_x.partial_cmp(&other.radius_x) {
132                Some(core::cmp::Ordering::Equal) | None => self
133                    .radius_y
134                    .partial_cmp(&other.radius_y)
135                    .unwrap_or(core::cmp::Ordering::Equal),
136                Some(other) => other,
137            },
138            other => other,
139        }
140    }
141}
142
143/// A polygon shape defined by a list of points (in clockwise order)
144#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
145#[repr(C)]
146pub struct ShapePolygon {
147    pub points: ShapePointVec,
148}
149
150/// An inset rectangle with optional border radius
151/// Defined by insets from the reference box edges
152#[derive(Debug, Copy, Clone, PartialEq)]
153#[repr(C)]
154pub struct ShapeInset {
155    pub inset_top: f32,
156    pub inset_right: f32,
157    pub inset_bottom: f32,
158    pub inset_left: f32,
159    pub border_radius: OptionF32,
160}
161
162impl Eq for ShapeInset {}
163impl core::hash::Hash for ShapeInset {
164    fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
165        self.inset_top.to_bits().hash(state);
166        self.inset_right.to_bits().hash(state);
167        self.inset_bottom.to_bits().hash(state);
168        self.inset_left.to_bits().hash(state);
169        self.border_radius.hash(state);
170    }
171}
172impl PartialOrd for ShapeInset {
173    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
174        Some(self.cmp(other))
175    }
176}
177impl Ord for ShapeInset {
178    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
179        cmp_f32(self.inset_top, other.inset_top)
180            .then_with(|| cmp_f32(self.inset_right, other.inset_right))
181            .then_with(|| cmp_f32(self.inset_bottom, other.inset_bottom))
182            .then_with(|| cmp_f32(self.inset_left, other.inset_left))
183            .then_with(|| self.border_radius.cmp(&other.border_radius))
184    }
185}
186
187/// An SVG-like path for shape definitions.
188/// TODO: path parsing is not yet implemented — `data` is stored but not interpreted.
189#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
190#[repr(C)]
191pub struct ShapePath {
192    pub data: AzString,
193}
194
195/// Represents a CSS shape for shape-inside, shape-outside, and clip-path.
196/// Used for both text layout (shape-inside/outside) and rendering clipping (clip-path).
197#[derive(Debug, Clone, PartialEq)]
198#[repr(C, u8)]
199pub enum CssShape {
200    Circle(ShapeCircle),
201    Ellipse(ShapeEllipse),
202    Polygon(ShapePolygon),
203    Inset(ShapeInset),
204    Path(ShapePath),
205}
206
207impl Eq for CssShape {}
208
209impl core::hash::Hash for CssShape {
210    fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
211        core::mem::discriminant(self).hash(state);
212        match self {
213            Self::Circle(c) => c.hash(state),
214            Self::Ellipse(e) => e.hash(state),
215            Self::Polygon(p) => p.hash(state),
216            Self::Inset(i) => i.hash(state),
217            Self::Path(p) => p.hash(state),
218        }
219    }
220}
221
222impl PartialOrd for CssShape {
223    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
224        Some(self.cmp(other))
225    }
226}
227
228impl Ord for CssShape {
229    // The tie-break arms `(Self::X(_), _) => Less` / `(_, Self::X(_)) => Greater`
230    // share bodies but are ORDER-DEPENDENT: they encode the variant ordering, so
231    // merging them (clippy::match_same_arms) would change the comparison result.
232    #[allow(clippy::match_same_arms)]
233    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
234        match (self, other) {
235            (Self::Circle(a), Self::Circle(b)) => a.cmp(b),
236            (Self::Ellipse(a), Self::Ellipse(b)) => a.cmp(b),
237            (Self::Polygon(a), Self::Polygon(b)) => a.cmp(b),
238            (Self::Inset(a), Self::Inset(b)) => a.cmp(b),
239            (Self::Path(a), Self::Path(b)) => a.cmp(b),
240            // Different variants: use discriminant ordering
241            (Self::Circle(_), _) => core::cmp::Ordering::Less,
242            (_, Self::Circle(_)) => core::cmp::Ordering::Greater,
243            (Self::Ellipse(_), _) => core::cmp::Ordering::Less,
244            (_, Self::Ellipse(_)) => core::cmp::Ordering::Greater,
245            (Self::Polygon(_), _) => core::cmp::Ordering::Less,
246            (_, Self::Polygon(_)) => core::cmp::Ordering::Greater,
247            (Self::Inset(_), Self::Path(_)) => core::cmp::Ordering::Less,
248            (Self::Path(_), Self::Inset(_)) => core::cmp::Ordering::Greater,
249        }
250    }
251}
252
253impl CssShape {
254    /// Creates a circle shape at the given position with the given radius
255    #[must_use] pub const fn circle(center: ShapePoint, radius: f32) -> Self {
256        Self::Circle(ShapeCircle { center, radius })
257    }
258
259    /// Creates an ellipse shape
260    #[must_use] pub const fn ellipse(center: ShapePoint, radius_x: f32, radius_y: f32) -> Self {
261        Self::Ellipse(ShapeEllipse {
262            center,
263            radius_x,
264            radius_y,
265        })
266    }
267
268    /// Creates a polygon from a list of points
269    #[must_use] pub const fn polygon(points: ShapePointVec) -> Self {
270        Self::Polygon(ShapePolygon { points })
271    }
272
273    /// Creates an inset rectangle
274    #[must_use] pub const fn inset(top: f32, right: f32, bottom: f32, left: f32) -> Self {
275        Self::Inset(ShapeInset {
276            inset_top: top,
277            inset_right: right,
278            inset_bottom: bottom,
279            inset_left: left,
280            border_radius: OptionF32::None,
281        })
282    }
283
284    /// Creates an inset rectangle with rounded corners
285    #[must_use] pub const fn inset_rounded(top: f32, right: f32, bottom: f32, left: f32, radius: f32) -> Self {
286        Self::Inset(ShapeInset {
287            inset_top: top,
288            inset_right: right,
289            inset_bottom: bottom,
290            inset_left: left,
291            border_radius: OptionF32::Some(radius),
292        })
293    }
294
295    #[must_use] pub fn print_as_css_value(&self) -> String {
296        use alloc::format;
297        match self {
298            Self::Circle(ShapeCircle { center, radius }) => {
299                format!("circle({}px at {}px {}px)", radius, center.x, center.y)
300            }
301            Self::Ellipse(ShapeEllipse { center, radius_x, radius_y }) => {
302                format!("ellipse({}px {}px at {}px {}px)", radius_x, radius_y, center.x, center.y)
303            }
304            Self::Polygon(ShapePolygon { points }) => {
305                let pts: Vec<String> = points.as_ref().iter()
306                    .map(|p| format!("{}px {}px", p.x, p.y))
307                    .collect();
308                format!("polygon({})", pts.join(", "))
309            }
310            Self::Inset(ShapeInset { inset_top, inset_right, inset_bottom, inset_left, border_radius }) => {
311                let base = format!("inset({inset_top}px {inset_right}px {inset_bottom}px {inset_left}px");
312                match border_radius {
313                    OptionF32::Some(r) => format!("{base} round {r}px)"),
314                    OptionF32::None => format!("{base})"),
315                }
316            }
317            Self::Path(ShapePath { data }) => {
318                format!("path(\"{}\")", data.as_str())
319            }
320        }
321    }
322
323    #[must_use] pub fn format_as_rust_code(&self) -> String {
324        use alloc::format;
325        match self {
326            Self::Circle(ShapeCircle { center, radius }) => {
327                format!(
328                    "CssShape::Circle(ShapeCircle {{ center: ShapePoint::new({}_f32, {}_f32), radius: {}_f32 }})",
329                    center.x, center.y, radius
330                )
331            }
332            Self::Ellipse(ShapeEllipse { center, radius_x, radius_y }) => {
333                format!(
334                    "CssShape::Ellipse(ShapeEllipse {{ center: ShapePoint::new({}_f32, {}_f32), radius_x: {}_f32, radius_y: {}_f32 }})",
335                    center.x, center.y, radius_x, radius_y
336                )
337            }
338            Self::Polygon(ShapePolygon { points }) => {
339                let pts: Vec<String> = points.as_ref().iter()
340                    .map(|p| format!("ShapePoint::new({}_f32, {}_f32)", p.x, p.y))
341                    .collect();
342                format!("CssShape::Polygon(ShapePolygon {{ points: vec![{}].into() }})", pts.join(", "))
343            }
344            Self::Inset(ShapeInset { inset_top, inset_right, inset_bottom, inset_left, border_radius }) => {
345                let br = match border_radius {
346                    OptionF32::Some(r) => format!("OptionF32::Some({r}_f32)"),
347                    OptionF32::None => String::from("OptionF32::None"),
348                };
349                format!(
350                    "CssShape::Inset(ShapeInset {{ inset_top: {inset_top}_f32, inset_right: {inset_right}_f32, inset_bottom: {inset_bottom}_f32, inset_left: {inset_left}_f32, border_radius: {br} }})"
351                )
352            }
353            Self::Path(ShapePath { data }) => {
354                format!("CssShape::Path(ShapePath {{ data: AzString::from_const_str(\"{}\") }})", data.as_str())
355            }
356        }
357    }
358}
359
360impl_option!(
361    CssShape,
362    OptionCssShape,
363    copy = false,
364    [Debug, Clone, PartialEq, Eq]
365);
366