Skip to main content

operad/render/
paint.rs

1//! Renderer-neutral paint primitives for dense application and editor surfaces.
2
3use lyon_tessellation::{
4    geometry_builder::{simple_builder, VertexBuffers},
5    math::point as lyon_point,
6    path::Path as LyonPath,
7    FillOptions, FillRule as LyonFillRule, FillTessellator, LineCap as LyonLineCap,
8    LineJoin as LyonLineJoin, StrokeOptions, StrokeTessellator,
9};
10
11use crate::{ColorRgba, StrokeStyle, TextStyle, UiPoint, UiRect};
12
13#[derive(Debug, Clone, Copy, PartialEq)]
14pub struct PixelSnapPolicy {
15    pub scale_factor: f32,
16}
17
18impl PixelSnapPolicy {
19    pub const DISABLED: Self = Self { scale_factor: 0.0 };
20
21    pub fn new(scale_factor: f32) -> Self {
22        if scale_factor.is_finite() && scale_factor > 0.0 {
23            Self { scale_factor }
24        } else {
25            Self::DISABLED
26        }
27    }
28
29    pub const fn disabled() -> Self {
30        Self::DISABLED
31    }
32
33    pub const fn enabled(self) -> bool {
34        self.scale_factor > 0.0
35    }
36
37    pub fn pixel_size(self) -> f32 {
38        if self.enabled() {
39            1.0 / self.scale_factor
40        } else {
41            0.0
42        }
43    }
44
45    pub fn snap_value(self, value: f32) -> f32 {
46        if !self.enabled() || !value.is_finite() {
47            return value;
48        }
49        (value * self.scale_factor).round() / self.scale_factor
50    }
51
52    pub fn snap_center_value(self, value: f32) -> f32 {
53        if !self.enabled() || !value.is_finite() {
54            return value;
55        }
56        ((value * self.scale_factor).floor() + 0.5) / self.scale_factor
57    }
58
59    pub fn snap_point(self, point: UiPoint) -> UiPoint {
60        UiPoint::new(self.snap_value(point.x), self.snap_value(point.y))
61    }
62
63    pub fn snap_center_point(self, point: UiPoint) -> UiPoint {
64        UiPoint::new(
65            self.snap_center_value(point.x),
66            self.snap_center_value(point.y),
67        )
68    }
69
70    pub fn snap_rect(self, rect: UiRect) -> UiRect {
71        if !self.enabled() {
72            return rect;
73        }
74        let left = self.snap_value(rect.x);
75        let top = self.snap_value(rect.y);
76        let right = self.snap_value(rect.right());
77        let bottom = self.snap_value(rect.bottom());
78        UiRect::new(left, top, (right - left).max(0.0), (bottom - top).max(0.0))
79    }
80
81    pub fn snap_line_segment(self, from: UiPoint, to: UiPoint) -> (UiPoint, UiPoint) {
82        if (from.x - to.x).abs() <= f32::EPSILON {
83            let x = self.snap_center_value(from.x);
84            return (
85                UiPoint::new(x, self.snap_value(from.y)),
86                UiPoint::new(x, self.snap_value(to.y)),
87            );
88        }
89        if (from.y - to.y).abs() <= f32::EPSILON {
90            let y = self.snap_center_value(from.y);
91            return (
92                UiPoint::new(self.snap_value(from.x), y),
93                UiPoint::new(self.snap_value(to.x), y),
94            );
95        }
96        (self.snap_point(from), self.snap_point(to))
97    }
98
99    pub fn snap_stroke_width(self, width: f32) -> f32 {
100        if !self.enabled() || !width.is_finite() || width <= 0.0 {
101            return width;
102        }
103        ((width * self.scale_factor).ceil().max(1.0)) / self.scale_factor
104    }
105
106    pub fn snap_stroke(self, stroke: StrokeStyle) -> StrokeStyle {
107        StrokeStyle::new(stroke.color, self.snap_stroke_width(stroke.width))
108    }
109}
110
111#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
112pub enum StrokeAlignment {
113    Inside,
114    #[default]
115    Center,
116    Outside,
117}
118
119#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
120pub enum StrokeLineCap {
121    Butt,
122    Square,
123    #[default]
124    Round,
125}
126
127#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
128pub enum StrokeLineJoin {
129    Miter,
130    Bevel,
131    #[default]
132    Round,
133}
134
135#[derive(Debug, Clone, Copy, PartialEq)]
136pub struct PathStrokeOptions {
137    pub line_cap: StrokeLineCap,
138    pub line_join: StrokeLineJoin,
139    pub miter_limit: f32,
140}
141
142impl PathStrokeOptions {
143    pub const DEFAULT_MITER_LIMIT: f32 = 4.0;
144
145    pub const fn new() -> Self {
146        Self {
147            line_cap: StrokeLineCap::Round,
148            line_join: StrokeLineJoin::Round,
149            miter_limit: Self::DEFAULT_MITER_LIMIT,
150        }
151    }
152
153    pub const fn line_cap(mut self, line_cap: StrokeLineCap) -> Self {
154        self.line_cap = line_cap;
155        self
156    }
157
158    pub const fn line_join(mut self, line_join: StrokeLineJoin) -> Self {
159        self.line_join = line_join;
160        self
161    }
162
163    pub const fn miter_limit(mut self, miter_limit: f32) -> Self {
164        self.miter_limit = miter_limit;
165        self
166    }
167}
168
169impl Default for PathStrokeOptions {
170    fn default() -> Self {
171        Self::new()
172    }
173}
174
175#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
176pub enum PathFillRule {
177    NonZero,
178    #[default]
179    EvenOdd,
180}
181
182#[derive(Debug, Clone, Copy, PartialEq)]
183pub struct AlignedStroke {
184    pub style: StrokeStyle,
185    pub alignment: StrokeAlignment,
186}
187
188impl AlignedStroke {
189    pub const fn new(style: StrokeStyle, alignment: StrokeAlignment) -> Self {
190        Self { style, alignment }
191    }
192
193    pub const fn is_visible(self) -> bool {
194        self.style.is_visible()
195    }
196
197    pub const fn inside(style: StrokeStyle) -> Self {
198        Self::new(style, StrokeAlignment::Inside)
199    }
200
201    pub const fn center(style: StrokeStyle) -> Self {
202        Self::new(style, StrokeAlignment::Center)
203    }
204
205    pub const fn outside(style: StrokeStyle) -> Self {
206        Self::new(style, StrokeAlignment::Outside)
207    }
208}
209
210impl From<StrokeStyle> for AlignedStroke {
211    fn from(style: StrokeStyle) -> Self {
212        Self::center(style)
213    }
214}
215
216#[derive(Debug, Clone, Copy, PartialEq)]
217pub struct GradientStop {
218    pub offset: f32,
219    pub color: ColorRgba,
220}
221
222impl GradientStop {
223    pub fn new(offset: f32, color: ColorRgba) -> Self {
224        Self {
225            offset: offset.clamp(0.0, 1.0),
226            color,
227        }
228    }
229}
230
231#[derive(Debug, Clone, PartialEq)]
232pub struct LinearGradient {
233    pub start: UiPoint,
234    pub end: UiPoint,
235    pub stops: Vec<GradientStop>,
236    pub fallback: ColorRgba,
237}
238
239impl LinearGradient {
240    pub fn new(start: UiPoint, end: UiPoint, from: ColorRgba, to: ColorRgba) -> Self {
241        Self {
242            start,
243            end,
244            stops: vec![GradientStop::new(0.0, from), GradientStop::new(1.0, to)],
245            fallback: from,
246        }
247    }
248
249    pub fn stop(mut self, offset: f32, color: ColorRgba) -> Self {
250        self.stops.push(GradientStop::new(offset, color));
251        self.stops.sort_by(|a, b| a.offset.total_cmp(&b.offset));
252        self
253    }
254
255    pub const fn fallback(mut self, fallback: ColorRgba) -> Self {
256        self.fallback = fallback;
257        self
258    }
259
260    pub fn translated(mut self, offset: UiPoint) -> Self {
261        self.start.x += offset.x;
262        self.start.y += offset.y;
263        self.end.x += offset.x;
264        self.end.y += offset.y;
265        self
266    }
267}
268
269#[derive(Debug, Clone, PartialEq)]
270pub enum PaintBrush {
271    Solid(ColorRgba),
272    LinearGradient(LinearGradient),
273}
274
275impl PaintBrush {
276    pub const fn solid(color: ColorRgba) -> Self {
277        Self::Solid(color)
278    }
279
280    pub fn linear_gradient(start: UiPoint, end: UiPoint, from: ColorRgba, to: ColorRgba) -> Self {
281        Self::LinearGradient(LinearGradient::new(start, end, from, to))
282    }
283
284    pub const fn fallback_color(&self) -> ColorRgba {
285        match self {
286            Self::Solid(color) => *color,
287            Self::LinearGradient(gradient) => gradient.fallback,
288        }
289    }
290
291    pub const fn is_visible(&self) -> bool {
292        self.fallback_color().a > 0
293    }
294
295    pub fn translated(&self, offset: UiPoint) -> Self {
296        match self {
297            Self::Solid(color) => Self::Solid(*color),
298            Self::LinearGradient(gradient) => {
299                Self::LinearGradient(gradient.clone().translated(offset))
300            }
301        }
302    }
303}
304
305impl From<ColorRgba> for PaintBrush {
306    fn from(color: ColorRgba) -> Self {
307        Self::Solid(color)
308    }
309}
310
311#[derive(Debug, Clone, Copy, PartialEq)]
312pub struct CornerRadii {
313    pub top_left: f32,
314    pub top_right: f32,
315    pub bottom_right: f32,
316    pub bottom_left: f32,
317}
318
319impl CornerRadii {
320    pub const ZERO: Self = Self::uniform(0.0);
321
322    pub const fn uniform(radius: f32) -> Self {
323        Self {
324            top_left: radius,
325            top_right: radius,
326            bottom_right: radius,
327            bottom_left: radius,
328        }
329    }
330
331    pub const fn new(top_left: f32, top_right: f32, bottom_right: f32, bottom_left: f32) -> Self {
332        Self {
333            top_left,
334            top_right,
335            bottom_right,
336            bottom_left,
337        }
338    }
339
340    pub fn max_radius(self) -> f32 {
341        self.top_left
342            .max(self.top_right)
343            .max(self.bottom_right)
344            .max(self.bottom_left)
345    }
346}
347
348impl Default for CornerRadii {
349    fn default() -> Self {
350        Self::ZERO
351    }
352}
353
354#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
355pub enum PaintEffectKind {
356    Shadow,
357    Glow,
358    InsetShadow,
359}
360
361#[derive(Debug, Clone, Copy, PartialEq)]
362pub struct PaintEffect {
363    pub kind: PaintEffectKind,
364    pub color: ColorRgba,
365    pub offset: UiPoint,
366    pub blur_radius: f32,
367    pub spread: f32,
368}
369
370impl PaintEffect {
371    pub const fn shadow(color: ColorRgba, offset: UiPoint, blur_radius: f32, spread: f32) -> Self {
372        Self {
373            kind: PaintEffectKind::Shadow,
374            color,
375            offset,
376            blur_radius,
377            spread,
378        }
379    }
380
381    pub const fn glow(color: ColorRgba, blur_radius: f32, spread: f32) -> Self {
382        Self {
383            kind: PaintEffectKind::Glow,
384            color,
385            offset: UiPoint::new(0.0, 0.0),
386            blur_radius,
387            spread,
388        }
389    }
390
391    pub const fn inset_shadow(
392        color: ColorRgba,
393        offset: UiPoint,
394        blur_radius: f32,
395        spread: f32,
396    ) -> Self {
397        Self {
398            kind: PaintEffectKind::InsetShadow,
399            color,
400            offset,
401            blur_radius,
402            spread,
403        }
404    }
405}
406
407#[derive(Debug, Clone, PartialEq)]
408pub struct PaintRect {
409    pub rect: UiRect,
410    pub fill: PaintBrush,
411    pub stroke: Option<AlignedStroke>,
412    pub corner_radii: CornerRadii,
413    pub effects: Vec<PaintEffect>,
414}
415
416impl PaintRect {
417    pub fn new(rect: UiRect, fill: impl Into<PaintBrush>) -> Self {
418        Self {
419            rect,
420            fill: fill.into(),
421            stroke: None,
422            corner_radii: CornerRadii::ZERO,
423            effects: Vec::new(),
424        }
425    }
426
427    pub fn solid(rect: UiRect, fill: ColorRgba) -> Self {
428        Self::new(rect, fill)
429    }
430
431    pub fn stroke(mut self, stroke: impl Into<AlignedStroke>) -> Self {
432        let stroke = stroke.into();
433        self.stroke = stroke.is_visible().then_some(stroke);
434        self
435    }
436
437    pub const fn corner_radii(mut self, corner_radii: CornerRadii) -> Self {
438        self.corner_radii = corner_radii;
439        self
440    }
441
442    pub fn effect(mut self, effect: PaintEffect) -> Self {
443        self.effects.push(effect);
444        self
445    }
446
447    pub fn translated(mut self, offset: UiPoint) -> Self {
448        self.rect.x += offset.x;
449        self.rect.y += offset.y;
450        self.fill = self.fill.translated(offset);
451        self
452    }
453
454    pub fn pixel_snapped(mut self, policy: PixelSnapPolicy) -> Self {
455        self.rect = policy.snap_rect(self.rect);
456        if let Some(stroke) = self.stroke {
457            self.stroke = Some(AlignedStroke {
458                style: policy.snap_stroke(stroke.style),
459                alignment: stroke.alignment,
460            });
461        }
462        self
463    }
464}
465
466#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
467pub enum TextHorizontalAlign {
468    #[default]
469    Start,
470    Center,
471    End,
472}
473
474#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
475pub enum TextVerticalAlign {
476    #[default]
477    Top,
478    Center,
479    Baseline,
480    Bottom,
481}
482
483#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
484pub enum TextOverflow {
485    #[default]
486    Clip,
487    Ellipsis,
488}
489
490#[derive(Debug, Clone, PartialEq)]
491pub struct PaintText {
492    pub text: String,
493    pub rect: UiRect,
494    pub style: TextStyle,
495    pub horizontal_align: TextHorizontalAlign,
496    pub vertical_align: TextVerticalAlign,
497    pub overflow: TextOverflow,
498    pub multiline: bool,
499}
500
501impl PaintText {
502    pub fn new(text: impl Into<String>, rect: UiRect, style: TextStyle) -> Self {
503        Self {
504            text: text.into(),
505            rect,
506            style,
507            horizontal_align: TextHorizontalAlign::Start,
508            vertical_align: TextVerticalAlign::Top,
509            overflow: TextOverflow::Clip,
510            multiline: true,
511        }
512    }
513
514    pub const fn horizontal_align(mut self, align: TextHorizontalAlign) -> Self {
515        self.horizontal_align = align;
516        self
517    }
518
519    pub const fn vertical_align(mut self, align: TextVerticalAlign) -> Self {
520        self.vertical_align = align;
521        self
522    }
523
524    pub const fn overflow(mut self, overflow: TextOverflow) -> Self {
525        self.overflow = overflow;
526        self
527    }
528
529    pub const fn multiline(mut self, multiline: bool) -> Self {
530        self.multiline = multiline;
531        self
532    }
533
534    pub fn translated(mut self, offset: UiPoint) -> Self {
535        self.rect.x += offset.x;
536        self.rect.y += offset.y;
537        self
538    }
539}
540
541#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
542pub enum ImageFit {
543    #[default]
544    Fill,
545    Contain,
546    Cover,
547    Original,
548}
549
550#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
551pub enum ImageAlignment {
552    #[default]
553    Center,
554    Start,
555    End,
556}
557
558#[derive(Debug, Clone, PartialEq)]
559pub struct PaintImage {
560    pub key: String,
561    pub rect: UiRect,
562    pub tint: Option<ColorRgba>,
563    pub fit: ImageFit,
564    pub horizontal_align: ImageAlignment,
565    pub vertical_align: ImageAlignment,
566}
567
568impl PaintImage {
569    pub fn new(key: impl Into<String>, rect: UiRect) -> Self {
570        Self {
571            key: key.into(),
572            rect,
573            tint: None,
574            fit: ImageFit::Fill,
575            horizontal_align: ImageAlignment::Center,
576            vertical_align: ImageAlignment::Center,
577        }
578    }
579
580    pub const fn tinted(mut self, tint: ColorRgba) -> Self {
581        self.tint = Some(tint);
582        self
583    }
584
585    pub const fn fit(mut self, fit: ImageFit) -> Self {
586        self.fit = fit;
587        self
588    }
589
590    pub const fn align(mut self, horizontal: ImageAlignment, vertical: ImageAlignment) -> Self {
591        self.horizontal_align = horizontal;
592        self.vertical_align = vertical;
593        self
594    }
595
596    pub fn translated(mut self, offset: UiPoint) -> Self {
597        self.rect.x += offset.x;
598        self.rect.y += offset.y;
599        self
600    }
601}
602
603#[derive(Debug, Clone, Copy, PartialEq)]
604pub enum PathVerb {
605    MoveTo(UiPoint),
606    LineTo(UiPoint),
607    QuadraticTo {
608        control: UiPoint,
609        to: UiPoint,
610    },
611    CubicTo {
612        control_a: UiPoint,
613        control_b: UiPoint,
614        to: UiPoint,
615    },
616    Close,
617}
618
619impl PathVerb {
620    pub fn translated(self, offset: UiPoint) -> Self {
621        match self {
622            Self::MoveTo(point) => Self::MoveTo(translated_point(point, offset)),
623            Self::LineTo(point) => Self::LineTo(translated_point(point, offset)),
624            Self::QuadraticTo { control, to } => Self::QuadraticTo {
625                control: translated_point(control, offset),
626                to: translated_point(to, offset),
627            },
628            Self::CubicTo {
629                control_a,
630                control_b,
631                to,
632            } => Self::CubicTo {
633                control_a: translated_point(control_a, offset),
634                control_b: translated_point(control_b, offset),
635                to: translated_point(to, offset),
636            },
637            Self::Close => Self::Close,
638        }
639    }
640
641    pub fn pixel_snapped(self, policy: PixelSnapPolicy) -> Self {
642        match self {
643            Self::MoveTo(point) => Self::MoveTo(policy.snap_point(point)),
644            Self::LineTo(point) => Self::LineTo(policy.snap_point(point)),
645            Self::QuadraticTo { control, to } => Self::QuadraticTo {
646                control: policy.snap_point(control),
647                to: policy.snap_point(to),
648            },
649            Self::CubicTo {
650                control_a,
651                control_b,
652                to,
653            } => Self::CubicTo {
654                control_a: policy.snap_point(control_a),
655                control_b: policy.snap_point(control_b),
656                to: policy.snap_point(to),
657            },
658            Self::Close => Self::Close,
659        }
660    }
661}
662
663#[derive(Debug, Clone, PartialEq)]
664pub struct PaintPath {
665    pub verbs: Vec<PathVerb>,
666    pub fill: Option<PaintBrush>,
667    pub stroke: Option<AlignedStroke>,
668    pub stroke_options: PathStrokeOptions,
669    pub fill_rule: PathFillRule,
670}
671
672impl PaintPath {
673    pub fn new() -> Self {
674        Self {
675            verbs: Vec::new(),
676            fill: None,
677            stroke: None,
678            stroke_options: PathStrokeOptions::default(),
679            fill_rule: PathFillRule::default(),
680        }
681    }
682
683    pub fn move_to(mut self, point: UiPoint) -> Self {
684        self.verbs.push(PathVerb::MoveTo(point));
685        self
686    }
687
688    pub fn line_to(mut self, point: UiPoint) -> Self {
689        self.verbs.push(PathVerb::LineTo(point));
690        self
691    }
692
693    pub fn quadratic_to(mut self, control: UiPoint, to: UiPoint) -> Self {
694        self.verbs.push(PathVerb::QuadraticTo { control, to });
695        self
696    }
697
698    pub fn cubic_to(mut self, control_a: UiPoint, control_b: UiPoint, to: UiPoint) -> Self {
699        self.verbs.push(PathVerb::CubicTo {
700            control_a,
701            control_b,
702            to,
703        });
704        self
705    }
706
707    pub fn close(mut self) -> Self {
708        self.verbs.push(PathVerb::Close);
709        self
710    }
711
712    pub fn fill(mut self, fill: impl Into<PaintBrush>) -> Self {
713        self.fill = Some(fill.into());
714        self
715    }
716
717    pub const fn fill_rule(mut self, fill_rule: PathFillRule) -> Self {
718        self.fill_rule = fill_rule;
719        self
720    }
721
722    pub fn stroke(mut self, stroke: impl Into<AlignedStroke>) -> Self {
723        let stroke = stroke.into();
724        self.stroke = stroke.is_visible().then_some(stroke);
725        self
726    }
727
728    pub const fn stroke_options(mut self, options: PathStrokeOptions) -> Self {
729        self.stroke_options = options;
730        self
731    }
732
733    pub const fn line_cap(mut self, line_cap: StrokeLineCap) -> Self {
734        self.stroke_options.line_cap = line_cap;
735        self
736    }
737
738    pub const fn line_join(mut self, line_join: StrokeLineJoin) -> Self {
739        self.stroke_options.line_join = line_join;
740        self
741    }
742
743    pub const fn miter_limit(mut self, miter_limit: f32) -> Self {
744        self.stroke_options.miter_limit = miter_limit;
745        self
746    }
747
748    pub fn translated(mut self, offset: UiPoint) -> Self {
749        self.verbs = self
750            .verbs
751            .into_iter()
752            .map(|verb| verb.translated(offset))
753            .collect();
754        if let Some(fill) = &self.fill {
755            self.fill = Some(fill.translated(offset));
756        }
757        self
758    }
759
760    pub fn pixel_snapped(mut self, policy: PixelSnapPolicy) -> Self {
761        self.verbs = self
762            .verbs
763            .into_iter()
764            .map(|verb| verb.pixel_snapped(policy))
765            .collect();
766        if let Some(stroke) = self.stroke {
767            self.stroke = Some(AlignedStroke {
768                style: policy.snap_stroke(stroke.style),
769                alignment: stroke.alignment,
770            });
771        }
772        self
773    }
774
775    pub fn bounds(&self) -> UiRect {
776        let mut points = Vec::new();
777        for verb in &self.verbs {
778            match *verb {
779                PathVerb::MoveTo(point) | PathVerb::LineTo(point) => points.push(point),
780                PathVerb::QuadraticTo { control, to } => {
781                    points.push(control);
782                    points.push(to);
783                }
784                PathVerb::CubicTo {
785                    control_a,
786                    control_b,
787                    to,
788                } => {
789                    points.push(control_a);
790                    points.push(control_b);
791                    points.push(to);
792                }
793                PathVerb::Close => {}
794            }
795        }
796
797        rect_from_points(&points)
798    }
799
800    pub fn flattened_points(&self, tolerance: f32) -> Vec<UiPoint> {
801        self.flattened_contours(tolerance)
802            .into_iter()
803            .flatten()
804            .collect()
805    }
806
807    pub fn flattened_contours(&self, tolerance: f32) -> Vec<Vec<UiPoint>> {
808        let tolerance = if tolerance.is_finite() && tolerance > 0.0 {
809            tolerance
810        } else {
811            1.0
812        };
813        let mut contours = Vec::<Vec<UiPoint>>::new();
814        let mut points = Vec::<UiPoint>::new();
815        let mut current = None;
816        let mut contour_start = None;
817        for verb in &self.verbs {
818            match *verb {
819                PathVerb::MoveTo(point) => {
820                    if !points.is_empty() {
821                        contours.push(std::mem::take(&mut points));
822                    }
823                    points.push(point);
824                    current = Some(point);
825                    contour_start = Some(point);
826                }
827                PathVerb::LineTo(point) => {
828                    points.push(point);
829                    current = Some(point);
830                }
831                PathVerb::QuadraticTo { control, to } => {
832                    let Some(from) = current else {
833                        points.push(to);
834                        current = Some(to);
835                        contour_start.get_or_insert(to);
836                        continue;
837                    };
838                    let segments = quadratic_segments(from, control, to, tolerance);
839                    for index in 1..=segments {
840                        let t = index as f32 / segments as f32;
841                        points.push(quadratic_point(from, control, to, t));
842                    }
843                    current = Some(to);
844                }
845                PathVerb::CubicTo {
846                    control_a,
847                    control_b,
848                    to,
849                } => {
850                    let Some(from) = current else {
851                        points.push(to);
852                        current = Some(to);
853                        contour_start.get_or_insert(to);
854                        continue;
855                    };
856                    let segments = cubic_segments(from, control_a, control_b, to, tolerance);
857                    for index in 1..=segments {
858                        let t = index as f32 / segments as f32;
859                        points.push(cubic_point(from, control_a, control_b, to, t));
860                    }
861                    current = Some(to);
862                }
863                PathVerb::Close => {
864                    if let (Some(start), Some(last)) = (contour_start, current) {
865                        if start != last {
866                            points.push(start);
867                        }
868                    }
869                    if !points.is_empty() {
870                        contours.push(std::mem::take(&mut points));
871                    }
872                    current = contour_start;
873                    contour_start = None;
874                }
875            }
876        }
877        if !points.is_empty() {
878            contours.push(points);
879        }
880        contours
881    }
882
883    pub fn is_closed(&self) -> bool {
884        self.verbs
885            .iter()
886            .any(|verb| matches!(verb, PathVerb::Close))
887    }
888
889    pub fn tessellated_fill(&self, tolerance: f32) -> Vec<[UiPoint; 3]> {
890        let path = self.to_lyon_path();
891        let mut buffers: VertexBuffers<lyon_tessellation::math::Point, u16> = VertexBuffers::new();
892        let mut tessellator = FillTessellator::new();
893        let options = FillOptions::tolerance(finite_positive_or(tolerance, 1.0)).with_fill_rule(
894            match self.fill_rule {
895                PathFillRule::NonZero => LyonFillRule::NonZero,
896                PathFillRule::EvenOdd => LyonFillRule::EvenOdd,
897            },
898        );
899        if tessellator
900            .tessellate_path(&path, &options, &mut simple_builder(&mut buffers))
901            .is_err()
902        {
903            return tessellate_polygon(&self.flattened_points(tolerance));
904        }
905        vertex_buffers_to_triangles(buffers)
906    }
907
908    pub fn tessellated_stroke(&self, tolerance: f32) -> Vec<[UiPoint; 3]> {
909        let Some(stroke) = self.stroke else {
910            return Vec::new();
911        };
912        let path = self.to_lyon_path();
913        let mut buffers: VertexBuffers<lyon_tessellation::math::Point, u16> = VertexBuffers::new();
914        let mut tessellator = StrokeTessellator::new();
915        let options = StrokeOptions::tolerance(finite_positive_or(tolerance, 1.0))
916            .with_line_width(stroke.style.width.max(1.0))
917            .with_line_cap(match self.stroke_options.line_cap {
918                StrokeLineCap::Butt => LyonLineCap::Butt,
919                StrokeLineCap::Square => LyonLineCap::Square,
920                StrokeLineCap::Round => LyonLineCap::Round,
921            })
922            .with_line_join(match self.stroke_options.line_join {
923                StrokeLineJoin::Miter => LyonLineJoin::Miter,
924                StrokeLineJoin::Bevel => LyonLineJoin::Bevel,
925                StrokeLineJoin::Round => LyonLineJoin::Round,
926            })
927            .with_miter_limit(
928                finite_positive_or(
929                    self.stroke_options.miter_limit,
930                    PathStrokeOptions::DEFAULT_MITER_LIMIT,
931                )
932                .max(StrokeOptions::MINIMUM_MITER_LIMIT),
933            );
934        if tessellator
935            .tessellate_path(&path, &options, &mut simple_builder(&mut buffers))
936            .is_err()
937        {
938            return tessellate_polyline_stroke(
939                &self.flattened_points(tolerance),
940                stroke.style,
941                self.stroke_options,
942                self.is_closed(),
943            );
944        }
945        vertex_buffers_to_triangles(buffers)
946    }
947
948    fn to_lyon_path(&self) -> LyonPath {
949        let mut builder = LyonPath::builder().with_svg();
950        for verb in &self.verbs {
951            match *verb {
952                PathVerb::MoveTo(point) => {
953                    builder.move_to(to_lyon_point(point));
954                }
955                PathVerb::LineTo(point) => {
956                    builder.line_to(to_lyon_point(point));
957                }
958                PathVerb::QuadraticTo { control, to } => {
959                    builder.quadratic_bezier_to(to_lyon_point(control), to_lyon_point(to));
960                }
961                PathVerb::CubicTo {
962                    control_a,
963                    control_b,
964                    to,
965                } => {
966                    builder.cubic_bezier_to(
967                        to_lyon_point(control_a),
968                        to_lyon_point(control_b),
969                        to_lyon_point(to),
970                    );
971                }
972                PathVerb::Close => {
973                    builder.close();
974                }
975            }
976        }
977        builder.build()
978    }
979}
980
981impl Default for PaintPath {
982    fn default() -> Self {
983        Self::new()
984    }
985}
986
987fn to_lyon_point(point: UiPoint) -> lyon_tessellation::math::Point {
988    lyon_point(point.x, point.y)
989}
990
991fn from_lyon_point(point: lyon_tessellation::math::Point) -> UiPoint {
992    UiPoint::new(point.x, point.y)
993}
994
995fn vertex_buffers_to_triangles(
996    buffers: VertexBuffers<lyon_tessellation::math::Point, u16>,
997) -> Vec<[UiPoint; 3]> {
998    buffers
999        .indices
1000        .chunks_exact(3)
1001        .filter_map(|indices| {
1002            let a = buffers.vertices.get(usize::from(indices[0]))?;
1003            let b = buffers.vertices.get(usize::from(indices[1]))?;
1004            let c = buffers.vertices.get(usize::from(indices[2]))?;
1005            Some([
1006                from_lyon_point(*a),
1007                from_lyon_point(*b),
1008                from_lyon_point(*c),
1009            ])
1010        })
1011        .collect()
1012}
1013
1014fn tessellate_polygon(points: &[UiPoint]) -> Vec<[UiPoint; 3]> {
1015    let mut polygon = sanitize_polygon(points);
1016    if polygon.len() < 3 {
1017        return Vec::new();
1018    }
1019    if signed_area(&polygon) < 0.0 {
1020        polygon.reverse();
1021    }
1022
1023    let mut indices = (0..polygon.len()).collect::<Vec<_>>();
1024    let mut triangles = Vec::with_capacity(polygon.len().saturating_sub(2));
1025    let mut guard = 0usize;
1026    while indices.len() > 3 && guard < polygon.len().saturating_mul(polygon.len()) {
1027        guard += 1;
1028        let mut clipped = false;
1029        for index in 0..indices.len() {
1030            let previous = indices[(index + indices.len() - 1) % indices.len()];
1031            let current = indices[index];
1032            let next = indices[(index + 1) % indices.len()];
1033            let a = polygon[previous];
1034            let b = polygon[current];
1035            let c = polygon[next];
1036            if cross(sub_points(b, a), sub_points(c, b)) <= 0.0 {
1037                continue;
1038            }
1039            if indices.iter().copied().any(|candidate| {
1040                candidate != previous
1041                    && candidate != current
1042                    && candidate != next
1043                    && point_in_triangle(polygon[candidate], a, b, c)
1044            }) {
1045                continue;
1046            }
1047            triangles.push([a, b, c]);
1048            indices.remove(index);
1049            clipped = true;
1050            break;
1051        }
1052        if !clipped {
1053            return polygon_fan_triangles(&polygon);
1054        }
1055    }
1056
1057    if indices.len() == 3 {
1058        triangles.push([
1059            polygon[indices[0]],
1060            polygon[indices[1]],
1061            polygon[indices[2]],
1062        ]);
1063    }
1064    triangles
1065}
1066
1067fn tessellate_polyline_stroke(
1068    points: &[UiPoint],
1069    stroke: StrokeStyle,
1070    options: PathStrokeOptions,
1071    closed: bool,
1072) -> Vec<[UiPoint; 3]> {
1073    if !stroke.is_visible() {
1074        return Vec::new();
1075    }
1076    let points = sanitize_polyline(points);
1077    if points.is_empty() {
1078        return Vec::new();
1079    }
1080    let width = stroke.width.max(1.0);
1081    let half = width * 0.5 + 0.75;
1082    if points.len() == 1 {
1083        return circle_triangles(points[0], half);
1084    }
1085
1086    let mut triangles = Vec::new();
1087    let segment_count = if closed {
1088        points.len()
1089    } else {
1090        points.len() - 1
1091    };
1092    let mut directions = Vec::with_capacity(segment_count);
1093    let mut normals = Vec::with_capacity(segment_count);
1094    for index in 0..segment_count {
1095        let from = points[index];
1096        let to = points[(index + 1) % points.len()];
1097        let direction = normalize(sub_points(to, from));
1098        directions.push(direction);
1099        normals.push(UiPoint::new(-direction.y, direction.x));
1100    }
1101
1102    for index in 0..segment_count {
1103        let mut from = points[index];
1104        let mut to = points[(index + 1) % points.len()];
1105        if !closed {
1106            if index == 0 && options.line_cap == StrokeLineCap::Square {
1107                from = add_points(from, scale_point(directions[index], -half));
1108            }
1109            if index == segment_count - 1 && options.line_cap == StrokeLineCap::Square {
1110                to = add_points(to, scale_point(directions[index], half));
1111            }
1112        }
1113        push_stroke_quad(&mut triangles, from, to, normals[index], half);
1114    }
1115
1116    if closed {
1117        for (index, point) in points.iter().copied().enumerate() {
1118            let previous = (index + segment_count - 1) % segment_count;
1119            let next = index % segment_count;
1120            push_join_triangles(
1121                &mut triangles,
1122                point,
1123                normals[previous],
1124                normals[next],
1125                half,
1126                options,
1127            );
1128        }
1129    } else {
1130        if options.line_cap == StrokeLineCap::Round {
1131            triangles.extend(circle_triangles(points[0], half));
1132            triangles.extend(circle_triangles(points[points.len() - 1], half));
1133        }
1134        for index in 1..points.len() - 1 {
1135            push_join_triangles(
1136                &mut triangles,
1137                points[index],
1138                normals[index - 1],
1139                normals[index],
1140                half,
1141                options,
1142            );
1143        }
1144    }
1145
1146    triangles
1147}
1148
1149fn translated_point(point: UiPoint, offset: UiPoint) -> UiPoint {
1150    UiPoint::new(point.x + offset.x, point.y + offset.y)
1151}
1152
1153fn rect_from_points(points: &[UiPoint]) -> UiRect {
1154    if points.is_empty() {
1155        return UiRect::new(0.0, 0.0, 0.0, 0.0);
1156    }
1157
1158    let mut left = points[0].x;
1159    let mut top = points[0].y;
1160    let mut right = points[0].x;
1161    let mut bottom = points[0].y;
1162    for point in points.iter().copied().skip(1) {
1163        left = left.min(point.x);
1164        top = top.min(point.y);
1165        right = right.max(point.x);
1166        bottom = bottom.max(point.y);
1167    }
1168
1169    UiRect::new(left, top, right - left, bottom - top)
1170}
1171
1172fn point_distance(left: UiPoint, right: UiPoint) -> f32 {
1173    let dx = right.x - left.x;
1174    let dy = right.y - left.y;
1175    (dx * dx + dy * dy).sqrt()
1176}
1177
1178fn quadratic_segments(from: UiPoint, control: UiPoint, to: UiPoint, tolerance: f32) -> usize {
1179    let length = point_distance(from, control) + point_distance(control, to);
1180    ((length / tolerance).ceil() as usize).clamp(4, 64)
1181}
1182
1183fn cubic_segments(
1184    from: UiPoint,
1185    control_a: UiPoint,
1186    control_b: UiPoint,
1187    to: UiPoint,
1188    tolerance: f32,
1189) -> usize {
1190    let length = point_distance(from, control_a)
1191        + point_distance(control_a, control_b)
1192        + point_distance(control_b, to);
1193    ((length / tolerance).ceil() as usize).clamp(6, 96)
1194}
1195
1196fn quadratic_point(from: UiPoint, control: UiPoint, to: UiPoint, t: f32) -> UiPoint {
1197    let inverse = 1.0 - t;
1198    UiPoint::new(
1199        inverse * inverse * from.x + 2.0 * inverse * t * control.x + t * t * to.x,
1200        inverse * inverse * from.y + 2.0 * inverse * t * control.y + t * t * to.y,
1201    )
1202}
1203
1204fn cubic_point(
1205    from: UiPoint,
1206    control_a: UiPoint,
1207    control_b: UiPoint,
1208    to: UiPoint,
1209    t: f32,
1210) -> UiPoint {
1211    let inverse = 1.0 - t;
1212    UiPoint::new(
1213        inverse * inverse * inverse * from.x
1214            + 3.0 * inverse * inverse * t * control_a.x
1215            + 3.0 * inverse * t * t * control_b.x
1216            + t * t * t * to.x,
1217        inverse * inverse * inverse * from.y
1218            + 3.0 * inverse * inverse * t * control_a.y
1219            + 3.0 * inverse * t * t * control_b.y
1220            + t * t * t * to.y,
1221    )
1222}
1223
1224fn sanitize_polygon(points: &[UiPoint]) -> Vec<UiPoint> {
1225    let mut clean = sanitize_polyline(points);
1226    if clean.len() > 1 && clean.first() == clean.last() {
1227        clean.pop();
1228    }
1229    clean
1230}
1231
1232fn sanitize_polyline(points: &[UiPoint]) -> Vec<UiPoint> {
1233    let mut clean = Vec::with_capacity(points.len());
1234    for point in points.iter().copied() {
1235        if point.x.is_finite() && point.y.is_finite() && clean.last() != Some(&point) {
1236            clean.push(point);
1237        }
1238    }
1239    clean
1240}
1241
1242fn polygon_fan_triangles(points: &[UiPoint]) -> Vec<[UiPoint; 3]> {
1243    if points.len() < 3 {
1244        return Vec::new();
1245    }
1246    let mut triangles = Vec::with_capacity(points.len().saturating_sub(2));
1247    for index in 1..points.len() - 1 {
1248        triangles.push([points[0], points[index], points[index + 1]]);
1249    }
1250    triangles
1251}
1252
1253fn signed_area(points: &[UiPoint]) -> f32 {
1254    let mut area = 0.0;
1255    for index in 0..points.len() {
1256        let next = (index + 1) % points.len();
1257        area += points[index].x * points[next].y - points[next].x * points[index].y;
1258    }
1259    area * 0.5
1260}
1261
1262fn point_in_triangle(point: UiPoint, a: UiPoint, b: UiPoint, c: UiPoint) -> bool {
1263    let ab = cross(sub_points(b, a), sub_points(point, a));
1264    let bc = cross(sub_points(c, b), sub_points(point, b));
1265    let ca = cross(sub_points(a, c), sub_points(point, c));
1266    (ab >= -f32::EPSILON && bc >= -f32::EPSILON && ca >= -f32::EPSILON)
1267        || (ab <= f32::EPSILON && bc <= f32::EPSILON && ca <= f32::EPSILON)
1268}
1269
1270fn push_stroke_quad(
1271    triangles: &mut Vec<[UiPoint; 3]>,
1272    from: UiPoint,
1273    to: UiPoint,
1274    normal: UiPoint,
1275    half_width: f32,
1276) {
1277    let offset = scale_point(normal, half_width);
1278    let a = add_points(from, offset);
1279    let b = add_points(to, offset);
1280    let c = sub_points(to, offset);
1281    let d = sub_points(from, offset);
1282    triangles.push([a, b, c]);
1283    triangles.push([a, c, d]);
1284}
1285
1286fn push_join_triangles(
1287    triangles: &mut Vec<[UiPoint; 3]>,
1288    point: UiPoint,
1289    previous_normal: UiPoint,
1290    next_normal: UiPoint,
1291    half_width: f32,
1292    options: PathStrokeOptions,
1293) {
1294    match options.line_join {
1295        StrokeLineJoin::Round => triangles.extend(circle_triangles(point, half_width)),
1296        StrokeLineJoin::Bevel => {
1297            push_bevel_join(triangles, point, previous_normal, next_normal, half_width);
1298        }
1299        StrokeLineJoin::Miter => {
1300            if !push_miter_join(
1301                triangles,
1302                point,
1303                previous_normal,
1304                next_normal,
1305                half_width,
1306                options.miter_limit,
1307            ) {
1308                push_bevel_join(triangles, point, previous_normal, next_normal, half_width);
1309            }
1310        }
1311    }
1312}
1313
1314fn push_bevel_join(
1315    triangles: &mut Vec<[UiPoint; 3]>,
1316    point: UiPoint,
1317    previous_normal: UiPoint,
1318    next_normal: UiPoint,
1319    half_width: f32,
1320) {
1321    triangles.push([
1322        point,
1323        add_points(point, scale_point(previous_normal, half_width)),
1324        add_points(point, scale_point(next_normal, half_width)),
1325    ]);
1326    triangles.push([
1327        point,
1328        sub_points(point, scale_point(previous_normal, half_width)),
1329        sub_points(point, scale_point(next_normal, half_width)),
1330    ]);
1331}
1332
1333fn push_miter_join(
1334    triangles: &mut Vec<[UiPoint; 3]>,
1335    point: UiPoint,
1336    previous_normal: UiPoint,
1337    next_normal: UiPoint,
1338    half_width: f32,
1339    miter_limit: f32,
1340) -> bool {
1341    let Some(miter) = try_miter(previous_normal, next_normal, half_width, miter_limit) else {
1342        return false;
1343    };
1344    let previous = add_points(point, scale_point(previous_normal, half_width));
1345    let next = add_points(point, scale_point(next_normal, half_width));
1346    let tip = add_points(point, miter);
1347    triangles.push([previous, tip, next]);
1348
1349    let previous = sub_points(point, scale_point(previous_normal, half_width));
1350    let next = sub_points(point, scale_point(next_normal, half_width));
1351    let tip = sub_points(point, miter);
1352    triangles.push([previous, next, tip]);
1353    true
1354}
1355
1356fn try_miter(
1357    previous_normal: UiPoint,
1358    next_normal: UiPoint,
1359    half_width: f32,
1360    miter_limit: f32,
1361) -> Option<UiPoint> {
1362    let sum = add_points(previous_normal, next_normal);
1363    let miter = normalize(sum);
1364    if vector_length(miter) <= f32::EPSILON {
1365        return None;
1366    }
1367    let denominator = dot(miter, next_normal);
1368    if denominator.abs() <= 0.01 {
1369        return None;
1370    }
1371    let length = half_width / denominator;
1372    let max_length =
1373        half_width * finite_positive_or(miter_limit, PathStrokeOptions::DEFAULT_MITER_LIMIT);
1374    if length.abs() > max_length {
1375        return None;
1376    }
1377    Some(scale_point(miter, length))
1378}
1379
1380fn circle_triangles(center: UiPoint, radius: f32) -> Vec<[UiPoint; 3]> {
1381    if radius <= 0.0 {
1382        return Vec::new();
1383    }
1384    let segments = ((radius * 4.0).ceil() as usize).clamp(12, 48);
1385    let mut triangles = Vec::with_capacity(segments);
1386    for index in 0..segments {
1387        let a0 = std::f32::consts::TAU * index as f32 / segments as f32;
1388        let a1 = std::f32::consts::TAU * (index + 1) as f32 / segments as f32;
1389        triangles.push([
1390            center,
1391            UiPoint::new(center.x + radius * a0.cos(), center.y + radius * a0.sin()),
1392            UiPoint::new(center.x + radius * a1.cos(), center.y + radius * a1.sin()),
1393        ]);
1394    }
1395    triangles
1396}
1397
1398fn add_points(left: UiPoint, right: UiPoint) -> UiPoint {
1399    UiPoint::new(left.x + right.x, left.y + right.y)
1400}
1401
1402fn sub_points(left: UiPoint, right: UiPoint) -> UiPoint {
1403    UiPoint::new(left.x - right.x, left.y - right.y)
1404}
1405
1406fn scale_point(point: UiPoint, scale: f32) -> UiPoint {
1407    UiPoint::new(point.x * scale, point.y * scale)
1408}
1409
1410fn dot(left: UiPoint, right: UiPoint) -> f32 {
1411    left.x * right.x + left.y * right.y
1412}
1413
1414fn cross(left: UiPoint, right: UiPoint) -> f32 {
1415    left.x * right.y - left.y * right.x
1416}
1417
1418fn vector_length(point: UiPoint) -> f32 {
1419    (point.x * point.x + point.y * point.y).sqrt()
1420}
1421
1422fn normalize(point: UiPoint) -> UiPoint {
1423    let length = vector_length(point);
1424    if length <= f32::EPSILON {
1425        UiPoint::new(0.0, 0.0)
1426    } else {
1427        UiPoint::new(point.x / length, point.y / length)
1428    }
1429}
1430
1431fn finite_positive_or(value: f32, fallback: f32) -> f32 {
1432    if value.is_finite() && value > 0.0 {
1433        value
1434    } else {
1435        fallback
1436    }
1437}
1438
1439#[cfg(test)]
1440mod tests {
1441    use super::*;
1442
1443    #[test]
1444    fn pixel_snap_policy_maps_values_rects_and_hairline_segments() {
1445        let policy = PixelSnapPolicy::new(2.0);
1446
1447        assert!(policy.enabled());
1448        assert_eq!(policy.pixel_size(), 0.5);
1449        assert_eq!(policy.snap_value(10.26), 10.5);
1450        assert_eq!(policy.snap_center_value(10.26), 10.25);
1451        assert_eq!(
1452            policy.snap_point(UiPoint::new(0.24, 0.26)),
1453            UiPoint::new(0.0, 0.5)
1454        );
1455        assert_eq!(
1456            policy.snap_rect(UiRect::new(0.24, 0.26, 10.51, 4.49)),
1457            UiRect::new(0.0, 0.5, 11.0, 4.5)
1458        );
1459
1460        let (from, to) = PixelSnapPolicy::new(1.0)
1461            .snap_line_segment(UiPoint::new(10.1, 0.2), UiPoint::new(10.1, 9.8));
1462        assert_eq!(from, UiPoint::new(10.5, 0.0));
1463        assert_eq!(to, UiPoint::new(10.5, 10.0));
1464
1465        let (from, to) = PixelSnapPolicy::new(1.0)
1466            .snap_line_segment(UiPoint::new(0.2, 5.1), UiPoint::new(9.8, 5.1));
1467        assert_eq!(from, UiPoint::new(0.0, 5.5));
1468        assert_eq!(to, UiPoint::new(10.0, 5.5));
1469    }
1470
1471    #[test]
1472    fn pixel_snap_policy_preserves_disabled_and_snaps_stroke_widths_up() {
1473        let disabled = PixelSnapPolicy::disabled();
1474        assert!(!disabled.enabled());
1475        assert_eq!(disabled.snap_value(10.26), 10.26);
1476        assert_eq!(PixelSnapPolicy::new(f32::NAN), PixelSnapPolicy::DISABLED);
1477
1478        let policy = PixelSnapPolicy::new(2.0);
1479        assert_eq!(policy.snap_stroke_width(0.1), 0.5);
1480        assert_eq!(policy.snap_stroke_width(1.2), 1.5);
1481        assert_eq!(policy.snap_stroke_width(0.0), 0.0);
1482    }
1483
1484    #[test]
1485    fn paint_rect_and_path_can_be_pixel_snapped() {
1486        let policy = PixelSnapPolicy::new(2.0);
1487        let rect = PaintRect::solid(UiRect::new(1.24, 2.26, 10.51, 4.49), ColorRgba::WHITE)
1488            .stroke(AlignedStroke::inside(StrokeStyle::new(
1489                ColorRgba::WHITE,
1490                0.3,
1491            )))
1492            .pixel_snapped(policy);
1493
1494        assert_eq!(rect.rect, UiRect::new(1.0, 2.5, 11.0, 4.5));
1495        assert_eq!(rect.stroke.unwrap().style.width, 0.5);
1496
1497        let path = PaintPath::new()
1498            .move_to(UiPoint::new(0.24, 0.26))
1499            .line_to(UiPoint::new(4.74, 3.24))
1500            .stroke(StrokeStyle::new(ColorRgba::WHITE, 0.2))
1501            .pixel_snapped(policy);
1502
1503        assert_eq!(
1504            path.verbs,
1505            vec![
1506                PathVerb::MoveTo(UiPoint::new(0.0, 0.5)),
1507                PathVerb::LineTo(UiPoint::new(4.5, 3.0))
1508            ]
1509        );
1510        assert_eq!(path.stroke.unwrap().style.width, 0.5);
1511    }
1512
1513    #[test]
1514    fn zero_width_strokes_are_not_published_as_visible_paint() {
1515        let rect = PaintRect::solid(UiRect::new(0.0, 0.0, 10.0, 10.0), ColorRgba::WHITE).stroke(
1516            AlignedStroke::inside(StrokeStyle::new(ColorRgba::WHITE, 0.0)),
1517        );
1518        assert_eq!(rect.stroke, None);
1519
1520        let path = PaintPath::new()
1521            .move_to(UiPoint::new(0.0, 0.0))
1522            .line_to(UiPoint::new(10.0, 0.0))
1523            .stroke(StrokeStyle::new(ColorRgba::WHITE, 0.0));
1524        assert_eq!(path.stroke, None);
1525        assert!(path.tessellated_stroke(1.0).is_empty());
1526    }
1527
1528    #[test]
1529    fn paint_path_flattens_quadratic_and_cubic_curves() {
1530        let path = PaintPath::new()
1531            .move_to(UiPoint::new(0.0, 10.0))
1532            .quadratic_to(UiPoint::new(10.0, 0.0), UiPoint::new(20.0, 10.0))
1533            .cubic_to(
1534                UiPoint::new(28.0, 18.0),
1535                UiPoint::new(34.0, 18.0),
1536                UiPoint::new(40.0, 10.0),
1537            );
1538
1539        let points = path.flattened_points(4.0);
1540
1541        assert!(points.len() > 6);
1542        assert_eq!(points.first(), Some(&UiPoint::new(0.0, 10.0)));
1543        assert_eq!(points.last(), Some(&UiPoint::new(40.0, 10.0)));
1544        assert!(
1545            points.iter().any(|point| point.y < 8.0),
1546            "quadratic control point should affect flattened curve"
1547        );
1548        assert!(
1549            points.iter().any(|point| point.y > 12.0),
1550            "cubic control points should affect flattened curve"
1551        );
1552    }
1553
1554    #[test]
1555    fn paint_path_preserves_contours_and_stroke_options() {
1556        let path = PaintPath::new()
1557            .move_to(UiPoint::new(0.0, 0.0))
1558            .line_to(UiPoint::new(8.0, 0.0))
1559            .move_to(UiPoint::new(0.0, 8.0))
1560            .line_to(UiPoint::new(8.0, 8.0))
1561            .stroke(StrokeStyle::new(ColorRgba::WHITE, 2.0))
1562            .line_cap(StrokeLineCap::Butt)
1563            .line_join(StrokeLineJoin::Miter)
1564            .miter_limit(2.0);
1565
1566        let contours = path.flattened_contours(1.0);
1567        assert_eq!(contours.len(), 2);
1568        assert_eq!(path.stroke_options.line_cap, StrokeLineCap::Butt);
1569        assert_eq!(path.stroke_options.line_join, StrokeLineJoin::Miter);
1570        assert_eq!(path.stroke_options.miter_limit, 2.0);
1571    }
1572
1573    #[test]
1574    fn tessellators_cover_non_convex_fill_and_configurable_strokes() {
1575        let polygon = [
1576            UiPoint::new(0.0, 0.0),
1577            UiPoint::new(16.0, 0.0),
1578            UiPoint::new(16.0, 16.0),
1579            UiPoint::new(8.0, 8.0),
1580            UiPoint::new(0.0, 16.0),
1581        ];
1582        assert!(tessellate_polygon(&polygon).len() >= 3);
1583
1584        let polyline = [
1585            UiPoint::new(0.0, 0.0),
1586            UiPoint::new(12.0, 0.0),
1587            UiPoint::new(12.0, 12.0),
1588        ];
1589        let butt = tessellate_polyline_stroke(
1590            &polyline,
1591            StrokeStyle::new(ColorRgba::WHITE, 3.0),
1592            PathStrokeOptions::new().line_cap(StrokeLineCap::Butt),
1593            false,
1594        );
1595        let round = tessellate_polyline_stroke(
1596            &polyline,
1597            StrokeStyle::new(ColorRgba::WHITE, 3.0),
1598            PathStrokeOptions::new()
1599                .line_cap(StrokeLineCap::Round)
1600                .line_join(StrokeLineJoin::Round),
1601            false,
1602        );
1603        assert!(round.len() > butt.len(), "round={round:?} butt={butt:?}");
1604    }
1605}