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