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