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