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