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