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 crate::corety::{AzString, OptionF32};
6
7/// A 2D point for shape coordinates (using f32 for precision)
8#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)]
9#[repr(C)]
10pub struct ShapePoint {
11    pub x: f32,
12    pub y: f32,
13}
14
15impl_option!(
16    ShapePoint,
17    OptionShapePoint,
18    [Debug, Copy, Clone, PartialEq, PartialOrd]
19);
20
21impl ShapePoint {
22    pub const fn new(x: f32, y: f32) -> Self {
23        Self { x, y }
24    }
25
26    pub const fn zero() -> Self {
27        Self { x: 0.0, y: 0.0 }
28    }
29}
30
31impl Eq for ShapePoint {}
32
33impl Ord for ShapePoint {
34    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
35        match self.x.partial_cmp(&other.x) {
36            Some(core::cmp::Ordering::Equal) => self
37                .y
38                .partial_cmp(&other.y)
39                .unwrap_or(core::cmp::Ordering::Equal),
40            other => other.unwrap_or(core::cmp::Ordering::Equal),
41        }
42    }
43}
44
45impl core::hash::Hash for ShapePoint {
46    fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
47        self.x.to_bits().hash(state);
48        self.y.to_bits().hash(state);
49    }
50}
51
52impl_vec!(ShapePoint, ShapePointVec, ShapePointVecDestructor, ShapePointVecDestructorType, ShapePointVecSlice, OptionShapePoint);
53impl_vec_debug!(ShapePoint, ShapePointVec);
54impl_vec_partialord!(ShapePoint, ShapePointVec);
55impl_vec_ord!(ShapePoint, ShapePointVec);
56impl_vec_clone!(ShapePoint, ShapePointVec, ShapePointVecDestructor);
57impl_vec_partialeq!(ShapePoint, ShapePointVec);
58impl_vec_eq!(ShapePoint, ShapePointVec);
59impl_vec_hash!(ShapePoint, ShapePointVec);
60
61/// A circle shape defined by center point and radius
62#[derive(Debug, Clone, PartialEq)]
63#[repr(C)]
64pub struct ShapeCircle {
65    pub center: ShapePoint,
66    pub radius: f32,
67}
68
69impl Eq for ShapeCircle {}
70impl core::hash::Hash for ShapeCircle {
71    fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
72        self.center.hash(state);
73        self.radius.to_bits().hash(state);
74    }
75}
76impl PartialOrd for ShapeCircle {
77    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
78        Some(self.cmp(other))
79    }
80}
81impl Ord for ShapeCircle {
82    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
83        match self.center.cmp(&other.center) {
84            core::cmp::Ordering::Equal => self
85                .radius
86                .partial_cmp(&other.radius)
87                .unwrap_or(core::cmp::Ordering::Equal),
88            other => other,
89        }
90    }
91}
92
93/// An ellipse shape defined by center point and two radii
94#[derive(Debug, Clone, PartialEq)]
95#[repr(C)]
96pub struct ShapeEllipse {
97    pub center: ShapePoint,
98    pub radius_x: f32,
99    pub radius_y: f32,
100}
101
102impl Eq for ShapeEllipse {}
103impl core::hash::Hash for ShapeEllipse {
104    fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
105        self.center.hash(state);
106        self.radius_x.to_bits().hash(state);
107        self.radius_y.to_bits().hash(state);
108    }
109}
110impl PartialOrd for ShapeEllipse {
111    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
112        Some(self.cmp(other))
113    }
114}
115impl Ord for ShapeEllipse {
116    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
117        match self.center.cmp(&other.center) {
118            core::cmp::Ordering::Equal => match self.radius_x.partial_cmp(&other.radius_x) {
119                Some(core::cmp::Ordering::Equal) | None => self
120                    .radius_y
121                    .partial_cmp(&other.radius_y)
122                    .unwrap_or(core::cmp::Ordering::Equal),
123                Some(other) => other,
124            },
125            other => other,
126        }
127    }
128}
129
130/// A polygon shape defined by a list of points (in clockwise order)
131#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
132#[repr(C)]
133pub struct ShapePolygon {
134    pub points: ShapePointVec,
135}
136
137/// An inset rectangle with optional border radius
138/// Defined by insets from the reference box edges
139#[derive(Debug, Clone, PartialEq)]
140#[repr(C)]
141pub struct ShapeInset {
142    pub inset_top: f32,
143    pub inset_right: f32,
144    pub inset_bottom: f32,
145    pub inset_left: f32,
146    pub border_radius: OptionF32,
147}
148
149impl Eq for ShapeInset {}
150impl core::hash::Hash for ShapeInset {
151    fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
152        self.inset_top.to_bits().hash(state);
153        self.inset_right.to_bits().hash(state);
154        self.inset_bottom.to_bits().hash(state);
155        self.inset_left.to_bits().hash(state);
156        self.border_radius.hash(state);
157    }
158}
159impl PartialOrd for ShapeInset {
160    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
161        Some(self.cmp(other))
162    }
163}
164impl Ord for ShapeInset {
165    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
166        match self.inset_top.partial_cmp(&other.inset_top) {
167            Some(core::cmp::Ordering::Equal) | None => {
168                match self.inset_right.partial_cmp(&other.inset_right) {
169                    Some(core::cmp::Ordering::Equal) | None => {
170                        match self.inset_bottom.partial_cmp(&other.inset_bottom) {
171                            Some(core::cmp::Ordering::Equal) | None => {
172                                match self.inset_left.partial_cmp(&other.inset_left) {
173                                    Some(core::cmp::Ordering::Equal) | None => {
174                                        self.border_radius.cmp(&other.border_radius)
175                                    }
176                                    Some(other) => other,
177                                }
178                            }
179                            Some(other) => other,
180                        }
181                    }
182                    Some(other) => other,
183                }
184            }
185            Some(other) => other,
186        }
187    }
188}
189
190/// An SVG-like path (for future use)
191#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
192#[repr(C)]
193pub struct ShapePath {
194    pub data: AzString,
195}
196
197/// Represents a CSS shape for shape-inside, shape-outside, and clip-path.
198/// Used for both text layout (shape-inside/outside) and rendering clipping (clip-path).
199#[derive(Debug, Clone, PartialEq)]
200#[repr(C, u8)]
201pub enum CssShape {
202    Circle(ShapeCircle),
203    Ellipse(ShapeEllipse),
204    Polygon(ShapePolygon),
205    Inset(ShapeInset),
206    Path(ShapePath),
207}
208
209impl Eq for CssShape {}
210
211impl core::hash::Hash for CssShape {
212    fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
213        core::mem::discriminant(self).hash(state);
214        match self {
215            CssShape::Circle(c) => c.hash(state),
216            CssShape::Ellipse(e) => e.hash(state),
217            CssShape::Polygon(p) => p.hash(state),
218            CssShape::Inset(i) => i.hash(state),
219            CssShape::Path(p) => p.hash(state),
220        }
221    }
222}
223
224impl PartialOrd for CssShape {
225    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
226        Some(self.cmp(other))
227    }
228}
229
230impl Ord for CssShape {
231    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
232        match (self, other) {
233            (CssShape::Circle(a), CssShape::Circle(b)) => a.cmp(b),
234            (CssShape::Ellipse(a), CssShape::Ellipse(b)) => a.cmp(b),
235            (CssShape::Polygon(a), CssShape::Polygon(b)) => a.cmp(b),
236            (CssShape::Inset(a), CssShape::Inset(b)) => a.cmp(b),
237            (CssShape::Path(a), CssShape::Path(b)) => a.cmp(b),
238            // Different variants: use discriminant ordering
239            (CssShape::Circle(_), _) => core::cmp::Ordering::Less,
240            (_, CssShape::Circle(_)) => core::cmp::Ordering::Greater,
241            (CssShape::Ellipse(_), _) => core::cmp::Ordering::Less,
242            (_, CssShape::Ellipse(_)) => core::cmp::Ordering::Greater,
243            (CssShape::Polygon(_), _) => core::cmp::Ordering::Less,
244            (_, CssShape::Polygon(_)) => core::cmp::Ordering::Greater,
245            (CssShape::Inset(_), CssShape::Path(_)) => core::cmp::Ordering::Less,
246            (CssShape::Path(_), CssShape::Inset(_)) => core::cmp::Ordering::Greater,
247        }
248    }
249}
250
251impl CssShape {
252    /// Creates a circle shape at the given position with the given radius
253    pub fn circle(center: ShapePoint, radius: f32) -> Self {
254        CssShape::Circle(ShapeCircle { center, radius })
255    }
256
257    /// Creates an ellipse shape
258    pub fn ellipse(center: ShapePoint, radius_x: f32, radius_y: f32) -> Self {
259        CssShape::Ellipse(ShapeEllipse {
260            center,
261            radius_x,
262            radius_y,
263        })
264    }
265
266    /// Creates a polygon from a list of points
267    pub fn polygon(points: ShapePointVec) -> Self {
268        CssShape::Polygon(ShapePolygon { points })
269    }
270
271    /// Creates an inset rectangle
272    pub fn inset(top: f32, right: f32, bottom: f32, left: f32) -> Self {
273        CssShape::Inset(ShapeInset {
274            inset_top: top,
275            inset_right: right,
276            inset_bottom: bottom,
277            inset_left: left,
278            border_radius: OptionF32::None,
279        })
280    }
281
282    /// Creates an inset rectangle with rounded corners
283    pub fn inset_rounded(top: f32, right: f32, bottom: f32, left: f32, radius: f32) -> Self {
284        CssShape::Inset(ShapeInset {
285            inset_top: top,
286            inset_right: right,
287            inset_bottom: bottom,
288            inset_left: left,
289            border_radius: OptionF32::Some(radius),
290        })
291    }
292}
293
294impl_option!(
295    CssShape,
296    OptionCssShape,
297    copy = false,
298    [Debug, Clone, PartialEq]
299);
300
301/// A line segment representing available horizontal space at a given y-position.
302/// Used for line breaking within shaped containers.
303#[derive(Debug, Clone, Copy, PartialEq)]
304#[repr(C)]
305pub struct LineSegment {
306    /// The x-coordinate where this segment starts
307    pub start_x: f32,
308
309    /// The width of this segment
310    pub width: f32,
311
312    /// Priority for choosing between overlapping segments (higher = preferred)
313    pub priority: i32,
314}
315
316impl_option!(
317    LineSegment,
318    OptionLineSegment,
319    [Debug, Copy, Clone, PartialEq]
320);
321
322impl LineSegment {
323    /// Creates a new line segment
324    pub const fn new(start_x: f32, width: f32) -> Self {
325        Self {
326            start_x,
327            width,
328            priority: 0,
329        }
330    }
331
332    /// Returns the end x-coordinate of this segment
333    #[inline]
334    pub fn end_x(&self) -> f32 {
335        self.start_x + self.width
336    }
337
338    /// Returns true if this segment overlaps with another segment
339    pub fn overlaps(&self, other: &Self) -> bool {
340        self.start_x < other.end_x() && other.start_x < self.end_x()
341    }
342
343    /// Computes the intersection of two segments, if any
344    pub fn intersection(&self, other: &Self) -> Option<Self> {
345        let start = self.start_x.max(other.start_x);
346        let end = self.end_x().min(other.end_x());
347
348        if start < end {
349            Some(Self {
350                start_x: start,
351                width: end - start,
352                priority: self.priority.max(other.priority),
353            })
354        } else {
355            None
356        }
357    }
358}
359
360impl_vec!(LineSegment, LineSegmentVec, LineSegmentVecDestructor, LineSegmentVecDestructorType, LineSegmentVecSlice, OptionLineSegment);
361impl_vec_debug!(LineSegment, LineSegmentVec);
362impl_vec_clone!(LineSegment, LineSegmentVec, LineSegmentVecDestructor);
363impl_vec_partialeq!(LineSegment, LineSegmentVec);
364
365/// A 2D rectangle for shape bounding boxes and reference boxes
366#[derive(Debug, Copy, Clone, PartialEq)]
367#[repr(C)]
368pub struct ShapeRect {
369    pub origin: ShapePoint,
370    pub width: f32,
371    pub height: f32,
372}
373
374impl ShapeRect {
375    pub const fn new(origin: ShapePoint, width: f32, height: f32) -> Self {
376        Self {
377            origin,
378            width,
379            height,
380        }
381    }
382
383    pub const fn zero() -> Self {
384        Self {
385            origin: ShapePoint::zero(),
386            width: 0.0,
387            height: 0.0,
388        }
389    }
390}
391
392impl_option!(ShapeRect, OptionShapeRect, [Debug, Copy, Clone, PartialEq]);
393
394impl CssShape {
395    /// Computes the bounding box of this shape
396    pub fn bounding_box(&self) -> ShapeRect {
397        match self {
398            CssShape::Circle(ShapeCircle { center, radius }) => ShapeRect {
399                origin: ShapePoint::new(center.x - radius, center.y - radius),
400                width: radius * 2.0,
401                height: radius * 2.0,
402            },
403
404            CssShape::Ellipse(ShapeEllipse {
405                center,
406                radius_x,
407                radius_y,
408            }) => ShapeRect {
409                origin: ShapePoint::new(center.x - radius_x, center.y - radius_y),
410                width: radius_x * 2.0,
411                height: radius_y * 2.0,
412            },
413
414            CssShape::Polygon(ShapePolygon { points }) => {
415                if points.as_ref().is_empty() {
416                    return ShapeRect::zero();
417                }
418
419                let first = points.as_ref()[0];
420                let mut min_x = first.x;
421                let mut min_y = first.y;
422                let mut max_x = first.x;
423                let mut max_y = first.y;
424
425                for point in points.as_ref().iter().skip(1) {
426                    min_x = min_x.min(point.x);
427                    min_y = min_y.min(point.y);
428                    max_x = max_x.max(point.x);
429                    max_y = max_y.max(point.y);
430                }
431
432                ShapeRect {
433                    origin: ShapePoint::new(min_x, min_y),
434                    width: max_x - min_x,
435                    height: max_y - min_y,
436                }
437            }
438
439            CssShape::Inset(ShapeInset {
440                inset_top,
441                inset_right,
442                inset_bottom,
443                inset_left,
444                ..
445            }) => {
446                // For inset, we need the reference box to compute actual bounds
447                // For now, return a placeholder that indicates the insets
448                ShapeRect {
449                    origin: ShapePoint::new(*inset_left, *inset_top),
450                    width: 0.0, // Will be computed relative to container
451                    height: 0.0,
452                }
453            }
454
455            CssShape::Path(_) => {
456                // Path bounding box computation requires parsing the path
457                // For now, return zero rect
458                ShapeRect::zero()
459            }
460        }
461    }
462
463    /// Computes available horizontal line segments at a given y-position.
464    /// Used for text layout with shape-inside.
465    ///
466    /// # Arguments
467    /// * `y` - The vertical position to compute segments for
468    /// * `margin` - Inward margin from the shape boundary
469    /// * `reference_box` - The containing box for inset shapes
470    ///
471    /// # Returns
472    /// A vector of line segments, sorted by start_x
473    pub fn compute_line_segments(
474        &self,
475        y: f32,
476        margin: f32,
477        reference_box: OptionShapeRect,
478    ) -> LineSegmentVec {
479        use alloc::vec::Vec;
480
481        let segments: Vec<LineSegment> = match self {
482            CssShape::Circle(ShapeCircle { center, radius }) => {
483                let dy = y - center.y;
484                let r_with_margin = radius - margin;
485
486                if dy.abs() > r_with_margin {
487                    Vec::new() // Outside circle
488                } else {
489                    // Chord width at y: w = 2*sqrt(r²-dy²)
490                    let half_width = (r_with_margin.powi(2) - dy.powi(2)).sqrt();
491
492                    alloc::vec![LineSegment {
493                        start_x: center.x - half_width,
494                        width: 2.0 * half_width,
495                        priority: 0,
496                    }]
497                }
498            }
499
500            CssShape::Ellipse(ShapeEllipse {
501                center,
502                radius_x,
503                radius_y,
504            }) => {
505                let dy = y - center.y;
506                let ry_with_margin = radius_y - margin;
507
508                if dy.abs() > ry_with_margin {
509                    Vec::new() // Outside ellipse
510                } else {
511                    // Ellipse equation: (x/rx)² + (y/ry)² = 1
512                    // Solve for x at given y: x = rx * sqrt(1 - (y/ry)²)
513                    let ratio = dy / ry_with_margin;
514                    let factor = (1.0 - ratio.powi(2)).sqrt();
515                    let half_width = (radius_x - margin) * factor;
516
517                    alloc::vec![LineSegment {
518                        start_x: center.x - half_width,
519                        width: 2.0 * half_width,
520                        priority: 0,
521                    }]
522                }
523            }
524
525            CssShape::Polygon(ShapePolygon { points }) => {
526                compute_polygon_line_segments(points.as_ref(), y, margin)
527            }
528
529            CssShape::Inset(ShapeInset {
530                inset_top: top,
531                inset_right: right,
532                inset_bottom: bottom,
533                inset_left: left,
534                border_radius,
535            }) => {
536                let ref_box = match reference_box {
537                    OptionShapeRect::Some(r) => r,
538                    OptionShapeRect::None => ShapeRect::zero(),
539                };
540
541                let inset_top = ref_box.origin.y + top + margin;
542                let inset_bottom = ref_box.origin.y + ref_box.height - bottom - margin;
543                let inset_left = ref_box.origin.x + left + margin;
544                let inset_right = ref_box.origin.x + ref_box.width - right - margin;
545
546                if y < inset_top || y > inset_bottom {
547                    Vec::new()
548                } else {
549                    // TODO: Handle border_radius for rounded corners
550                    // For now, just return full width
551                    alloc::vec![LineSegment {
552                        start_x: inset_left,
553                        width: inset_right - inset_left,
554                        priority: 0,
555                    }]
556                }
557            }
558
559            CssShape::Path(_) => {
560                // Path intersection requires path parsing
561                // For now, return empty
562                Vec::new()
563            }
564        };
565
566        segments.into()
567    }
568}
569
570/// Computes line segments for a polygon at a given y-position.
571/// Uses a scanline algorithm to find intersections with polygon edges.
572fn compute_polygon_line_segments(
573    points: &[ShapePoint],
574    y: f32,
575    margin: f32,
576) -> alloc::vec::Vec<LineSegment> {
577    use alloc::vec::Vec;
578
579    if points.len() < 3 {
580        return Vec::new();
581    }
582
583    // Find all intersections of the horizontal line y with polygon edges
584    let mut intersections = Vec::new();
585
586    for i in 0..points.len() {
587        let p1 = points[i];
588        let p2 = points[(i + 1) % points.len()];
589
590        // Check if edge crosses the scanline
591        let min_y = p1.y.min(p2.y);
592        let max_y = p1.y.max(p2.y);
593
594        if y >= min_y && y < max_y {
595            // Compute x-intersection using linear interpolation
596            let t = (y - p1.y) / (p2.y - p1.y);
597            let x = p1.x + t * (p2.x - p1.x);
598            intersections.push(x);
599        }
600    }
601
602    // Sort intersections
603    intersections.sort_by(|a, b| a.partial_cmp(b).unwrap_or(core::cmp::Ordering::Equal));
604
605    // Pair up intersections to form segments
606    // Polygon fill rule: pairs of intersections form filled regions
607    let mut segments = Vec::new();
608
609    for chunk in intersections.chunks(2) {
610        if chunk.len() == 2 {
611            let start = chunk[0] + margin;
612            let end = chunk[1] - margin;
613
614            if start < end {
615                segments.push(LineSegment {
616                    start_x: start,
617                    width: end - start,
618                    priority: 0,
619                });
620            }
621        }
622    }
623
624    segments
625}