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]
2569 fn a_reused_recording_buffer_records_what_a_fresh_one_would() {
2570 let size = Size::new(16.0, 16.0);
2571 let measurer: Rc<dyn DrawTextMeasurer> = FixedAdvanceTextMeasurer::shared(10.0, 20.0);
2572
2573 let draw = |scope: &mut DrawScopeDefault| {
2574 scope.draw_rect(Brush::solid(Color::WHITE));
2575 scope.draw_content();
2576 scope.draw_rect(Brush::solid(Color::BLACK));
2577 scope.draw_content();
2578 };
2579
2580 let mut fresh = DrawScopeDefault::with_text_measurer(size, Rc::clone(&measurer));
2581 draw(&mut fresh);
2582 assert_eq!(fresh.content_marker_count(), 2);
2583 let expected = fresh.finish();
2584
2585 let storage = Vec::with_capacity(64);
2588 let mut reused = DrawScopeDefault::with_text_measurer_reusing(size, measurer, storage);
2589 draw(&mut reused);
2590 assert_eq!(reused.content_marker_count(), 2);
2591 let reused = reused.finish();
2592
2593 assert_eq!(reused.primitives.len(), expected.primitives.len());
2594 assert_eq!(reused.content_markers, expected.content_markers);
2595 assert_eq!(reused.dropped, expected.dropped);
2596 }
2597
2598 #[test]
2602 fn finishing_recording_only_materializes_nothing_but_still_reports_markers() {
2603 let mut scope = DrawScopeDefault::new(Size::new(8.0, 8.0));
2604 scope.draw_rect(Brush::solid(Color::WHITE));
2605 scope.draw_content();
2606 assert_eq!(scope.content_marker_count(), 1);
2607
2608 let finished = scope.finish_recording_only();
2609 assert!(
2610 finished.primitives.is_empty(),
2611 "nothing should have been materialized"
2612 );
2613 assert_eq!(finished.content_markers, 1);
2614 assert!(finished.dropped.is_empty());
2615 assert_eq!(
2616 finished.recording.tape.len(),
2617 0,
2618 "the recording buffer comes back cleared, ready to be recorded into again"
2619 );
2620 }
2621
2622 #[test]
2626 fn a_text_block_is_placed_by_its_alignment_inside_the_rect() {
2627 let rect = Rect {
2628 x: 10.0,
2629 y: 20.0,
2630 width: 100.0,
2631 height: 40.0,
2632 };
2633 let measurement = TextMeasurement {
2634 size: Size::new(60.0, 16.0),
2635 line_height: 16.0,
2636 first_baseline: 12.0,
2637 line_count: 1,
2638 };
2639 let style = |align, vertical| {
2640 TextStyle::default()
2641 .with_align(align)
2642 .with_vertical_align(vertical)
2643 };
2644
2645 let left = align_text_block(
2646 rect,
2647 measurement,
2648 &style(TextAlign::Left, TextVerticalAlign::Top),
2649 );
2650 assert_eq!(left, Point::new(10.0, 20.0));
2651
2652 let centered = align_text_block(
2653 rect,
2654 measurement,
2655 &style(TextAlign::Center, TextVerticalAlign::Center),
2656 );
2657 assert_eq!(centered, Point::new(10.0 + 20.0, 20.0 + 12.0));
2658
2659 let right = align_text_block(
2660 rect,
2661 measurement,
2662 &style(TextAlign::Right, TextVerticalAlign::Bottom),
2663 );
2664 assert_eq!(right, Point::new(50.0, 44.0));
2665
2666 let baseline = align_text_block(
2669 rect,
2670 measurement,
2671 &style(TextAlign::Left, TextVerticalAlign::Baseline),
2672 );
2673 assert_eq!(baseline, Point::new(10.0, 20.0 - 12.0));
2674 }
2675
2676 #[test]
2677 fn draw_rect_blend_wraps_non_default_modes() {
2678 let mut scope = DrawScopeDefault::new(Size::new(10.0, 10.0));
2679 scope.draw_rect_blend(Brush::solid(Color::RED), BlendMode::DstOut);
2680
2681 let primitives = scope.into_primitives();
2682 assert_eq!(primitives.len(), 1);
2683 match &primitives[0] {
2684 DrawPrimitive::Blend {
2685 primitive,
2686 blend_mode,
2687 } => {
2688 assert_eq!(*blend_mode, BlendMode::DstOut);
2689 assert!(matches!(**primitive, DrawPrimitive::Rect { .. }));
2690 }
2691 other => panic!("expected blended primitive, got {other:?}"),
2692 }
2693 }
2694
2695 #[test]
2696 fn draw_circle_records_centered_round_rect() {
2697 let mut scope = DrawScopeDefault::new(Size::new(40.0, 40.0));
2698 scope.draw_circle(Brush::solid(Color::BLUE), Point::new(12.0, 16.0), 5.0);
2699
2700 let primitives = scope.into_primitives();
2701 assert_eq!(primitives.len(), 1);
2702 match &primitives[0] {
2703 DrawPrimitive::RoundRect { rect, radii, .. } => {
2704 assert_eq!(
2705 *rect,
2706 Rect {
2707 x: 7.0,
2708 y: 11.0,
2709 width: 10.0,
2710 height: 10.0,
2711 }
2712 );
2713 assert_eq!(*radii, CornerRadii::uniform(5.0));
2714 }
2715 other => panic!("expected circular round rect, got {other:?}"),
2716 }
2717 }
2718
2719 #[test]
2720 fn draw_circle_blend_wraps_non_default_modes() {
2721 let mut scope = DrawScopeDefault::new(Size::new(10.0, 10.0));
2722 scope.draw_circle_blend(
2723 Brush::solid(Color::RED),
2724 Point::new(5.0, 5.0),
2725 3.0,
2726 BlendMode::Plus,
2727 );
2728
2729 let primitives = scope.into_primitives();
2730 assert_eq!(primitives.len(), 1);
2731 match &primitives[0] {
2732 DrawPrimitive::Blend {
2733 primitive,
2734 blend_mode,
2735 } => {
2736 assert_eq!(*blend_mode, BlendMode::Plus);
2737 assert!(matches!(**primitive, DrawPrimitive::RoundRect { .. }));
2738 }
2739 other => panic!("expected blended circle primitive, got {other:?}"),
2740 }
2741 }
2742
2743 #[test]
2744 fn rect_union_encloses_both_inputs() {
2745 let lhs = Rect {
2746 x: 10.0,
2747 y: 5.0,
2748 width: 8.0,
2749 height: 4.0,
2750 };
2751 let rhs = Rect {
2752 x: 4.0,
2753 y: 7.0,
2754 width: 10.0,
2755 height: 6.0,
2756 };
2757
2758 assert_eq!(
2759 lhs.union(rhs),
2760 Rect {
2761 x: 4.0,
2762 y: 5.0,
2763 width: 14.0,
2764 height: 8.0,
2765 }
2766 );
2767 }
2768
2769 #[test]
2770 fn draw_image_uses_scope_size_as_default_rect() {
2771 let mut scope = DrawScopeDefault::new(Size::new(40.0, 24.0));
2772 let image = ImageBitmap::from_rgba8(2, 2, vec![255; 16]).expect("image");
2773 scope.draw_image(image.clone());
2774 let primitives = scope.into_primitives();
2775 assert_eq!(primitives.len(), 1);
2776 match unwrap_image(&primitives[0]) {
2777 DrawPrimitive::Image {
2778 rect,
2779 image: actual,
2780 alpha,
2781 color_filter,
2782 sampling,
2783 src_rect,
2784 } => {
2785 assert_eq!(*rect, Rect::from_size(Size::new(40.0, 24.0)));
2786 assert_eq!(*actual, image);
2787 assert_eq!(*alpha, 1.0);
2788 assert!(color_filter.is_none());
2789 assert_eq!(*sampling, ImageSampling::Nearest);
2790 assert!(src_rect.is_none());
2791 }
2792 other => panic!("expected image primitive, got {other:?}"),
2793 }
2794 }
2795
2796 #[test]
2797 fn draw_image_src_stores_src_rect() {
2798 let mut scope = DrawScopeDefault::new(Size::new(100.0, 100.0));
2799 let image = ImageBitmap::from_rgba8(64, 64, vec![255; 64 * 64 * 4]).expect("image");
2800 let src = Rect {
2801 x: 10.0,
2802 y: 20.0,
2803 width: 30.0,
2804 height: 40.0,
2805 };
2806 let dst = Rect {
2807 x: 0.0,
2808 y: 0.0,
2809 width: 60.0,
2810 height: 80.0,
2811 };
2812 scope.draw_image_src(image.clone(), src, dst, 0.8, None);
2813 let primitives = scope.into_primitives();
2814 assert_eq!(primitives.len(), 1);
2815 match unwrap_image(&primitives[0]) {
2816 DrawPrimitive::Image {
2817 rect,
2818 image: actual,
2819 alpha,
2820 sampling,
2821 src_rect,
2822 ..
2823 } => {
2824 assert_eq!(*rect, dst);
2825 assert_eq!(*actual, image);
2826 assert!((alpha - 0.8).abs() < 1e-5);
2827 assert_eq!(*sampling, ImageSampling::Nearest);
2828 assert_eq!(*src_rect, Some(src));
2829 }
2830 other => panic!("expected image primitive, got {other:?}"),
2831 }
2832 }
2833
2834 #[test]
2835 fn draw_image_at_sampled_records_requested_sampling() {
2836 let mut scope = DrawScopeDefault::new(Size::new(100.0, 100.0));
2837 let image = ImageBitmap::from_rgba8(8, 8, vec![255; 8 * 8 * 4]).expect("image");
2838 let dst = Rect {
2839 x: 2.0,
2840 y: 3.0,
2841 width: 40.0,
2842 height: 30.0,
2843 };
2844
2845 scope.draw_image_at_sampled(dst, image.clone(), 0.7, None, ImageSampling::Linear);
2846
2847 let primitives = scope.into_primitives();
2848 assert_eq!(primitives.len(), 1);
2849 match unwrap_image(&primitives[0]) {
2850 DrawPrimitive::Image {
2851 rect,
2852 image: actual,
2853 alpha,
2854 sampling,
2855 src_rect,
2856 ..
2857 } => {
2858 assert_eq!(*rect, dst);
2859 assert_eq!(*actual, image);
2860 assert!((alpha - 0.7).abs() < 1e-5);
2861 assert_eq!(*sampling, ImageSampling::Linear);
2862 assert!(src_rect.is_none());
2863 }
2864 other => panic!("expected image primitive, got {other:?}"),
2865 }
2866 }
2867
2868 #[test]
2869 fn draw_image_src_sampled_records_requested_sampling() {
2870 let mut scope = DrawScopeDefault::new(Size::new(100.0, 100.0));
2871 let image = ImageBitmap::from_rgba8(64, 64, vec![255; 64 * 64 * 4]).expect("image");
2872 let src = Rect {
2873 x: 4.0,
2874 y: 6.0,
2875 width: 16.0,
2876 height: 20.0,
2877 };
2878 let dst = Rect {
2879 x: 8.0,
2880 y: 10.0,
2881 width: 32.0,
2882 height: 40.0,
2883 };
2884
2885 scope.draw_image_src_sampled(image.clone(), src, dst, 0.5, None, ImageSampling::Linear);
2886
2887 let primitives = scope.into_primitives();
2888 assert_eq!(primitives.len(), 1);
2889 match unwrap_image(&primitives[0]) {
2890 DrawPrimitive::Image {
2891 rect,
2892 image: actual,
2893 alpha,
2894 sampling,
2895 src_rect,
2896 ..
2897 } => {
2898 assert_eq!(*rect, dst);
2899 assert_eq!(*actual, image);
2900 assert!((alpha - 0.5).abs() < 1e-5);
2901 assert_eq!(*sampling, ImageSampling::Linear);
2902 assert_eq!(*src_rect, Some(src));
2903 }
2904 other => panic!("expected image primitive, got {other:?}"),
2905 }
2906 }
2907
2908 #[test]
2909 fn draw_image_at_clamps_alpha() {
2910 let mut scope = DrawScopeDefault::new(Size::new(10.0, 10.0));
2911 let image = ImageBitmap::from_rgba8(1, 1, vec![255, 255, 255, 255]).expect("image");
2912 scope.draw_image_at(
2913 Rect::from_origin_size(Point::new(2.0, 3.0), Size::new(5.0, 6.0)),
2914 image,
2915 3.0,
2916 Some(ColorFilter::Tint(Color::from_rgba_u8(128, 128, 255, 255))),
2917 );
2918 assert_image_alpha(&scope.into_primitives()[0], 1.0);
2919 }
2920
2921 #[test]
2922 fn graphics_layer_clone_with_render_effect() {
2923 let layer = GraphicsLayer {
2924 render_effect: Some(RenderEffect::blur(10.0)),
2925 backdrop_effect: Some(RenderEffect::blur(6.0)),
2926 color_filter: Some(ColorFilter::tint(Color::from_rgba_u8(128, 200, 255, 255))),
2927 alpha: 0.5,
2928 rotation_z: 12.0,
2929 shadow_elevation: 4.0,
2930 shape: LayerShape::Rounded(RoundedCornerShape::uniform(6.0)),
2931 clip: true,
2932 compositing_strategy: CompositingStrategy::Offscreen,
2933 blend_mode: BlendMode::SrcOver,
2934 ..Default::default()
2935 };
2936 let cloned = layer.clone();
2937 assert_eq!(cloned.alpha, 0.5);
2938 assert!(cloned.render_effect.is_some());
2939 assert!(cloned.backdrop_effect.is_some());
2940 assert_eq!(layer.color_filter, cloned.color_filter);
2941 assert_eq!(layer.render_effect, cloned.render_effect);
2942 assert_eq!(layer.backdrop_effect, cloned.backdrop_effect);
2943 assert!((cloned.rotation_z - 12.0).abs() < 1e-6);
2944 assert!((cloned.shadow_elevation - 4.0).abs() < 1e-6);
2945 assert_eq!(
2946 cloned.shape,
2947 LayerShape::Rounded(RoundedCornerShape::uniform(6.0))
2948 );
2949 assert!(cloned.clip);
2950 assert_eq!(cloned.compositing_strategy, CompositingStrategy::Offscreen);
2951 assert_eq!(cloned.blend_mode, BlendMode::SrcOver);
2952 }
2953
2954 #[test]
2955 fn graphics_layer_default_has_no_effect() {
2956 let layer = GraphicsLayer::default();
2957 assert!(layer.color_filter.is_none());
2958 assert!(layer.render_effect.is_none());
2959 assert!(layer.backdrop_effect.is_none());
2960 assert_eq!(layer.compositing_strategy, CompositingStrategy::Auto);
2961 assert_eq!(layer.blend_mode, BlendMode::SrcOver);
2962 assert_eq!(layer.alpha, 1.0);
2963 assert_eq!(layer.transform_origin, TransformOrigin::CENTER);
2964 assert!((layer.camera_distance - 8.0).abs() < 1e-6);
2965 assert_eq!(layer.shape, LayerShape::Rectangle);
2966 assert!(!layer.clip);
2967 assert_eq!(layer.ambient_shadow_color, Color::BLACK);
2968 assert_eq!(layer.spot_shadow_color, Color::BLACK);
2969 }
2970
2971 #[test]
2972 fn transform_origin_construction() {
2973 let origin = TransformOrigin::new(0.25, 0.75);
2974 assert!((origin.pivot_fraction_x - 0.25).abs() < 1e-6);
2975 assert!((origin.pivot_fraction_y - 0.75).abs() < 1e-6);
2976 }
2977
2978 #[test]
2979 fn layer_shape_default_is_rectangle() {
2980 assert_eq!(LayerShape::default(), LayerShape::Rectangle);
2981 }
2982
2983 use crate::{StrokeCap, StrokeJoin};
2986 use std::f32::consts::{FRAC_PI_2, PI};
2987
2988 fn approx(a: f32, b: f32) -> bool {
2993 (a - b).abs() < 0.25
2994 }
2995
2996 fn scope(size: f32) -> DrawScopeDefault {
2997 DrawScopeDefault::new(Size::new(size, size))
2998 }
2999
3000 #[test]
3001 fn draw_rect_stroked_records_scope_rect_and_stroke() {
3002 let mut scope = scope(20.0);
3003 scope.draw_rect_stroked(
3004 Brush::solid(Color::RED),
3005 Stroke::new(3.0).with_join(StrokeJoin::Bevel),
3006 );
3007
3008 let primitives = scope.into_primitives();
3009 assert_eq!(primitives.len(), 1);
3010 match &primitives[0] {
3011 DrawPrimitive::Rect {
3012 rect,
3013 stroke: Some(stroke),
3014 ..
3015 } => {
3016 assert_eq!(*rect, Rect::from_size(Size::new(20.0, 20.0)));
3019 assert_eq!(stroke.width, 3.0);
3020 assert_eq!(stroke.join, StrokeJoin::Bevel);
3021 }
3022 other => panic!("expected stroked rect, got {other:?}"),
3023 }
3024 }
3025
3026 #[test]
3027 fn draw_rect_at_stroked_records_requested_rect() {
3028 let mut scope = scope(50.0);
3029 let rect = Rect {
3030 x: 4.0,
3031 y: 6.0,
3032 width: 12.0,
3033 height: 9.0,
3034 };
3035 scope.draw_rect_at_stroked(rect, Brush::solid(Color::BLUE), Stroke::new(2.0));
3036 match &scope.into_primitives()[0] {
3037 DrawPrimitive::Rect {
3038 rect: actual,
3039 stroke: Some(stroke),
3040 ..
3041 } => {
3042 assert_eq!(*actual, rect);
3043 assert_eq!(stroke.width, 2.0);
3044 }
3045 other => panic!("expected stroked rect, got {other:?}"),
3046 }
3047 }
3048
3049 #[test]
3050 fn draw_round_rect_stroked_keeps_radii_and_stroke() {
3051 let mut scope = scope(30.0);
3052 scope.draw_round_rect_stroked(
3053 Brush::solid(Color::GREEN),
3054 CornerRadii::uniform(5.0),
3055 Stroke::new(4.0).with_join(StrokeJoin::Round),
3056 );
3057 match &scope.into_primitives()[0] {
3058 DrawPrimitive::RoundRect {
3059 rect,
3060 radii,
3061 stroke: Some(stroke),
3062 ..
3063 } => {
3064 assert_eq!(*rect, Rect::from_size(Size::new(30.0, 30.0)));
3065 assert_eq!(*radii, CornerRadii::uniform(5.0));
3066 assert_eq!(stroke.width, 4.0);
3067 assert_eq!(stroke.join, StrokeJoin::Round);
3068 }
3069 other => panic!("expected stroked round rect, got {other:?}"),
3070 }
3071 }
3072
3073 #[test]
3074 fn draw_round_rect_at_stroked_records_requested_rect() {
3075 let mut scope = scope(60.0);
3076 let rect = Rect {
3077 x: 1.0,
3078 y: 2.0,
3079 width: 20.0,
3080 height: 10.0,
3081 };
3082 scope.draw_round_rect_at_stroked(
3083 rect,
3084 Brush::solid(Color::WHITE),
3085 CornerRadii::uniform(3.0),
3086 Stroke::new(1.5),
3087 );
3088 match &scope.into_primitives()[0] {
3089 DrawPrimitive::RoundRect {
3090 rect: actual,
3091 radii,
3092 stroke: Some(stroke),
3093 ..
3094 } => {
3095 assert_eq!(*actual, rect);
3096 assert_eq!(*radii, CornerRadii::uniform(3.0));
3097 assert_eq!(stroke.width, 1.5);
3098 }
3099 other => panic!("expected stroked round rect, got {other:?}"),
3100 }
3101 }
3102
3103 #[test]
3104 fn draw_circle_stroked_lowers_to_stroked_round_rect() {
3105 let mut scope = scope(40.0);
3108 scope.draw_circle_stroked(
3109 Brush::solid(Color::BLUE),
3110 Point::new(12.0, 16.0),
3111 5.0,
3112 Stroke::new(2.0),
3113 );
3114 match &scope.into_primitives()[0] {
3115 DrawPrimitive::RoundRect {
3116 rect,
3117 radii,
3118 stroke: Some(stroke),
3119 ..
3120 } => {
3121 assert_eq!(
3122 *rect,
3123 Rect {
3124 x: 7.0,
3125 y: 11.0,
3126 width: 10.0,
3127 height: 10.0,
3128 }
3129 );
3130 assert_eq!(*radii, CornerRadii::uniform(5.0));
3131 assert_eq!(stroke.width, 2.0);
3132 }
3133 other => panic!("expected stroked circular round rect, got {other:?}"),
3134 }
3135 }
3136
3137 #[test]
3138 fn draw_arc_records_arc_primitive_with_tight_bounds() {
3139 let mut scope = scope(200.0);
3140 scope.draw_arc(
3141 Brush::solid(Color::RED),
3142 Point::new(100.0, 100.0),
3143 50.0,
3144 0.0,
3145 FRAC_PI_2,
3146 Stroke::new(10.0),
3147 );
3148 let primitives = scope.into_primitives();
3149 assert_eq!(primitives.len(), 1);
3150 match &primitives[0] {
3151 DrawPrimitive::Arc {
3152 rect,
3153 center,
3154 radius,
3155 start_angle,
3156 sweep_angle,
3157 stroke: Some(stroke),
3158 inner_radius,
3159 ..
3160 } => {
3161 assert_eq!(*center, Point::new(100.0, 100.0));
3162 assert_eq!(*radius, 50.0);
3163 assert_eq!(*start_angle, 0.0);
3164 assert!(approx(*sweep_angle, FRAC_PI_2));
3165 assert_eq!(stroke.width, 10.0);
3166 assert_eq!(*inner_radius, 0.0);
3167 assert!(approx(rect.x, 100.0), "{rect:?}");
3170 assert!(approx(rect.y, 100.0), "{rect:?}");
3171 assert!(approx(rect.width, 55.0), "{rect:?}");
3172 assert!(approx(rect.height, 55.0), "{rect:?}");
3173 }
3174 other => panic!("expected arc primitive, got {other:?}"),
3175 }
3176 }
3177
3178 #[test]
3179 fn draw_arc_bounds_cover_a_quadrant_spanning_sweep() {
3180 let mut scope = scope(200.0);
3181 scope.draw_arc(
3184 Brush::solid(Color::RED),
3185 Point::new(100.0, 100.0),
3186 50.0,
3187 0.0,
3188 3.0 * FRAC_PI_2,
3189 Stroke::new(4.0),
3190 );
3191 let DrawPrimitive::Arc { rect, .. } = &scope.into_primitives()[0] else {
3192 panic!("expected arc primitive");
3193 };
3194 assert!(approx(rect.x, 48.0), "{rect:?}");
3195 assert!(approx(rect.y, 48.0), "{rect:?}");
3196 assert!(approx(rect.width, 104.0), "{rect:?}");
3197 assert!(approx(rect.height, 104.0), "{rect:?}");
3198 }
3199
3200 #[test]
3201 fn draw_annular_sector_records_inner_radius_and_no_stroke() {
3202 let mut scope = scope(200.0);
3203 scope.draw_annular_sector(
3204 Brush::solid(Color::WHITE),
3205 Point::new(100.0, 100.0),
3206 30.0,
3207 50.0,
3208 0.0,
3209 PI,
3210 );
3211 match &scope.into_primitives()[0] {
3212 DrawPrimitive::Arc {
3213 rect,
3214 center,
3215 radius,
3216 inner_radius,
3217 stroke,
3218 sweep_angle,
3219 ..
3220 } => {
3221 assert!(stroke.is_none(), "annular sectors are filled, not stroked");
3222 assert_eq!(*center, Point::new(100.0, 100.0));
3223 assert_eq!(*radius, 50.0);
3224 assert_eq!(*inner_radius, 30.0);
3225 assert!(approx(*sweep_angle, PI));
3226 assert!(approx(rect.x, 50.0), "{rect:?}");
3228 assert!(approx(rect.y, 100.0), "{rect:?}");
3229 assert!(approx(rect.width, 100.0), "{rect:?}");
3230 assert!(approx(rect.height, 50.0), "{rect:?}");
3231 }
3232 other => panic!("expected arc primitive, got {other:?}"),
3233 }
3234 }
3235
3236 #[test]
3237 fn draw_arc_blend_wraps_non_default_modes() {
3238 let mut scope = scope(100.0);
3239 scope.draw_arc_blend(
3240 Brush::solid(Color::RED),
3241 Point::new(50.0, 50.0),
3242 20.0,
3243 0.0,
3244 1.0,
3245 Stroke::new(2.0),
3246 BlendMode::DstOut,
3247 );
3248 match &scope.into_primitives()[0] {
3249 DrawPrimitive::Blend {
3250 primitive,
3251 blend_mode,
3252 } => {
3253 assert_eq!(*blend_mode, BlendMode::DstOut);
3254 assert!(matches!(**primitive, DrawPrimitive::Arc { .. }));
3255 }
3256 other => panic!("expected blended arc, got {other:?}"),
3257 }
3258 }
3259
3260 #[test]
3261 fn draw_annular_sector_blend_wraps_non_default_modes() {
3262 let mut scope = scope(100.0);
3263 scope.draw_annular_sector_blend(
3264 Brush::solid(Color::RED),
3265 Point::new(50.0, 50.0),
3266 5.0,
3267 20.0,
3268 0.0,
3269 1.0,
3270 BlendMode::Plus,
3271 );
3272 assert!(matches!(
3273 &scope.into_primitives()[0],
3274 DrawPrimitive::Blend {
3275 blend_mode: BlendMode::Plus,
3276 ..
3277 }
3278 ));
3279 }
3280
3281 #[test]
3282 fn stroked_blend_variants_wrap_non_default_modes() {
3283 let mut scope = scope(20.0);
3284 scope.draw_rect_stroked_blend(
3285 Brush::solid(Color::RED),
3286 Stroke::new(2.0),
3287 BlendMode::DstOut,
3288 );
3289 scope.draw_round_rect_stroked_blend(
3290 Brush::solid(Color::RED),
3291 CornerRadii::uniform(2.0),
3292 Stroke::new(2.0),
3293 BlendMode::DstOut,
3294 );
3295 scope.draw_circle_stroked_blend(
3296 Brush::solid(Color::RED),
3297 Point::new(10.0, 10.0),
3298 5.0,
3299 Stroke::new(2.0),
3300 BlendMode::DstOut,
3301 );
3302 let primitives = scope.into_primitives();
3303 assert_eq!(primitives.len(), 3);
3304 for primitive in &primitives {
3305 assert!(
3306 matches!(
3307 primitive,
3308 DrawPrimitive::Blend {
3309 blend_mode: BlendMode::DstOut,
3310 ..
3311 }
3312 ),
3313 "expected blended primitive, got {primitive:?}"
3314 );
3315 }
3316 }
3317
3318 #[test]
3319 fn negative_sweeps_and_overlong_sweeps_produce_finite_bounds() {
3320 let mut scope = scope(200.0);
3321 scope.draw_arc(
3322 Brush::solid(Color::RED),
3323 Point::new(100.0, 100.0),
3324 40.0,
3325 FRAC_PI_2,
3326 -FRAC_PI_2,
3327 Stroke::new(4.0),
3328 );
3329 scope.draw_arc(
3330 Brush::solid(Color::RED),
3331 Point::new(100.0, 100.0),
3332 40.0,
3333 0.3,
3334 crate::stroke::TAU * 4.0,
3335 Stroke::new(4.0),
3336 );
3337 let primitives = scope.into_primitives();
3338 assert_eq!(primitives.len(), 2);
3339
3340 let DrawPrimitive::Arc { rect: negative, .. } = &primitives[0] else {
3341 panic!("expected arc");
3342 };
3343 assert!(approx(negative.x, 100.0), "{negative:?}");
3345 assert!(approx(negative.y, 100.0), "{negative:?}");
3346 assert!(approx(negative.width, 42.0), "{negative:?}");
3347
3348 let DrawPrimitive::Arc { rect: full, .. } = &primitives[1] else {
3349 panic!("expected arc");
3350 };
3351 assert!(approx(full.x, 58.0), "{full:?}");
3353 assert!(approx(full.width, 84.0), "{full:?}");
3354 assert!(approx(full.height, 84.0), "{full:?}");
3355 }
3356
3357 #[test]
3358 fn degenerate_stroke_and_arc_inputs_emit_nothing_and_never_panic() {
3359 let mut scope = scope(50.0);
3360 let brush = Brush::solid(Color::RED);
3361 let center = Point::new(25.0, 25.0);
3362
3363 scope.draw_rect_stroked(brush.clone(), Stroke::new(0.0));
3365 scope.draw_rect_stroked(brush.clone(), Stroke::new(-4.0));
3366 scope.draw_rect_stroked(brush.clone(), Stroke::new(f32::NAN));
3367 scope.draw_round_rect_stroked(brush.clone(), CornerRadii::uniform(2.0), Stroke::new(0.0));
3368 scope.draw_circle_stroked(brush.clone(), center, 10.0, Stroke::new(0.0));
3369 scope.draw_circle_stroked(brush.clone(), center, f32::NAN, Stroke::new(2.0));
3370 scope.draw_arc(brush.clone(), center, 10.0, 0.0, 0.0, Stroke::new(2.0));
3372 scope.draw_arc(brush.clone(), center, 10.0, 0.0, f32::NAN, Stroke::new(2.0));
3373 scope.draw_arc(
3374 brush.clone(),
3375 center,
3376 f32::INFINITY,
3377 0.0,
3378 1.0,
3379 Stroke::new(2.0),
3380 );
3381 scope.draw_arc(brush.clone(), center, 10.0, 0.0, 1.0, Stroke::new(0.0));
3383 scope.draw_arc(brush.clone(), center, 0.0, 0.0, 1.0, Stroke::new(0.0));
3384 scope.draw_annular_sector(brush.clone(), center, 10.0, 10.0, 0.0, 1.0);
3386 scope.draw_annular_sector(brush.clone(), center, 20.0, 10.0, 0.0, 1.0);
3387 scope.draw_annular_sector(brush.clone(), center, 0.0, 0.0, 0.0, 1.0);
3388 scope.draw_annular_sector(brush.clone(), center, 0.0, 10.0, 0.0, 0.0);
3389 scope.draw_annular_sector(brush, center, f32::NAN, 10.0, 0.0, 1.0);
3390
3391 assert!(
3392 scope.into_primitives().is_empty(),
3393 "degenerate stroke/arc requests must not reach the renderer"
3394 );
3395 }
3396
3397 #[test]
3398 fn zero_radius_arc_with_positive_width_stays_finite() {
3399 let mut scope = scope(50.0);
3402 scope.draw_arc(
3403 Brush::solid(Color::RED),
3404 Point::new(25.0, 25.0),
3405 0.0,
3406 0.0,
3407 FRAC_PI_2,
3408 Stroke::new(6.0).with_cap(StrokeCap::Round),
3409 );
3410 let primitives = scope.into_primitives();
3411 assert_eq!(primitives.len(), 1);
3412 let DrawPrimitive::Arc { rect, .. } = &primitives[0] else {
3413 panic!("expected arc");
3414 };
3415 for value in [rect.x, rect.y, rect.width, rect.height] {
3416 assert!(value.is_finite(), "{rect:?}");
3417 }
3418 assert!(rect.width > 0.0 && rect.height > 0.0, "{rect:?}");
3419 }
3420
3421 struct FixedAdvanceTextMeasurer {
3426 advance: f32,
3427 line_height: f32,
3428 calls: std::cell::Cell<usize>,
3429 }
3430
3431 impl FixedAdvanceTextMeasurer {
3432 fn shared(advance: f32, line_height: f32) -> Rc<Self> {
3433 Rc::new(Self {
3434 advance,
3435 line_height,
3436 calls: std::cell::Cell::new(0),
3437 })
3438 }
3439 }
3440
3441 impl DrawTextMeasurer for FixedAdvanceTextMeasurer {
3442 fn measure_text(&self, text: &str, _style: &TextStyle) -> TextMeasurement {
3443 self.calls.set(self.calls.get() + 1);
3444 let lines: Vec<&str> = text.split('\n').collect();
3445 let width = lines
3446 .iter()
3447 .map(|line| line.chars().count() as f32 * self.advance)
3448 .fold(0.0_f32, f32::max);
3449 TextMeasurement {
3450 size: Size::new(width, lines.len() as f32 * self.line_height),
3451 line_height: self.line_height,
3452 first_baseline: self.line_height * 0.75,
3453 line_count: lines.len(),
3454 }
3455 }
3456 }
3457
3458 fn text_scope(size: Size) -> (DrawScopeDefault, Rc<FixedAdvanceTextMeasurer>) {
3459 let measurer = FixedAdvanceTextMeasurer::shared(10.0, 20.0);
3460 (
3461 DrawScopeDefault::with_text_measurer(size, measurer.clone()),
3462 measurer,
3463 )
3464 }
3465
3466 fn unwrap_text(primitive: &DrawPrimitive) -> &TextPrimitive {
3467 match primitive {
3468 DrawPrimitive::Text(text) => text,
3469 other => panic!("expected text primitive, got {other:?}"),
3470 }
3471 }
3472
3473 #[test]
3474 fn drawn_text_occupies_exactly_the_box_measure_text_reported() {
3475 let (mut scope, _) = text_scope(Size::new(200.0, 100.0));
3476 let style = TextStyle::new(16.0);
3477 let measured = scope.measure_text("ABCD", &style);
3478
3479 scope.draw_text_from(
3480 Point::new(7.0, 11.0),
3481 Brush::solid(Color::WHITE),
3482 "ABCD",
3483 &style,
3484 );
3485
3486 let primitives = scope.into_primitives();
3487 assert_eq!(primitives.len(), 1);
3488 let text = unwrap_text(&primitives[0]);
3489 assert_eq!(
3490 text.rect,
3491 Rect {
3492 x: 7.0,
3493 y: 11.0,
3494 width: measured.size.width,
3495 height: measured.size.height,
3496 },
3497 "the drawn block must be the measured block, or callers cannot center text"
3498 );
3499 assert_eq!(&*text.text, "ABCD");
3500 assert_eq!(text.color, Color::WHITE);
3501 }
3502
3503 #[test]
3504 fn text_alignment_positions_the_measured_block_inside_the_box() {
3505 let box_rect = Rect {
3506 x: 100.0,
3507 y: 50.0,
3508 width: 200.0,
3509 height: 80.0,
3510 };
3511 let cases = [
3513 (TextAlign::Left, TextVerticalAlign::Top, 100.0, 50.0),
3514 (TextAlign::Center, TextVerticalAlign::Center, 190.0, 80.0),
3515 (TextAlign::Right, TextVerticalAlign::Bottom, 280.0, 110.0),
3516 ];
3517 for (align, vertical_align, expected_x, expected_y) in cases {
3518 let (mut scope, _) = text_scope(Size::new(400.0, 400.0));
3519 let style = TextStyle::new(16.0)
3520 .with_align(align)
3521 .with_vertical_align(vertical_align);
3522 scope.draw_text_at(box_rect, Brush::solid(Color::WHITE), "AB", &style);
3523 let primitives = scope.into_primitives();
3524 let text = unwrap_text(&primitives[0]);
3525 assert!(
3526 approx(text.rect.x, expected_x) && approx(text.rect.y, expected_y),
3527 "{align:?}/{vertical_align:?} placed the block at {:?}",
3528 text.rect
3529 );
3530 assert!(approx(text.rect.width, 20.0) && approx(text.rect.height, 20.0));
3531 }
3532 }
3533
3534 #[test]
3535 fn baseline_aligned_text_hangs_above_the_box_edge() {
3536 let (mut scope, _) = text_scope(Size::new(200.0, 200.0));
3537 let style = TextStyle::new(16.0).with_vertical_align(TextVerticalAlign::Baseline);
3538 let measured = scope.measure_text("Ag", &style);
3539 scope.draw_text_at(
3540 Rect {
3541 x: 0.0,
3542 y: 100.0,
3543 width: 200.0,
3544 height: 0.0,
3545 },
3546 Brush::solid(Color::WHITE),
3547 "Ag",
3548 &style,
3549 );
3550 let primitives = scope.into_primitives();
3551 let text = unwrap_text(&primitives[0]);
3552 assert!(
3554 approx(text.rect.y, 100.0 - measured.first_baseline),
3555 "{:?}",
3556 text.rect
3557 );
3558 }
3559
3560 #[test]
3561 fn draw_text_fills_the_whole_scope_rect() {
3562 let (mut scope, _) = text_scope(Size::new(120.0, 60.0));
3563 let style = TextStyle::new(16.0)
3564 .with_align(TextAlign::Right)
3565 .with_vertical_align(TextVerticalAlign::Bottom);
3566 scope.draw_text(Brush::solid(Color::WHITE), "AB", &style);
3567 let primitives = scope.into_primitives();
3568 let text = unwrap_text(&primitives[0]);
3569 assert!(
3570 approx(text.rect.x, 100.0) && approx(text.rect.y, 40.0),
3571 "{:?}",
3572 text.rect
3573 );
3574 }
3575
3576 #[test]
3577 fn draw_text_from_ignores_alignment_and_anchors_the_top_left() {
3578 let (mut scope, _) = text_scope(Size::new(400.0, 400.0));
3579 let style = TextStyle::new(16.0)
3581 .with_align(TextAlign::Center)
3582 .with_vertical_align(TextVerticalAlign::Bottom);
3583 scope.draw_text_from(
3584 Point::new(30.0, 40.0),
3585 Brush::solid(Color::WHITE),
3586 "AB",
3587 &style,
3588 );
3589 let primitives = scope.into_primitives();
3590 let text = unwrap_text(&primitives[0]);
3591 assert!(
3592 approx(text.rect.x, 30.0) && approx(text.rect.y, 40.0),
3593 "{:?}",
3594 text.rect
3595 );
3596 }
3597
3598 #[test]
3599 fn multiline_text_measures_the_widest_line_and_stacks_the_lines() {
3600 let (mut scope, _) = text_scope(Size::new(400.0, 400.0));
3601 let style = TextStyle::new(16.0);
3602 scope.draw_text_from(Point::ZERO, Brush::solid(Color::WHITE), "AB\nABCDE", &style);
3603 let primitives = scope.into_primitives();
3604 let text = unwrap_text(&primitives[0]);
3605 assert!(approx(text.rect.width, 50.0), "{:?}", text.rect);
3606 assert!(approx(text.rect.height, 40.0), "{:?}", text.rect);
3607 }
3608
3609 #[test]
3610 fn empty_text_draws_nothing_and_never_measures() {
3611 let (mut scope, measurer) = text_scope(Size::new(100.0, 100.0));
3612 scope.draw_text(Brush::solid(Color::WHITE), "", &TextStyle::new(16.0));
3613 scope.draw_text_at(
3614 Rect::from_size(Size::new(10.0, 10.0)),
3615 Brush::solid(Color::WHITE),
3616 "",
3617 &TextStyle::new(16.0),
3618 );
3619 scope.draw_text_from(
3620 Point::ZERO,
3621 Brush::solid(Color::WHITE),
3622 "",
3623 &TextStyle::new(16.0),
3624 );
3625 assert!(scope.into_primitives().is_empty());
3626 assert_eq!(
3627 measurer.calls.get(),
3628 0,
3629 "an empty string must not cost a measurement"
3630 );
3631 }
3632
3633 #[test]
3634 fn invisible_text_draws_nothing() {
3635 let (mut scope, _) = text_scope(Size::new(100.0, 100.0));
3636 let style = TextStyle::new(16.0);
3637 scope.draw_text(Brush::solid(Color(1.0, 1.0, 1.0, 0.0)), "AB", &style);
3638 scope.draw_text(
3639 Brush::LinearGradient {
3640 colors: Vec::new(),
3641 stops: None,
3642 start: Point::ZERO,
3643 end: Point::new(1.0, 1.0),
3644 tile_mode: crate::render_effect::TileMode::Clamp,
3645 },
3646 "AB",
3647 &style,
3648 );
3649 assert!(scope.into_primitives().is_empty());
3650 }
3651
3652 #[test]
3653 fn gradient_text_brushes_fall_back_to_their_first_stop() {
3654 let (mut scope, _) = text_scope(Size::new(100.0, 100.0));
3655 scope.draw_text(
3656 Brush::linear_gradient(vec![Color::RED, Color::BLUE]),
3657 "AB",
3658 &TextStyle::new(16.0),
3659 );
3660 let primitives = scope.into_primitives();
3661 assert_eq!(unwrap_text(&primitives[0]).color, Color::RED);
3662 }
3663
3664 #[test]
3665 fn a_scope_without_a_measurer_falls_back_to_the_font_free_estimate() {
3666 let mut scope = DrawScopeDefault::new(Size::new(100.0, 100.0));
3667 let style = TextStyle::new(16.0);
3668 assert_eq!(
3669 scope.measure_text("ABC", &style),
3670 crate::estimate_text_measurement("ABC", &style)
3671 );
3672 scope.draw_text_from(Point::ZERO, Brush::solid(Color::WHITE), "ABC", &style);
3673 let primitives = scope.into_primitives();
3674 let text = unwrap_text(&primitives[0]);
3675 assert!(text.rect.width > 0.0 && text.rect.height > 0.0);
3676 }
3677
3678 #[test]
3679 fn degenerate_text_geometry_emits_nothing_and_never_panics() {
3680 struct DegenerateTextMeasurer;
3681 impl DrawTextMeasurer for DegenerateTextMeasurer {
3682 fn measure_text(&self, _text: &str, _style: &TextStyle) -> TextMeasurement {
3683 TextMeasurement {
3684 size: Size::new(f32::NAN, 0.0),
3685 line_height: f32::NAN,
3686 first_baseline: f32::NAN,
3687 line_count: 1,
3688 }
3689 }
3690 }
3691
3692 let mut scope = DrawScopeDefault::with_text_measurer(
3693 Size::new(50.0, 50.0),
3694 Rc::new(DegenerateTextMeasurer),
3695 );
3696 scope.draw_text(Brush::solid(Color::WHITE), "AB", &TextStyle::new(16.0));
3697 scope.draw_text_at(
3698 Rect {
3699 x: f32::NAN,
3700 y: 0.0,
3701 width: 10.0,
3702 height: 10.0,
3703 },
3704 Brush::solid(Color::WHITE),
3705 "AB",
3706 &TextStyle::new(16.0),
3707 );
3708 assert!(
3709 scope.into_primitives().is_empty(),
3710 "unmeasurable text must not reach the renderer"
3711 );
3712 }
3713
3714 #[test]
3715 fn text_style_survives_lowering_into_the_primitive() {
3716 let (mut scope, _) = text_scope(Size::new(100.0, 100.0));
3717 let style = TextStyle::new(21.0)
3718 .with_font_family("Fira Sans")
3719 .with_weight(FontWeight::BOLD)
3720 .with_style(FontStyle::Italic)
3721 .with_letter_spacing(2.0)
3722 .with_line_height(26.0);
3723 scope.draw_text(Brush::solid(Color::WHITE), "AB", &style);
3724 let primitives = scope.into_primitives();
3725 assert_eq!(unwrap_text(&primitives[0]).style, style);
3726 }
3727
3728 #[test]
3729 fn a_layers_composite_alpha_is_a_truncated_byte() {
3730 for byte in 0..=255u32 {
3731 let exact = byte as f32 / 255.0;
3732 assert!(
3733 (GraphicsLayer::composite_alpha_8bit(exact) - exact).abs() < 1e-6,
3734 "byte {byte} moved"
3735 );
3736 if byte < 255 {
3737 let nearly_next = (byte as f32 + 0.999) / 255.0;
3742 assert!(
3743 (GraphicsLayer::composite_alpha_8bit(nearly_next) - exact).abs() < 1e-6,
3744 "byte {byte} + 0.999 did not truncate"
3745 );
3746 }
3747 }
3748 assert_eq!(GraphicsLayer::composite_alpha_8bit(1.0), 1.0);
3749 assert_eq!(GraphicsLayer::composite_alpha_8bit(0.0), 0.0);
3750 assert_eq!(GraphicsLayer::composite_alpha_8bit(-3.0), 0.0);
3751 assert_eq!(GraphicsLayer::composite_alpha_8bit(7.0), 1.0);
3752 }
3753}