Skip to main content

azul_core/
svg.rs

1//! SVG rendering and path tessellation.
2//!
3//! This module provides functionality for parsing, manipulating, and rendering SVG paths.
4//! It includes:
5//!
6//! - **Path tessellation**: Converts SVG paths into triangle meshes for GPU rendering
7//! - **Stroke generation**: Creates stroked paths with various line join and cap styles
8//! - **Transform support**: Applies CSS transforms to SVG elements
9//! - **Style parsing**: Handles SVG fill, stroke, opacity, and other attributes
10//!
11//! The module uses Lyon for geometric tessellation and generates vertex/index buffers
12//! that can be uploaded to WebRender for hardware-accelerated rendering.
13
14use alloc::{
15    string::{String, ToString},
16    vec::Vec,
17};
18use core::fmt;
19
20use azul_css::{
21    props::{
22        basic::{
23            ColorF, ColorU, OptionColorU, OptionLayoutSize, PixelValue, SvgCubicCurve, SvgPoint,
24            SvgQuadraticCurve, SvgRect, SvgVector,
25        },
26        style::{StyleTransform, StyleTransformOrigin, StyleTransformVec},
27    },
28    AzString, OptionString, StringVec, U32Vec,
29};
30
31use crate::{
32    geom::PhysicalSizeU32,
33    gl::{
34        GlContextPtr, GlShader, IndexBufferFormat, Texture, Uniform, UniformType, VertexAttribute,
35        VertexAttributeType, VertexBuffer, VertexLayout, VertexLayoutDescription,
36    },
37    transform::{ComputedTransform3D, RotationMode},
38    xml::XmlError,
39};
40
41/// Default miter limit for stroke joins (ratio of miter length to stroke width)
42const DEFAULT_MITER_LIMIT: f32 = 4.0;
43/// Default stroke width in pixels
44const DEFAULT_LINE_WIDTH: f32 = 1.0;
45/// Default tessellation tolerance in pixels (smaller = more vertices, higher quality)
46const DEFAULT_TOLERANCE: f32 = 0.1;
47
48/// Represents the dimensions of an SVG viewport or element.
49#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)]
50#[repr(C)]
51pub struct SvgSize {
52    /// Width in SVG user units
53    pub width: f32,
54    /// Height in SVG user units
55    pub height: f32,
56}
57
58/// A line segment in 2D space.
59#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)]
60#[repr(C)]
61pub struct SvgLine {
62    /// Start point of the line
63    pub start: SvgPoint,
64    /// End point of the line
65    pub end: SvgPoint,
66}
67
68impl SvgLine {
69    /// Creates a new line segment from start to end point
70    #[inline]
71    #[must_use]
72    pub const fn new(start: SvgPoint, end: SvgPoint) -> Self {
73        Self { start, end }
74    }
75
76    /// Computes the inward-facing normal vector for this line.
77    ///
78    /// The normal points 90 degrees to the right of the line direction.
79    /// Returns `None` if the line has zero length.
80    #[must_use]
81    pub fn inwards_normal(&self) -> Option<SvgPoint> {
82        let dx = self.end.x - self.start.x;
83        let dy = self.end.y - self.start.y;
84        let edge_length = dx.hypot(dy);
85        let x = -dy / edge_length;
86        let y = dx / edge_length;
87
88        if x.is_finite() && y.is_finite() {
89            Some(SvgPoint { x, y })
90        } else {
91            None
92        }
93    }
94
95    /// Computes the outward-facing normal vector for this line (opposite of `inwards_normal`).
96    #[must_use]
97    pub fn outwards_normal(&self) -> Option<SvgPoint> {
98        let inwards = self.inwards_normal()?;
99        Some(SvgPoint {
100            x: -inwards.x,
101            y: -inwards.y,
102        })
103    }
104
105    /// Reverses the direction of the line by swapping start and end points.
106    pub const fn reverse(&mut self) {
107        core::mem::swap(&mut self.start, &mut self.end);
108    }
109    /// Returns the start point of the line.
110    #[must_use]
111    pub const fn get_start(&self) -> SvgPoint {
112        self.start
113    }
114    /// Returns the end point of the line.
115    #[must_use]
116    pub const fn get_end(&self) -> SvgPoint {
117        self.end
118    }
119
120    /// Returns the parametric `t` value (0.0–1.0) at the given arc-length offset.
121    #[must_use]
122    pub fn get_t_at_offset(&self, offset: f64) -> f64 {
123        offset / self.get_length()
124    }
125
126    /// Returns the tangent vector of the line.
127    /// For a line, the tangent is constant (same direction everywhere),
128    /// so no `t` parameter is needed.
129    #[must_use]
130    pub fn get_tangent_vector_at_t(&self) -> SvgVector {
131        let dx = self.end.x - self.start.x;
132        let dy = self.end.y - self.start.y;
133        SvgVector {
134            x: f64::from(dx),
135            y: f64::from(dy),
136        }
137        .normalize()
138    }
139
140    /// Returns the X coordinate at parametric position `t` (0.0 = start, 1.0 = end).
141    #[allow(clippy::suboptimal_flops)] // mul_add not guaranteed faster/available without target +fma; keep explicit a*b+c
142    #[must_use]
143    pub fn get_x_at_t(&self, t: f64) -> f64 {
144        f64::from(self.start.x) + (f64::from(self.end.x) - f64::from(self.start.x)) * t
145    }
146
147    /// Returns the Y coordinate at parametric position `t` (0.0 = start, 1.0 = end).
148    #[allow(clippy::suboptimal_flops)] // mul_add not guaranteed faster/available without target +fma; keep explicit a*b+c
149    #[must_use]
150    pub fn get_y_at_t(&self, t: f64) -> f64 {
151        f64::from(self.start.y) + (f64::from(self.end.y) - f64::from(self.start.y)) * t
152    }
153
154    /// Returns the Euclidean length of the line segment.
155    #[must_use]
156    pub fn get_length(&self) -> f64 {
157        let dx = self.end.x - self.start.x;
158        let dy = self.end.y - self.start.y;
159        f64::from(libm::hypotf(dx, dy))
160    }
161
162    /// Returns the axis-aligned bounding rectangle of this line segment.
163    #[must_use]
164    pub fn get_bounds(&self) -> SvgRect {
165        let min_x = self.start.x.min(self.end.x);
166        let max_x = self.start.x.max(self.end.x);
167
168        let min_y = self.start.y.min(self.end.y);
169        let max_y = self.start.y.max(self.end.y);
170
171        let width = (max_x - min_x).abs();
172        let height = (max_y - min_y).abs();
173
174        SvgRect {
175            width,
176            height,
177            x: min_x,
178            y: min_y,
179            radius_top_left: 0.0,
180            radius_top_right: 0.0,
181            radius_bottom_left: 0.0,
182            radius_bottom_right: 0.0,
183        }
184    }
185}
186
187#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)]
188#[repr(C, u8)]
189pub enum SvgPathElement {
190    Line(SvgLine),
191    QuadraticCurve(SvgQuadraticCurve),
192    CubicCurve(SvgCubicCurve),
193}
194
195impl_option!(
196    SvgPathElement,
197    OptionSvgPathElement,
198    [Debug, Copy, Clone, PartialEq, PartialOrd]
199);
200
201impl SvgPathElement {
202    /// Creates a line path element from a `SvgLine`
203    #[inline]
204    #[must_use]
205    pub const fn line(l: SvgLine) -> Self {
206        Self::Line(l)
207    }
208
209    /// Creates a quadratic curve path element from a `SvgQuadraticCurve`
210    #[inline]
211    #[must_use]
212    pub const fn quadratic_curve(qc: SvgQuadraticCurve) -> Self {
213        Self::QuadraticCurve(qc)
214    }
215
216    /// Creates a cubic curve path element from a `SvgCubicCurve`
217    #[inline]
218    #[must_use]
219    pub const fn cubic_curve(cc: SvgCubicCurve) -> Self {
220        Self::CubicCurve(cc)
221    }
222
223    /// Sets the end point of this path element.
224    pub const fn set_last(&mut self, point: SvgPoint) {
225        match self {
226            Self::Line(l) => l.end = point,
227            Self::QuadraticCurve(qc) => qc.end = point,
228            Self::CubicCurve(cc) => cc.end = point,
229        }
230    }
231
232    /// Sets the start point of this path element.
233    pub const fn set_first(&mut self, point: SvgPoint) {
234        match self {
235            Self::Line(l) => l.start = point,
236            Self::QuadraticCurve(qc) => qc.start = point,
237            Self::CubicCurve(cc) => cc.start = point,
238        }
239    }
240
241    /// Reverses the direction of this path element.
242    pub const fn reverse(&mut self) {
243        match self {
244            Self::Line(l) => l.reverse(),
245            Self::QuadraticCurve(qc) => qc.reverse(),
246            Self::CubicCurve(cc) => cc.reverse(),
247        }
248    }
249    /// Returns the start point of this path element.
250    #[must_use]
251    pub const fn get_start(&self) -> SvgPoint {
252        match self {
253            Self::Line(l) => l.get_start(),
254            Self::QuadraticCurve(qc) => qc.get_start(),
255            Self::CubicCurve(cc) => cc.get_start(),
256        }
257    }
258    /// Returns the end point of this path element.
259    #[must_use]
260    pub const fn get_end(&self) -> SvgPoint {
261        match self {
262            Self::Line(l) => l.get_end(),
263            Self::QuadraticCurve(qc) => qc.get_end(),
264            Self::CubicCurve(cc) => cc.get_end(),
265        }
266    }
267    /// Returns the axis-aligned bounding rectangle of this path element.
268    #[must_use]
269    pub fn get_bounds(&self) -> SvgRect {
270        match self {
271            Self::Line(l) => l.get_bounds(),
272            Self::QuadraticCurve(qc) => qc.get_bounds(),
273            Self::CubicCurve(cc) => cc.get_bounds(),
274        }
275    }
276    /// Returns the arc length of this path element.
277    #[must_use]
278    pub fn get_length(&self) -> f64 {
279        match self {
280            Self::Line(l) => l.get_length(),
281            Self::QuadraticCurve(qc) => qc.get_length(),
282            Self::CubicCurve(cc) => cc.get_length(),
283        }
284    }
285    /// Returns the parametric `t` value at the given arc-length offset.
286    #[must_use]
287    pub fn get_t_at_offset(&self, offset: f64) -> f64 {
288        match self {
289            Self::Line(l) => l.get_t_at_offset(offset),
290            Self::QuadraticCurve(qc) => qc.get_t_at_offset(offset),
291            Self::CubicCurve(cc) => cc.get_t_at_offset(offset),
292        }
293    }
294    /// Returns the normalized tangent vector at parametric position `t`.
295    #[must_use]
296    pub fn get_tangent_vector_at_t(&self, t: f64) -> SvgVector {
297        match self {
298            Self::Line(l) => l.get_tangent_vector_at_t(),
299            Self::QuadraticCurve(qc) => qc.get_tangent_vector_at_t(t),
300            Self::CubicCurve(cc) => cc.get_tangent_vector_at_t(t),
301        }
302    }
303    /// Returns the X coordinate at parametric position `t`.
304    #[must_use]
305    pub fn get_x_at_t(&self, t: f64) -> f64 {
306        match self {
307            Self::Line(l) => l.get_x_at_t(t),
308            Self::QuadraticCurve(qc) => qc.get_x_at_t(t),
309            Self::CubicCurve(cc) => cc.get_x_at_t(t),
310        }
311    }
312    /// Returns the Y coordinate at parametric position `t`.
313    #[must_use]
314    pub fn get_y_at_t(&self, t: f64) -> f64 {
315        match self {
316            Self::Line(l) => l.get_y_at_t(t),
317            Self::QuadraticCurve(qc) => qc.get_y_at_t(t),
318            Self::CubicCurve(cc) => cc.get_y_at_t(t),
319        }
320    }
321}
322
323impl_vec!(
324    SvgPathElement,
325    SvgPathElementVec,
326    SvgPathElementVecDestructor,
327    SvgPathElementVecDestructorType,
328    SvgPathElementVecSlice,
329    OptionSvgPathElement
330);
331impl_vec_debug!(SvgPathElement, SvgPathElementVec);
332impl_vec_clone!(
333    SvgPathElement,
334    SvgPathElementVec,
335    SvgPathElementVecDestructor
336);
337impl_vec_partialeq!(SvgPathElement, SvgPathElementVec);
338impl_vec_partialord!(SvgPathElement, SvgPathElementVec);
339
340#[derive(Debug, Clone, PartialEq, PartialOrd)]
341#[repr(C)]
342pub struct SvgPath {
343    pub items: SvgPathElementVec,
344}
345
346impl_option!(
347    SvgPath,
348    OptionSvgPath,
349    copy = false,
350    [Debug, Clone, PartialEq, PartialOrd]
351);
352
353impl SvgPath {
354    /// Creates a new `SvgPath` from a vector of path elements
355    #[inline]
356    #[must_use]
357    pub const fn create(items: SvgPathElementVec) -> Self {
358        Self { items }
359    }
360
361    /// Returns the start point of the first element, or `None` if the path is empty.
362    #[must_use]
363    pub fn get_start(&self) -> Option<SvgPoint> {
364        self.items.as_ref().first().map(SvgPathElement::get_start)
365    }
366
367    /// Returns the end point of the last element, or `None` if the path is empty.
368    #[must_use]
369    pub fn get_end(&self) -> Option<SvgPoint> {
370        self.items.as_ref().last().map(SvgPathElement::get_end)
371    }
372
373    /// Closes the path by appending a line from the last point to the first point, if needed.
374    pub fn close(&mut self) {
375        let Some(first) = self.items.as_ref().first() else {
376            return;
377        };
378        let Some(last) = self.items.as_ref().last() else {
379            return;
380        };
381        if first.get_start() != last.get_end() {
382            let mut elements = self.items.as_slice().to_vec();
383            elements.push(SvgPathElement::Line(SvgLine {
384                start: last.get_end(),
385                end: first.get_start(),
386            }));
387            self.items = elements.into();
388        }
389    }
390
391    /// Returns `true` if the path's first start point equals its last end point.
392    #[must_use]
393    pub fn is_closed(&self) -> bool {
394        let first = self.items.as_ref().first();
395        let last = self.items.as_ref().last();
396        match (first, last) {
397            (Some(f), Some(l)) => (f.get_start() == l.get_end()),
398            _ => false,
399        }
400    }
401
402    /// Reverses the order and direction of all elements in the path.
403    pub fn reverse(&mut self) {
404        // swap self.items with a default vec
405        let mut vec = SvgPathElementVec::from_const_slice(&[]);
406        core::mem::swap(&mut vec, &mut self.items);
407        let mut vec = vec.into_library_owned_vec();
408
409        // reverse the order of items in the vec
410        vec.reverse();
411
412        // reverse the order inside the item itself
413        // i.e. swap line.start and line.end
414        for item in &mut vec {
415            item.reverse();
416        }
417
418        // swap back
419        let mut vec = SvgPathElementVec::from_vec(vec);
420        core::mem::swap(&mut vec, &mut self.items);
421    }
422
423    /// Joins another path onto the end of this one, interpolating the join point.
424    pub fn join_with(&mut self, mut path: Self) -> Option<()> {
425        let self_last_point = self.items.as_ref().last()?.get_end();
426        let other_start_point = path.items.as_ref().first()?.get_start();
427        let interpolated_join_point = SvgPoint {
428            x: f32::midpoint(self_last_point.x, other_start_point.x),
429            y: f32::midpoint(self_last_point.y, other_start_point.y),
430        };
431
432        // swap self.items with a default vec
433        let mut vec = SvgPathElementVec::from_const_slice(&[]);
434        core::mem::swap(&mut vec, &mut self.items);
435        let mut vec = vec.into_library_owned_vec();
436
437        let mut other = SvgPathElementVec::from_const_slice(&[]);
438        core::mem::swap(&mut other, &mut path.items);
439        let mut other = other.into_library_owned_vec();
440
441        let vec_len = vec.len() - 1;
442        vec.get_mut(vec_len)?.set_last(interpolated_join_point);
443        other.get_mut(0)?.set_first(interpolated_join_point);
444        vec.append(&mut other);
445
446        // swap back
447        let mut vec = SvgPathElementVec::from_vec(vec);
448        core::mem::swap(&mut vec, &mut self.items);
449
450        Some(())
451    }
452    /// Returns the axis-aligned bounding rectangle of the entire path.
453    #[must_use]
454    pub fn get_bounds(&self) -> SvgRect {
455        let mut first_bounds = match self.items.as_ref().first() {
456            Some(s) => s.get_bounds(),
457            None => return SvgRect::default(),
458        };
459
460        for mp in self.items.as_ref().iter().skip(1) {
461            let mp_bounds = mp.get_bounds();
462            first_bounds.union_with(&mp_bounds);
463        }
464
465        first_bounds
466    }
467}
468
469#[derive(Debug, Clone, PartialEq, PartialOrd)]
470#[repr(C)]
471pub struct SvgMultiPolygon {
472    /// NOTE: If a ring represents a hole, simply reverse the order of points
473    pub rings: SvgPathVec,
474}
475
476impl_option!(
477    SvgMultiPolygon,
478    OptionSvgMultiPolygon,
479    copy = false,
480    [Debug, Clone, PartialEq, PartialOrd]
481);
482
483impl SvgMultiPolygon {
484    /// How finely a curve is subdivided when the path is treated as an area.
485    ///
486    /// Sixteen segments per curve keeps the error under a tenth of a pixel for
487    /// any curve small enough to be clicked on, and the cost is a handful of
488    /// multiplies on a path that is already bounded by the shape it draws.
489    const FLATTEN_STEPS: usize = 16;
490
491    /// Is `(x, y)` inside this path, by the NONZERO winding rule?
492    ///
493    /// What a shape's own geometry means for input: an SVG element occupies a
494    /// rectangular BOX in layout, but the thing the user sees and aims at is
495    /// the path. Hit-testing the box makes the transparent corners of a
496    /// circular button clickable and, worse, makes them SHADOW whatever is
497    /// behind them - which is exactly the complaint a clip-path is supposed to
498    /// answer.
499    ///
500    /// Coordinates are in the path's own space; the caller maps the pointer
501    /// into it. Open subpaths are treated as closed, as SVG does when filling.
502    #[must_use]
503    pub fn contains_point(&self, x: f32, y: f32) -> bool {
504        let mut winding = 0i32;
505        for ring in self.rings.as_ref() {
506            let points = ring.flatten_to_points(Self::FLATTEN_STEPS);
507            if points.len() < 2 {
508                continue;
509            }
510            for i in 0..points.len() {
511                let a = points[i];
512                let b = points[(i + 1) % points.len()];
513                // Standard nonzero crossing test: count upward crossings to
514                // the right of the point positively, downward negatively.
515                if a.y <= y {
516                    if b.y > y && cross_sign(a, b, x, y) > 0.0 {
517                        winding += 1;
518                    }
519                } else if b.y <= y && cross_sign(a, b, x, y) < 0.0 {
520                    winding -= 1;
521                }
522            }
523        }
524        winding != 0
525    }
526
527    /// Creates a new `SvgMultiPolygon` from a vector of paths (rings)
528    /// NOTE: If a ring represents a hole, simply reverse the order of points
529    #[inline]
530    #[must_use]
531    pub const fn create(rings: SvgPathVec) -> Self {
532        Self { rings }
533    }
534
535    /// Returns the axis-aligned bounding rectangle of all rings in this multi-polygon.
536    #[must_use]
537    pub fn get_bounds(&self) -> SvgRect {
538        // Seed from the FIRST item found in ANY ring, not specifically rings[0].items[0]:
539        // an empty first ring used to make the old seed-or-bail return SvgRect::default()
540        // and silently drop every later ring's geometry.
541        let mut bounds: Option<SvgRect> = None;
542        for ring in &self.rings {
543            for item in &ring.items {
544                let item_bounds = item.get_bounds();
545                match &mut bounds {
546                    Some(b) => b.union_with(&item_bounds),
547                    None => bounds = Some(item_bounds),
548                }
549            }
550        }
551        // Empty polygon (no items in any ring) has zero-sized bounds at origin.
552        bounds.unwrap_or_default()
553    }
554}
555
556impl_vec!(
557    SvgPath,
558    SvgPathVec,
559    SvgPathVecDestructor,
560    SvgPathVecDestructorType,
561    SvgPathVecSlice,
562    OptionSvgPath
563);
564impl_vec_debug!(SvgPath, SvgPathVec);
565impl_vec_clone!(SvgPath, SvgPathVec, SvgPathVecDestructor);
566impl_vec_partialeq!(SvgPath, SvgPathVec);
567impl_vec_partialord!(SvgPath, SvgPathVec);
568
569impl_vec!(
570    SvgMultiPolygon,
571    SvgMultiPolygonVec,
572    SvgMultiPolygonVecDestructor,
573    SvgMultiPolygonVecDestructorType,
574    SvgMultiPolygonVecSlice,
575    OptionSvgMultiPolygon
576);
577impl_vec_debug!(SvgMultiPolygon, SvgMultiPolygonVec);
578impl_vec_clone!(
579    SvgMultiPolygon,
580    SvgMultiPolygonVec,
581    SvgMultiPolygonVecDestructor
582);
583impl_vec_partialeq!(SvgMultiPolygon, SvgMultiPolygonVec);
584impl_vec_partialord!(SvgMultiPolygon, SvgMultiPolygonVec);
585
586/// One `SvgNode` corresponds to one SVG `<path></path>` element
587#[derive(Debug, Clone, PartialOrd, PartialEq)]
588#[repr(C, u8)]
589pub enum SvgNode {
590    /// Multiple multipolygons, merged to one CPU buf for efficient drawing
591    MultiPolygonCollection(SvgMultiPolygonVec),
592    MultiPolygon(SvgMultiPolygon),
593    MultiShape(SvgSimpleNodeVec),
594    Path(SvgPath),
595    Circle(SvgCircle),
596    Rect(SvgRect),
597}
598
599/// One `SvgSimpleNode` is either a path, a rect or a circle
600#[derive(Debug, Clone, PartialOrd, PartialEq)]
601#[repr(C, u8)]
602pub enum SvgSimpleNode {
603    Path(SvgPath),
604    Circle(SvgCircle),
605    Rect(SvgRect),
606    CircleHole(SvgCircle),
607    RectHole(SvgRect),
608}
609
610impl_option!(
611    SvgSimpleNode,
612    OptionSvgSimpleNode,
613    copy = false,
614    [Debug, Clone, PartialOrd, PartialEq]
615);
616
617impl_vec!(
618    SvgSimpleNode,
619    SvgSimpleNodeVec,
620    SvgSimpleNodeVecDestructor,
621    SvgSimpleNodeVecDestructorType,
622    SvgSimpleNodeVecSlice,
623    OptionSvgSimpleNode
624);
625impl_vec_debug!(SvgSimpleNode, SvgSimpleNodeVec);
626impl_vec_clone!(SvgSimpleNode, SvgSimpleNodeVec, SvgSimpleNodeVecDestructor);
627impl_vec_partialeq!(SvgSimpleNode, SvgSimpleNodeVec);
628impl_vec_partialord!(SvgSimpleNode, SvgSimpleNodeVec);
629
630impl SvgSimpleNode {
631    /// Returns the axis-aligned bounding rectangle of this node.
632    // Same-body arms dispatch on differently-typed bindings (SvgPath vs SvgCircle),
633    // so the identical `a.get_bounds()` bodies cannot be combined into one or-pattern.
634    #[allow(clippy::match_same_arms)]
635    #[must_use]
636    pub fn get_bounds(&self) -> SvgRect {
637        match self {
638            Self::Path(a) => a.get_bounds(),
639            Self::Circle(a) => a.get_bounds(),
640            Self::Rect(a) => *a,
641            Self::CircleHole(a) => a.get_bounds(),
642            Self::RectHole(a) => *a,
643        }
644    }
645    /// Returns `true` if this node represents a closed shape.
646    #[must_use]
647    pub fn is_closed(&self) -> bool {
648        match self {
649            Self::Path(a) => a.is_closed(),
650            Self::Circle(_) | Self::Rect(_) | Self::CircleHole(_) | Self::RectHole(_) => true,
651        }
652    }
653}
654
655impl SvgNode {
656    /// Returns the axis-aligned bounding rectangle of this SVG node.
657    #[must_use]
658    pub fn get_bounds(&self) -> SvgRect {
659        match self {
660            Self::MultiPolygonCollection(a) => {
661                let mut first_mp_bounds = match a.get(0) {
662                    Some(s) => s.get_bounds(),
663                    None => return SvgRect::default(),
664                };
665                for mp in a.iter().skip(1) {
666                    let mp_bounds = mp.get_bounds();
667                    first_mp_bounds.union_with(&mp_bounds);
668                }
669
670                first_mp_bounds
671            }
672            Self::MultiPolygon(a) => a.get_bounds(),
673            Self::MultiShape(a) => {
674                let mut first_mp_bounds = match a.get(0) {
675                    Some(s) => s.get_bounds(),
676                    None => return SvgRect::default(),
677                };
678                for mp in a.iter().skip(1) {
679                    let mp_bounds = mp.get_bounds();
680                    first_mp_bounds.union_with(&mp_bounds);
681                }
682
683                first_mp_bounds
684            }
685            Self::Path(a) => a.get_bounds(),
686            Self::Circle(a) => a.get_bounds(),
687            Self::Rect(a) => *a,
688        }
689    }
690    /// Returns `true` if all sub-paths in this node are closed.
691    #[must_use]
692    pub fn is_closed(&self) -> bool {
693        match self {
694            Self::MultiPolygonCollection(a) => {
695                for mp in a {
696                    for p in mp.rings.as_ref() {
697                        if !p.is_closed() {
698                            return false;
699                        }
700                    }
701                }
702
703                true
704            }
705            Self::MultiPolygon(a) => {
706                for p in a.rings.as_ref() {
707                    if !p.is_closed() {
708                        return false;
709                    }
710                }
711
712                true
713            }
714            Self::MultiShape(a) => {
715                for p in a.as_ref() {
716                    if !p.is_closed() {
717                        return false;
718                    }
719                }
720
721                true
722            }
723            Self::Path(a) => a.is_closed(),
724            Self::Circle(_) | Self::Rect(_) => true,
725        }
726    }
727}
728
729/// An SVG node paired with its visual style (fill or stroke).
730#[derive(Debug, Clone, PartialOrd, PartialEq)]
731#[repr(C)]
732pub struct SvgStyledNode {
733    pub geometry: SvgNode,
734    pub style: SvgStyle,
735}
736
737/// A 2D vertex used in tessellated SVG geometry.
738#[derive(Debug, Copy, Clone, PartialOrd, PartialEq)]
739#[repr(C)]
740pub struct SvgVertex {
741    pub x: f32,
742    pub y: f32,
743}
744
745impl_option!(
746    SvgVertex,
747    OptionSvgVertex,
748    [Debug, Copy, Clone, PartialOrd, PartialEq]
749);
750
751impl VertexLayoutDescription for SvgVertex {
752    fn get_description() -> VertexLayout {
753        VertexLayout {
754            fields: vec![VertexAttribute {
755                va_name: String::from("vAttrXY").into(),
756                layout_location: None.into(),
757                attribute_type: VertexAttributeType::Float,
758                item_count: 2,
759            }]
760            .into(),
761        }
762    }
763}
764
765/// A 3D vertex with per-vertex RGBA color, used in multi-colored SVG tessellation.
766#[derive(Debug, Copy, Clone, PartialOrd, PartialEq)]
767#[repr(C)]
768pub struct SvgColoredVertex {
769    pub x: f32,
770    pub y: f32,
771    pub z: f32,
772    pub r: f32,
773    pub g: f32,
774    pub b: f32,
775    pub a: f32,
776}
777
778impl_option!(
779    SvgColoredVertex,
780    OptionSvgColoredVertex,
781    [Debug, Copy, Clone, PartialOrd, PartialEq]
782);
783
784impl VertexLayoutDescription for SvgColoredVertex {
785    fn get_description() -> VertexLayout {
786        VertexLayout {
787            fields: vec![
788                VertexAttribute {
789                    va_name: String::from("vAttrXY").into(),
790                    layout_location: None.into(),
791                    attribute_type: VertexAttributeType::Float,
792                    item_count: 3,
793                },
794                VertexAttribute {
795                    va_name: String::from("vColor").into(),
796                    layout_location: None.into(),
797                    attribute_type: VertexAttributeType::Float,
798                    item_count: 4,
799                },
800            ]
801            .into(),
802        }
803    }
804}
805
806/// A circle defined by center coordinates and radius.
807#[derive(Debug, Copy, Clone, PartialOrd, PartialEq)]
808#[repr(C)]
809pub struct SvgCircle {
810    pub center_x: f32,
811    pub center_y: f32,
812    pub radius: f32,
813}
814
815/// Which side of the segment `a -> b` the point `(x, y)` is on.
816///
817/// Positive = left. Used by the winding test; a separate function because the
818/// sign convention is the whole subtlety and naming it makes the two branches
819/// above readable.
820#[allow(clippy::suboptimal_flops)] // mul_add not guaranteed faster/available without target +fma
821fn cross_sign(a: azul_css::props::basic::SvgPoint, b: azul_css::props::basic::SvgPoint, x: f32, y: f32) -> f32 {
822    (b.x - a.x) * (y - a.y) - (x - a.x) * (b.y - a.y)
823}
824
825impl SvgPath {
826    /// Flatten this ring into a polyline, subdividing every curve into
827    /// `steps` segments.
828    ///
829    /// Deliberately not de-duplicating shared endpoints: the winding test
830    /// walks the list cyclically and a repeated point contributes a
831    /// zero-length segment, which crosses nothing.
832    #[must_use]
833    pub fn flatten_to_points(&self, steps: usize) -> alloc::vec::Vec<azul_css::props::basic::SvgPoint> {
834        use azul_css::props::basic::SvgPoint;
835
836        let steps = steps.max(1);
837        let mut out: alloc::vec::Vec<SvgPoint> = alloc::vec::Vec::new();
838        let mut push = |p: SvgPoint| {
839            if out.last().is_none_or(|last| {
840                (last.x - p.x).abs() > f32::EPSILON || (last.y - p.y).abs() > f32::EPSILON
841            }) {
842                out.push(p);
843            }
844        };
845        for item in self.items.as_ref() {
846            match item {
847                SvgPathElement::Line(l) => {
848                    push(l.start);
849                    push(l.end);
850                }
851                SvgPathElement::QuadraticCurve(q) => {
852                    push(q.start);
853                    for i in 1..=steps {
854                        #[allow(clippy::cast_precision_loss)] // steps is <= 64
855                        let t = i as f32 / steps as f32;
856                        let inv = 1.0 - t;
857                        push(SvgPoint {
858                            x: inv * inv * q.start.x + 2.0 * inv * t * q.ctrl.x + t * t * q.end.x,
859                            y: inv * inv * q.start.y + 2.0 * inv * t * q.ctrl.y + t * t * q.end.y,
860                        });
861                    }
862                }
863                SvgPathElement::CubicCurve(c) => {
864                    push(c.start);
865                    for i in 1..=steps {
866                        #[allow(clippy::cast_precision_loss)] // steps is <= 64
867                        let t = i as f32 / steps as f32;
868                        let inv = 1.0 - t;
869                        let (a, b, cc, d) = (
870                            inv * inv * inv,
871                            3.0 * inv * inv * t,
872                            3.0 * inv * t * t,
873                            t * t * t,
874                        );
875                        push(SvgPoint {
876                            x: a * c.start.x + b * c.ctrl_1.x + cc * c.ctrl_2.x + d * c.end.x,
877                            y: a * c.start.y + b * c.ctrl_1.y + cc * c.ctrl_2.y + d * c.end.y,
878                        });
879                    }
880                }
881            }
882        }
883        out
884    }
885}
886
887impl SvgCircle {
888    /// Returns `true` if the given point lies inside the circle.
889    #[allow(clippy::suboptimal_flops)] // mul_add not guaranteed faster/available without target +fma; keep explicit a*b+c
890    #[must_use]
891    pub fn contains_point(&self, x: f32, y: f32) -> bool {
892        let x_diff = libm::fabsf(x - self.center_x);
893        let y_diff = libm::fabsf(y - self.center_y);
894        (x_diff * x_diff) + (y_diff * y_diff) < (self.radius * self.radius)
895    }
896    /// Returns the axis-aligned bounding rectangle of this circle.
897    #[must_use]
898    pub fn get_bounds(&self) -> SvgRect {
899        SvgRect {
900            width: self.radius * 2.0,
901            height: self.radius * 2.0,
902            x: self.center_x - self.radius,
903            y: self.center_y - self.radius,
904            radius_top_left: 0.0,
905            radius_top_right: 0.0,
906            radius_bottom_left: 0.0,
907            radius_bottom_right: 0.0,
908        }
909    }
910}
911
912#[derive(Debug, Clone, PartialEq, PartialOrd)]
913#[repr(C)]
914pub struct TessellatedSvgNode {
915    pub vertices: SvgVertexVec,
916    pub indices: U32Vec,
917}
918
919impl_option!(
920    TessellatedSvgNode,
921    OptionTessellatedSvgNode,
922    copy = false,
923    [Debug, Clone, PartialEq, PartialOrd]
924);
925
926impl Default for TessellatedSvgNode {
927    fn default() -> Self {
928        Self {
929            vertices: Vec::new().into(),
930            indices: Vec::new().into(),
931        }
932    }
933}
934
935impl_vec!(
936    TessellatedSvgNode,
937    TessellatedSvgNodeVec,
938    TessellatedSvgNodeVecDestructor,
939    TessellatedSvgNodeVecDestructorType,
940    TessellatedSvgNodeVecSlice,
941    OptionTessellatedSvgNode
942);
943impl_vec_debug!(TessellatedSvgNode, TessellatedSvgNodeVec);
944impl_vec_partialord!(TessellatedSvgNode, TessellatedSvgNodeVec);
945impl_vec_clone!(
946    TessellatedSvgNode,
947    TessellatedSvgNodeVec,
948    TessellatedSvgNodeVecDestructor
949);
950impl_vec_partialeq!(TessellatedSvgNode, TessellatedSvgNodeVec);
951
952impl TessellatedSvgNode {
953    #[must_use]
954    pub fn empty() -> Self {
955        Self::default()
956    }
957}
958
959impl TessellatedSvgNodeVec {
960    #[must_use]
961    pub fn get_ref(&self) -> TessellatedSvgNodeVecRef {
962        let slice = self.as_ref();
963        TessellatedSvgNodeVecRef {
964            ptr: slice.as_ptr(),
965            len: slice.len(),
966        }
967    }
968}
969
970impl fmt::Debug for TessellatedSvgNodeVecRef {
971    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
972        self.as_slice().fmt(f)
973    }
974}
975
976// C ABI wrapper over &[TessellatedSvgNode]
977#[repr(C)]
978pub struct TessellatedSvgNodeVecRef {
979    pub ptr: *const TessellatedSvgNode,
980    pub len: usize,
981}
982
983impl Clone for TessellatedSvgNodeVecRef {
984    fn clone(&self) -> Self {
985        Self {
986            ptr: self.ptr,
987            len: self.len,
988        }
989    }
990}
991
992impl TessellatedSvgNodeVecRef {
993    #[must_use]
994    pub const fn as_slice(&self) -> &[TessellatedSvgNode] {
995        unsafe { core::slice::from_raw_parts(self.ptr, self.len) }
996    }
997}
998
999#[derive(Debug, Clone, PartialEq, PartialOrd)]
1000#[repr(C)]
1001pub struct TessellatedColoredSvgNode {
1002    pub vertices: SvgColoredVertexVec,
1003    pub indices: U32Vec,
1004}
1005
1006impl_option!(
1007    TessellatedColoredSvgNode,
1008    OptionTessellatedColoredSvgNode,
1009    copy = false,
1010    [Debug, Clone, PartialEq, PartialOrd]
1011);
1012
1013impl Default for TessellatedColoredSvgNode {
1014    fn default() -> Self {
1015        Self {
1016            vertices: Vec::new().into(),
1017            indices: Vec::new().into(),
1018        }
1019    }
1020}
1021
1022impl_vec!(
1023    TessellatedColoredSvgNode,
1024    TessellatedColoredSvgNodeVec,
1025    TessellatedColoredSvgNodeVecDestructor,
1026    TessellatedColoredSvgNodeVecDestructorType,
1027    TessellatedColoredSvgNodeVecSlice,
1028    OptionTessellatedColoredSvgNode
1029);
1030impl_vec_debug!(TessellatedColoredSvgNode, TessellatedColoredSvgNodeVec);
1031impl_vec_partialord!(TessellatedColoredSvgNode, TessellatedColoredSvgNodeVec);
1032impl_vec_clone!(
1033    TessellatedColoredSvgNode,
1034    TessellatedColoredSvgNodeVec,
1035    TessellatedColoredSvgNodeVecDestructor
1036);
1037impl_vec_partialeq!(TessellatedColoredSvgNode, TessellatedColoredSvgNodeVec);
1038
1039impl TessellatedColoredSvgNode {
1040    #[must_use]
1041    pub fn empty() -> Self {
1042        Self::default()
1043    }
1044}
1045
1046impl TessellatedColoredSvgNodeVec {
1047    #[must_use]
1048    pub fn get_ref(&self) -> TessellatedColoredSvgNodeVecRef {
1049        let slice = self.as_ref();
1050        TessellatedColoredSvgNodeVecRef {
1051            ptr: slice.as_ptr(),
1052            len: slice.len(),
1053        }
1054    }
1055}
1056
1057impl fmt::Debug for TessellatedColoredSvgNodeVecRef {
1058    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1059        self.as_slice().fmt(f)
1060    }
1061}
1062
1063// C ABI wrapper over &[TessellatedColoredSvgNode]
1064#[repr(C)]
1065pub struct TessellatedColoredSvgNodeVecRef {
1066    pub ptr: *const TessellatedColoredSvgNode,
1067    pub len: usize,
1068}
1069
1070impl Clone for TessellatedColoredSvgNodeVecRef {
1071    fn clone(&self) -> Self {
1072        Self {
1073            ptr: self.ptr,
1074            len: self.len,
1075        }
1076    }
1077}
1078
1079impl TessellatedColoredSvgNodeVecRef {
1080    #[must_use]
1081    pub const fn as_slice(&self) -> &[TessellatedColoredSvgNode] {
1082        unsafe { core::slice::from_raw_parts(self.ptr, self.len) }
1083    }
1084}
1085
1086impl_vec!(
1087    SvgVertex,
1088    SvgVertexVec,
1089    SvgVertexVecDestructor,
1090    SvgVertexVecDestructorType,
1091    SvgVertexVecSlice,
1092    OptionSvgVertex
1093);
1094impl_vec_debug!(SvgVertex, SvgVertexVec);
1095impl_vec_partialord!(SvgVertex, SvgVertexVec);
1096impl_vec_clone!(SvgVertex, SvgVertexVec, SvgVertexVecDestructor);
1097impl_vec_partialeq!(SvgVertex, SvgVertexVec);
1098
1099impl_vec!(
1100    SvgColoredVertex,
1101    SvgColoredVertexVec,
1102    SvgColoredVertexVecDestructor,
1103    SvgColoredVertexVecDestructorType,
1104    SvgColoredVertexVecSlice,
1105    OptionSvgColoredVertex
1106);
1107impl_vec_debug!(SvgColoredVertex, SvgColoredVertexVec);
1108impl_vec_partialord!(SvgColoredVertex, SvgColoredVertexVec);
1109impl_vec_clone!(
1110    SvgColoredVertex,
1111    SvgColoredVertexVec,
1112    SvgColoredVertexVecDestructor
1113);
1114impl_vec_partialeq!(SvgColoredVertex, SvgColoredVertexVec);
1115
1116/// Computes the bbox size and transform matrix uniforms shared by SVG draw methods.
1117///
1118/// Converts `StyleTransform` list into column-major `[f32; 16]` for OpenGL,
1119/// and packages it along with the bbox size uniform.
1120// target_size is physical pixel dimensions (u32); GL uniforms are f32. Pixel
1121// counts are always well within f32's exact-integer range (2^24), so the
1122// precision loss the lint warns about cannot occur for any real render target.
1123#[allow(clippy::cast_precision_loss)]
1124fn compute_svg_transform_uniforms(
1125    target_size: PhysicalSizeU32,
1126    transforms: &[StyleTransform],
1127) -> (Uniform, Uniform) {
1128    let transform_origin = StyleTransformOrigin {
1129        x: PixelValue::px(target_size.width as f32 / 2.0),
1130        y: PixelValue::px(target_size.height as f32 / 2.0),
1131    };
1132
1133    let computed_transform = ComputedTransform3D::from_style_transform_vec(
1134        transforms,
1135        &transform_origin,
1136        target_size.width as f32,
1137        target_size.height as f32,
1138        RotationMode::ForWebRender,
1139    );
1140
1141    // NOTE: OpenGL draws are column-major, while ComputedTransform3D
1142    // is row-major! Need to transpose the matrix!
1143    let m = computed_transform.get_column_major().m;
1144    let matrix: [f32; 16] = core::array::from_fn(|i| m[i / 4][i % 4]);
1145
1146    let bbox_uniform = Uniform {
1147        uniform_name: "vBboxSize".into(),
1148        uniform_type: UniformType::FloatVec2([target_size.width as f32, target_size.height as f32]),
1149    };
1150
1151    let transform_uniform = Uniform {
1152        uniform_name: "vTransformMatrix".into(),
1153        uniform_type: UniformType::Matrix4 {
1154            transpose: false,
1155            matrix,
1156        },
1157    };
1158
1159    (bbox_uniform, transform_uniform)
1160}
1161
1162#[derive(Debug, Clone, PartialEq, Eq, PartialOrd)]
1163#[repr(C)]
1164pub struct TessellatedGPUSvgNode {
1165    pub vertex_index_buffer: VertexBuffer,
1166}
1167
1168impl TessellatedGPUSvgNode {
1169    /// Uploads the tesselated SVG node to GPU memory
1170    #[must_use]
1171    pub fn new(node: &TessellatedSvgNode, gl: GlContextPtr) -> Self {
1172        let svg_shader_id = gl.ptr.svg_shader;
1173        Self {
1174            vertex_index_buffer: VertexBuffer::new(
1175                gl,
1176                svg_shader_id,
1177                node.vertices.as_ref(),
1178                node.indices.as_ref(),
1179                IndexBufferFormat::Triangles,
1180            ),
1181        }
1182    }
1183
1184    /// Draw the vertex buffer to the texture with the given color and transform
1185    #[allow(clippy::needless_pass_by_value)] // owned azul value taken by value (public API / ownership-transfer convention)
1186    pub fn draw(
1187        &self,
1188        texture: &mut Texture,
1189        target_size: PhysicalSizeU32,
1190        color: ColorU,
1191        transforms: StyleTransformVec,
1192    ) -> bool {
1193        let (bbox_uniform, transform_uniform) =
1194            compute_svg_transform_uniforms(target_size, transforms.as_ref());
1195
1196        let color: ColorF = color.into();
1197
1198        let uniforms = [
1199            bbox_uniform,
1200            Uniform {
1201                uniform_name: "fDrawColor".into(),
1202                uniform_type: UniformType::FloatVec4([color.r, color.g, color.b, color.a]),
1203            },
1204            transform_uniform,
1205        ];
1206
1207        GlShader::draw(
1208            texture.gl_context.ptr.svg_shader,
1209            texture,
1210            &[(&self.vertex_index_buffer, &uniforms[..])],
1211        );
1212
1213        true
1214    }
1215}
1216
1217#[derive(Debug, Clone, PartialEq, Eq, PartialOrd)]
1218#[repr(C)]
1219pub struct TessellatedColoredGPUSvgNode {
1220    pub vertex_index_buffer: VertexBuffer,
1221}
1222
1223impl TessellatedColoredGPUSvgNode {
1224    /// Uploads the tesselated SVG node to GPU memory
1225    #[must_use]
1226    pub fn new(node: &TessellatedColoredSvgNode, gl: GlContextPtr) -> Self {
1227        let svg_shader_id = gl.ptr.svg_multicolor_shader;
1228        Self {
1229            vertex_index_buffer: VertexBuffer::new(
1230                gl,
1231                svg_shader_id,
1232                node.vertices.as_ref(),
1233                node.indices.as_ref(),
1234                IndexBufferFormat::Triangles,
1235            ),
1236        }
1237    }
1238
1239    /// Draw the vertex buffer to the texture with the given color and transform
1240    #[allow(clippy::needless_pass_by_value)] // owned azul value taken by value (public API / ownership-transfer convention)
1241    pub fn draw(
1242        &self,
1243        texture: &mut Texture,
1244        target_size: PhysicalSizeU32,
1245        transforms: StyleTransformVec,
1246    ) -> bool {
1247        let (bbox_uniform, transform_uniform) =
1248            compute_svg_transform_uniforms(target_size, transforms.as_ref());
1249
1250        // two separately-named GL uniforms collected into the draw-call array;
1251        // not a tuple->array conversion.
1252        #[allow(clippy::tuple_array_conversions)]
1253        let uniforms = [bbox_uniform, transform_uniform];
1254
1255        GlShader::draw(
1256            texture.gl_context.ptr.svg_multicolor_shader,
1257            texture,
1258            &[(&self.vertex_index_buffer, &uniforms[..])],
1259        );
1260
1261        true
1262    }
1263}
1264
1265#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)]
1266#[repr(C, u8)]
1267pub enum SvgStyle {
1268    Fill(SvgFillStyle),
1269    Stroke(SvgStrokeStyle),
1270}
1271
1272impl SvgStyle {
1273    #[must_use]
1274    pub const fn get_antialias(&self) -> bool {
1275        match self {
1276            Self::Fill(f) => f.anti_alias,
1277            Self::Stroke(s) => s.anti_alias,
1278        }
1279    }
1280    #[must_use]
1281    pub const fn get_high_quality_aa(&self) -> bool {
1282        match self {
1283            Self::Fill(f) => f.high_quality_aa,
1284            Self::Stroke(s) => s.high_quality_aa,
1285        }
1286    }
1287    #[must_use]
1288    pub const fn get_transform(&self) -> SvgTransform {
1289        match self {
1290            Self::Fill(f) => f.transform,
1291            Self::Stroke(s) => s.transform,
1292        }
1293    }
1294}
1295/// SVG fill rule for determining the interior of a shape.
1296#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd)]
1297#[repr(C)]
1298#[derive(Default)]
1299pub enum SvgFillRule {
1300    #[default]
1301    Winding,
1302    EvenOdd,
1303}
1304
1305#[derive(Default, Debug, Copy, Clone, PartialEq, PartialOrd)]
1306#[repr(C)]
1307pub struct SvgTransform {
1308    pub sx: f32,
1309    pub kx: f32,
1310    pub ky: f32,
1311    pub sy: f32,
1312    pub tx: f32,
1313    pub ty: f32,
1314}
1315
1316#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)]
1317#[repr(C)]
1318pub struct SvgFillStyle {
1319    /// See the SVG specification.
1320    ///
1321    /// Default value: `LineJoin::Miter`.
1322    pub line_join: SvgLineJoin,
1323    /// See the SVG specification.
1324    ///
1325    /// Must be greater than or equal to 1.0.
1326    /// Default value: `StrokeOptions::DEFAULT_MITER_LIMIT`.
1327    pub miter_limit: f32,
1328    /// Maximum allowed distance to the path when building an approximation.
1329    ///
1330    /// See [Flattening and tolerance](index.html#flattening-and-tolerance).
1331    /// Default value: `StrokeOptions::DEFAULT_TOLERANCE`.
1332    pub tolerance: f32,
1333    /// Whether to use the "winding" or "even / odd" fill rule when tesselating the path
1334    pub fill_rule: SvgFillRule,
1335    /// Whether to apply a transform to the points in the path (warning: will be done on the CPU -
1336    /// expensive)
1337    pub transform: SvgTransform,
1338    /// Whether the fill is intended to be anti-aliased (default: true)
1339    pub anti_alias: bool,
1340    /// Whether the anti-aliasing has to be of high quality (default: false)
1341    pub high_quality_aa: bool,
1342}
1343
1344impl Default for SvgFillStyle {
1345    fn default() -> Self {
1346        Self {
1347            line_join: SvgLineJoin::Miter,
1348            miter_limit: DEFAULT_MITER_LIMIT,
1349            tolerance: DEFAULT_TOLERANCE,
1350            fill_rule: SvgFillRule::default(),
1351            transform: SvgTransform::default(),
1352            anti_alias: true,
1353            high_quality_aa: false,
1354        }
1355    }
1356}
1357
1358#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)]
1359#[repr(C)]
1360pub struct SvgStrokeStyle {
1361    /// Dash pattern
1362    pub dash_pattern: OptionSvgDashPattern,
1363    /// Whether to apply a transform to the points in the path (warning: will be done on the CPU -
1364    /// expensive)
1365    pub transform: SvgTransform,
1366    /// What cap to use at the start of each sub-path.
1367    ///
1368    /// Default value: `LineCap::Butt`.
1369    pub start_cap: SvgLineCap,
1370    /// What cap to use at the end of each sub-path.
1371    ///
1372    /// Default value: `LineCap::Butt`.
1373    pub end_cap: SvgLineCap,
1374    /// See the SVG specification.
1375    ///
1376    /// Default value: `LineJoin::Miter`.
1377    pub line_join: SvgLineJoin,
1378    /// Line width
1379    ///
1380    /// Default value: `StrokeOptions::DEFAULT_LINE_WIDTH`.
1381    pub line_width: f32,
1382    /// See the SVG specification.
1383    ///
1384    /// Must be greater than or equal to 1.0.
1385    /// Default value: `StrokeOptions::DEFAULT_MITER_LIMIT`.
1386    pub miter_limit: f32,
1387    /// Maximum allowed distance to the path when building an approximation.
1388    ///
1389    /// See [Flattening and tolerance](index.html#flattening-and-tolerance).
1390    /// Default value: `StrokeOptions::DEFAULT_TOLERANCE`.
1391    pub tolerance: f32,
1392    /// Apply line width
1393    ///
1394    /// When set to false, the generated vertices will all be positioned in the centre
1395    /// of the line. The width can be applied later on (eg in a vertex shader) by adding
1396    /// the vertex normal multiplied by the line with to each vertex position.
1397    ///
1398    /// Default value: `true`. NOTE: currently unused!
1399    pub apply_line_width: bool,
1400    /// Whether the fill is intended to be anti-aliased (default: true)
1401    pub anti_alias: bool,
1402    /// Whether the anti-aliasing has to be of high quality (default: false)
1403    pub high_quality_aa: bool,
1404}
1405
1406impl Default for SvgStrokeStyle {
1407    fn default() -> Self {
1408        Self {
1409            dash_pattern: OptionSvgDashPattern::None,
1410            transform: SvgTransform::default(),
1411            start_cap: SvgLineCap::default(),
1412            end_cap: SvgLineCap::default(),
1413            line_join: SvgLineJoin::default(),
1414            line_width: DEFAULT_LINE_WIDTH,
1415            miter_limit: DEFAULT_MITER_LIMIT,
1416            tolerance: DEFAULT_TOLERANCE,
1417            apply_line_width: true,
1418            anti_alias: true,
1419            high_quality_aa: false,
1420        }
1421    }
1422}
1423
1424#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)]
1425#[repr(C)]
1426pub struct SvgDashPattern {
1427    pub offset: f32,
1428    pub length_1: f32,
1429    pub gap_1: f32,
1430    pub length_2: f32,
1431    pub gap_2: f32,
1432    pub length_3: f32,
1433    pub gap_3: f32,
1434}
1435
1436impl_option!(
1437    SvgDashPattern,
1438    OptionSvgDashPattern,
1439    [Debug, Copy, Clone, PartialEq, PartialOrd]
1440);
1441
1442/// The shape used at the end of open sub-paths when they are stroked.
1443#[derive(Debug, Copy, Clone, PartialEq, Hash, Eq, PartialOrd, Ord)]
1444#[repr(C)]
1445#[derive(Default)]
1446pub enum SvgLineCap {
1447    #[default]
1448    Butt,
1449    Square,
1450    Round,
1451}
1452
1453/// The shape used at the corners of stroked paths.
1454#[derive(Debug, Copy, Clone, PartialEq, Hash, Eq, PartialOrd, Ord)]
1455#[repr(C)]
1456#[derive(Default)]
1457pub enum SvgLineJoin {
1458    #[default]
1459    Miter,
1460    MiterClip,
1461    Round,
1462    Bevel,
1463}
1464
1465pub use core::ffi::c_void;
1466
1467#[derive(Debug, Clone)]
1468#[repr(C)]
1469pub struct SvgXmlNode {
1470    pub node: *const c_void, // usvg::Node
1471    pub run_destructor: bool,
1472}
1473
1474#[derive(Debug, Clone)]
1475#[repr(C)]
1476pub struct Svg {
1477    pub tree: *const c_void, // *mut usvg::Tree,
1478    pub run_destructor: bool,
1479}
1480
1481/// SVG `shape-rendering` property controlling quality vs speed tradeoffs.
1482#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
1483#[repr(C)]
1484pub enum ShapeRendering {
1485    OptimizeSpeed,
1486    CrispEdges,
1487    GeometricPrecision,
1488}
1489
1490/// SVG `image-rendering` property controlling image quality vs speed.
1491#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
1492#[repr(C)]
1493pub enum ImageRendering {
1494    OptimizeQuality,
1495    OptimizeSpeed,
1496}
1497
1498/// SVG `text-rendering` property controlling text quality vs speed.
1499#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
1500#[repr(C)]
1501pub enum TextRendering {
1502    OptimizeSpeed,
1503    OptimizeLegibility,
1504    GeometricPrecision,
1505}
1506
1507/// Font database source for SVG text rendering.
1508#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
1509#[repr(C)]
1510pub enum FontDatabase {
1511    Empty,
1512    System,
1513}
1514
1515#[derive(Debug, Default, Copy, Clone, PartialEq, PartialOrd)]
1516#[repr(C)]
1517pub struct SvgRenderOptions {
1518    pub target_size: OptionLayoutSize,
1519    pub background_color: OptionColorU,
1520    pub fit: SvgFitTo,
1521    pub transform: SvgRenderTransform,
1522}
1523
1524#[derive(Debug, Default, Copy, Clone, PartialEq, PartialOrd)]
1525#[repr(C)]
1526pub struct SvgRenderTransform {
1527    pub sx: f32,
1528    pub kx: f32,
1529    pub ky: f32,
1530    pub sy: f32,
1531    pub tx: f32,
1532    pub ty: f32,
1533}
1534
1535#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)]
1536#[repr(C, u8)]
1537#[derive(Default)]
1538pub enum SvgFitTo {
1539    #[default]
1540    Original,
1541    Width(u32),
1542    Height(u32),
1543    Zoom(f32),
1544}
1545
1546#[derive(Debug, Clone, PartialEq, PartialOrd)]
1547#[repr(C)]
1548pub struct SvgParseOptions {
1549    /// SVG image path. Used to resolve relative image paths.
1550    pub relative_image_path: OptionString,
1551    /// Default font family. Will be used when no font-family attribute is set in the SVG. Default:
1552    /// Times New Roman
1553    pub default_font_family: AzString,
1554    /// A list of languages. Will be used to resolve a systemLanguage conditional attribute.
1555    /// Format: en, en-US. Default: [en]
1556    pub languages: StringVec,
1557    /// Target DPI. Impact units conversion. Default: 96.0
1558    pub dpi: f32,
1559    /// A default font size. Will be used when no font-size attribute is set in the SVG. Default:
1560    /// 12
1561    pub font_size: f32,
1562    /// Specifies the default shape rendering method. Will be used when an SVG element's
1563    /// shape-rendering property is set to auto. Default: `GeometricPrecision`
1564    pub shape_rendering: ShapeRendering,
1565    /// Specifies the default text rendering method. Will be used when an SVG element's
1566    /// text-rendering property is set to auto. Default: `OptimizeLegibility`
1567    pub text_rendering: TextRendering,
1568    /// Specifies the default image rendering method. Will be used when an SVG element's
1569    /// image-rendering property is set to auto. Default: `OptimizeQuality`
1570    pub image_rendering: ImageRendering,
1571    /// When empty, text elements will be skipped. Default: `System`
1572    pub fontdb: FontDatabase,
1573    /// Keep named groups. If set to true, all non-empty groups with id attribute will not be
1574    /// removed. Default: false
1575    pub keep_named_groups: bool,
1576}
1577
1578impl Default for SvgParseOptions {
1579    fn default() -> Self {
1580        let lang_vec: Vec<AzString> = vec![String::from("en").into()];
1581        Self {
1582            relative_image_path: OptionString::None,
1583            default_font_family: "Times New Roman".to_string().into(),
1584            languages: lang_vec.into(),
1585            dpi: 96.0,
1586            font_size: 12.0,
1587            shape_rendering: ShapeRendering::GeometricPrecision,
1588            text_rendering: TextRendering::OptimizeLegibility,
1589            image_rendering: ImageRendering::OptimizeQuality,
1590            fontdb: FontDatabase::System,
1591            keep_named_groups: false,
1592        }
1593    }
1594}
1595
1596#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd)]
1597#[repr(C)]
1598pub struct SvgXmlOptions {
1599    pub use_single_quote: bool,
1600    pub indent: Indent,
1601    pub attributes_indent: Indent,
1602}
1603
1604impl Default for SvgXmlOptions {
1605    fn default() -> Self {
1606        Self {
1607            use_single_quote: false,
1608            indent: Indent::Spaces(2),
1609            attributes_indent: Indent::Spaces(2),
1610        }
1611    }
1612}
1613#[allow(variant_size_differences)]
1614// repr(C,u8) FFI enum: boxing the large variant would change the C ABI (api.json bindings); size disparity accepted
1615#[derive(Debug, PartialEq, Eq, PartialOrd, Clone)]
1616#[repr(C, u8)]
1617pub enum SvgParseError {
1618    NoParserAvailable,
1619    ElementsLimitReached,
1620    NotAnUtf8Str,
1621    MalformedGZip,
1622    InvalidSize,
1623    ParsingFailed(XmlError),
1624}
1625
1626impl fmt::Display for SvgParseError {
1627    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1628        use self::SvgParseError::{
1629            ElementsLimitReached, InvalidSize, MalformedGZip, NoParserAvailable, NotAnUtf8Str,
1630            ParsingFailed,
1631        };
1632        match self {
1633            NoParserAvailable => write!(
1634                f,
1635                "Library was compiled without SVG support (no parser available)"
1636            ),
1637            ElementsLimitReached => write!(f, "Error parsing SVG: Elements limit reached"),
1638            NotAnUtf8Str => write!(f, "Error parsing SVG: Not an UTF-8 String"),
1639            MalformedGZip => write!(
1640                f,
1641                "Error parsing SVG: SVG is compressed with a malformed GZIP compression"
1642            ),
1643            InvalidSize => write!(f, "Error parsing SVG: Invalid size"),
1644            ParsingFailed(e) => write!(f, "Error parsing SVG: Parsing SVG as XML failed: {e}"),
1645        }
1646    }
1647}
1648
1649impl_result!(
1650    SvgXmlNode,
1651    SvgParseError,
1652    ResultSvgXmlNodeSvgParseError,
1653    copy = false,
1654    [Debug, Clone]
1655);
1656impl_result!(
1657    Svg,
1658    SvgParseError,
1659    ResultSvgSvgParseError,
1660    copy = false,
1661    [Debug, Clone]
1662);
1663
1664/// Indentation style for SVG XML serialization.
1665#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd)]
1666#[repr(C, u8)]
1667pub enum Indent {
1668    None,
1669    Spaces(u8),
1670    Tabs,
1671}
1672
1673#[cfg(test)]
1674#[path = "svg_test.rs"]
1675mod svg_test;
1676