1use std::{ops::AddAssign, rc::Rc};
4
5use crate::{
6 Brush, Color, ColorFilter, ImageBitmap, ImageSampling,
7 stroke::{ArcGeometry, Stroke, arc_band},
8 typography::{
9 DrawTextMeasurer, DrawTextStyle, TextAlign, TextMeasurement, TextVerticalAlign,
10 estimate_text_measurement,
11 },
12};
13
14const VECTOR_PATH_MASK_CACHE_ENTRIES: usize = 96;
15const VECTOR_PATH_MASK_CACHE_BYTES: usize = 8 * 1024 * 1024;
16
17struct VectorPathMaskCache {
18 entries: Vec<(u64, ImageBitmap)>,
19 bytes: usize,
20}
21
22impl VectorPathMaskCache {
23 const fn new() -> Self {
24 Self {
25 entries: Vec::new(),
26 bytes: 0,
27 }
28 }
29
30 fn get(&mut self, key: u64) -> Option<ImageBitmap> {
31 let index = self.entries.iter().position(|(seen, _)| *seen == key)?;
32 let entry = self.entries.remove(index);
33 let image = entry.1.clone();
34 self.entries.push(entry);
35 Some(image)
36 }
37
38 fn put(&mut self, key: u64, image: ImageBitmap) {
39 let bytes = image.width() as usize * image.height() as usize * 4;
40 if bytes > VECTOR_PATH_MASK_CACHE_BYTES {
41 return;
42 }
43 self.bytes += bytes;
44 self.entries.push((key, image));
45 while self.entries.len() > VECTOR_PATH_MASK_CACHE_ENTRIES
46 || self.bytes > VECTOR_PATH_MASK_CACHE_BYTES
47 {
48 let (_, dropped) = self.entries.remove(0);
49 self.bytes = self
50 .bytes
51 .saturating_sub(dropped.width() as usize * dropped.height() as usize * 4);
52 }
53 }
54}
55
56thread_local! {
57 static VECTOR_PATH_MASKS: std::cell::RefCell<VectorPathMaskCache> =
58 const { std::cell::RefCell::new(VectorPathMaskCache::new()) };
59}
60
61fn vector_path_mask_key(
62 path: &crate::VectorPath,
63 origin: Point,
64 mask_size: (usize, usize),
65 rgb: [u8; 3],
66 alpha: f32,
67) -> u64 {
68 use std::hash::Hasher;
69 let mut hasher = crate::fx_hash::FxHasher::default();
70 hasher.write_u8(path.fill_rule() as u8);
71 hasher.write_u32(origin.x.to_bits());
72 hasher.write_u32(origin.y.to_bits());
73 hasher.write_usize(mask_size.0);
74 hasher.write_usize(mask_size.1);
75 hasher.write(&rgb);
76 hasher.write_u32(alpha.to_bits());
77 for subpath in path.subpaths() {
78 hasher.write_usize(subpath.len());
79 for point in subpath {
80 hasher.write_u32(point.x.to_bits());
81 hasher.write_u32(point.y.to_bits());
82 }
83 }
84 hasher.finish()
85}
86
87fn vector_path_mask_cache_get(key: u64) -> Option<ImageBitmap> {
88 VECTOR_PATH_MASKS.with(|cache| cache.borrow_mut().get(key))
89}
90
91fn vector_path_mask_cache_put(key: u64, image: ImageBitmap) {
92 VECTOR_PATH_MASKS.with(|cache| cache.borrow_mut().put(key, image));
93}
94
95#[derive(Clone, Copy, Debug, PartialEq, Default)]
96pub struct Point {
97 pub x: f32,
98 pub y: f32,
99}
100
101impl Point {
102 pub const fn new(x: f32, y: f32) -> Self {
103 Self { x, y }
104 }
105
106 pub const ZERO: Point = Point { x: 0.0, y: 0.0 };
107}
108
109#[derive(Clone, Copy, Debug, PartialEq, Default)]
110pub struct Size {
111 pub width: f32,
112 pub height: f32,
113}
114
115impl Size {
116 pub const fn new(width: f32, height: f32) -> Self {
117 Self { width, height }
118 }
119
120 pub const ZERO: Size = Size {
121 width: 0.0,
122 height: 0.0,
123 };
124}
125
126#[derive(Clone, Copy, Debug, PartialEq)]
127pub struct Rect {
128 pub x: f32,
129 pub y: f32,
130 pub width: f32,
131 pub height: f32,
132}
133
134impl Rect {
135 pub fn from_origin_size(origin: Point, size: Size) -> Self {
136 Self {
137 x: origin.x,
138 y: origin.y,
139 width: size.width,
140 height: size.height,
141 }
142 }
143
144 pub fn from_size(size: Size) -> Self {
145 Self {
146 x: 0.0,
147 y: 0.0,
148 width: size.width,
149 height: size.height,
150 }
151 }
152
153 pub fn translate(&self, dx: f32, dy: f32) -> Self {
154 Self {
155 x: self.x + dx,
156 y: self.y + dy,
157 width: self.width,
158 height: self.height,
159 }
160 }
161
162 pub fn contains(&self, x: f32, y: f32) -> bool {
163 x >= self.x && y >= self.y && x <= self.x + self.width && y <= self.y + self.height
164 }
165
166 pub fn intersect(&self, other: Rect) -> Option<Rect> {
168 let left = self.x.max(other.x);
169 let top = self.y.max(other.y);
170 let right = (self.x + self.width).min(other.x + other.width);
171 let bottom = (self.y + self.height).min(other.y + other.height);
172 let width = right - left;
173 let height = bottom - top;
174 if width <= 0.0 || height <= 0.0 {
175 None
176 } else {
177 Some(Rect {
178 x: left,
179 y: top,
180 width,
181 height,
182 })
183 }
184 }
185
186 pub fn union(&self, other: Rect) -> Rect {
187 let left = self.x.min(other.x);
188 let top = self.y.min(other.y);
189 let right = (self.x + self.width).max(other.x + other.width);
190 let bottom = (self.y + self.height).max(other.y + other.height);
191 Rect {
192 x: left,
193 y: top,
194 width: (right - left).max(0.0),
195 height: (bottom - top).max(0.0),
196 }
197 }
198}
199
200#[derive(Clone, Copy, Debug, Default, PartialEq)]
202pub struct EdgeInsets {
203 pub left: f32,
204 pub top: f32,
205 pub right: f32,
206 pub bottom: f32,
207}
208
209impl EdgeInsets {
210 pub fn uniform(all: f32) -> Self {
211 Self {
212 left: all,
213 top: all,
214 right: all,
215 bottom: all,
216 }
217 }
218
219 pub fn horizontal(horizontal: f32) -> Self {
220 Self {
221 left: horizontal,
222 right: horizontal,
223 ..Self::default()
224 }
225 }
226
227 pub fn vertical(vertical: f32) -> Self {
228 Self {
229 top: vertical,
230 bottom: vertical,
231 ..Self::default()
232 }
233 }
234
235 pub fn symmetric(horizontal: f32, vertical: f32) -> Self {
236 Self {
237 left: horizontal,
238 right: horizontal,
239 top: vertical,
240 bottom: vertical,
241 }
242 }
243
244 pub fn from_components(left: f32, top: f32, right: f32, bottom: f32) -> Self {
245 Self {
246 left,
247 top,
248 right,
249 bottom,
250 }
251 }
252
253 pub fn is_zero(&self) -> bool {
254 self.left == 0.0 && self.top == 0.0 && self.right == 0.0 && self.bottom == 0.0
255 }
256
257 pub fn horizontal_sum(&self) -> f32 {
258 self.left + self.right
259 }
260
261 pub fn vertical_sum(&self) -> f32 {
262 self.top + self.bottom
263 }
264}
265
266impl AddAssign for EdgeInsets {
267 fn add_assign(&mut self, rhs: Self) {
268 self.left += rhs.left;
269 self.top += rhs.top;
270 self.right += rhs.right;
271 self.bottom += rhs.bottom;
272 }
273}
274
275#[derive(Clone, Copy, Debug, Default, PartialEq)]
276pub struct CornerRadii {
277 pub top_left: f32,
278 pub top_right: f32,
279 pub bottom_right: f32,
280 pub bottom_left: f32,
281}
282
283impl CornerRadii {
284 pub fn uniform(radius: f32) -> Self {
285 Self {
286 top_left: radius,
287 top_right: radius,
288 bottom_right: radius,
289 bottom_left: radius,
290 }
291 }
292}
293
294#[derive(Clone, Copy, Debug, PartialEq)]
295pub struct RoundedCornerShape {
296 radii: CornerRadii,
297}
298
299impl RoundedCornerShape {
300 pub fn new(top_left: f32, top_right: f32, bottom_right: f32, bottom_left: f32) -> Self {
301 Self {
302 radii: CornerRadii {
303 top_left,
304 top_right,
305 bottom_right,
306 bottom_left,
307 },
308 }
309 }
310
311 pub fn uniform(radius: f32) -> Self {
312 Self {
313 radii: CornerRadii::uniform(radius),
314 }
315 }
316
317 pub fn with_radii(radii: CornerRadii) -> Self {
318 Self { radii }
319 }
320
321 pub fn resolve(&self, width: f32, height: f32) -> CornerRadii {
322 let mut resolved = self.radii;
323 let max_width = (width / 2.0).max(0.0);
324 let max_height = (height / 2.0).max(0.0);
325 resolved.top_left = resolved.top_left.clamp(0.0, max_width).min(max_height);
326 resolved.top_right = resolved.top_right.clamp(0.0, max_width).min(max_height);
327 resolved.bottom_right = resolved.bottom_right.clamp(0.0, max_width).min(max_height);
328 resolved.bottom_left = resolved.bottom_left.clamp(0.0, max_width).min(max_height);
329 resolved
330 }
331
332 pub fn radii(&self) -> CornerRadii {
333 self.radii
334 }
335}
336
337#[derive(Clone, Copy, Debug, PartialEq)]
338pub struct TransformOrigin {
339 pub pivot_fraction_x: f32,
340 pub pivot_fraction_y: f32,
341}
342
343impl TransformOrigin {
344 pub const fn new(pivot_fraction_x: f32, pivot_fraction_y: f32) -> Self {
345 Self {
346 pivot_fraction_x,
347 pivot_fraction_y,
348 }
349 }
350
351 pub const CENTER: TransformOrigin = TransformOrigin::new(0.5, 0.5);
352}
353
354impl Default for TransformOrigin {
355 fn default() -> Self {
356 Self::CENTER
357 }
358}
359
360#[derive(Clone, Copy, Debug, Default, PartialEq)]
361pub enum LayerShape {
362 #[default]
363 Rectangle,
364 Rounded(RoundedCornerShape),
365}
366
367#[derive(Clone, Debug, PartialEq)]
368pub struct GraphicsLayer {
369 pub alpha: f32,
370 pub scale: f32,
371 pub scale_x: f32,
372 pub scale_y: f32,
373 pub rotation_x: f32,
374 pub rotation_y: f32,
375 pub rotation_z: f32,
376 pub camera_distance: f32,
377 pub transform_origin: TransformOrigin,
378 pub translation_x: f32,
379 pub translation_y: f32,
380 pub shadow_elevation: f32,
381 pub ambient_shadow_color: Color,
382 pub spot_shadow_color: Color,
383 pub shape: LayerShape,
384 pub clip: bool,
385 pub compositing_strategy: CompositingStrategy,
386 pub blend_mode: BlendMode,
387 pub color_filter: Option<ColorFilter>,
388 pub render_effect: Option<crate::render_effect::RenderEffect>,
389 pub backdrop_effect: Option<crate::render_effect::RenderEffect>,
390}
391
392impl GraphicsLayer {
393 pub fn composite_alpha_8bit(alpha: f32) -> f32 {
417 (alpha.clamp(0.0, 1.0) * 255.0).floor() / 255.0
418 }
419}
420
421impl Default for GraphicsLayer {
422 fn default() -> Self {
423 Self {
424 alpha: 1.0,
425 scale: 1.0,
426 scale_x: 1.0,
427 scale_y: 1.0,
428 rotation_x: 0.0,
429 rotation_y: 0.0,
430 rotation_z: 0.0,
431 camera_distance: 8.0,
432 transform_origin: TransformOrigin::CENTER,
433 translation_x: 0.0,
434 translation_y: 0.0,
435 shadow_elevation: 0.0,
436 ambient_shadow_color: Color::BLACK,
437 spot_shadow_color: Color::BLACK,
438 shape: LayerShape::Rectangle,
439 clip: false,
440 compositing_strategy: CompositingStrategy::Auto,
441 blend_mode: BlendMode::SrcOver,
442 color_filter: None,
443 render_effect: None,
444 backdrop_effect: None,
445 }
446 }
447}
448
449#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
454pub enum BlendMode {
455 Clear,
456 Src,
457 Dst,
458 #[default]
459 SrcOver,
460 DstOver,
461 SrcIn,
462 DstIn,
463 SrcOut,
464 DstOut,
465 SrcAtop,
466 DstAtop,
467 Xor,
468 Plus,
469 Modulate,
470 Screen,
471 Overlay,
472 Darken,
473 Lighten,
474 ColorDodge,
475 ColorBurn,
476 HardLight,
477 SoftLight,
478 Difference,
479 Exclusion,
480 Multiply,
481 Hue,
482 Saturation,
483 Color,
484 Luminosity,
485}
486
487#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
489pub enum CompositingStrategy {
490 #[default]
492 Auto,
493 Offscreen,
495 ModulateAlpha,
497}
498
499#[derive(Clone, Debug, PartialEq)]
500pub enum DrawPrimitive {
501 Content,
504 Blend {
506 primitive: Box<DrawPrimitive>,
507 blend_mode: BlendMode,
508 },
509 Rect {
510 rect: Rect,
511 brush: Brush,
512 stroke: Option<Stroke>,
515 },
516 RoundRect {
517 rect: Rect,
518 brush: Brush,
519 radii: CornerRadii,
520 stroke: Option<Stroke>,
523 },
524 Arc {
534 rect: Rect,
538 brush: Brush,
539 center: Point,
540 radius: f32,
541 start_angle: f32,
542 sweep_angle: f32,
543 stroke: Option<Stroke>,
544 inner_radius: f32,
546 },
547 Image {
548 rect: Rect,
549 image: ImageBitmap,
550 alpha: f32,
551 color_filter: Option<ColorFilter>,
552 sampling: ImageSampling,
553 src_rect: Option<Rect>,
557 },
558 Text(Box<TextPrimitive>),
560 Shadow(ShadowPrimitive),
563}
564
565#[derive(Clone, Debug, PartialEq)]
574pub struct TextPrimitive {
575 pub rect: Rect,
578 pub text: std::rc::Rc<str>,
581 pub style: DrawTextStyle,
582 pub color: Color,
586}
587
588fn shared_text_str(text: &str) -> Rc<str> {
598 use std::{
599 cell::RefCell,
600 collections::HashMap,
601 hash::{Hash, Hasher},
602 };
603
604 const POOL_CAPACITY: usize = 256;
605 thread_local! {
606 static POOL: RefCell<HashMap<u64, Rc<str>>> = RefCell::new(HashMap::new());
607 }
608
609 let mut hasher = crate::FxHasher::default();
610 text.hash(&mut hasher);
611 let key = hasher.finish();
612
613 POOL.with(|pool| {
614 let mut pool = pool.borrow_mut();
615 if let Some(shared) = pool.get(&key)
616 && &**shared == text
617 {
618 return Rc::clone(shared);
619 }
620 let shared: Rc<str> = Rc::from(text);
621 if pool.len() >= POOL_CAPACITY {
622 pool.clear();
623 }
624 pool.insert(key, Rc::clone(&shared));
625 shared
626 })
627}
628
629#[derive(Clone, Debug, PartialEq)]
631pub enum ShadowPrimitive {
632 Drop {
636 shape: Box<DrawPrimitive>,
637 cutout: Option<Box<DrawPrimitive>>,
638 blur_radius: f32,
639 blend_mode: BlendMode,
640 },
641 Inner {
643 fill: Box<DrawPrimitive>,
644 cutout: Box<DrawPrimitive>,
645 blur_radius: f32,
646 blend_mode: BlendMode,
647 clip_rect: Rect,
649 },
650}
651
652pub trait DrawScope {
653 fn size(&self) -> Size;
654 fn draw_content(&mut self);
655 fn draw_rect(&mut self, brush: Brush);
656 fn draw_rect_blend(&mut self, brush: Brush, blend_mode: BlendMode);
657 fn draw_rect_at(&mut self, rect: Rect, brush: Brush);
659 fn draw_rect_at_blend(&mut self, rect: Rect, brush: Brush, blend_mode: BlendMode);
660 fn draw_round_rect(&mut self, brush: Brush, radii: CornerRadii);
661 fn draw_round_rect_blend(&mut self, brush: Brush, radii: CornerRadii, blend_mode: BlendMode);
662 fn draw_round_rect_at(&mut self, rect: Rect, brush: Brush, radii: CornerRadii);
664 fn draw_circle(&mut self, brush: Brush, center: Point, radius: f32);
665 fn draw_circle_blend(
666 &mut self,
667 brush: Brush,
668 center: Point,
669 radius: f32,
670 blend_mode: BlendMode,
671 );
672
673 fn draw_rect_stroked(&mut self, brush: Brush, stroke: Stroke);
675 fn draw_rect_stroked_blend(&mut self, brush: Brush, stroke: Stroke, blend_mode: BlendMode);
676 fn draw_rect_at_stroked(&mut self, rect: Rect, brush: Brush, stroke: Stroke);
678 fn draw_rect_at_stroked_blend(
679 &mut self,
680 rect: Rect,
681 brush: Brush,
682 stroke: Stroke,
683 blend_mode: BlendMode,
684 );
685 fn draw_round_rect_stroked(&mut self, brush: Brush, radii: CornerRadii, stroke: Stroke);
687 fn draw_round_rect_stroked_blend(
688 &mut self,
689 brush: Brush,
690 radii: CornerRadii,
691 stroke: Stroke,
692 blend_mode: BlendMode,
693 );
694 fn draw_round_rect_at_stroked(
696 &mut self,
697 rect: Rect,
698 brush: Brush,
699 radii: CornerRadii,
700 stroke: Stroke,
701 );
702 #[allow(clippy::too_many_arguments)]
703 fn draw_round_rect_at_stroked_blend(
704 &mut self,
705 rect: Rect,
706 brush: Brush,
707 radii: CornerRadii,
708 stroke: Stroke,
709 blend_mode: BlendMode,
710 );
711 fn draw_circle_stroked(&mut self, brush: Brush, center: Point, radius: f32, stroke: Stroke);
714 fn draw_circle_stroked_blend(
715 &mut self,
716 brush: Brush,
717 center: Point,
718 radius: f32,
719 stroke: Stroke,
720 blend_mode: BlendMode,
721 );
722
723 #[allow(clippy::too_many_arguments)]
736 fn draw_arc(
737 &mut self,
738 brush: Brush,
739 center: Point,
740 radius: f32,
741 start_angle: f32,
742 sweep_angle: f32,
743 stroke: Stroke,
744 );
745 #[allow(clippy::too_many_arguments)]
746 fn draw_arc_blend(
747 &mut self,
748 brush: Brush,
749 center: Point,
750 radius: f32,
751 start_angle: f32,
752 sweep_angle: f32,
753 stroke: Stroke,
754 blend_mode: BlendMode,
755 );
756
757 #[allow(clippy::too_many_arguments)]
766 fn draw_annular_sector(
767 &mut self,
768 brush: Brush,
769 center: Point,
770 inner_radius: f32,
771 outer_radius: f32,
772 start_angle: f32,
773 sweep_angle: f32,
774 );
775 #[allow(clippy::too_many_arguments)]
776 fn draw_annular_sector_blend(
777 &mut self,
778 brush: Brush,
779 center: Point,
780 inner_radius: f32,
781 outer_radius: f32,
782 start_angle: f32,
783 sweep_angle: f32,
784 blend_mode: BlendMode,
785 );
786
787 fn draw_image(&mut self, image: ImageBitmap);
788 fn draw_image_blend(&mut self, image: ImageBitmap, blend_mode: BlendMode);
789 fn draw_image_at(
790 &mut self,
791 rect: Rect,
792 image: ImageBitmap,
793 alpha: f32,
794 color_filter: Option<ColorFilter>,
795 );
796 fn draw_image_at_sampled(
797 &mut self,
798 rect: Rect,
799 image: ImageBitmap,
800 alpha: f32,
801 color_filter: Option<ColorFilter>,
802 sampling: ImageSampling,
803 );
804 fn draw_image_at_blend(
805 &mut self,
806 rect: Rect,
807 image: ImageBitmap,
808 alpha: f32,
809 color_filter: Option<ColorFilter>,
810 blend_mode: BlendMode,
811 );
812 fn draw_image_src(
815 &mut self,
816 image: ImageBitmap,
817 src_rect: Rect,
818 dst_rect: Rect,
819 alpha: f32,
820 color_filter: Option<ColorFilter>,
821 );
822 fn draw_image_src_sampled(
823 &mut self,
824 image: ImageBitmap,
825 src_rect: Rect,
826 dst_rect: Rect,
827 alpha: f32,
828 color_filter: Option<ColorFilter>,
829 sampling: ImageSampling,
830 );
831 fn draw_image_src_blend(
832 &mut self,
833 image: ImageBitmap,
834 src_rect: Rect,
835 dst_rect: Rect,
836 alpha: f32,
837 color_filter: Option<ColorFilter>,
838 blend_mode: BlendMode,
839 );
840 fn draw_vector_path(&mut self, path: &crate::VectorPath, brush: Brush);
849 fn draw_svg_path(&mut self, d: &str, brush: Brush) {
855 if let Ok(path) = crate::VectorPath::parse(d) {
856 self.draw_vector_path(&path, brush);
857 }
858 }
859
860 fn measure_text(&self, text: &str, style: &DrawTextStyle) -> TextMeasurement;
867
868 fn draw_text(&mut self, brush: Brush, text: &str, style: &DrawTextStyle) {
871 self.draw_text_at(Rect::from_size(self.size()), brush, text, style);
872 }
873
874 fn draw_text_at(&mut self, rect: Rect, brush: Brush, text: &str, style: &DrawTextStyle);
881
882 fn draw_text_from(&mut self, top_left: Point, brush: Brush, text: &str, style: &DrawTextStyle) {
888 if text.is_empty() {
889 return;
890 }
891 let measurement = self.measure_text(text, style);
892 self.draw_text_at(
893 Rect::from_origin_size(top_left, measurement.size),
894 brush,
895 text,
896 &DrawTextStyle {
897 align: TextAlign::Left,
898 vertical_align: TextVerticalAlign::Top,
899 ..style.clone()
900 },
901 );
902 }
903
904 fn into_primitives(self) -> Vec<DrawPrimitive>;
905}
906
907pub fn align_text_block(rect: Rect, measurement: TextMeasurement, style: &DrawTextStyle) -> Point {
913 let x = match style.align {
914 TextAlign::Left => rect.x,
915 TextAlign::Center => rect.x + (rect.width - measurement.size.width) * 0.5,
916 TextAlign::Right => rect.x + rect.width - measurement.size.width,
917 };
918 let y = match style.vertical_align {
919 TextVerticalAlign::Top => rect.y,
920 TextVerticalAlign::Center => rect.y + (rect.height - measurement.size.height) * 0.5,
921 TextVerticalAlign::Bottom => rect.y + rect.height - measurement.size.height,
922 TextVerticalAlign::Baseline => rect.y - measurement.first_baseline,
923 };
924 Point::new(x, y)
925}
926
927#[derive(Clone, Copy, Debug, PartialEq, Eq)]
930#[repr(u8)]
931pub(crate) enum RecordKind {
932 SolidRect,
933 SolidRoundRect,
934 SolidArc,
935 Other,
936}
937
938#[derive(Clone, Copy, Debug, PartialEq, Eq)]
943pub(crate) struct TapeRef(u32);
944
945impl TapeRef {
946 const KIND_SHIFT: u32 = 30;
947 const INDEX_MASK: u32 = (1 << Self::KIND_SHIFT) - 1;
948
949 pub(crate) fn new(kind: RecordKind, index: usize) -> Self {
950 debug_assert!(index < (1usize << Self::KIND_SHIFT));
951 Self(((kind as u32) << Self::KIND_SHIFT) | index as u32)
952 }
953
954 pub(crate) fn kind(self) -> RecordKind {
955 match self.0 >> Self::KIND_SHIFT {
956 0 => RecordKind::SolidRect,
957 1 => RecordKind::SolidRoundRect,
958 2 => RecordKind::SolidArc,
959 _ => RecordKind::Other,
960 }
961 }
962
963 pub(crate) fn index(self) -> usize {
964 (self.0 & Self::INDEX_MASK) as usize
965 }
966
967 pub(crate) fn raw(self) -> u32 {
968 self.0
969 }
970}
971
972#[derive(Clone, Copy, Debug, PartialEq)]
974pub struct SolidRectRecord {
975 pub rect: Rect,
976 pub color: Color,
977 pub stroke: Option<Stroke>,
978}
979
980#[derive(Clone, Copy, Debug, PartialEq)]
983pub struct SolidRoundRectRecord {
984 pub rect: Rect,
985 pub radii: CornerRadii,
986 pub color: Color,
987 pub stroke: Option<Stroke>,
988}
989
990#[derive(Clone, Copy, Debug, PartialEq)]
995pub struct SolidArcRecord {
996 pub center: Point,
997 pub radius: f32,
998 pub start_angle: f32,
999 pub sweep_angle: f32,
1000 pub inner_radius: f32,
1001 pub color: Color,
1002 pub stroke: Option<Stroke>,
1003}
1004
1005#[derive(Clone, Debug, Default)]
1013pub struct CommandRecording {
1014 pub(crate) tape: Vec<TapeRef>,
1015 pub(crate) rects: Vec<SolidRectRecord>,
1016 pub(crate) round_rects: Vec<SolidRoundRectRecord>,
1017 pub(crate) arcs: Vec<SolidArcRecord>,
1018 pub(crate) others: Vec<DrawPrimitive>,
1019}
1020
1021impl CommandRecording {
1022 pub fn len(&self) -> usize {
1024 self.tape.len()
1025 }
1026
1027 pub fn materialize_range(
1032 &self,
1033 tape_start: usize,
1034 tape_end: usize,
1035 ) -> Option<Vec<DrawPrimitive>> {
1036 if tape_start > tape_end || tape_end > self.tape.len() {
1037 return None;
1038 }
1039 let mut out = Vec::with_capacity(tape_end - tape_start);
1040 for entry in &self.tape[tape_start..tape_end] {
1041 match entry.kind() {
1042 RecordKind::SolidRect => {
1043 let record = self.rects.get(entry.index())?;
1044 out.push(DrawPrimitive::Rect {
1045 rect: record.rect,
1046 brush: Brush::Solid(record.color),
1047 stroke: record.stroke,
1048 });
1049 }
1050 RecordKind::SolidRoundRect => {
1051 let record = self.round_rects.get(entry.index())?;
1052 out.push(DrawPrimitive::RoundRect {
1053 rect: record.rect,
1054 brush: Brush::Solid(record.color),
1055 radii: record.radii,
1056 stroke: record.stroke,
1057 });
1058 }
1059 RecordKind::SolidArc => {
1060 let record = self.arcs.get(entry.index())?;
1061 if let Some(primitive) = materialize_solid_arc(record) {
1062 out.push(primitive);
1063 }
1064 }
1065 RecordKind::Other => {
1066 out.push(self.others.get(entry.index())?.clone());
1067 }
1068 }
1069 }
1070 Some(out)
1071 }
1072
1073 pub fn is_empty(&self) -> bool {
1074 self.tape.is_empty()
1075 }
1076
1077 #[doc(hidden)]
1078 pub fn tape_ptr(&self) -> *const u8 {
1079 self.tape.as_ptr() as *const u8
1080 }
1081
1082 fn clear(&mut self) {
1083 self.tape.clear();
1084 self.rects.clear();
1085 self.round_rects.clear();
1086 self.arcs.clear();
1087 self.others.clear();
1088 }
1089
1090 pub(crate) fn clone_records_from(&mut self, source: &Self) {
1091 self.tape.clone_from(&source.tape);
1092 self.rects.clone_from(&source.rects);
1093 self.round_rects.clone_from(&source.round_rects);
1094 self.arcs.clone_from(&source.arcs);
1095 self.others.clone_from(&source.others);
1096 }
1097}
1098
1099pub struct FinishedRecording {
1103 pub primitives: Vec<DrawPrimitive>,
1104 pub content_markers: u32,
1105 pub recording: CommandRecording,
1106 pub dropped: Vec<u32>,
1111}
1112
1113#[derive(Default)]
1114pub struct DrawScopeDefault {
1115 size: Size,
1116 rec: CommandRecording,
1117 out: Vec<DrawPrimitive>,
1118 content_markers: u32,
1119 text_measurer: Option<Rc<dyn DrawTextMeasurer>>,
1120}
1121
1122const RECORDED_PRIMITIVE_COUNTS_LIMIT: usize = 64;
1129
1130thread_local! {
1131 static RECORDED_PRIMITIVE_COUNTS: std::cell::RefCell<std::collections::HashMap<(u32, u32), usize>> =
1132 std::cell::RefCell::new(std::collections::HashMap::new());
1133}
1134
1135fn recorded_primitive_capacity(size: Size) -> usize {
1136 RECORDED_PRIMITIVE_COUNTS.with(|counts| {
1137 counts
1138 .borrow()
1139 .get(&(size.width.to_bits(), size.height.to_bits()))
1140 .copied()
1141 .unwrap_or(0)
1142 })
1143}
1144
1145fn note_recorded_primitive_count(size: Size, count: usize) {
1146 RECORDED_PRIMITIVE_COUNTS.with(|counts| {
1147 let mut counts = counts.borrow_mut();
1148 if counts.len() >= RECORDED_PRIMITIVE_COUNTS_LIMIT {
1149 counts.clear();
1150 }
1151 counts.insert((size.width.to_bits(), size.height.to_bits()), count);
1152 });
1153}
1154
1155impl DrawScopeDefault {
1156 pub fn new(size: Size) -> Self {
1157 Self::with_recording(size, None, CommandRecording::default(), Vec::new())
1158 }
1159
1160 pub fn with_text_measurer(size: Size, text_measurer: Rc<dyn DrawTextMeasurer>) -> Self {
1165 Self::with_recording(
1166 size,
1167 Some(text_measurer),
1168 CommandRecording::default(),
1169 Vec::new(),
1170 )
1171 }
1172
1173 pub fn with_text_measurer_reusing(
1179 size: Size,
1180 text_measurer: Rc<dyn DrawTextMeasurer>,
1181 storage: Vec<DrawPrimitive>,
1182 ) -> Self {
1183 Self::with_recording(
1184 size,
1185 Some(text_measurer),
1186 CommandRecording::default(),
1187 storage,
1188 )
1189 }
1190
1191 pub fn with_recording(
1195 size: Size,
1196 text_measurer: Option<Rc<dyn DrawTextMeasurer>>,
1197 mut recording: CommandRecording,
1198 out: Vec<DrawPrimitive>,
1199 ) -> Self {
1200 recording.clear();
1201 recording.tape.reserve(recorded_primitive_capacity(size));
1202 Self {
1203 size,
1204 rec: recording,
1205 out,
1206 content_markers: 0,
1207 text_measurer,
1208 }
1209 }
1210
1211 pub fn content_marker_count(&self) -> u32 {
1215 self.content_markers
1216 }
1217
1218 pub fn recorded(&self) -> &CommandRecording {
1221 &self.rec
1222 }
1223
1224 pub fn push_recorded(&mut self, primitives: Vec<DrawPrimitive>) {
1229 self.content_markers += primitives
1230 .iter()
1231 .filter(|primitive| matches!(primitive, DrawPrimitive::Content))
1232 .count() as u32;
1233 let base = self.rec.others.len();
1234 self.rec
1235 .tape
1236 .extend((0..primitives.len()).map(|i| TapeRef::new(RecordKind::Other, base + i)));
1237 self.rec.others.extend(primitives);
1238 }
1239
1240 pub fn finish(mut self) -> FinishedRecording {
1246 if std::env::var_os("CRANPOSE_RECORD_MIX_DIAG").is_some() && self.rec.tape.len() > 400 {
1247 eprintln!(
1248 "[record-mix] tape={} rects={} round_rects={} arcs={} others={}",
1249 self.rec.tape.len(),
1250 self.rec.rects.len(),
1251 self.rec.round_rects.len(),
1252 self.rec.arcs.len(),
1253 self.rec.others.len(),
1254 );
1255 }
1256 let mut out = std::mem::take(&mut self.out);
1257 out.clear();
1258 out.reserve(self.rec.tape.len());
1259 let mut dropped: Vec<u32> = Vec::new();
1260 {
1261 let mut others = self.rec.others.drain(..);
1262 for (tape_index, entry) in self.rec.tape.iter().enumerate() {
1263 match entry.kind() {
1264 RecordKind::SolidRect => {
1265 let record = &self.rec.rects[entry.index()];
1266 out.push(DrawPrimitive::Rect {
1267 rect: record.rect,
1268 brush: Brush::Solid(record.color),
1269 stroke: record.stroke,
1270 });
1271 }
1272 RecordKind::SolidRoundRect => {
1273 let record = &self.rec.round_rects[entry.index()];
1274 out.push(DrawPrimitive::RoundRect {
1275 rect: record.rect,
1276 brush: Brush::Solid(record.color),
1277 radii: record.radii,
1278 stroke: record.stroke,
1279 });
1280 }
1281 RecordKind::SolidArc => {
1282 let record = &self.rec.arcs[entry.index()];
1283 if let Some(primitive) = materialize_solid_arc(record) {
1284 out.push(primitive);
1285 } else {
1286 dropped.push(tape_index as u32);
1287 }
1288 }
1289 RecordKind::Other => {
1290 out.push(others.next().expect("tape/others in sync"));
1291 }
1292 }
1293 }
1294 }
1295 self.rec.clear();
1296 note_recorded_primitive_count(self.size, out.len());
1297 FinishedRecording {
1298 primitives: out,
1299 content_markers: self.content_markers,
1300 recording: self.rec,
1301 dropped,
1302 }
1303 }
1304
1305 pub fn finish_recording_only(mut self) -> FinishedRecording {
1314 let mut out = std::mem::take(&mut self.out);
1315 out.clear();
1316 note_recorded_primitive_count(self.size, self.rec.tape.len());
1317 self.rec.clear();
1318 FinishedRecording {
1319 primitives: out,
1320 content_markers: self.content_markers,
1321 recording: self.rec,
1322 dropped: Vec::new(),
1323 }
1324 }
1325
1326 pub fn finish_replay(
1334 mut self,
1335 center: Point,
1336 outcome: crate::record_replay::ReplayOutcome,
1337 bypass: &mut dyn FnMut(u32) -> bool,
1338 ) -> (
1339 FinishedRecording,
1340 Option<crate::record_replay::CommandReplayFrame>,
1341 ) {
1342 use crate::record_replay::{CommandReplayFrame, FrameSpan, ReplayOutcome, ReplaySpan};
1343 let ReplayOutcome::Spans(replay_spans) = outcome else {
1344 return (self.finish(), None);
1345 };
1346 let tape_len = self.rec.tape.len();
1347 let mut out = std::mem::take(&mut self.out);
1348 out.clear();
1349 let mut dropped: Vec<u32> = Vec::new();
1350 let mut spans: Vec<FrameSpan> = Vec::with_capacity(replay_spans.len());
1351 let mut any_retained = false;
1352 {
1353 let mut others = self.rec.others.drain(..);
1354 let tape = &self.rec.tape;
1355 let rects = &self.rec.rects;
1356 let round_rects = &self.rec.round_rects;
1357 let arcs = &self.rec.arcs;
1358 macro_rules! materialize_range {
1359 ($start:expr, $end:expr) => {{
1360 let prim_start = out.len() as u32;
1361 for tape_index in $start..$end {
1362 let entry = tape[tape_index];
1363 match entry.kind() {
1364 RecordKind::SolidRect => {
1365 let record = &rects[entry.index()];
1366 out.push(DrawPrimitive::Rect {
1367 rect: record.rect,
1368 brush: Brush::Solid(record.color),
1369 stroke: record.stroke,
1370 });
1371 }
1372 RecordKind::SolidRoundRect => {
1373 let record = &round_rects[entry.index()];
1374 out.push(DrawPrimitive::RoundRect {
1375 rect: record.rect,
1376 brush: Brush::Solid(record.color),
1377 radii: record.radii,
1378 stroke: record.stroke,
1379 });
1380 }
1381 RecordKind::SolidArc => {
1382 let record = &arcs[entry.index()];
1383 if let Some(primitive) = materialize_solid_arc(record) {
1384 out.push(primitive);
1385 } else {
1386 dropped.push(tape_index as u32);
1387 }
1388 }
1389 RecordKind::Other => {
1390 out.push(others.next().expect("tape/others in sync"));
1391 }
1392 }
1393 }
1394 (prim_start, out.len() as u32)
1395 }};
1396 }
1397 for span in replay_spans {
1398 match span {
1399 ReplaySpan::Dynamic {
1400 tape_start,
1401 tape_end,
1402 } => {
1403 let range = materialize_range!(tape_start, tape_end);
1404 if range.1 > range.0 {
1405 spans.push(FrameSpan::Dynamic { range });
1406 }
1407 }
1408 ReplaySpan::Retained {
1409 slot,
1410 capture,
1411 slot_offset,
1412 tape_start,
1413 tape_end,
1414 transform,
1415 recolors,
1416 bounds,
1417 } => {
1418 let compact = tape[tape_start..tape_end]
1419 .iter()
1420 .all(|entry| entry.kind() != RecordKind::Other);
1421 if compact && !capture && bypass(slot) {
1422 any_retained = true;
1423 let position = out.len() as u32;
1424 spans.push(FrameSpan::Retained {
1425 slot,
1426 capture: false,
1427 slot_offset: slot_offset as u32,
1428 range: (position, position),
1429 tape_range: (tape_start as u32, tape_end as u32),
1430 transform,
1431 recolors,
1432 bounds,
1433 });
1434 continue;
1435 }
1436 let drops_before = dropped.len();
1437 let range = materialize_range!(tape_start, tape_end);
1438 if compact && dropped.len() == drops_before {
1439 any_retained = true;
1440 spans.push(FrameSpan::Retained {
1441 slot,
1442 capture,
1443 slot_offset: slot_offset as u32,
1444 range,
1445 tape_range: (tape_start as u32, tape_end as u32),
1446 transform,
1447 recolors,
1448 bounds,
1449 });
1450 } else if range.1 > range.0 {
1451 spans.push(FrameSpan::Dynamic { range });
1452 }
1453 }
1454 }
1455 }
1456 }
1457 note_recorded_primitive_count(self.size, tape_len);
1458 let frame = any_retained.then_some(CommandReplayFrame {
1459 center,
1460 spans,
1461 fallback: None,
1462 });
1463 (
1464 FinishedRecording {
1465 primitives: out,
1466 content_markers: self.content_markers,
1467 recording: self.rec,
1468 dropped,
1469 },
1470 frame,
1471 )
1472 }
1473
1474 fn push_other(&mut self, primitive: DrawPrimitive) {
1475 let idx = self.rec.others.len();
1476 self.rec.tape.push(TapeRef::new(RecordKind::Other, idx));
1477 self.rec.others.push(primitive);
1478 }
1479
1480 fn push_blended_primitive(&mut self, primitive: DrawPrimitive, blend_mode: BlendMode) {
1481 if blend_mode != BlendMode::SrcOver {
1482 self.push_other(DrawPrimitive::Blend {
1483 primitive: Box::new(primitive),
1484 blend_mode,
1485 });
1486 return;
1487 }
1488 match primitive {
1489 DrawPrimitive::Rect {
1490 rect,
1491 brush: Brush::Solid(color),
1492 stroke,
1493 } => {
1494 let idx = self.rec.rects.len();
1495 self.rec.tape.push(TapeRef::new(RecordKind::SolidRect, idx));
1496 self.rec.rects.push(SolidRectRecord {
1497 rect,
1498 color,
1499 stroke,
1500 });
1501 }
1502 DrawPrimitive::RoundRect {
1503 rect,
1504 brush: Brush::Solid(color),
1505 radii,
1506 stroke,
1507 } => {
1508 let idx = self.rec.round_rects.len();
1509 self.rec
1510 .tape
1511 .push(TapeRef::new(RecordKind::SolidRoundRect, idx));
1512 self.rec.round_rects.push(SolidRoundRectRecord {
1513 rect,
1514 radii,
1515 color,
1516 stroke,
1517 });
1518 }
1519 other => self.push_other(other),
1520 }
1521 }
1522
1523 #[allow(clippy::too_many_arguments)]
1524 fn push_arc(
1525 &mut self,
1526 brush: Brush,
1527 center: Point,
1528 radius: f32,
1529 start_angle: f32,
1530 sweep_angle: f32,
1531 stroke: Option<Stroke>,
1532 inner_radius: f32,
1533 blend_mode: BlendMode,
1534 ) {
1535 if blend_mode == BlendMode::SrcOver
1536 && let Brush::Solid(color) = brush
1537 {
1538 let idx = self.rec.arcs.len();
1539 self.rec.tape.push(TapeRef::new(RecordKind::SolidArc, idx));
1540 self.rec.arcs.push(SolidArcRecord {
1541 center,
1542 radius,
1543 start_angle,
1544 sweep_angle,
1545 inner_radius,
1546 color,
1547 stroke,
1548 });
1549 return;
1550 }
1551 let (band_inner, band_outer, cap) = arc_band(radius, inner_radius, stroke);
1552 let geometry = ArcGeometry::new(
1553 center,
1554 band_inner,
1555 band_outer,
1556 start_angle,
1557 sweep_angle,
1558 cap,
1559 );
1560 if geometry.is_degenerate() {
1561 return;
1562 }
1563 self.push_blended_primitive(
1564 DrawPrimitive::Arc {
1565 rect: geometry.bounds(),
1566 brush,
1567 center,
1568 radius,
1569 start_angle,
1570 sweep_angle,
1571 stroke,
1572 inner_radius,
1573 },
1574 blend_mode,
1575 );
1576 }
1577}
1578
1579fn materialize_solid_arc(record: &SolidArcRecord) -> Option<DrawPrimitive> {
1584 let (band_inner, band_outer, cap) = arc_band(record.radius, record.inner_radius, record.stroke);
1585 let geometry = ArcGeometry::new(
1586 record.center,
1587 band_inner,
1588 band_outer,
1589 record.start_angle,
1590 record.sweep_angle,
1591 cap,
1592 );
1593 if geometry.is_degenerate() {
1594 return None;
1595 }
1596 Some(DrawPrimitive::Arc {
1597 rect: geometry.bounds(),
1598 brush: Brush::Solid(record.color),
1599 center: record.center,
1600 radius: record.radius,
1601 start_angle: record.start_angle,
1602 sweep_angle: record.sweep_angle,
1603 stroke: record.stroke,
1604 inner_radius: record.inner_radius,
1605 })
1606}
1607
1608impl DrawScope for DrawScopeDefault {
1609 fn size(&self) -> Size {
1610 self.size
1611 }
1612
1613 fn draw_content(&mut self) {
1614 self.content_markers += 1;
1615 self.push_other(DrawPrimitive::Content);
1616 }
1617
1618 fn draw_rect(&mut self, brush: Brush) {
1619 self.draw_rect_blend(brush, BlendMode::SrcOver);
1620 }
1621
1622 fn draw_rect_blend(&mut self, brush: Brush, blend_mode: BlendMode) {
1623 self.push_blended_primitive(
1624 DrawPrimitive::Rect {
1625 rect: Rect::from_size(self.size),
1626 brush,
1627 stroke: None,
1628 },
1629 blend_mode,
1630 );
1631 }
1632
1633 fn draw_rect_at(&mut self, rect: Rect, brush: Brush) {
1634 self.draw_rect_at_blend(rect, brush, BlendMode::SrcOver);
1635 }
1636
1637 fn draw_rect_at_blend(&mut self, rect: Rect, brush: Brush, blend_mode: BlendMode) {
1638 self.push_blended_primitive(
1639 DrawPrimitive::Rect {
1640 rect,
1641 brush,
1642 stroke: None,
1643 },
1644 blend_mode,
1645 );
1646 }
1647
1648 fn draw_round_rect(&mut self, brush: Brush, radii: CornerRadii) {
1649 self.draw_round_rect_blend(brush, radii, BlendMode::SrcOver);
1650 }
1651
1652 fn draw_round_rect_blend(&mut self, brush: Brush, radii: CornerRadii, blend_mode: BlendMode) {
1653 self.push_blended_primitive(
1654 DrawPrimitive::RoundRect {
1655 rect: Rect::from_size(self.size),
1656 brush,
1657 radii,
1658 stroke: None,
1659 },
1660 blend_mode,
1661 );
1662 }
1663
1664 fn draw_round_rect_at(&mut self, rect: Rect, brush: Brush, radii: CornerRadii) {
1665 self.push_blended_primitive(
1666 DrawPrimitive::RoundRect {
1667 rect,
1668 brush,
1669 radii,
1670 stroke: None,
1671 },
1672 BlendMode::SrcOver,
1673 );
1674 }
1675
1676 fn draw_rect_stroked(&mut self, brush: Brush, stroke: Stroke) {
1677 self.draw_rect_stroked_blend(brush, stroke, BlendMode::SrcOver);
1678 }
1679
1680 fn draw_rect_stroked_blend(&mut self, brush: Brush, stroke: Stroke, blend_mode: BlendMode) {
1681 self.draw_rect_at_stroked_blend(Rect::from_size(self.size), brush, stroke, blend_mode);
1682 }
1683
1684 fn draw_rect_at_stroked(&mut self, rect: Rect, brush: Brush, stroke: Stroke) {
1685 self.draw_rect_at_stroked_blend(rect, brush, stroke, BlendMode::SrcOver);
1686 }
1687
1688 fn draw_rect_at_stroked_blend(
1689 &mut self,
1690 rect: Rect,
1691 brush: Brush,
1692 stroke: Stroke,
1693 blend_mode: BlendMode,
1694 ) {
1695 if !stroke.is_visible() {
1696 return;
1697 }
1698 self.push_blended_primitive(
1699 DrawPrimitive::Rect {
1700 rect,
1701 brush,
1702 stroke: Some(stroke),
1703 },
1704 blend_mode,
1705 );
1706 }
1707
1708 fn draw_round_rect_stroked(&mut self, brush: Brush, radii: CornerRadii, stroke: Stroke) {
1709 self.draw_round_rect_stroked_blend(brush, radii, stroke, BlendMode::SrcOver);
1710 }
1711
1712 fn draw_round_rect_stroked_blend(
1713 &mut self,
1714 brush: Brush,
1715 radii: CornerRadii,
1716 stroke: Stroke,
1717 blend_mode: BlendMode,
1718 ) {
1719 self.draw_round_rect_at_stroked_blend(
1720 Rect::from_size(self.size),
1721 brush,
1722 radii,
1723 stroke,
1724 blend_mode,
1725 );
1726 }
1727
1728 fn draw_round_rect_at_stroked(
1729 &mut self,
1730 rect: Rect,
1731 brush: Brush,
1732 radii: CornerRadii,
1733 stroke: Stroke,
1734 ) {
1735 self.draw_round_rect_at_stroked_blend(rect, brush, radii, stroke, BlendMode::SrcOver);
1736 }
1737
1738 fn draw_round_rect_at_stroked_blend(
1739 &mut self,
1740 rect: Rect,
1741 brush: Brush,
1742 radii: CornerRadii,
1743 stroke: Stroke,
1744 blend_mode: BlendMode,
1745 ) {
1746 if !stroke.is_visible() {
1747 return;
1748 }
1749 self.push_blended_primitive(
1750 DrawPrimitive::RoundRect {
1751 rect,
1752 brush,
1753 radii,
1754 stroke: Some(stroke),
1755 },
1756 blend_mode,
1757 );
1758 }
1759
1760 fn draw_circle_stroked(&mut self, brush: Brush, center: Point, radius: f32, stroke: Stroke) {
1761 self.draw_circle_stroked_blend(brush, center, radius, stroke, BlendMode::SrcOver);
1762 }
1763
1764 fn draw_circle_stroked_blend(
1765 &mut self,
1766 brush: Brush,
1767 center: Point,
1768 radius: f32,
1769 stroke: Stroke,
1770 blend_mode: BlendMode,
1771 ) {
1772 if !stroke.is_visible() || !radius.is_finite() {
1773 return;
1774 }
1775 let radius = radius.max(0.0);
1776 let diameter = radius * 2.0;
1777 self.draw_round_rect_at_stroked_blend(
1778 Rect {
1779 x: center.x - radius,
1780 y: center.y - radius,
1781 width: diameter,
1782 height: diameter,
1783 },
1784 brush,
1785 CornerRadii::uniform(radius),
1786 stroke,
1787 blend_mode,
1788 );
1789 }
1790
1791 fn draw_arc(
1792 &mut self,
1793 brush: Brush,
1794 center: Point,
1795 radius: f32,
1796 start_angle: f32,
1797 sweep_angle: f32,
1798 stroke: Stroke,
1799 ) {
1800 self.draw_arc_blend(
1801 brush,
1802 center,
1803 radius,
1804 start_angle,
1805 sweep_angle,
1806 stroke,
1807 BlendMode::SrcOver,
1808 );
1809 }
1810
1811 fn draw_arc_blend(
1812 &mut self,
1813 brush: Brush,
1814 center: Point,
1815 radius: f32,
1816 start_angle: f32,
1817 sweep_angle: f32,
1818 stroke: Stroke,
1819 blend_mode: BlendMode,
1820 ) {
1821 if !stroke.is_visible() {
1822 return;
1823 }
1824 self.push_arc(
1825 brush,
1826 center,
1827 radius,
1828 start_angle,
1829 sweep_angle,
1830 Some(stroke),
1831 0.0,
1832 blend_mode,
1833 );
1834 }
1835
1836 fn draw_annular_sector(
1837 &mut self,
1838 brush: Brush,
1839 center: Point,
1840 inner_radius: f32,
1841 outer_radius: f32,
1842 start_angle: f32,
1843 sweep_angle: f32,
1844 ) {
1845 self.draw_annular_sector_blend(
1846 brush,
1847 center,
1848 inner_radius,
1849 outer_radius,
1850 start_angle,
1851 sweep_angle,
1852 BlendMode::SrcOver,
1853 );
1854 }
1855
1856 fn draw_annular_sector_blend(
1857 &mut self,
1858 brush: Brush,
1859 center: Point,
1860 inner_radius: f32,
1861 outer_radius: f32,
1862 start_angle: f32,
1863 sweep_angle: f32,
1864 blend_mode: BlendMode,
1865 ) {
1866 self.push_arc(
1867 brush,
1868 center,
1869 outer_radius,
1870 start_angle,
1871 sweep_angle,
1872 None,
1873 inner_radius,
1874 blend_mode,
1875 );
1876 }
1877
1878 fn draw_circle(&mut self, brush: Brush, center: Point, radius: f32) {
1879 self.draw_circle_blend(brush, center, radius, BlendMode::SrcOver);
1880 }
1881
1882 fn draw_circle_blend(
1883 &mut self,
1884 brush: Brush,
1885 center: Point,
1886 radius: f32,
1887 blend_mode: BlendMode,
1888 ) {
1889 let radius = radius.max(0.0);
1890 let diameter = radius * 2.0;
1891 self.push_blended_primitive(
1892 DrawPrimitive::RoundRect {
1893 rect: Rect {
1894 x: center.x - radius,
1895 y: center.y - radius,
1896 width: diameter,
1897 height: diameter,
1898 },
1899 brush,
1900 radii: CornerRadii::uniform(radius),
1901 stroke: None,
1902 },
1903 blend_mode,
1904 );
1905 }
1906
1907 fn draw_image(&mut self, image: ImageBitmap) {
1908 self.draw_image_blend(image, BlendMode::SrcOver);
1909 }
1910
1911 fn draw_image_blend(&mut self, image: ImageBitmap, blend_mode: BlendMode) {
1912 self.push_blended_primitive(
1913 DrawPrimitive::Image {
1914 rect: Rect::from_size(self.size),
1915 image,
1916 alpha: 1.0,
1917 color_filter: None,
1918 sampling: ImageSampling::Nearest,
1919 src_rect: None,
1920 },
1921 blend_mode,
1922 );
1923 }
1924
1925 fn draw_image_at(
1926 &mut self,
1927 rect: Rect,
1928 image: ImageBitmap,
1929 alpha: f32,
1930 color_filter: Option<ColorFilter>,
1931 ) {
1932 self.draw_image_at_sampled(rect, image, alpha, color_filter, ImageSampling::Nearest);
1933 }
1934
1935 fn draw_image_at_sampled(
1936 &mut self,
1937 rect: Rect,
1938 image: ImageBitmap,
1939 alpha: f32,
1940 color_filter: Option<ColorFilter>,
1941 sampling: ImageSampling,
1942 ) {
1943 self.push_blended_primitive(
1944 DrawPrimitive::Image {
1945 rect,
1946 image,
1947 alpha: alpha.clamp(0.0, 1.0),
1948 color_filter,
1949 sampling,
1950 src_rect: None,
1951 },
1952 BlendMode::SrcOver,
1953 );
1954 }
1955
1956 fn draw_image_at_blend(
1957 &mut self,
1958 rect: Rect,
1959 image: ImageBitmap,
1960 alpha: f32,
1961 color_filter: Option<ColorFilter>,
1962 blend_mode: BlendMode,
1963 ) {
1964 self.push_blended_primitive(
1965 DrawPrimitive::Image {
1966 rect,
1967 image,
1968 alpha: alpha.clamp(0.0, 1.0),
1969 color_filter,
1970 sampling: ImageSampling::Nearest,
1971 src_rect: None,
1972 },
1973 blend_mode,
1974 );
1975 }
1976
1977 fn draw_image_src(
1978 &mut self,
1979 image: ImageBitmap,
1980 src_rect: Rect,
1981 dst_rect: Rect,
1982 alpha: f32,
1983 color_filter: Option<ColorFilter>,
1984 ) {
1985 self.draw_image_src_blend(
1986 image,
1987 src_rect,
1988 dst_rect,
1989 alpha,
1990 color_filter,
1991 BlendMode::SrcOver,
1992 );
1993 }
1994
1995 fn draw_image_src_sampled(
1996 &mut self,
1997 image: ImageBitmap,
1998 src_rect: Rect,
1999 dst_rect: Rect,
2000 alpha: f32,
2001 color_filter: Option<ColorFilter>,
2002 sampling: ImageSampling,
2003 ) {
2004 self.push_blended_primitive(
2005 DrawPrimitive::Image {
2006 rect: dst_rect,
2007 image,
2008 alpha: alpha.clamp(0.0, 1.0),
2009 color_filter,
2010 sampling,
2011 src_rect: Some(src_rect),
2012 },
2013 BlendMode::SrcOver,
2014 );
2015 }
2016
2017 fn draw_image_src_blend(
2018 &mut self,
2019 image: ImageBitmap,
2020 src_rect: Rect,
2021 dst_rect: Rect,
2022 alpha: f32,
2023 color_filter: Option<ColorFilter>,
2024 blend_mode: BlendMode,
2025 ) {
2026 self.push_blended_primitive(
2027 DrawPrimitive::Image {
2028 rect: dst_rect,
2029 image,
2030 alpha: alpha.clamp(0.0, 1.0),
2031 color_filter,
2032 sampling: ImageSampling::Nearest,
2033 src_rect: Some(src_rect),
2034 },
2035 blend_mode,
2036 );
2037 }
2038
2039 fn draw_vector_path(&mut self, path: &crate::VectorPath, brush: Brush) {
2040 const SUPERSAMPLE: f32 = 2.0;
2041 const MAX_MASK_PIXELS: f32 = 4096.0;
2042
2043 if path.is_empty() {
2044 return;
2045 }
2046 let bounds = path.bounds();
2047 if bounds.width <= 0.0 || bounds.height <= 0.0 {
2048 return;
2049 }
2050
2051 let color = match &brush {
2052 Brush::Solid(color) => *color,
2053 Brush::LinearGradient { colors, .. }
2054 | Brush::RadialGradient { colors, .. }
2055 | Brush::SweepGradient { colors, .. } => match colors.first() {
2056 Some(color) => *color,
2057 None => return,
2058 },
2059 };
2060 if color.3 <= 0.0 {
2061 return;
2062 }
2063
2064 let origin = Point::new(bounds.x.floor() - 1.0, bounds.y.floor() - 1.0);
2065 let rect_width = (bounds.x + bounds.width).ceil() - origin.x + 1.0;
2066 let rect_height = (bounds.y + bounds.height).ceil() - origin.y + 1.0;
2067 let mask_width = (rect_width * SUPERSAMPLE)
2068 .ceil()
2069 .clamp(1.0, MAX_MASK_PIXELS) as usize;
2070 let mask_height = (rect_height * SUPERSAMPLE)
2071 .ceil()
2072 .clamp(1.0, MAX_MASK_PIXELS) as usize;
2073
2074 let red = (color.0.clamp(0.0, 1.0) * 255.0 + 0.5) as u8;
2075 let green = (color.1.clamp(0.0, 1.0) * 255.0 + 0.5) as u8;
2076 let blue = (color.2.clamp(0.0, 1.0) * 255.0 + 0.5) as u8;
2077 let alpha = color.3.clamp(0.0, 1.0);
2078 let key = vector_path_mask_key(
2079 path,
2080 origin,
2081 (mask_width, mask_height),
2082 [red, green, blue],
2083 alpha,
2084 );
2085 let cached = vector_path_mask_cache_get(key);
2086 let image = match cached {
2087 Some(image) => image,
2088 None => {
2089 let mask = path.coverage_mask(mask_width, mask_height, origin, SUPERSAMPLE);
2090 let mut pixels = Vec::with_capacity(mask.len() * 4);
2091 for coverage in mask {
2092 pixels.extend_from_slice(&[
2093 red,
2094 green,
2095 blue,
2096 (alpha * coverage as f32 + 0.5) as u8,
2097 ]);
2098 }
2099 let Ok(image) =
2100 ImageBitmap::from_rgba8(mask_width as u32, mask_height as u32, pixels)
2101 else {
2102 return;
2103 };
2104 vector_path_mask_cache_put(key, image.clone());
2105 image
2106 }
2107 };
2108
2109 self.push_other(DrawPrimitive::Image {
2110 rect: Rect {
2111 x: origin.x,
2112 y: origin.y,
2113 width: rect_width,
2114 height: rect_height,
2115 },
2116 image,
2117 alpha: 1.0,
2118 color_filter: None,
2119 sampling: ImageSampling::Linear,
2120 src_rect: None,
2121 });
2122 }
2123
2124 fn measure_text(&self, text: &str, style: &DrawTextStyle) -> TextMeasurement {
2125 match &self.text_measurer {
2126 Some(measurer) => measurer.measure_text(text, style),
2127 None => estimate_text_measurement(text, style),
2128 }
2129 }
2130
2131 fn draw_text_at(&mut self, rect: Rect, brush: Brush, text: &str, style: &DrawTextStyle) {
2132 if text.is_empty() {
2133 return;
2134 }
2135 let Some(color) = solid_fill_color(&brush) else {
2136 return;
2137 };
2138 if color.3 <= 0.0 {
2139 return;
2140 }
2141 let measurement = self.measure_text(text, style);
2142 if !(measurement.size.width > 0.0 && measurement.size.height > 0.0) {
2143 return;
2144 }
2145 let origin = align_text_block(rect, measurement, style);
2146 if !origin.x.is_finite() || !origin.y.is_finite() {
2147 return;
2148 }
2149 self.push_other(DrawPrimitive::Text(Box::new(TextPrimitive {
2150 rect: Rect::from_origin_size(origin, measurement.size),
2151 text: shared_text_str(text),
2152 style: style.clone(),
2153 color,
2154 })));
2155 }
2156
2157 fn into_primitives(self) -> Vec<DrawPrimitive> {
2158 self.finish().primitives
2159 }
2160}
2161
2162fn solid_fill_color(brush: &Brush) -> Option<Color> {
2167 match brush {
2168 Brush::Solid(color) => Some(*color),
2169 Brush::LinearGradient { colors, .. }
2170 | Brush::RadialGradient { colors, .. }
2171 | Brush::SweepGradient { colors, .. } => colors.first().copied(),
2172 }
2173}
2174
2175#[cfg(test)]
2176mod tests {
2177 use super::*;
2178 use crate::{Color, FontStyle, FontWeight, ImageBitmap, RenderEffect};
2179
2180 #[test]
2181 fn compact_recording_materializes_in_recorded_order() {
2182 let size = Size::new(100.0, 100.0);
2183 let solid = Brush::solid(Color::WHITE);
2184 let gradient = Brush::vertical_gradient(vec![Color::RED, Color::BLUE], 0.0, 100.0);
2185 let center = Point::new(50.0, 50.0);
2186 let stroke = Stroke::new(4.0);
2187 let rect = Rect {
2188 x: 10.0,
2189 y: 20.0,
2190 width: 30.0,
2191 height: 40.0,
2192 };
2193 let batch = vec![
2194 DrawPrimitive::Content,
2195 DrawPrimitive::Rect {
2196 rect,
2197 brush: solid.clone(),
2198 stroke: None,
2199 },
2200 ];
2201
2202 let record = |scope: &mut DrawScopeDefault| {
2203 scope.draw_rect_at(rect, solid.clone());
2204 scope.draw_arc(solid.clone(), center, 30.0, 0.5, 1.5, stroke);
2205 scope.draw_rect_at(rect, gradient.clone());
2206 scope.draw_circle(solid.clone(), center, 12.0);
2207 scope.draw_arc(solid.clone(), center, 30.0, 0.5, 0.0, stroke);
2208 scope.draw_rect_at_blend(rect, solid.clone(), BlendMode::Plus);
2209 scope.draw_content();
2210 scope.draw_annular_sector(gradient.clone(), center, 10.0, 20.0, 0.0, 2.0);
2211 scope.push_recorded(batch.clone());
2212 };
2213
2214 let mut compact = DrawScopeDefault::new(size);
2215 record(&mut compact);
2216 let finished = compact.finish();
2217
2218 let arc_via_ordinary = |brush: Brush, radius: f32, start: f32, sweep: f32| {
2219 let mut scope = DrawScopeDefault::new(size);
2220 scope.draw_arc(brush, center, radius, start, sweep, stroke);
2221 scope.into_primitives().remove(0)
2222 };
2223 let expected = vec![
2224 DrawPrimitive::Rect {
2225 rect,
2226 brush: solid.clone(),
2227 stroke: None,
2228 },
2229 arc_via_ordinary(solid.clone(), 30.0, 0.5, 1.5),
2230 DrawPrimitive::Rect {
2231 rect,
2232 brush: gradient.clone(),
2233 stroke: None,
2234 },
2235 DrawPrimitive::RoundRect {
2236 rect: Rect {
2237 x: center.x - 12.0,
2238 y: center.y - 12.0,
2239 width: 24.0,
2240 height: 24.0,
2241 },
2242 brush: solid.clone(),
2243 radii: CornerRadii::uniform(12.0),
2244 stroke: None,
2245 },
2246 DrawPrimitive::Blend {
2247 primitive: Box::new(DrawPrimitive::Rect {
2248 rect,
2249 brush: solid.clone(),
2250 stroke: None,
2251 }),
2252 blend_mode: BlendMode::Plus,
2253 },
2254 DrawPrimitive::Content,
2255 {
2256 let mut scope = DrawScopeDefault::new(size);
2257 scope.draw_annular_sector(gradient.clone(), center, 10.0, 20.0, 0.0, 2.0);
2258 scope.into_primitives().remove(0)
2259 },
2260 DrawPrimitive::Content,
2261 DrawPrimitive::Rect {
2262 rect,
2263 brush: solid.clone(),
2264 stroke: None,
2265 },
2266 ];
2267 assert_eq!(finished.primitives, expected);
2268 assert_eq!(finished.content_markers, 2);
2269 }
2270
2271 #[test]
2272 fn reused_recording_buffers_record_identically_to_fresh() {
2273 let size = Size::new(64.0, 64.0);
2274 let record = |scope: &mut DrawScopeDefault| {
2275 scope.draw_circle(Brush::solid(Color::RED), Point::new(32.0, 32.0), 10.0);
2276 scope.draw_arc(
2277 Brush::solid(Color::BLUE),
2278 Point::new(32.0, 32.0),
2279 20.0,
2280 0.0,
2281 3.0,
2282 Stroke::new(2.0),
2283 );
2284 };
2285
2286 let mut fresh = DrawScopeDefault::new(size);
2287 record(&mut fresh);
2288 let fresh = fresh.finish();
2289
2290 let mut dirty = DrawScopeDefault::new(size);
2291 dirty.draw_rect(Brush::solid(Color::BLACK));
2292 dirty.draw_content();
2293 dirty.draw_arc(
2294 Brush::solid(Color::WHITE),
2295 Point::new(1.0, 1.0),
2296 5.0,
2297 1.0,
2298 1.0,
2299 Stroke::new(1.0),
2300 );
2301 let dirty = dirty.finish();
2302
2303 let mut reused =
2304 DrawScopeDefault::with_recording(size, None, dirty.recording, dirty.primitives);
2305 record(&mut reused);
2306 let reused = reused.finish();
2307
2308 assert_eq!(fresh.primitives, reused.primitives);
2309 assert_eq!(fresh.content_markers, reused.content_markers);
2310 }
2311
2312 #[test]
2313 fn redrawing_the_same_text_shares_one_str_allocation() {
2314 let first = shared_text_str("BREAK THE RING");
2315 let second = shared_text_str("BREAK THE RING");
2316 assert!(Rc::ptr_eq(&first, &second));
2317 assert_eq!(&*second, "BREAK THE RING");
2318 }
2319
2320 #[test]
2321 fn different_text_gets_its_own_str() {
2322 let first = shared_text_str("340");
2323 let second = shared_text_str("350");
2324 assert!(!Rc::ptr_eq(&first, &second));
2325 assert_eq!(&*first, "340");
2326 assert_eq!(&*second, "350");
2327 }
2328
2329 #[test]
2330 fn the_text_pool_survives_overflowing_its_capacity() {
2331 for index in 0..600 {
2332 let text = format!("run-{index}");
2333 assert_eq!(&*shared_text_str(&text), text.as_str());
2334 }
2335 assert_eq!(&*shared_text_str("still correct"), "still correct");
2336 }
2337
2338 fn assert_image_alpha(primitive: &DrawPrimitive, expected: f32) {
2339 match primitive {
2340 DrawPrimitive::Image { alpha, .. } => assert!((alpha - expected).abs() < 1e-5),
2341 DrawPrimitive::Blend { primitive, .. } => assert_image_alpha(primitive, expected),
2342 other => panic!("expected image primitive, got {other:?}"),
2343 }
2344 }
2345
2346 fn unwrap_image(primitive: &DrawPrimitive) -> &DrawPrimitive {
2347 match primitive {
2348 DrawPrimitive::Image { .. } => primitive,
2349 DrawPrimitive::Blend { primitive, .. } => unwrap_image(primitive),
2350 other => panic!("expected image primitive, got {other:?}"),
2351 }
2352 }
2353
2354 #[test]
2355 fn draw_svg_path_emits_supersampled_image_over_path_bounds() {
2356 let mut scope = DrawScopeDefault::new(Size::new(32.0, 32.0));
2357 scope.draw_svg_path("M 4 4 H 20 V 20 H 4 Z", Brush::solid(Color::RED));
2358
2359 let primitives = scope.into_primitives();
2360 assert_eq!(primitives.len(), 1);
2361 let DrawPrimitive::Image { rect, image, .. } = &primitives[0] else {
2362 panic!("expected image primitive, got {:?}", primitives[0]);
2363 };
2364
2365 assert_eq!((rect.x, rect.y), (3.0, 3.0));
2366 assert_eq!((rect.width, rect.height), (18.0, 18.0));
2367 assert_eq!((image.width(), image.height()), (36, 36));
2368
2369 let pixels = image.pixels();
2370 let index = (18 * 36 + 18) * 4;
2371 assert_eq!(
2372 &pixels[index..index + 4],
2373 &[255, 0, 0, 255],
2374 "path interior must be opaque brush color"
2375 );
2376 assert_eq!(pixels[3], 0, "outside the path must stay transparent");
2377 }
2378
2379 #[test]
2380 fn draw_svg_path_ignores_invalid_data() {
2381 let mut scope = DrawScopeDefault::new(Size::new(16.0, 16.0));
2382 scope.draw_svg_path("definitely not a path", Brush::solid(Color::WHITE));
2383 assert!(scope.into_primitives().is_empty());
2384 }
2385
2386 #[test]
2387 fn draw_vector_path_applies_brush_alpha() {
2388 let path = crate::VectorPath::parse("M 0 0 H 8 V 8 H 0 Z").expect("valid path");
2389 let mut scope = DrawScopeDefault::new(Size::new(16.0, 16.0));
2390 scope.draw_vector_path(&path, Brush::solid(Color::rgba(0.0, 0.0, 1.0, 0.5)));
2391
2392 let primitives = scope.into_primitives();
2393 let DrawPrimitive::Image { image, .. } = &primitives[0] else {
2394 panic!("expected image primitive");
2395 };
2396 let pixels = image.pixels();
2397 let width = image.width() as usize;
2398 let index = ((image.height() as usize / 2) * width + width / 2) * 4;
2399 assert_eq!(&pixels[index..index + 3], &[0, 0, 255]);
2400 let alpha = pixels[index + 3];
2401 assert!(
2402 (alpha as i32 - 128).abs() <= 2,
2403 "interior alpha must honor the brush alpha, got {alpha}"
2404 );
2405 }
2406
2407 #[test]
2408 fn the_same_path_and_color_reuse_one_raster() {
2409 let path = crate::VectorPath::parse("M 0 0 H 7 V 7 H 0 Z").expect("valid path");
2410 let raster_of = |brush: Brush| {
2411 let mut scope = DrawScopeDefault::new(Size::new(16.0, 16.0));
2412 scope.draw_vector_path(&path, brush);
2413 let primitives = scope.into_primitives();
2414 let DrawPrimitive::Image { image, .. } = &primitives[0] else {
2415 panic!("expected image primitive");
2416 };
2417 image.clone()
2418 };
2419
2420 let first = raster_of(Brush::solid(Color::rgba(0.0, 0.0, 1.0, 1.0)));
2421 let second = raster_of(Brush::solid(Color::rgba(0.0, 0.0, 1.0, 1.0)));
2422 assert_eq!(first.id(), second.id());
2423
2424 let other_color = raster_of(Brush::solid(Color::rgba(1.0, 0.0, 0.0, 1.0)));
2425 assert_ne!(first.id(), other_color.id());
2426
2427 let wider = crate::VectorPath::parse("M 0 0 H 9 V 7 H 0 Z").expect("valid path");
2428 let mut scope = DrawScopeDefault::new(Size::new(16.0, 16.0));
2429 scope.draw_vector_path(&wider, Brush::solid(Color::rgba(0.0, 0.0, 1.0, 1.0)));
2430 let primitives = scope.into_primitives();
2431 let DrawPrimitive::Image { image, .. } = &primitives[0] else {
2432 panic!("expected image primitive");
2433 };
2434 assert_ne!(first.id(), image.id());
2435 }
2436
2437 #[test]
2438 fn draw_content_inserts_content_marker() {
2439 let mut scope = DrawScopeDefault::new(Size::new(8.0, 8.0));
2440 scope.draw_rect(Brush::solid(Color::WHITE));
2441 scope.draw_content();
2442 scope.draw_rect_blend(Brush::solid(Color::BLACK), BlendMode::DstOut);
2443
2444 let primitives = scope.into_primitives();
2445 assert!(matches!(primitives[1], DrawPrimitive::Content));
2446 assert!(matches!(
2447 primitives[2],
2448 DrawPrimitive::Blend {
2449 blend_mode: BlendMode::DstOut,
2450 ..
2451 }
2452 ));
2453 }
2454
2455 #[test]
2456 fn a_reused_recording_buffer_records_what_a_fresh_one_would() {
2457 let size = Size::new(16.0, 16.0);
2458 let measurer: Rc<dyn DrawTextMeasurer> = FixedAdvanceTextMeasurer::shared(10.0, 20.0);
2459
2460 let draw = |scope: &mut DrawScopeDefault| {
2461 scope.draw_rect(Brush::solid(Color::WHITE));
2462 scope.draw_content();
2463 scope.draw_rect(Brush::solid(Color::BLACK));
2464 scope.draw_content();
2465 };
2466
2467 let mut fresh = DrawScopeDefault::with_text_measurer(size, Rc::clone(&measurer));
2468 draw(&mut fresh);
2469 assert_eq!(fresh.content_marker_count(), 2);
2470 let expected = fresh.finish();
2471
2472 let storage = Vec::with_capacity(64);
2473 let mut reused = DrawScopeDefault::with_text_measurer_reusing(size, measurer, storage);
2474 draw(&mut reused);
2475 assert_eq!(reused.content_marker_count(), 2);
2476 let reused = reused.finish();
2477
2478 assert_eq!(reused.primitives.len(), expected.primitives.len());
2479 assert_eq!(reused.content_markers, expected.content_markers);
2480 assert_eq!(reused.dropped, expected.dropped);
2481 }
2482
2483 #[test]
2484 fn finishing_recording_only_materializes_nothing_but_still_reports_markers() {
2485 let mut scope = DrawScopeDefault::new(Size::new(8.0, 8.0));
2486 scope.draw_rect(Brush::solid(Color::WHITE));
2487 scope.draw_content();
2488 assert_eq!(scope.content_marker_count(), 1);
2489
2490 let finished = scope.finish_recording_only();
2491 assert!(
2492 finished.primitives.is_empty(),
2493 "nothing should have been materialized"
2494 );
2495 assert_eq!(finished.content_markers, 1);
2496 assert!(finished.dropped.is_empty());
2497 assert_eq!(
2498 finished.recording.tape.len(),
2499 0,
2500 "the recording buffer comes back cleared, ready to be recorded into again"
2501 );
2502 }
2503
2504 #[test]
2505 fn a_text_block_is_placed_by_its_alignment_inside_the_rect() {
2506 let rect = Rect {
2507 x: 10.0,
2508 y: 20.0,
2509 width: 100.0,
2510 height: 40.0,
2511 };
2512 let measurement = TextMeasurement {
2513 size: Size::new(60.0, 16.0),
2514 line_height: 16.0,
2515 first_baseline: 12.0,
2516 line_count: 1,
2517 };
2518 let style = |align, vertical| {
2519 DrawTextStyle::default()
2520 .with_align(align)
2521 .with_vertical_align(vertical)
2522 };
2523
2524 let left = align_text_block(
2525 rect,
2526 measurement,
2527 &style(TextAlign::Left, TextVerticalAlign::Top),
2528 );
2529 assert_eq!(left, Point::new(10.0, 20.0));
2530
2531 let centered = align_text_block(
2532 rect,
2533 measurement,
2534 &style(TextAlign::Center, TextVerticalAlign::Center),
2535 );
2536 assert_eq!(centered, Point::new(10.0 + 20.0, 20.0 + 12.0));
2537
2538 let right = align_text_block(
2539 rect,
2540 measurement,
2541 &style(TextAlign::Right, TextVerticalAlign::Bottom),
2542 );
2543 assert_eq!(right, Point::new(50.0, 44.0));
2544
2545 let baseline = align_text_block(
2546 rect,
2547 measurement,
2548 &style(TextAlign::Left, TextVerticalAlign::Baseline),
2549 );
2550 assert_eq!(baseline, Point::new(10.0, 20.0 - 12.0));
2551 }
2552
2553 #[test]
2554 fn draw_rect_blend_wraps_non_default_modes() {
2555 let mut scope = DrawScopeDefault::new(Size::new(10.0, 10.0));
2556 scope.draw_rect_blend(Brush::solid(Color::RED), BlendMode::DstOut);
2557
2558 let primitives = scope.into_primitives();
2559 assert_eq!(primitives.len(), 1);
2560 match &primitives[0] {
2561 DrawPrimitive::Blend {
2562 primitive,
2563 blend_mode,
2564 } => {
2565 assert_eq!(*blend_mode, BlendMode::DstOut);
2566 assert!(matches!(**primitive, DrawPrimitive::Rect { .. }));
2567 }
2568 other => panic!("expected blended primitive, got {other:?}"),
2569 }
2570 }
2571
2572 #[test]
2573 fn draw_circle_records_centered_round_rect() {
2574 let mut scope = DrawScopeDefault::new(Size::new(40.0, 40.0));
2575 scope.draw_circle(Brush::solid(Color::BLUE), Point::new(12.0, 16.0), 5.0);
2576
2577 let primitives = scope.into_primitives();
2578 assert_eq!(primitives.len(), 1);
2579 match &primitives[0] {
2580 DrawPrimitive::RoundRect { rect, radii, .. } => {
2581 assert_eq!(
2582 *rect,
2583 Rect {
2584 x: 7.0,
2585 y: 11.0,
2586 width: 10.0,
2587 height: 10.0,
2588 }
2589 );
2590 assert_eq!(*radii, CornerRadii::uniform(5.0));
2591 }
2592 other => panic!("expected circular round rect, got {other:?}"),
2593 }
2594 }
2595
2596 #[test]
2597 fn draw_circle_blend_wraps_non_default_modes() {
2598 let mut scope = DrawScopeDefault::new(Size::new(10.0, 10.0));
2599 scope.draw_circle_blend(
2600 Brush::solid(Color::RED),
2601 Point::new(5.0, 5.0),
2602 3.0,
2603 BlendMode::Plus,
2604 );
2605
2606 let primitives = scope.into_primitives();
2607 assert_eq!(primitives.len(), 1);
2608 match &primitives[0] {
2609 DrawPrimitive::Blend {
2610 primitive,
2611 blend_mode,
2612 } => {
2613 assert_eq!(*blend_mode, BlendMode::Plus);
2614 assert!(matches!(**primitive, DrawPrimitive::RoundRect { .. }));
2615 }
2616 other => panic!("expected blended circle primitive, got {other:?}"),
2617 }
2618 }
2619
2620 #[test]
2621 fn rect_union_encloses_both_inputs() {
2622 let lhs = Rect {
2623 x: 10.0,
2624 y: 5.0,
2625 width: 8.0,
2626 height: 4.0,
2627 };
2628 let rhs = Rect {
2629 x: 4.0,
2630 y: 7.0,
2631 width: 10.0,
2632 height: 6.0,
2633 };
2634
2635 assert_eq!(
2636 lhs.union(rhs),
2637 Rect {
2638 x: 4.0,
2639 y: 5.0,
2640 width: 14.0,
2641 height: 8.0,
2642 }
2643 );
2644 }
2645
2646 #[test]
2647 fn draw_image_uses_scope_size_as_default_rect() {
2648 let mut scope = DrawScopeDefault::new(Size::new(40.0, 24.0));
2649 let image = ImageBitmap::from_rgba8(2, 2, vec![255; 16]).expect("image");
2650 scope.draw_image(image.clone());
2651 let primitives = scope.into_primitives();
2652 assert_eq!(primitives.len(), 1);
2653 match unwrap_image(&primitives[0]) {
2654 DrawPrimitive::Image {
2655 rect,
2656 image: actual,
2657 alpha,
2658 color_filter,
2659 sampling,
2660 src_rect,
2661 } => {
2662 assert_eq!(*rect, Rect::from_size(Size::new(40.0, 24.0)));
2663 assert_eq!(*actual, image);
2664 assert_eq!(*alpha, 1.0);
2665 assert!(color_filter.is_none());
2666 assert_eq!(*sampling, ImageSampling::Nearest);
2667 assert!(src_rect.is_none());
2668 }
2669 other => panic!("expected image primitive, got {other:?}"),
2670 }
2671 }
2672
2673 #[test]
2674 fn draw_image_src_stores_src_rect() {
2675 let mut scope = DrawScopeDefault::new(Size::new(100.0, 100.0));
2676 let image = ImageBitmap::from_rgba8(64, 64, vec![255; 64 * 64 * 4]).expect("image");
2677 let src = Rect {
2678 x: 10.0,
2679 y: 20.0,
2680 width: 30.0,
2681 height: 40.0,
2682 };
2683 let dst = Rect {
2684 x: 0.0,
2685 y: 0.0,
2686 width: 60.0,
2687 height: 80.0,
2688 };
2689 scope.draw_image_src(image.clone(), src, dst, 0.8, None);
2690 let primitives = scope.into_primitives();
2691 assert_eq!(primitives.len(), 1);
2692 match unwrap_image(&primitives[0]) {
2693 DrawPrimitive::Image {
2694 rect,
2695 image: actual,
2696 alpha,
2697 sampling,
2698 src_rect,
2699 ..
2700 } => {
2701 assert_eq!(*rect, dst);
2702 assert_eq!(*actual, image);
2703 assert!((alpha - 0.8).abs() < 1e-5);
2704 assert_eq!(*sampling, ImageSampling::Nearest);
2705 assert_eq!(*src_rect, Some(src));
2706 }
2707 other => panic!("expected image primitive, got {other:?}"),
2708 }
2709 }
2710
2711 #[test]
2712 fn draw_image_at_sampled_records_requested_sampling() {
2713 let mut scope = DrawScopeDefault::new(Size::new(100.0, 100.0));
2714 let image = ImageBitmap::from_rgba8(8, 8, vec![255; 8 * 8 * 4]).expect("image");
2715 let dst = Rect {
2716 x: 2.0,
2717 y: 3.0,
2718 width: 40.0,
2719 height: 30.0,
2720 };
2721
2722 scope.draw_image_at_sampled(dst, image.clone(), 0.7, None, ImageSampling::Linear);
2723
2724 let primitives = scope.into_primitives();
2725 assert_eq!(primitives.len(), 1);
2726 match unwrap_image(&primitives[0]) {
2727 DrawPrimitive::Image {
2728 rect,
2729 image: actual,
2730 alpha,
2731 sampling,
2732 src_rect,
2733 ..
2734 } => {
2735 assert_eq!(*rect, dst);
2736 assert_eq!(*actual, image);
2737 assert!((alpha - 0.7).abs() < 1e-5);
2738 assert_eq!(*sampling, ImageSampling::Linear);
2739 assert!(src_rect.is_none());
2740 }
2741 other => panic!("expected image primitive, got {other:?}"),
2742 }
2743 }
2744
2745 #[test]
2746 fn draw_image_src_sampled_records_requested_sampling() {
2747 let mut scope = DrawScopeDefault::new(Size::new(100.0, 100.0));
2748 let image = ImageBitmap::from_rgba8(64, 64, vec![255; 64 * 64 * 4]).expect("image");
2749 let src = Rect {
2750 x: 4.0,
2751 y: 6.0,
2752 width: 16.0,
2753 height: 20.0,
2754 };
2755 let dst = Rect {
2756 x: 8.0,
2757 y: 10.0,
2758 width: 32.0,
2759 height: 40.0,
2760 };
2761
2762 scope.draw_image_src_sampled(image.clone(), src, dst, 0.5, None, ImageSampling::Linear);
2763
2764 let primitives = scope.into_primitives();
2765 assert_eq!(primitives.len(), 1);
2766 match unwrap_image(&primitives[0]) {
2767 DrawPrimitive::Image {
2768 rect,
2769 image: actual,
2770 alpha,
2771 sampling,
2772 src_rect,
2773 ..
2774 } => {
2775 assert_eq!(*rect, dst);
2776 assert_eq!(*actual, image);
2777 assert!((alpha - 0.5).abs() < 1e-5);
2778 assert_eq!(*sampling, ImageSampling::Linear);
2779 assert_eq!(*src_rect, Some(src));
2780 }
2781 other => panic!("expected image primitive, got {other:?}"),
2782 }
2783 }
2784
2785 #[test]
2786 fn draw_image_at_clamps_alpha() {
2787 let mut scope = DrawScopeDefault::new(Size::new(10.0, 10.0));
2788 let image = ImageBitmap::from_rgba8(1, 1, vec![255, 255, 255, 255]).expect("image");
2789 scope.draw_image_at(
2790 Rect::from_origin_size(Point::new(2.0, 3.0), Size::new(5.0, 6.0)),
2791 image,
2792 3.0,
2793 Some(ColorFilter::Tint(Color::from_rgba_u8(128, 128, 255, 255))),
2794 );
2795 assert_image_alpha(&scope.into_primitives()[0], 1.0);
2796 }
2797
2798 #[test]
2799 fn graphics_layer_clone_with_render_effect() {
2800 let layer = GraphicsLayer {
2801 render_effect: Some(RenderEffect::blur(10.0)),
2802 backdrop_effect: Some(RenderEffect::blur(6.0)),
2803 color_filter: Some(ColorFilter::tint(Color::from_rgba_u8(128, 200, 255, 255))),
2804 alpha: 0.5,
2805 rotation_z: 12.0,
2806 shadow_elevation: 4.0,
2807 shape: LayerShape::Rounded(RoundedCornerShape::uniform(6.0)),
2808 clip: true,
2809 compositing_strategy: CompositingStrategy::Offscreen,
2810 blend_mode: BlendMode::SrcOver,
2811 ..Default::default()
2812 };
2813 let cloned = layer.clone();
2814 assert_eq!(cloned.alpha, 0.5);
2815 assert!(cloned.render_effect.is_some());
2816 assert!(cloned.backdrop_effect.is_some());
2817 assert_eq!(layer.color_filter, cloned.color_filter);
2818 assert_eq!(layer.render_effect, cloned.render_effect);
2819 assert_eq!(layer.backdrop_effect, cloned.backdrop_effect);
2820 assert!((cloned.rotation_z - 12.0).abs() < 1e-6);
2821 assert!((cloned.shadow_elevation - 4.0).abs() < 1e-6);
2822 assert_eq!(
2823 cloned.shape,
2824 LayerShape::Rounded(RoundedCornerShape::uniform(6.0))
2825 );
2826 assert!(cloned.clip);
2827 assert_eq!(cloned.compositing_strategy, CompositingStrategy::Offscreen);
2828 assert_eq!(cloned.blend_mode, BlendMode::SrcOver);
2829 }
2830
2831 #[test]
2832 fn graphics_layer_default_has_no_effect() {
2833 let layer = GraphicsLayer::default();
2834 assert!(layer.color_filter.is_none());
2835 assert!(layer.render_effect.is_none());
2836 assert!(layer.backdrop_effect.is_none());
2837 assert_eq!(layer.compositing_strategy, CompositingStrategy::Auto);
2838 assert_eq!(layer.blend_mode, BlendMode::SrcOver);
2839 assert_eq!(layer.alpha, 1.0);
2840 assert_eq!(layer.transform_origin, TransformOrigin::CENTER);
2841 assert!((layer.camera_distance - 8.0).abs() < 1e-6);
2842 assert_eq!(layer.shape, LayerShape::Rectangle);
2843 assert!(!layer.clip);
2844 assert_eq!(layer.ambient_shadow_color, Color::BLACK);
2845 assert_eq!(layer.spot_shadow_color, Color::BLACK);
2846 }
2847
2848 #[test]
2849 fn transform_origin_construction() {
2850 let origin = TransformOrigin::new(0.25, 0.75);
2851 assert!((origin.pivot_fraction_x - 0.25).abs() < 1e-6);
2852 assert!((origin.pivot_fraction_y - 0.75).abs() < 1e-6);
2853 }
2854
2855 #[test]
2856 fn layer_shape_default_is_rectangle() {
2857 assert_eq!(LayerShape::default(), LayerShape::Rectangle);
2858 }
2859
2860 use std::f32::consts::{FRAC_PI_2, PI};
2861
2862 use crate::{StrokeCap, StrokeJoin};
2863
2864 fn approx(a: f32, b: f32) -> bool {
2865 (a - b).abs() < 0.25
2866 }
2867
2868 fn scope(size: f32) -> DrawScopeDefault {
2869 DrawScopeDefault::new(Size::new(size, size))
2870 }
2871
2872 #[test]
2873 fn draw_rect_stroked_records_scope_rect_and_stroke() {
2874 let mut scope = scope(20.0);
2875 scope.draw_rect_stroked(
2876 Brush::solid(Color::RED),
2877 Stroke::new(3.0).with_join(StrokeJoin::Bevel),
2878 );
2879
2880 let primitives = scope.into_primitives();
2881 assert_eq!(primitives.len(), 1);
2882 match &primitives[0] {
2883 DrawPrimitive::Rect {
2884 rect,
2885 stroke: Some(stroke),
2886 ..
2887 } => {
2888 assert_eq!(*rect, Rect::from_size(Size::new(20.0, 20.0)));
2889 assert_eq!(stroke.width, 3.0);
2890 assert_eq!(stroke.join, StrokeJoin::Bevel);
2891 }
2892 other => panic!("expected stroked rect, got {other:?}"),
2893 }
2894 }
2895
2896 #[test]
2897 fn draw_rect_at_stroked_records_requested_rect() {
2898 let mut scope = scope(50.0);
2899 let rect = Rect {
2900 x: 4.0,
2901 y: 6.0,
2902 width: 12.0,
2903 height: 9.0,
2904 };
2905 scope.draw_rect_at_stroked(rect, Brush::solid(Color::BLUE), Stroke::new(2.0));
2906 match &scope.into_primitives()[0] {
2907 DrawPrimitive::Rect {
2908 rect: actual,
2909 stroke: Some(stroke),
2910 ..
2911 } => {
2912 assert_eq!(*actual, rect);
2913 assert_eq!(stroke.width, 2.0);
2914 }
2915 other => panic!("expected stroked rect, got {other:?}"),
2916 }
2917 }
2918
2919 #[test]
2920 fn draw_round_rect_stroked_keeps_radii_and_stroke() {
2921 let mut scope = scope(30.0);
2922 scope.draw_round_rect_stroked(
2923 Brush::solid(Color::GREEN),
2924 CornerRadii::uniform(5.0),
2925 Stroke::new(4.0).with_join(StrokeJoin::Round),
2926 );
2927 match &scope.into_primitives()[0] {
2928 DrawPrimitive::RoundRect {
2929 rect,
2930 radii,
2931 stroke: Some(stroke),
2932 ..
2933 } => {
2934 assert_eq!(*rect, Rect::from_size(Size::new(30.0, 30.0)));
2935 assert_eq!(*radii, CornerRadii::uniform(5.0));
2936 assert_eq!(stroke.width, 4.0);
2937 assert_eq!(stroke.join, StrokeJoin::Round);
2938 }
2939 other => panic!("expected stroked round rect, got {other:?}"),
2940 }
2941 }
2942
2943 #[test]
2944 fn draw_round_rect_at_stroked_records_requested_rect() {
2945 let mut scope = scope(60.0);
2946 let rect = Rect {
2947 x: 1.0,
2948 y: 2.0,
2949 width: 20.0,
2950 height: 10.0,
2951 };
2952 scope.draw_round_rect_at_stroked(
2953 rect,
2954 Brush::solid(Color::WHITE),
2955 CornerRadii::uniform(3.0),
2956 Stroke::new(1.5),
2957 );
2958 match &scope.into_primitives()[0] {
2959 DrawPrimitive::RoundRect {
2960 rect: actual,
2961 radii,
2962 stroke: Some(stroke),
2963 ..
2964 } => {
2965 assert_eq!(*actual, rect);
2966 assert_eq!(*radii, CornerRadii::uniform(3.0));
2967 assert_eq!(stroke.width, 1.5);
2968 }
2969 other => panic!("expected stroked round rect, got {other:?}"),
2970 }
2971 }
2972
2973 #[test]
2974 fn draw_circle_stroked_lowers_to_stroked_round_rect() {
2975 let mut scope = scope(40.0);
2976 scope.draw_circle_stroked(
2977 Brush::solid(Color::BLUE),
2978 Point::new(12.0, 16.0),
2979 5.0,
2980 Stroke::new(2.0),
2981 );
2982 match &scope.into_primitives()[0] {
2983 DrawPrimitive::RoundRect {
2984 rect,
2985 radii,
2986 stroke: Some(stroke),
2987 ..
2988 } => {
2989 assert_eq!(
2990 *rect,
2991 Rect {
2992 x: 7.0,
2993 y: 11.0,
2994 width: 10.0,
2995 height: 10.0,
2996 }
2997 );
2998 assert_eq!(*radii, CornerRadii::uniform(5.0));
2999 assert_eq!(stroke.width, 2.0);
3000 }
3001 other => panic!("expected stroked circular round rect, got {other:?}"),
3002 }
3003 }
3004
3005 #[test]
3006 fn draw_arc_records_arc_primitive_with_tight_bounds() {
3007 let mut scope = scope(200.0);
3008 scope.draw_arc(
3009 Brush::solid(Color::RED),
3010 Point::new(100.0, 100.0),
3011 50.0,
3012 0.0,
3013 FRAC_PI_2,
3014 Stroke::new(10.0),
3015 );
3016 let primitives = scope.into_primitives();
3017 assert_eq!(primitives.len(), 1);
3018 match &primitives[0] {
3019 DrawPrimitive::Arc {
3020 rect,
3021 center,
3022 radius,
3023 start_angle,
3024 sweep_angle,
3025 stroke: Some(stroke),
3026 inner_radius,
3027 ..
3028 } => {
3029 assert_eq!(*center, Point::new(100.0, 100.0));
3030 assert_eq!(*radius, 50.0);
3031 assert_eq!(*start_angle, 0.0);
3032 assert!(approx(*sweep_angle, FRAC_PI_2));
3033 assert_eq!(stroke.width, 10.0);
3034 assert_eq!(*inner_radius, 0.0);
3035 assert!(approx(rect.x, 100.0), "{rect:?}");
3036 assert!(approx(rect.y, 100.0), "{rect:?}");
3037 assert!(approx(rect.width, 55.0), "{rect:?}");
3038 assert!(approx(rect.height, 55.0), "{rect:?}");
3039 }
3040 other => panic!("expected arc primitive, got {other:?}"),
3041 }
3042 }
3043
3044 #[test]
3045 fn draw_arc_bounds_cover_a_quadrant_spanning_sweep() {
3046 let mut scope = scope(200.0);
3047 scope.draw_arc(
3048 Brush::solid(Color::RED),
3049 Point::new(100.0, 100.0),
3050 50.0,
3051 0.0,
3052 3.0 * FRAC_PI_2,
3053 Stroke::new(4.0),
3054 );
3055 let DrawPrimitive::Arc { rect, .. } = &scope.into_primitives()[0] else {
3056 panic!("expected arc primitive");
3057 };
3058 assert!(approx(rect.x, 48.0), "{rect:?}");
3059 assert!(approx(rect.y, 48.0), "{rect:?}");
3060 assert!(approx(rect.width, 104.0), "{rect:?}");
3061 assert!(approx(rect.height, 104.0), "{rect:?}");
3062 }
3063
3064 #[test]
3065 fn draw_annular_sector_records_inner_radius_and_no_stroke() {
3066 let mut scope = scope(200.0);
3067 scope.draw_annular_sector(
3068 Brush::solid(Color::WHITE),
3069 Point::new(100.0, 100.0),
3070 30.0,
3071 50.0,
3072 0.0,
3073 PI,
3074 );
3075 match &scope.into_primitives()[0] {
3076 DrawPrimitive::Arc {
3077 rect,
3078 center,
3079 radius,
3080 inner_radius,
3081 stroke,
3082 sweep_angle,
3083 ..
3084 } => {
3085 assert!(stroke.is_none(), "annular sectors are filled, not stroked");
3086 assert_eq!(*center, Point::new(100.0, 100.0));
3087 assert_eq!(*radius, 50.0);
3088 assert_eq!(*inner_radius, 30.0);
3089 assert!(approx(*sweep_angle, PI));
3090 assert!(approx(rect.x, 50.0), "{rect:?}");
3091 assert!(approx(rect.y, 100.0), "{rect:?}");
3092 assert!(approx(rect.width, 100.0), "{rect:?}");
3093 assert!(approx(rect.height, 50.0), "{rect:?}");
3094 }
3095 other => panic!("expected arc primitive, got {other:?}"),
3096 }
3097 }
3098
3099 #[test]
3100 fn draw_arc_blend_wraps_non_default_modes() {
3101 let mut scope = scope(100.0);
3102 scope.draw_arc_blend(
3103 Brush::solid(Color::RED),
3104 Point::new(50.0, 50.0),
3105 20.0,
3106 0.0,
3107 1.0,
3108 Stroke::new(2.0),
3109 BlendMode::DstOut,
3110 );
3111 match &scope.into_primitives()[0] {
3112 DrawPrimitive::Blend {
3113 primitive,
3114 blend_mode,
3115 } => {
3116 assert_eq!(*blend_mode, BlendMode::DstOut);
3117 assert!(matches!(**primitive, DrawPrimitive::Arc { .. }));
3118 }
3119 other => panic!("expected blended arc, got {other:?}"),
3120 }
3121 }
3122
3123 #[test]
3124 fn draw_annular_sector_blend_wraps_non_default_modes() {
3125 let mut scope = scope(100.0);
3126 scope.draw_annular_sector_blend(
3127 Brush::solid(Color::RED),
3128 Point::new(50.0, 50.0),
3129 5.0,
3130 20.0,
3131 0.0,
3132 1.0,
3133 BlendMode::Plus,
3134 );
3135 assert!(matches!(
3136 &scope.into_primitives()[0],
3137 DrawPrimitive::Blend {
3138 blend_mode: BlendMode::Plus,
3139 ..
3140 }
3141 ));
3142 }
3143
3144 #[test]
3145 fn stroked_blend_variants_wrap_non_default_modes() {
3146 let mut scope = scope(20.0);
3147 scope.draw_rect_stroked_blend(
3148 Brush::solid(Color::RED),
3149 Stroke::new(2.0),
3150 BlendMode::DstOut,
3151 );
3152 scope.draw_round_rect_stroked_blend(
3153 Brush::solid(Color::RED),
3154 CornerRadii::uniform(2.0),
3155 Stroke::new(2.0),
3156 BlendMode::DstOut,
3157 );
3158 scope.draw_circle_stroked_blend(
3159 Brush::solid(Color::RED),
3160 Point::new(10.0, 10.0),
3161 5.0,
3162 Stroke::new(2.0),
3163 BlendMode::DstOut,
3164 );
3165 let primitives = scope.into_primitives();
3166 assert_eq!(primitives.len(), 3);
3167 for primitive in &primitives {
3168 assert!(
3169 matches!(
3170 primitive,
3171 DrawPrimitive::Blend {
3172 blend_mode: BlendMode::DstOut,
3173 ..
3174 }
3175 ),
3176 "expected blended primitive, got {primitive:?}"
3177 );
3178 }
3179 }
3180
3181 #[test]
3182 fn negative_sweeps_and_overlong_sweeps_produce_finite_bounds() {
3183 let mut scope = scope(200.0);
3184 scope.draw_arc(
3185 Brush::solid(Color::RED),
3186 Point::new(100.0, 100.0),
3187 40.0,
3188 FRAC_PI_2,
3189 -FRAC_PI_2,
3190 Stroke::new(4.0),
3191 );
3192 scope.draw_arc(
3193 Brush::solid(Color::RED),
3194 Point::new(100.0, 100.0),
3195 40.0,
3196 0.3,
3197 crate::stroke::TAU * 4.0,
3198 Stroke::new(4.0),
3199 );
3200 let primitives = scope.into_primitives();
3201 assert_eq!(primitives.len(), 2);
3202
3203 let DrawPrimitive::Arc { rect: negative, .. } = &primitives[0] else {
3204 panic!("expected arc");
3205 };
3206 assert!(approx(negative.x, 100.0), "{negative:?}");
3207 assert!(approx(negative.y, 100.0), "{negative:?}");
3208 assert!(approx(negative.width, 42.0), "{negative:?}");
3209
3210 let DrawPrimitive::Arc { rect: full, .. } = &primitives[1] else {
3211 panic!("expected arc");
3212 };
3213 assert!(approx(full.x, 58.0), "{full:?}");
3214 assert!(approx(full.width, 84.0), "{full:?}");
3215 assert!(approx(full.height, 84.0), "{full:?}");
3216 }
3217
3218 #[test]
3219 fn degenerate_stroke_and_arc_inputs_emit_nothing_and_never_panic() {
3220 let mut scope = scope(50.0);
3221 let brush = Brush::solid(Color::RED);
3222 let center = Point::new(25.0, 25.0);
3223
3224 scope.draw_rect_stroked(brush.clone(), Stroke::new(0.0));
3225 scope.draw_rect_stroked(brush.clone(), Stroke::new(-4.0));
3226 scope.draw_rect_stroked(brush.clone(), Stroke::new(f32::NAN));
3227 scope.draw_round_rect_stroked(brush.clone(), CornerRadii::uniform(2.0), Stroke::new(0.0));
3228 scope.draw_circle_stroked(brush.clone(), center, 10.0, Stroke::new(0.0));
3229 scope.draw_circle_stroked(brush.clone(), center, f32::NAN, Stroke::new(2.0));
3230 scope.draw_arc(brush.clone(), center, 10.0, 0.0, 0.0, Stroke::new(2.0));
3231 scope.draw_arc(brush.clone(), center, 10.0, 0.0, f32::NAN, Stroke::new(2.0));
3232 scope.draw_arc(
3233 brush.clone(),
3234 center,
3235 f32::INFINITY,
3236 0.0,
3237 1.0,
3238 Stroke::new(2.0),
3239 );
3240 scope.draw_arc(brush.clone(), center, 10.0, 0.0, 1.0, Stroke::new(0.0));
3241 scope.draw_arc(brush.clone(), center, 0.0, 0.0, 1.0, Stroke::new(0.0));
3242 scope.draw_annular_sector(brush.clone(), center, 10.0, 10.0, 0.0, 1.0);
3243 scope.draw_annular_sector(brush.clone(), center, 20.0, 10.0, 0.0, 1.0);
3244 scope.draw_annular_sector(brush.clone(), center, 0.0, 0.0, 0.0, 1.0);
3245 scope.draw_annular_sector(brush.clone(), center, 0.0, 10.0, 0.0, 0.0);
3246 scope.draw_annular_sector(brush, center, f32::NAN, 10.0, 0.0, 1.0);
3247
3248 assert!(
3249 scope.into_primitives().is_empty(),
3250 "degenerate stroke/arc requests must not reach the renderer"
3251 );
3252 }
3253
3254 #[test]
3255 fn zero_radius_arc_with_positive_width_stays_finite() {
3256 let mut scope = scope(50.0);
3257 scope.draw_arc(
3258 Brush::solid(Color::RED),
3259 Point::new(25.0, 25.0),
3260 0.0,
3261 0.0,
3262 FRAC_PI_2,
3263 Stroke::new(6.0).with_cap(StrokeCap::Round),
3264 );
3265 let primitives = scope.into_primitives();
3266 assert_eq!(primitives.len(), 1);
3267 let DrawPrimitive::Arc { rect, .. } = &primitives[0] else {
3268 panic!("expected arc");
3269 };
3270 for value in [rect.x, rect.y, rect.width, rect.height] {
3271 assert!(value.is_finite(), "{rect:?}");
3272 }
3273 assert!(rect.width > 0.0 && rect.height > 0.0, "{rect:?}");
3274 }
3275
3276 struct FixedAdvanceTextMeasurer {
3277 advance: f32,
3278 line_height: f32,
3279 calls: std::cell::Cell<usize>,
3280 }
3281
3282 impl FixedAdvanceTextMeasurer {
3283 fn shared(advance: f32, line_height: f32) -> Rc<Self> {
3284 Rc::new(Self {
3285 advance,
3286 line_height,
3287 calls: std::cell::Cell::new(0),
3288 })
3289 }
3290 }
3291
3292 impl DrawTextMeasurer for FixedAdvanceTextMeasurer {
3293 fn measure_text(&self, text: &str, _style: &DrawTextStyle) -> TextMeasurement {
3294 self.calls.set(self.calls.get() + 1);
3295 let lines: Vec<&str> = text.split('\n').collect();
3296 let width = lines
3297 .iter()
3298 .map(|line| line.chars().count() as f32 * self.advance)
3299 .fold(0.0_f32, f32::max);
3300 TextMeasurement {
3301 size: Size::new(width, lines.len() as f32 * self.line_height),
3302 line_height: self.line_height,
3303 first_baseline: self.line_height * 0.75,
3304 line_count: lines.len(),
3305 }
3306 }
3307 }
3308
3309 fn text_scope(size: Size) -> (DrawScopeDefault, Rc<FixedAdvanceTextMeasurer>) {
3310 let measurer = FixedAdvanceTextMeasurer::shared(10.0, 20.0);
3311 (
3312 DrawScopeDefault::with_text_measurer(size, measurer.clone()),
3313 measurer,
3314 )
3315 }
3316
3317 fn unwrap_text(primitive: &DrawPrimitive) -> &TextPrimitive {
3318 match primitive {
3319 DrawPrimitive::Text(text) => text,
3320 other => panic!("expected text primitive, got {other:?}"),
3321 }
3322 }
3323
3324 #[test]
3325 fn drawn_text_occupies_exactly_the_box_measure_text_reported() {
3326 let (mut scope, _) = text_scope(Size::new(200.0, 100.0));
3327 let style = DrawTextStyle::new(16.0);
3328 let measured = scope.measure_text("ABCD", &style);
3329
3330 scope.draw_text_from(
3331 Point::new(7.0, 11.0),
3332 Brush::solid(Color::WHITE),
3333 "ABCD",
3334 &style,
3335 );
3336
3337 let primitives = scope.into_primitives();
3338 assert_eq!(primitives.len(), 1);
3339 let text = unwrap_text(&primitives[0]);
3340 assert_eq!(
3341 text.rect,
3342 Rect {
3343 x: 7.0,
3344 y: 11.0,
3345 width: measured.size.width,
3346 height: measured.size.height,
3347 },
3348 "the drawn block must be the measured block, or callers cannot center text"
3349 );
3350 assert_eq!(&*text.text, "ABCD");
3351 assert_eq!(text.color, Color::WHITE);
3352 }
3353
3354 #[test]
3355 fn text_alignment_positions_the_measured_block_inside_the_box() {
3356 let box_rect = Rect {
3357 x: 100.0,
3358 y: 50.0,
3359 width: 200.0,
3360 height: 80.0,
3361 };
3362 let cases = [
3363 (TextAlign::Left, TextVerticalAlign::Top, 100.0, 50.0),
3364 (TextAlign::Center, TextVerticalAlign::Center, 190.0, 80.0),
3365 (TextAlign::Right, TextVerticalAlign::Bottom, 280.0, 110.0),
3366 ];
3367 for (align, vertical_align, expected_x, expected_y) in cases {
3368 let (mut scope, _) = text_scope(Size::new(400.0, 400.0));
3369 let style = DrawTextStyle::new(16.0)
3370 .with_align(align)
3371 .with_vertical_align(vertical_align);
3372 scope.draw_text_at(box_rect, Brush::solid(Color::WHITE), "AB", &style);
3373 let primitives = scope.into_primitives();
3374 let text = unwrap_text(&primitives[0]);
3375 assert!(
3376 approx(text.rect.x, expected_x) && approx(text.rect.y, expected_y),
3377 "{align:?}/{vertical_align:?} placed the block at {:?}",
3378 text.rect
3379 );
3380 assert!(approx(text.rect.width, 20.0) && approx(text.rect.height, 20.0));
3381 }
3382 }
3383
3384 #[test]
3385 fn baseline_aligned_text_hangs_above_the_box_edge() {
3386 let (mut scope, _) = text_scope(Size::new(200.0, 200.0));
3387 let style = DrawTextStyle::new(16.0).with_vertical_align(TextVerticalAlign::Baseline);
3388 let measured = scope.measure_text("Ag", &style);
3389 scope.draw_text_at(
3390 Rect {
3391 x: 0.0,
3392 y: 100.0,
3393 width: 200.0,
3394 height: 0.0,
3395 },
3396 Brush::solid(Color::WHITE),
3397 "Ag",
3398 &style,
3399 );
3400 let primitives = scope.into_primitives();
3401 let text = unwrap_text(&primitives[0]);
3402 assert!(
3403 approx(text.rect.y, 100.0 - measured.first_baseline),
3404 "{:?}",
3405 text.rect
3406 );
3407 }
3408
3409 #[test]
3410 fn draw_text_fills_the_whole_scope_rect() {
3411 let (mut scope, _) = text_scope(Size::new(120.0, 60.0));
3412 let style = DrawTextStyle::new(16.0)
3413 .with_align(TextAlign::Right)
3414 .with_vertical_align(TextVerticalAlign::Bottom);
3415 scope.draw_text(Brush::solid(Color::WHITE), "AB", &style);
3416 let primitives = scope.into_primitives();
3417 let text = unwrap_text(&primitives[0]);
3418 assert!(
3419 approx(text.rect.x, 100.0) && approx(text.rect.y, 40.0),
3420 "{:?}",
3421 text.rect
3422 );
3423 }
3424
3425 #[test]
3426 fn draw_text_from_ignores_alignment_and_anchors_the_top_left() {
3427 let (mut scope, _) = text_scope(Size::new(400.0, 400.0));
3428 let style = DrawTextStyle::new(16.0)
3429 .with_align(TextAlign::Center)
3430 .with_vertical_align(TextVerticalAlign::Bottom);
3431 scope.draw_text_from(
3432 Point::new(30.0, 40.0),
3433 Brush::solid(Color::WHITE),
3434 "AB",
3435 &style,
3436 );
3437 let primitives = scope.into_primitives();
3438 let text = unwrap_text(&primitives[0]);
3439 assert!(
3440 approx(text.rect.x, 30.0) && approx(text.rect.y, 40.0),
3441 "{:?}",
3442 text.rect
3443 );
3444 }
3445
3446 #[test]
3447 fn multiline_text_measures_the_widest_line_and_stacks_the_lines() {
3448 let (mut scope, _) = text_scope(Size::new(400.0, 400.0));
3449 let style = DrawTextStyle::new(16.0);
3450 scope.draw_text_from(Point::ZERO, Brush::solid(Color::WHITE), "AB\nABCDE", &style);
3451 let primitives = scope.into_primitives();
3452 let text = unwrap_text(&primitives[0]);
3453 assert!(approx(text.rect.width, 50.0), "{:?}", text.rect);
3454 assert!(approx(text.rect.height, 40.0), "{:?}", text.rect);
3455 }
3456
3457 #[test]
3458 fn empty_text_draws_nothing_and_never_measures() {
3459 let (mut scope, measurer) = text_scope(Size::new(100.0, 100.0));
3460 scope.draw_text(Brush::solid(Color::WHITE), "", &DrawTextStyle::new(16.0));
3461 scope.draw_text_at(
3462 Rect::from_size(Size::new(10.0, 10.0)),
3463 Brush::solid(Color::WHITE),
3464 "",
3465 &DrawTextStyle::new(16.0),
3466 );
3467 scope.draw_text_from(
3468 Point::ZERO,
3469 Brush::solid(Color::WHITE),
3470 "",
3471 &DrawTextStyle::new(16.0),
3472 );
3473 assert!(scope.into_primitives().is_empty());
3474 assert_eq!(
3475 measurer.calls.get(),
3476 0,
3477 "an empty string must not cost a measurement"
3478 );
3479 }
3480
3481 #[test]
3482 fn invisible_text_draws_nothing() {
3483 let (mut scope, _) = text_scope(Size::new(100.0, 100.0));
3484 let style = DrawTextStyle::new(16.0);
3485 scope.draw_text(Brush::solid(Color(1.0, 1.0, 1.0, 0.0)), "AB", &style);
3486 scope.draw_text(
3487 Brush::LinearGradient {
3488 colors: Vec::new(),
3489 stops: None,
3490 start: Point::ZERO,
3491 end: Point::new(1.0, 1.0),
3492 tile_mode: crate::render_effect::TileMode::Clamp,
3493 },
3494 "AB",
3495 &style,
3496 );
3497 assert!(scope.into_primitives().is_empty());
3498 }
3499
3500 #[test]
3501 fn gradient_text_brushes_fall_back_to_their_first_stop() {
3502 let (mut scope, _) = text_scope(Size::new(100.0, 100.0));
3503 scope.draw_text(
3504 Brush::linear_gradient(vec![Color::RED, Color::BLUE]),
3505 "AB",
3506 &DrawTextStyle::new(16.0),
3507 );
3508 let primitives = scope.into_primitives();
3509 assert_eq!(unwrap_text(&primitives[0]).color, Color::RED);
3510 }
3511
3512 #[test]
3513 fn a_scope_without_a_measurer_falls_back_to_the_font_free_estimate() {
3514 let mut scope = DrawScopeDefault::new(Size::new(100.0, 100.0));
3515 let style = DrawTextStyle::new(16.0);
3516 assert_eq!(
3517 scope.measure_text("ABC", &style),
3518 crate::estimate_text_measurement("ABC", &style)
3519 );
3520 scope.draw_text_from(Point::ZERO, Brush::solid(Color::WHITE), "ABC", &style);
3521 let primitives = scope.into_primitives();
3522 let text = unwrap_text(&primitives[0]);
3523 assert!(text.rect.width > 0.0 && text.rect.height > 0.0);
3524 }
3525
3526 #[test]
3527 fn degenerate_text_geometry_emits_nothing_and_never_panics() {
3528 struct DegenerateTextMeasurer;
3529 impl DrawTextMeasurer for DegenerateTextMeasurer {
3530 fn measure_text(&self, _text: &str, _style: &DrawTextStyle) -> TextMeasurement {
3531 TextMeasurement {
3532 size: Size::new(f32::NAN, 0.0),
3533 line_height: f32::NAN,
3534 first_baseline: f32::NAN,
3535 line_count: 1,
3536 }
3537 }
3538 }
3539
3540 let mut scope = DrawScopeDefault::with_text_measurer(
3541 Size::new(50.0, 50.0),
3542 Rc::new(DegenerateTextMeasurer),
3543 );
3544 scope.draw_text(Brush::solid(Color::WHITE), "AB", &DrawTextStyle::new(16.0));
3545 scope.draw_text_at(
3546 Rect {
3547 x: f32::NAN,
3548 y: 0.0,
3549 width: 10.0,
3550 height: 10.0,
3551 },
3552 Brush::solid(Color::WHITE),
3553 "AB",
3554 &DrawTextStyle::new(16.0),
3555 );
3556 assert!(
3557 scope.into_primitives().is_empty(),
3558 "unmeasurable text must not reach the renderer"
3559 );
3560 }
3561
3562 #[test]
3563 fn text_style_survives_lowering_into_the_primitive() {
3564 let (mut scope, _) = text_scope(Size::new(100.0, 100.0));
3565 let style = DrawTextStyle::new(21.0)
3566 .with_font_family("Fira Sans")
3567 .with_weight(FontWeight::BOLD)
3568 .with_style(FontStyle::Italic)
3569 .with_letter_spacing(2.0)
3570 .with_line_height(26.0);
3571 scope.draw_text(Brush::solid(Color::WHITE), "AB", &style);
3572 let primitives = scope.into_primitives();
3573 assert_eq!(unwrap_text(&primitives[0]).style, style);
3574 }
3575
3576 #[test]
3577 fn a_layers_composite_alpha_is_a_truncated_byte() {
3578 for byte in 0..=255u32 {
3579 let exact = byte as f32 / 255.0;
3580 assert!(
3581 (GraphicsLayer::composite_alpha_8bit(exact) - exact).abs() < 1e-6,
3582 "byte {byte} moved"
3583 );
3584 if byte < 255 {
3585 let nearly_next = (byte as f32 + 0.999) / 255.0;
3586 assert!(
3587 (GraphicsLayer::composite_alpha_8bit(nearly_next) - exact).abs() < 1e-6,
3588 "byte {byte} + 0.999 did not truncate"
3589 );
3590 }
3591 }
3592 assert_eq!(GraphicsLayer::composite_alpha_8bit(1.0), 1.0);
3593 assert_eq!(GraphicsLayer::composite_alpha_8bit(0.0), 0.0);
3594 assert_eq!(GraphicsLayer::composite_alpha_8bit(-3.0), 0.0);
3595 assert_eq!(GraphicsLayer::composite_alpha_8bit(7.0), 1.0);
3596 }
3597}