1use std::{ops::AddAssign, rc::Rc};
4
5use crate::{
6 ArcRecordArgs, Brush, Color, ColorFilter, CommandRecorder, CommandRecording, ImageBitmap,
7 ImageSampling, normalized_band,
8 stroke::Stroke,
9 typography::{
10 DrawTextMeasurer, DrawTextStyle, TextAlign, TextMeasurement, TextVerticalAlign,
11 estimate_text_measurement,
12 },
13};
14
15const VECTOR_PATH_MASK_CACHE_ENTRIES: usize = 96;
16const VECTOR_PATH_MASK_CACHE_BYTES: usize = 8 * 1024 * 1024;
17
18struct VectorPathMaskCache {
19 entries: Vec<(u64, ImageBitmap)>,
20 bytes: usize,
21}
22
23impl VectorPathMaskCache {
24 const fn new() -> Self {
25 Self {
26 entries: Vec::new(),
27 bytes: 0,
28 }
29 }
30
31 fn get(&mut self, key: u64) -> Option<ImageBitmap> {
32 let index = self.entries.iter().position(|(seen, _)| *seen == key)?;
33 let entry = self.entries.remove(index);
34 let image = entry.1.clone();
35 self.entries.push(entry);
36 Some(image)
37 }
38
39 fn put(&mut self, key: u64, image: ImageBitmap) {
40 let bytes = image.width() as usize * image.height() as usize * 4;
41 if bytes > VECTOR_PATH_MASK_CACHE_BYTES {
42 return;
43 }
44 self.bytes += bytes;
45 self.entries.push((key, image));
46 while self.entries.len() > VECTOR_PATH_MASK_CACHE_ENTRIES
47 || self.bytes > VECTOR_PATH_MASK_CACHE_BYTES
48 {
49 let (_, dropped) = self.entries.remove(0);
50 self.bytes = self
51 .bytes
52 .saturating_sub(dropped.width() as usize * dropped.height() as usize * 4);
53 }
54 }
55}
56
57thread_local! {
58 static VECTOR_PATH_MASKS: std::cell::RefCell<VectorPathMaskCache> =
59 const { std::cell::RefCell::new(VectorPathMaskCache::new()) };
60}
61
62fn vector_path_mask_key(
63 path: &crate::VectorPath,
64 origin: Point,
65 mask_size: (usize, usize),
66 rgb: [u8; 3],
67 alpha: f32,
68) -> u64 {
69 use std::hash::Hasher;
70 let mut hasher = crate::fx_hash::FxHasher::default();
71 hasher.write_u8(path.fill_rule() as u8);
72 hasher.write_u32(origin.x.to_bits());
73 hasher.write_u32(origin.y.to_bits());
74 hasher.write_usize(mask_size.0);
75 hasher.write_usize(mask_size.1);
76 hasher.write(&rgb);
77 hasher.write_u32(alpha.to_bits());
78 for subpath in path.subpaths() {
79 hasher.write_usize(subpath.len());
80 for point in subpath {
81 hasher.write_u32(point.x.to_bits());
82 hasher.write_u32(point.y.to_bits());
83 }
84 }
85 hasher.finish()
86}
87
88fn vector_path_mask_cache_get(key: u64) -> Option<ImageBitmap> {
89 VECTOR_PATH_MASKS.with(|cache| cache.borrow_mut().get(key))
90}
91
92fn vector_path_mask_cache_put(key: u64, image: ImageBitmap) {
93 VECTOR_PATH_MASKS.with(|cache| cache.borrow_mut().put(key, image));
94}
95
96#[derive(Clone, Copy, Debug, PartialEq, Default)]
97pub struct Point {
98 pub x: f32,
99 pub y: f32,
100}
101
102impl Point {
103 pub const fn new(x: f32, y: f32) -> Self {
104 Self { x, y }
105 }
106
107 pub const ZERO: Point = Point { x: 0.0, y: 0.0 };
108}
109
110#[derive(Clone, Copy, Debug, PartialEq, Default)]
111pub struct Size {
112 pub width: f32,
113 pub height: f32,
114}
115
116impl Size {
117 pub const fn new(width: f32, height: f32) -> Self {
118 Self { width, height }
119 }
120
121 pub const ZERO: Size = Size {
122 width: 0.0,
123 height: 0.0,
124 };
125}
126
127#[derive(Clone, Copy, Debug, PartialEq)]
128pub struct Rect {
129 pub x: f32,
130 pub y: f32,
131 pub width: f32,
132 pub height: f32,
133}
134
135impl Rect {
136 pub const EMPTY: Rect = Rect {
140 x: 0.0,
141 y: 0.0,
142 width: 0.0,
143 height: 0.0,
144 };
145
146 pub fn from_origin_size(origin: Point, size: Size) -> Self {
147 Self {
148 x: origin.x,
149 y: origin.y,
150 width: size.width,
151 height: size.height,
152 }
153 }
154
155 pub fn from_size(size: Size) -> Self {
156 Self {
157 x: 0.0,
158 y: 0.0,
159 width: size.width,
160 height: size.height,
161 }
162 }
163
164 pub fn translate(&self, dx: f32, dy: f32) -> Self {
165 Self {
166 x: self.x + dx,
167 y: self.y + dy,
168 width: self.width,
169 height: self.height,
170 }
171 }
172
173 pub fn contains(&self, x: f32, y: f32) -> bool {
174 x >= self.x && y >= self.y && x <= self.x + self.width && y <= self.y + self.height
175 }
176
177 pub fn is_empty(&self) -> bool {
179 self.width <= 0.0 || self.height <= 0.0
180 }
181
182 pub fn intersect(&self, other: Rect) -> Option<Rect> {
184 let left = self.x.max(other.x);
185 let top = self.y.max(other.y);
186 let right = (self.x + self.width).min(other.x + other.width);
187 let bottom = (self.y + self.height).min(other.y + other.height);
188 let width = right - left;
189 let height = bottom - top;
190 if width <= 0.0 || height <= 0.0 {
191 None
192 } else {
193 Some(Rect {
194 x: left,
195 y: top,
196 width,
197 height,
198 })
199 }
200 }
201
202 pub fn union(&self, other: Rect) -> Rect {
203 let left = self.x.min(other.x);
204 let top = self.y.min(other.y);
205 let right = (self.x + self.width).max(other.x + other.width);
206 let bottom = (self.y + self.height).max(other.y + other.height);
207 Rect {
208 x: left,
209 y: top,
210 width: (right - left).max(0.0),
211 height: (bottom - top).max(0.0),
212 }
213 }
214}
215
216#[derive(Clone, Copy, Debug, Default, PartialEq)]
218pub struct EdgeInsets {
219 pub left: f32,
220 pub top: f32,
221 pub right: f32,
222 pub bottom: f32,
223}
224
225impl EdgeInsets {
226 pub fn uniform(all: f32) -> Self {
227 Self {
228 left: all,
229 top: all,
230 right: all,
231 bottom: all,
232 }
233 }
234
235 pub fn horizontal(horizontal: f32) -> Self {
236 Self {
237 left: horizontal,
238 right: horizontal,
239 ..Self::default()
240 }
241 }
242
243 pub fn vertical(vertical: f32) -> Self {
244 Self {
245 top: vertical,
246 bottom: vertical,
247 ..Self::default()
248 }
249 }
250
251 pub fn symmetric(horizontal: f32, vertical: f32) -> Self {
252 Self {
253 left: horizontal,
254 right: horizontal,
255 top: vertical,
256 bottom: vertical,
257 }
258 }
259
260 pub fn from_components(left: f32, top: f32, right: f32, bottom: f32) -> Self {
261 Self {
262 left,
263 top,
264 right,
265 bottom,
266 }
267 }
268
269 pub fn is_zero(&self) -> bool {
270 self.left == 0.0 && self.top == 0.0 && self.right == 0.0 && self.bottom == 0.0
271 }
272
273 pub fn horizontal_sum(&self) -> f32 {
274 self.left + self.right
275 }
276
277 pub fn vertical_sum(&self) -> f32 {
278 self.top + self.bottom
279 }
280}
281
282impl AddAssign for EdgeInsets {
283 fn add_assign(&mut self, rhs: Self) {
284 self.left += rhs.left;
285 self.top += rhs.top;
286 self.right += rhs.right;
287 self.bottom += rhs.bottom;
288 }
289}
290
291#[derive(Clone, Copy, Debug, Default, PartialEq)]
292pub struct CornerRadii {
293 pub top_left: f32,
294 pub top_right: f32,
295 pub bottom_right: f32,
296 pub bottom_left: f32,
297}
298
299impl CornerRadii {
300 pub fn uniform(radius: f32) -> Self {
301 Self {
302 top_left: radius,
303 top_right: radius,
304 bottom_right: radius,
305 bottom_left: radius,
306 }
307 }
308}
309
310#[derive(Clone, Copy, Debug, PartialEq)]
311pub struct RoundedCornerShape {
312 radii: CornerRadii,
313}
314
315impl RoundedCornerShape {
316 pub fn new(top_left: f32, top_right: f32, bottom_right: f32, bottom_left: f32) -> Self {
317 Self {
318 radii: CornerRadii {
319 top_left,
320 top_right,
321 bottom_right,
322 bottom_left,
323 },
324 }
325 }
326
327 pub fn uniform(radius: f32) -> Self {
328 Self {
329 radii: CornerRadii::uniform(radius),
330 }
331 }
332
333 pub fn with_radii(radii: CornerRadii) -> Self {
334 Self { radii }
335 }
336
337 pub fn resolve(&self, width: f32, height: f32) -> CornerRadii {
338 let mut resolved = self.radii;
339 let max_width = (width / 2.0).max(0.0);
340 let max_height = (height / 2.0).max(0.0);
341 resolved.top_left = resolved.top_left.clamp(0.0, max_width).min(max_height);
342 resolved.top_right = resolved.top_right.clamp(0.0, max_width).min(max_height);
343 resolved.bottom_right = resolved.bottom_right.clamp(0.0, max_width).min(max_height);
344 resolved.bottom_left = resolved.bottom_left.clamp(0.0, max_width).min(max_height);
345 resolved
346 }
347
348 pub fn radii(&self) -> CornerRadii {
349 self.radii
350 }
351}
352
353#[derive(Clone, Copy, Debug, PartialEq)]
354pub struct TransformOrigin {
355 pub pivot_fraction_x: f32,
356 pub pivot_fraction_y: f32,
357}
358
359impl TransformOrigin {
360 pub const fn new(pivot_fraction_x: f32, pivot_fraction_y: f32) -> Self {
361 Self {
362 pivot_fraction_x,
363 pivot_fraction_y,
364 }
365 }
366
367 pub const CENTER: TransformOrigin = TransformOrigin::new(0.5, 0.5);
368}
369
370impl Default for TransformOrigin {
371 fn default() -> Self {
372 Self::CENTER
373 }
374}
375
376#[derive(Clone, Copy, Debug, Default, PartialEq)]
377pub enum LayerShape {
378 #[default]
379 Rectangle,
380 Rounded(RoundedCornerShape),
381}
382
383#[derive(Clone, Debug, PartialEq)]
384pub struct GraphicsLayer {
385 pub alpha: f32,
386 pub scale: f32,
387 pub scale_x: f32,
388 pub scale_y: f32,
389 pub rotation_x: f32,
390 pub rotation_y: f32,
391 pub rotation_z: f32,
392 pub camera_distance: f32,
393 pub transform_origin: TransformOrigin,
394 pub translation_x: f32,
395 pub translation_y: f32,
396 pub shadow_elevation: f32,
397 pub ambient_shadow_color: Color,
398 pub spot_shadow_color: Color,
399 pub shape: LayerShape,
400 pub clip: bool,
401 pub compositing_strategy: CompositingStrategy,
402 pub blend_mode: BlendMode,
403 pub color_filter: Option<ColorFilter>,
404 pub render_effect: Option<crate::render_effect::RenderEffect>,
405 pub backdrop_effect: Option<crate::render_effect::RenderEffect>,
406}
407
408impl GraphicsLayer {
409 pub fn composite_alpha_8bit(alpha: f32) -> f32 {
433 (alpha.clamp(0.0, 1.0) * 255.0).floor() / 255.0
434 }
435}
436
437impl Default for GraphicsLayer {
438 fn default() -> Self {
439 Self {
440 alpha: 1.0,
441 scale: 1.0,
442 scale_x: 1.0,
443 scale_y: 1.0,
444 rotation_x: 0.0,
445 rotation_y: 0.0,
446 rotation_z: 0.0,
447 camera_distance: 8.0,
448 transform_origin: TransformOrigin::CENTER,
449 translation_x: 0.0,
450 translation_y: 0.0,
451 shadow_elevation: 0.0,
452 ambient_shadow_color: Color::BLACK,
453 spot_shadow_color: Color::BLACK,
454 shape: LayerShape::Rectangle,
455 clip: false,
456 compositing_strategy: CompositingStrategy::Auto,
457 blend_mode: BlendMode::SrcOver,
458 color_filter: None,
459 render_effect: None,
460 backdrop_effect: None,
461 }
462 }
463}
464
465#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
470pub enum BlendMode {
471 Clear,
472 Src,
473 Dst,
474 #[default]
475 SrcOver,
476 DstOver,
477 SrcIn,
478 DstIn,
479 SrcOut,
480 DstOut,
481 SrcAtop,
482 DstAtop,
483 Xor,
484 Plus,
485 Modulate,
486 Screen,
487 Overlay,
488 Darken,
489 Lighten,
490 ColorDodge,
491 ColorBurn,
492 HardLight,
493 SoftLight,
494 Difference,
495 Exclusion,
496 Multiply,
497 Hue,
498 Saturation,
499 Color,
500 Luminosity,
501}
502
503impl BlendMode {
505 pub const ALL: [BlendMode; 29] = [
507 BlendMode::Clear,
508 BlendMode::Src,
509 BlendMode::Dst,
510 BlendMode::SrcOver,
511 BlendMode::DstOver,
512 BlendMode::SrcIn,
513 BlendMode::DstIn,
514 BlendMode::SrcOut,
515 BlendMode::DstOut,
516 BlendMode::SrcAtop,
517 BlendMode::DstAtop,
518 BlendMode::Xor,
519 BlendMode::Plus,
520 BlendMode::Modulate,
521 BlendMode::Screen,
522 BlendMode::Overlay,
523 BlendMode::Darken,
524 BlendMode::Lighten,
525 BlendMode::ColorDodge,
526 BlendMode::ColorBurn,
527 BlendMode::HardLight,
528 BlendMode::SoftLight,
529 BlendMode::Difference,
530 BlendMode::Exclusion,
531 BlendMode::Multiply,
532 BlendMode::Hue,
533 BlendMode::Saturation,
534 BlendMode::Color,
535 BlendMode::Luminosity,
536 ];
537}
538
539#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
540pub enum CompositingStrategy {
541 #[default]
543 Auto,
544 Offscreen,
546 ModulateAlpha,
548}
549
550#[derive(Clone, Debug, PartialEq)]
551pub enum DrawPrimitive {
552 Content,
555 Blend {
557 primitive: Box<DrawPrimitive>,
558 blend_mode: BlendMode,
559 },
560 Rect {
561 rect: Rect,
562 brush: Brush,
563 stroke: Option<Stroke>,
566 },
567 RoundRect {
568 rect: Rect,
569 brush: Brush,
570 radii: CornerRadii,
571 stroke: Option<Stroke>,
574 },
575 Arc {
585 rect: Rect,
589 brush: Brush,
590 center: Point,
591 radius: f32,
592 start_angle: f32,
593 sweep_angle: f32,
594 stroke: Option<Stroke>,
595 inner_radius: f32,
597 },
598 Image {
599 rect: Rect,
600 image: ImageBitmap,
601 alpha: f32,
602 color_filter: Option<ColorFilter>,
603 sampling: ImageSampling,
604 src_rect: Option<Rect>,
608 },
609 Text(Box<TextPrimitive>),
611 Shadow(ShadowPrimitive),
614}
615
616#[derive(Clone, Debug, PartialEq)]
625pub struct TextPrimitive {
626 pub rect: Rect,
629 pub text: std::rc::Rc<str>,
632 pub style: DrawTextStyle,
633 pub color: Color,
637}
638
639fn shared_text_str(text: &str) -> Rc<str> {
649 use std::{
650 cell::RefCell,
651 collections::HashMap,
652 hash::{Hash, Hasher},
653 };
654
655 const POOL_CAPACITY: usize = 256;
656 thread_local! {
657 static POOL: RefCell<HashMap<u64, Rc<str>>> = RefCell::new(HashMap::new());
658 }
659
660 let mut hasher = crate::FxHasher::default();
661 text.hash(&mut hasher);
662 let key = hasher.finish();
663
664 POOL.with(|pool| {
665 let mut pool = pool.borrow_mut();
666 if let Some(shared) = pool.get(&key)
667 && &**shared == text
668 {
669 return Rc::clone(shared);
670 }
671 let shared: Rc<str> = Rc::from(text);
672 if pool.len() >= POOL_CAPACITY {
673 pool.clear();
674 }
675 pool.insert(key, Rc::clone(&shared));
676 shared
677 })
678}
679
680#[derive(Clone, Debug, PartialEq)]
682pub enum ShadowPrimitive {
683 Drop {
687 shape: Box<DrawPrimitive>,
688 cutout: Option<Box<DrawPrimitive>>,
689 blur_radius: f32,
690 blend_mode: BlendMode,
691 },
692 Inner {
694 fill: Box<DrawPrimitive>,
695 cutout: Box<DrawPrimitive>,
696 blur_radius: f32,
697 blend_mode: BlendMode,
698 clip_rect: Rect,
700 },
701}
702
703pub trait DrawScope {
704 fn size(&self) -> Size;
705 fn draw_content(&mut self);
706 fn draw_rect(&mut self, brush: Brush);
707 fn draw_rect_blend(&mut self, brush: Brush, blend_mode: BlendMode);
708 fn draw_rect_at(&mut self, rect: Rect, brush: Brush);
710 fn draw_rect_at_blend(&mut self, rect: Rect, brush: Brush, blend_mode: BlendMode);
711 fn draw_round_rect(&mut self, brush: Brush, radii: CornerRadii);
712 fn draw_round_rect_blend(&mut self, brush: Brush, radii: CornerRadii, blend_mode: BlendMode);
713 fn draw_round_rect_at(&mut self, rect: Rect, brush: Brush, radii: CornerRadii);
715 fn draw_circle(&mut self, brush: Brush, center: Point, radius: f32);
716 fn draw_circle_blend(
717 &mut self,
718 brush: Brush,
719 center: Point,
720 radius: f32,
721 blend_mode: BlendMode,
722 );
723
724 fn draw_rect_stroked(&mut self, brush: Brush, stroke: Stroke);
726 fn draw_rect_stroked_blend(&mut self, brush: Brush, stroke: Stroke, blend_mode: BlendMode);
727 fn draw_rect_at_stroked(&mut self, rect: Rect, brush: Brush, stroke: Stroke);
729 fn draw_rect_at_stroked_blend(
730 &mut self,
731 rect: Rect,
732 brush: Brush,
733 stroke: Stroke,
734 blend_mode: BlendMode,
735 );
736 fn draw_round_rect_stroked(&mut self, brush: Brush, radii: CornerRadii, stroke: Stroke);
738 fn draw_round_rect_stroked_blend(
739 &mut self,
740 brush: Brush,
741 radii: CornerRadii,
742 stroke: Stroke,
743 blend_mode: BlendMode,
744 );
745 fn draw_round_rect_at_stroked(
747 &mut self,
748 rect: Rect,
749 brush: Brush,
750 radii: CornerRadii,
751 stroke: Stroke,
752 );
753 #[allow(clippy::too_many_arguments)]
754 fn draw_round_rect_at_stroked_blend(
755 &mut self,
756 rect: Rect,
757 brush: Brush,
758 radii: CornerRadii,
759 stroke: Stroke,
760 blend_mode: BlendMode,
761 );
762 fn draw_circle_stroked(&mut self, brush: Brush, center: Point, radius: f32, stroke: Stroke);
765 fn draw_circle_stroked_blend(
766 &mut self,
767 brush: Brush,
768 center: Point,
769 radius: f32,
770 stroke: Stroke,
771 blend_mode: BlendMode,
772 );
773
774 #[allow(clippy::too_many_arguments)]
787 fn draw_arc(
788 &mut self,
789 brush: Brush,
790 center: Point,
791 radius: f32,
792 start_angle: f32,
793 sweep_angle: f32,
794 stroke: Stroke,
795 );
796 #[allow(clippy::too_many_arguments)]
797 fn draw_arc_blend(
798 &mut self,
799 brush: Brush,
800 center: Point,
801 radius: f32,
802 start_angle: f32,
803 sweep_angle: f32,
804 stroke: Stroke,
805 blend_mode: BlendMode,
806 );
807
808 #[allow(clippy::too_many_arguments)]
817 fn draw_annular_sector(
818 &mut self,
819 brush: Brush,
820 center: Point,
821 inner_radius: f32,
822 outer_radius: f32,
823 start_angle: f32,
824 sweep_angle: f32,
825 );
826 #[allow(clippy::too_many_arguments)]
827 fn draw_annular_sector_blend(
828 &mut self,
829 brush: Brush,
830 center: Point,
831 inner_radius: f32,
832 outer_radius: f32,
833 start_angle: f32,
834 sweep_angle: f32,
835 blend_mode: BlendMode,
836 );
837
838 fn draw_image(&mut self, image: ImageBitmap);
839 fn draw_image_blend(&mut self, image: ImageBitmap, blend_mode: BlendMode);
840 fn draw_image_at(
841 &mut self,
842 rect: Rect,
843 image: ImageBitmap,
844 alpha: f32,
845 color_filter: Option<ColorFilter>,
846 );
847 fn draw_image_at_sampled(
848 &mut self,
849 rect: Rect,
850 image: ImageBitmap,
851 alpha: f32,
852 color_filter: Option<ColorFilter>,
853 sampling: ImageSampling,
854 );
855 fn draw_image_at_blend(
856 &mut self,
857 rect: Rect,
858 image: ImageBitmap,
859 alpha: f32,
860 color_filter: Option<ColorFilter>,
861 blend_mode: BlendMode,
862 );
863 fn draw_image_src(
866 &mut self,
867 image: ImageBitmap,
868 src_rect: Rect,
869 dst_rect: Rect,
870 alpha: f32,
871 color_filter: Option<ColorFilter>,
872 );
873 fn draw_image_src_sampled(
874 &mut self,
875 image: ImageBitmap,
876 src_rect: Rect,
877 dst_rect: Rect,
878 alpha: f32,
879 color_filter: Option<ColorFilter>,
880 sampling: ImageSampling,
881 );
882 fn draw_image_src_blend(
883 &mut self,
884 image: ImageBitmap,
885 src_rect: Rect,
886 dst_rect: Rect,
887 alpha: f32,
888 color_filter: Option<ColorFilter>,
889 blend_mode: BlendMode,
890 );
891 fn draw_vector_path(&mut self, path: &crate::VectorPath, brush: Brush);
900 fn draw_svg_path(&mut self, d: &str, brush: Brush) {
906 if let Ok(path) = crate::VectorPath::parse(d) {
907 self.draw_vector_path(&path, brush);
908 }
909 }
910
911 fn measure_text(&self, text: &str, style: &DrawTextStyle) -> TextMeasurement;
918
919 fn draw_text(&mut self, brush: Brush, text: &str, style: &DrawTextStyle) {
922 self.draw_text_at(Rect::from_size(self.size()), brush, text, style);
923 }
924
925 fn draw_text_at(&mut self, rect: Rect, brush: Brush, text: &str, style: &DrawTextStyle);
932
933 fn draw_text_from(&mut self, top_left: Point, brush: Brush, text: &str, style: &DrawTextStyle) {
939 if text.is_empty() {
940 return;
941 }
942 let measurement = self.measure_text(text, style);
943 self.draw_text_at(
944 Rect::from_origin_size(top_left, measurement.size),
945 brush,
946 text,
947 &DrawTextStyle {
948 align: TextAlign::Left,
949 vertical_align: TextVerticalAlign::Top,
950 ..style.clone()
951 },
952 );
953 }
954
955 fn into_primitives(self) -> Vec<DrawPrimitive>;
956}
957
958pub fn align_text_block(rect: Rect, measurement: TextMeasurement, style: &DrawTextStyle) -> Point {
964 let x = match style.align {
965 TextAlign::Left => rect.x,
966 TextAlign::Center => rect.x + (rect.width - measurement.size.width) * 0.5,
967 TextAlign::Right => rect.x + rect.width - measurement.size.width,
968 };
969 let y = match style.vertical_align {
970 TextVerticalAlign::Top => rect.y,
971 TextVerticalAlign::Center => rect.y + (rect.height - measurement.size.height) * 0.5,
972 TextVerticalAlign::Bottom => rect.y + rect.height - measurement.size.height,
973 TextVerticalAlign::Baseline => rect.y - measurement.first_baseline,
974 };
975 Point::new(x, y)
976}
977
978#[derive(Default)]
979pub struct DrawScopeDefault {
980 size: Size,
981 recording: CommandRecorder,
982 text_measurer: Option<Rc<dyn DrawTextMeasurer>>,
983}
984
985const RECORDED_PRIMITIVE_COUNTS_LIMIT: usize = 64;
986
987thread_local! {
988 static RECORDED_PRIMITIVE_COUNTS: std::cell::RefCell<std::collections::HashMap<(u32, u32), usize>> =
989 std::cell::RefCell::new(std::collections::HashMap::new());
990}
991
992fn recorded_primitive_capacity(size: Size) -> usize {
993 RECORDED_PRIMITIVE_COUNTS.with(|counts| {
994 counts
995 .borrow()
996 .get(&(size.width.to_bits(), size.height.to_bits()))
997 .copied()
998 .unwrap_or(0)
999 })
1000}
1001
1002fn note_recorded_primitive_count(size: Size, count: usize) {
1003 RECORDED_PRIMITIVE_COUNTS.with(|counts| {
1004 let mut counts = counts.borrow_mut();
1005 if counts.len() >= RECORDED_PRIMITIVE_COUNTS_LIMIT {
1006 counts.clear();
1007 }
1008 counts.insert((size.width.to_bits(), size.height.to_bits()), count);
1009 });
1010}
1011
1012impl DrawScopeDefault {
1013 pub fn new(size: Size) -> Self {
1014 Self::with_storage(size, None, CommandRecording::default())
1015 }
1016
1017 pub fn with_text_measurer(size: Size, text_measurer: Rc<dyn DrawTextMeasurer>) -> Self {
1022 Self::with_storage(size, Some(text_measurer), CommandRecording::default())
1023 }
1024
1025 pub fn with_text_measurer_reusing(
1028 size: Size,
1029 text_measurer: Rc<dyn DrawTextMeasurer>,
1030 storage: CommandRecording,
1031 ) -> Self {
1032 Self::with_storage(size, Some(text_measurer), storage)
1033 }
1034
1035 fn with_storage(
1036 size: Size,
1037 text_measurer: Option<Rc<dyn DrawTextMeasurer>>,
1038 recording: CommandRecording,
1039 ) -> Self {
1040 let mut recording = CommandRecorder::reusing(recording);
1041 recording.reserve_shapes(recorded_primitive_capacity(size));
1042 Self {
1043 size,
1044 recording,
1045 text_measurer,
1046 }
1047 }
1048
1049 pub fn content_marker_count(&self) -> u32 {
1051 self.recording.content_markers()
1052 }
1053
1054 pub fn push_recorded(&mut self, primitives: impl IntoIterator<Item = DrawPrimitive>) {
1056 for primitive in primitives {
1057 self.recording.push_primitive(primitive);
1058 }
1059 }
1060
1061 pub fn finish(self) -> CommandRecording {
1064 note_recorded_primitive_count(self.size, self.recording.len());
1065 self.recording.finish()
1066 }
1067
1068 fn push_blended_primitive(&mut self, primitive: DrawPrimitive, blend_mode: BlendMode) {
1069 if blend_mode != BlendMode::SrcOver {
1070 self.recording.push_other(DrawPrimitive::Blend {
1071 primitive: Box::new(primitive),
1072 blend_mode,
1073 });
1074 return;
1075 }
1076 self.recording.push_other(primitive);
1077 }
1078
1079 #[allow(clippy::too_many_arguments)]
1080 #[inline]
1081 fn push_arc(
1082 &mut self,
1083 brush: Brush,
1084 center: Point,
1085 radius: f32,
1086 start_angle: f32,
1087 sweep_angle: f32,
1088 stroke: Option<Stroke>,
1089 inner_radius: f32,
1090 blend_mode: BlendMode,
1091 ) {
1092 let args = ArcRecordArgs {
1093 brush: &brush,
1094 center,
1095 radius,
1096 start_angle,
1097 sweep_angle,
1098 stroke,
1099 inner_radius,
1100 blend_mode,
1101 };
1102 let geometry = normalized_band(&args);
1103 if geometry.is_degenerate() {
1104 return;
1105 }
1106 self.recording.push_scope_arc(&args, &geometry);
1107 }
1108}
1109
1110impl DrawScope for DrawScopeDefault {
1111 fn size(&self) -> Size {
1112 self.size
1113 }
1114
1115 fn draw_content(&mut self) {
1116 self.recording.push_content();
1117 }
1118
1119 fn draw_rect(&mut self, brush: Brush) {
1120 self.draw_rect_blend(brush, BlendMode::SrcOver);
1121 }
1122
1123 fn draw_rect_blend(&mut self, brush: Brush, blend_mode: BlendMode) {
1124 self.recording
1125 .push_rect(Rect::from_size(self.size), &brush, None, blend_mode);
1126 }
1127
1128 fn draw_rect_at(&mut self, rect: Rect, brush: Brush) {
1129 self.draw_rect_at_blend(rect, brush, BlendMode::SrcOver);
1130 }
1131
1132 fn draw_rect_at_blend(&mut self, rect: Rect, brush: Brush, blend_mode: BlendMode) {
1133 self.recording.push_rect(rect, &brush, None, blend_mode);
1134 }
1135
1136 fn draw_round_rect(&mut self, brush: Brush, radii: CornerRadii) {
1137 self.draw_round_rect_blend(brush, radii, BlendMode::SrcOver);
1138 }
1139
1140 fn draw_round_rect_blend(&mut self, brush: Brush, radii: CornerRadii, blend_mode: BlendMode) {
1141 self.recording
1142 .push_round_rect(Rect::from_size(self.size), &brush, radii, None, blend_mode);
1143 }
1144
1145 fn draw_round_rect_at(&mut self, rect: Rect, brush: Brush, radii: CornerRadii) {
1146 self.recording
1147 .push_round_rect(rect, &brush, radii, None, BlendMode::SrcOver);
1148 }
1149
1150 fn draw_rect_stroked(&mut self, brush: Brush, stroke: Stroke) {
1151 self.draw_rect_stroked_blend(brush, stroke, BlendMode::SrcOver);
1152 }
1153
1154 fn draw_rect_stroked_blend(&mut self, brush: Brush, stroke: Stroke, blend_mode: BlendMode) {
1155 self.draw_rect_at_stroked_blend(Rect::from_size(self.size), brush, stroke, blend_mode);
1156 }
1157
1158 fn draw_rect_at_stroked(&mut self, rect: Rect, brush: Brush, stroke: Stroke) {
1159 self.draw_rect_at_stroked_blend(rect, brush, stroke, BlendMode::SrcOver);
1160 }
1161
1162 fn draw_rect_at_stroked_blend(
1163 &mut self,
1164 rect: Rect,
1165 brush: Brush,
1166 stroke: Stroke,
1167 blend_mode: BlendMode,
1168 ) {
1169 if !stroke.is_visible() {
1170 return;
1171 }
1172 self.recording
1173 .push_rect(rect, &brush, Some(stroke), blend_mode);
1174 }
1175
1176 fn draw_round_rect_stroked(&mut self, brush: Brush, radii: CornerRadii, stroke: Stroke) {
1177 self.draw_round_rect_stroked_blend(brush, radii, stroke, BlendMode::SrcOver);
1178 }
1179
1180 fn draw_round_rect_stroked_blend(
1181 &mut self,
1182 brush: Brush,
1183 radii: CornerRadii,
1184 stroke: Stroke,
1185 blend_mode: BlendMode,
1186 ) {
1187 self.draw_round_rect_at_stroked_blend(
1188 Rect::from_size(self.size),
1189 brush,
1190 radii,
1191 stroke,
1192 blend_mode,
1193 );
1194 }
1195
1196 fn draw_round_rect_at_stroked(
1197 &mut self,
1198 rect: Rect,
1199 brush: Brush,
1200 radii: CornerRadii,
1201 stroke: Stroke,
1202 ) {
1203 self.draw_round_rect_at_stroked_blend(rect, brush, radii, stroke, BlendMode::SrcOver);
1204 }
1205
1206 fn draw_round_rect_at_stroked_blend(
1207 &mut self,
1208 rect: Rect,
1209 brush: Brush,
1210 radii: CornerRadii,
1211 stroke: Stroke,
1212 blend_mode: BlendMode,
1213 ) {
1214 if !stroke.is_visible() {
1215 return;
1216 }
1217 self.recording
1218 .push_round_rect(rect, &brush, radii, Some(stroke), blend_mode);
1219 }
1220
1221 fn draw_circle_stroked(&mut self, brush: Brush, center: Point, radius: f32, stroke: Stroke) {
1222 self.draw_circle_stroked_blend(brush, center, radius, stroke, BlendMode::SrcOver);
1223 }
1224
1225 fn draw_circle_stroked_blend(
1226 &mut self,
1227 brush: Brush,
1228 center: Point,
1229 radius: f32,
1230 stroke: Stroke,
1231 blend_mode: BlendMode,
1232 ) {
1233 if !stroke.is_visible() || !radius.is_finite() {
1234 return;
1235 }
1236 let radius = radius.max(0.0);
1237 let diameter = radius * 2.0;
1238 self.draw_round_rect_at_stroked_blend(
1239 Rect {
1240 x: center.x - radius,
1241 y: center.y - radius,
1242 width: diameter,
1243 height: diameter,
1244 },
1245 brush,
1246 CornerRadii::uniform(radius),
1247 stroke,
1248 blend_mode,
1249 );
1250 }
1251
1252 fn draw_arc(
1253 &mut self,
1254 brush: Brush,
1255 center: Point,
1256 radius: f32,
1257 start_angle: f32,
1258 sweep_angle: f32,
1259 stroke: Stroke,
1260 ) {
1261 self.draw_arc_blend(
1262 brush,
1263 center,
1264 radius,
1265 start_angle,
1266 sweep_angle,
1267 stroke,
1268 BlendMode::SrcOver,
1269 );
1270 }
1271
1272 fn draw_arc_blend(
1273 &mut self,
1274 brush: Brush,
1275 center: Point,
1276 radius: f32,
1277 start_angle: f32,
1278 sweep_angle: f32,
1279 stroke: Stroke,
1280 blend_mode: BlendMode,
1281 ) {
1282 if !stroke.is_visible() {
1283 return;
1284 }
1285 self.push_arc(
1286 brush,
1287 center,
1288 radius,
1289 start_angle,
1290 sweep_angle,
1291 Some(stroke),
1292 0.0,
1293 blend_mode,
1294 );
1295 }
1296
1297 fn draw_annular_sector(
1298 &mut self,
1299 brush: Brush,
1300 center: Point,
1301 inner_radius: f32,
1302 outer_radius: f32,
1303 start_angle: f32,
1304 sweep_angle: f32,
1305 ) {
1306 self.draw_annular_sector_blend(
1307 brush,
1308 center,
1309 inner_radius,
1310 outer_radius,
1311 start_angle,
1312 sweep_angle,
1313 BlendMode::SrcOver,
1314 );
1315 }
1316
1317 fn draw_annular_sector_blend(
1318 &mut self,
1319 brush: Brush,
1320 center: Point,
1321 inner_radius: f32,
1322 outer_radius: f32,
1323 start_angle: f32,
1324 sweep_angle: f32,
1325 blend_mode: BlendMode,
1326 ) {
1327 self.push_arc(
1328 brush,
1329 center,
1330 outer_radius,
1331 start_angle,
1332 sweep_angle,
1333 None,
1334 inner_radius,
1335 blend_mode,
1336 );
1337 }
1338
1339 fn draw_circle(&mut self, brush: Brush, center: Point, radius: f32) {
1340 self.draw_circle_blend(brush, center, radius, BlendMode::SrcOver);
1341 }
1342
1343 fn draw_circle_blend(
1344 &mut self,
1345 brush: Brush,
1346 center: Point,
1347 radius: f32,
1348 blend_mode: BlendMode,
1349 ) {
1350 let radius = radius.max(0.0);
1351 let diameter = radius * 2.0;
1352 self.recording.push_round_rect(
1353 Rect {
1354 x: center.x - radius,
1355 y: center.y - radius,
1356 width: diameter,
1357 height: diameter,
1358 },
1359 &brush,
1360 CornerRadii::uniform(radius),
1361 None,
1362 blend_mode,
1363 );
1364 }
1365
1366 fn draw_image(&mut self, image: ImageBitmap) {
1367 self.draw_image_blend(image, BlendMode::SrcOver);
1368 }
1369
1370 fn draw_image_blend(&mut self, image: ImageBitmap, blend_mode: BlendMode) {
1371 self.push_blended_primitive(
1372 DrawPrimitive::Image {
1373 rect: Rect::from_size(self.size),
1374 image,
1375 alpha: 1.0,
1376 color_filter: None,
1377 sampling: ImageSampling::Nearest,
1378 src_rect: None,
1379 },
1380 blend_mode,
1381 );
1382 }
1383
1384 fn draw_image_at(
1385 &mut self,
1386 rect: Rect,
1387 image: ImageBitmap,
1388 alpha: f32,
1389 color_filter: Option<ColorFilter>,
1390 ) {
1391 self.draw_image_at_sampled(rect, image, alpha, color_filter, ImageSampling::Nearest);
1392 }
1393
1394 fn draw_image_at_sampled(
1395 &mut self,
1396 rect: Rect,
1397 image: ImageBitmap,
1398 alpha: f32,
1399 color_filter: Option<ColorFilter>,
1400 sampling: ImageSampling,
1401 ) {
1402 self.push_blended_primitive(
1403 DrawPrimitive::Image {
1404 rect,
1405 image,
1406 alpha: alpha.clamp(0.0, 1.0),
1407 color_filter,
1408 sampling,
1409 src_rect: None,
1410 },
1411 BlendMode::SrcOver,
1412 );
1413 }
1414
1415 fn draw_image_at_blend(
1416 &mut self,
1417 rect: Rect,
1418 image: ImageBitmap,
1419 alpha: f32,
1420 color_filter: Option<ColorFilter>,
1421 blend_mode: BlendMode,
1422 ) {
1423 self.push_blended_primitive(
1424 DrawPrimitive::Image {
1425 rect,
1426 image,
1427 alpha: alpha.clamp(0.0, 1.0),
1428 color_filter,
1429 sampling: ImageSampling::Nearest,
1430 src_rect: None,
1431 },
1432 blend_mode,
1433 );
1434 }
1435
1436 fn draw_image_src(
1437 &mut self,
1438 image: ImageBitmap,
1439 src_rect: Rect,
1440 dst_rect: Rect,
1441 alpha: f32,
1442 color_filter: Option<ColorFilter>,
1443 ) {
1444 self.draw_image_src_blend(
1445 image,
1446 src_rect,
1447 dst_rect,
1448 alpha,
1449 color_filter,
1450 BlendMode::SrcOver,
1451 );
1452 }
1453
1454 fn draw_image_src_sampled(
1455 &mut self,
1456 image: ImageBitmap,
1457 src_rect: Rect,
1458 dst_rect: Rect,
1459 alpha: f32,
1460 color_filter: Option<ColorFilter>,
1461 sampling: ImageSampling,
1462 ) {
1463 self.push_blended_primitive(
1464 DrawPrimitive::Image {
1465 rect: dst_rect,
1466 image,
1467 alpha: alpha.clamp(0.0, 1.0),
1468 color_filter,
1469 sampling,
1470 src_rect: Some(src_rect),
1471 },
1472 BlendMode::SrcOver,
1473 );
1474 }
1475
1476 fn draw_image_src_blend(
1477 &mut self,
1478 image: ImageBitmap,
1479 src_rect: Rect,
1480 dst_rect: Rect,
1481 alpha: f32,
1482 color_filter: Option<ColorFilter>,
1483 blend_mode: BlendMode,
1484 ) {
1485 self.push_blended_primitive(
1486 DrawPrimitive::Image {
1487 rect: dst_rect,
1488 image,
1489 alpha: alpha.clamp(0.0, 1.0),
1490 color_filter,
1491 sampling: ImageSampling::Nearest,
1492 src_rect: Some(src_rect),
1493 },
1494 blend_mode,
1495 );
1496 }
1497
1498 fn draw_vector_path(&mut self, path: &crate::VectorPath, brush: Brush) {
1499 const SUPERSAMPLE: f32 = 2.0;
1500 const MAX_MASK_PIXELS: f32 = 4096.0;
1501
1502 if path.is_empty() {
1503 return;
1504 }
1505 let bounds = path.bounds();
1506 if bounds.width <= 0.0 || bounds.height <= 0.0 {
1507 return;
1508 }
1509
1510 let color = match &brush {
1511 Brush::Solid(color) => *color,
1512 Brush::LinearGradient { colors, .. }
1513 | Brush::RadialGradient { colors, .. }
1514 | Brush::SweepGradient { colors, .. } => match colors.first() {
1515 Some(color) => *color,
1516 None => return,
1517 },
1518 };
1519 if color.3 <= 0.0 {
1520 return;
1521 }
1522
1523 let origin = Point::new(bounds.x.floor() - 1.0, bounds.y.floor() - 1.0);
1524 let rect_width = (bounds.x + bounds.width).ceil() - origin.x + 1.0;
1525 let rect_height = (bounds.y + bounds.height).ceil() - origin.y + 1.0;
1526 let mask_width = (rect_width * SUPERSAMPLE)
1527 .ceil()
1528 .clamp(1.0, MAX_MASK_PIXELS) as usize;
1529 let mask_height = (rect_height * SUPERSAMPLE)
1530 .ceil()
1531 .clamp(1.0, MAX_MASK_PIXELS) as usize;
1532
1533 let red = (color.0.clamp(0.0, 1.0) * 255.0 + 0.5) as u8;
1534 let green = (color.1.clamp(0.0, 1.0) * 255.0 + 0.5) as u8;
1535 let blue = (color.2.clamp(0.0, 1.0) * 255.0 + 0.5) as u8;
1536 let alpha = color.3.clamp(0.0, 1.0);
1537 let key = vector_path_mask_key(
1538 path,
1539 origin,
1540 (mask_width, mask_height),
1541 [red, green, blue],
1542 alpha,
1543 );
1544 let cached = vector_path_mask_cache_get(key);
1545 let image = match cached {
1546 Some(image) => image,
1547 None => {
1548 let mask = path.coverage_mask(mask_width, mask_height, origin, SUPERSAMPLE);
1549 let mut pixels = Vec::with_capacity(mask.len() * 4);
1550 for coverage in mask {
1551 pixels.extend_from_slice(&[
1552 red,
1553 green,
1554 blue,
1555 (alpha * coverage as f32 + 0.5) as u8,
1556 ]);
1557 }
1558 let Ok(image) =
1559 ImageBitmap::from_rgba8(mask_width as u32, mask_height as u32, pixels)
1560 else {
1561 return;
1562 };
1563 vector_path_mask_cache_put(key, image.clone());
1564 image
1565 }
1566 };
1567
1568 self.recording.push_other(DrawPrimitive::Image {
1569 rect: Rect {
1570 x: origin.x,
1571 y: origin.y,
1572 width: rect_width,
1573 height: rect_height,
1574 },
1575 image,
1576 alpha: 1.0,
1577 color_filter: None,
1578 sampling: ImageSampling::Linear,
1579 src_rect: None,
1580 });
1581 }
1582
1583 fn measure_text(&self, text: &str, style: &DrawTextStyle) -> TextMeasurement {
1584 match &self.text_measurer {
1585 Some(measurer) => measurer.measure_text(text, style),
1586 None => estimate_text_measurement(text, style),
1587 }
1588 }
1589
1590 fn draw_text_at(&mut self, rect: Rect, brush: Brush, text: &str, style: &DrawTextStyle) {
1591 if text.is_empty() {
1592 return;
1593 }
1594 let Some(color) = solid_fill_color(&brush) else {
1595 return;
1596 };
1597 if color.3 <= 0.0 {
1598 return;
1599 }
1600 let measurement = self.measure_text(text, style);
1601 if !(measurement.size.width > 0.0 && measurement.size.height > 0.0) {
1602 return;
1603 }
1604 let origin = align_text_block(rect, measurement, style);
1605 if !origin.x.is_finite() || !origin.y.is_finite() {
1606 return;
1607 }
1608 self.recording
1609 .push_other(DrawPrimitive::Text(Box::new(TextPrimitive {
1610 rect: Rect::from_origin_size(origin, measurement.size),
1611 text: shared_text_str(text),
1612 style: style.clone(),
1613 color,
1614 })));
1615 }
1616
1617 fn into_primitives(self) -> Vec<DrawPrimitive> {
1618 self.finish().into_primitives_with_markers()
1619 }
1620}
1621
1622fn solid_fill_color(brush: &Brush) -> Option<Color> {
1627 match brush {
1628 Brush::Solid(color) => Some(*color),
1629 Brush::LinearGradient { colors, .. }
1630 | Brush::RadialGradient { colors, .. }
1631 | Brush::SweepGradient { colors, .. } => colors.first().copied(),
1632 }
1633}
1634
1635#[cfg(test)]
1636mod tests {
1637 use super::*;
1638 use crate::{Color, FontStyle, FontWeight, ImageBitmap, RenderEffect};
1639
1640 #[test]
1641 fn recorded_iterators_preserve_shapes_content_and_shadows() {
1642 let shape = DrawPrimitive::Rect {
1643 rect: Rect::from_size(Size::new(12.0, 8.0)),
1644 brush: Brush::solid(Color::RED),
1645 stroke: None,
1646 };
1647 let shadow = DrawPrimitive::Shadow(ShadowPrimitive::Drop {
1648 shape: Box::new(shape.clone()),
1649 cutout: None,
1650 blur_radius: 4.0,
1651 blend_mode: BlendMode::SrcOver,
1652 });
1653 let mut scope = DrawScopeDefault::new(Size::new(24.0, 24.0));
1654 scope.push_recorded(None);
1655 scope.push_recorded(Some(shadow.clone()));
1656 scope.push_recorded([DrawPrimitive::Content, shape.clone()]);
1657 scope.push_recorded(std::iter::empty());
1658 let recording = scope.finish();
1659 assert_eq!(recording.content_markers(), 1);
1660 assert_eq!(
1661 recording.into_primitives_with_markers(),
1662 vec![shadow, DrawPrimitive::Content, shape]
1663 );
1664 }
1665
1666 #[test]
1667 fn compact_recording_materializes_in_recorded_order() {
1668 let size = Size::new(100.0, 100.0);
1669 let solid = Brush::solid(Color::WHITE);
1670 let gradient = Brush::vertical_gradient(vec![Color::RED, Color::BLUE], 0.0, 100.0);
1671 let center = Point::new(50.0, 50.0);
1672 let stroke = Stroke::new(4.0);
1673 let rect = Rect {
1674 x: 10.0,
1675 y: 20.0,
1676 width: 30.0,
1677 height: 40.0,
1678 };
1679 let batch = vec![
1680 DrawPrimitive::Content,
1681 DrawPrimitive::Rect {
1682 rect,
1683 brush: solid.clone(),
1684 stroke: None,
1685 },
1686 ];
1687
1688 let record = |scope: &mut DrawScopeDefault| {
1689 scope.draw_rect_at(rect, solid.clone());
1690 scope.draw_arc(solid.clone(), center, 30.0, 0.5, 1.5, stroke);
1691 scope.draw_rect_at(rect, gradient.clone());
1692 scope.draw_circle(solid.clone(), center, 12.0);
1693 scope.draw_arc(solid.clone(), center, 30.0, 0.5, 0.0, stroke);
1694 scope.draw_rect_at_blend(rect, solid.clone(), BlendMode::Plus);
1695 scope.draw_content();
1696 scope.draw_annular_sector(gradient.clone(), center, 10.0, 20.0, 0.0, 2.0);
1697 scope.push_recorded(batch.clone());
1698 };
1699
1700 let mut compact = DrawScopeDefault::new(size);
1701 record(&mut compact);
1702 let finished = compact.finish();
1703
1704 let arc_via_ordinary = |brush: Brush, radius: f32, start: f32, sweep: f32| {
1705 let mut scope = DrawScopeDefault::new(size);
1706 scope.draw_arc(brush, center, radius, start, sweep, stroke);
1707 scope.into_primitives().remove(0)
1708 };
1709 let expected = vec![
1710 DrawPrimitive::Rect {
1711 rect,
1712 brush: solid.clone(),
1713 stroke: None,
1714 },
1715 arc_via_ordinary(solid.clone(), 30.0, 0.5, 1.5),
1716 DrawPrimitive::Rect {
1717 rect,
1718 brush: gradient.clone(),
1719 stroke: None,
1720 },
1721 DrawPrimitive::RoundRect {
1722 rect: Rect {
1723 x: center.x - 12.0,
1724 y: center.y - 12.0,
1725 width: 24.0,
1726 height: 24.0,
1727 },
1728 brush: solid.clone(),
1729 radii: CornerRadii::uniform(12.0),
1730 stroke: None,
1731 },
1732 DrawPrimitive::Blend {
1733 primitive: Box::new(DrawPrimitive::Rect {
1734 rect,
1735 brush: solid.clone(),
1736 stroke: None,
1737 }),
1738 blend_mode: BlendMode::Plus,
1739 },
1740 DrawPrimitive::Content,
1741 {
1742 let mut scope = DrawScopeDefault::new(size);
1743 scope.draw_annular_sector(gradient.clone(), center, 10.0, 20.0, 0.0, 2.0);
1744 scope.into_primitives().remove(0)
1745 },
1746 DrawPrimitive::Content,
1747 DrawPrimitive::Rect {
1748 rect,
1749 brush: solid.clone(),
1750 stroke: None,
1751 },
1752 ];
1753 assert_eq!(finished.content_markers(), 2);
1754 assert_eq!(finished.into_primitives_with_markers(), expected);
1755 }
1756
1757 #[test]
1758 fn redrawing_the_same_text_shares_one_str_allocation() {
1759 let first = shared_text_str("BREAK THE RING");
1760 let second = shared_text_str("BREAK THE RING");
1761 assert!(Rc::ptr_eq(&first, &second));
1762 assert_eq!(&*second, "BREAK THE RING");
1763 }
1764
1765 #[test]
1766 fn different_text_gets_its_own_str() {
1767 let first = shared_text_str("340");
1768 let second = shared_text_str("350");
1769 assert!(!Rc::ptr_eq(&first, &second));
1770 assert_eq!(&*first, "340");
1771 assert_eq!(&*second, "350");
1772 }
1773
1774 #[test]
1775 fn the_text_pool_survives_overflowing_its_capacity() {
1776 for index in 0..600 {
1777 let text = format!("run-{index}");
1778 assert_eq!(&*shared_text_str(&text), text.as_str());
1779 }
1780 assert_eq!(&*shared_text_str("still correct"), "still correct");
1781 }
1782
1783 fn assert_image_alpha(primitive: &DrawPrimitive, expected: f32) {
1784 match primitive {
1785 DrawPrimitive::Image { alpha, .. } => assert!((alpha - expected).abs() < 1e-5),
1786 DrawPrimitive::Blend { primitive, .. } => assert_image_alpha(primitive, expected),
1787 other => panic!("expected image primitive, got {other:?}"),
1788 }
1789 }
1790
1791 fn unwrap_image(primitive: &DrawPrimitive) -> &DrawPrimitive {
1792 match primitive {
1793 DrawPrimitive::Image { .. } => primitive,
1794 DrawPrimitive::Blend { primitive, .. } => unwrap_image(primitive),
1795 other => panic!("expected image primitive, got {other:?}"),
1796 }
1797 }
1798
1799 #[test]
1800 fn draw_svg_path_emits_supersampled_image_over_path_bounds() {
1801 let mut scope = DrawScopeDefault::new(Size::new(32.0, 32.0));
1802 scope.draw_svg_path("M 4 4 H 20 V 20 H 4 Z", Brush::solid(Color::RED));
1803
1804 let primitives = scope.into_primitives();
1805 assert_eq!(primitives.len(), 1);
1806 let DrawPrimitive::Image { rect, image, .. } = &primitives[0] else {
1807 panic!("expected image primitive, got {:?}", primitives[0]);
1808 };
1809
1810 assert_eq!((rect.x, rect.y), (3.0, 3.0));
1811 assert_eq!((rect.width, rect.height), (18.0, 18.0));
1812 assert_eq!((image.width(), image.height()), (36, 36));
1813
1814 let pixels = image.pixels();
1815 let index = (18 * 36 + 18) * 4;
1816 assert_eq!(
1817 &pixels[index..index + 4],
1818 &[255, 0, 0, 255],
1819 "path interior must be opaque brush color"
1820 );
1821 assert_eq!(pixels[3], 0, "outside the path must stay transparent");
1822 }
1823
1824 #[test]
1825 fn draw_svg_path_ignores_invalid_data() {
1826 let mut scope = DrawScopeDefault::new(Size::new(16.0, 16.0));
1827 scope.draw_svg_path("definitely not a path", Brush::solid(Color::WHITE));
1828 assert!(scope.into_primitives().is_empty());
1829 }
1830
1831 #[test]
1832 fn draw_vector_path_applies_brush_alpha() {
1833 let path = crate::VectorPath::parse("M 0 0 H 8 V 8 H 0 Z").expect("valid path");
1834 let mut scope = DrawScopeDefault::new(Size::new(16.0, 16.0));
1835 scope.draw_vector_path(&path, Brush::solid(Color::rgba(0.0, 0.0, 1.0, 0.5)));
1836
1837 let primitives = scope.into_primitives();
1838 let DrawPrimitive::Image { image, .. } = &primitives[0] else {
1839 panic!("expected image primitive");
1840 };
1841 let pixels = image.pixels();
1842 let width = image.width() as usize;
1843 let index = ((image.height() as usize / 2) * width + width / 2) * 4;
1844 assert_eq!(&pixels[index..index + 3], &[0, 0, 255]);
1845 let alpha = pixels[index + 3];
1846 assert!(
1847 (alpha as i32 - 128).abs() <= 2,
1848 "interior alpha must honor the brush alpha, got {alpha}"
1849 );
1850 }
1851
1852 #[test]
1853 fn the_same_path_and_color_reuse_one_raster() {
1854 let path = crate::VectorPath::parse("M 0 0 H 7 V 7 H 0 Z").expect("valid path");
1855 let raster_of = |brush: Brush| {
1856 let mut scope = DrawScopeDefault::new(Size::new(16.0, 16.0));
1857 scope.draw_vector_path(&path, brush);
1858 let primitives = scope.into_primitives();
1859 let DrawPrimitive::Image { image, .. } = &primitives[0] else {
1860 panic!("expected image primitive");
1861 };
1862 image.clone()
1863 };
1864
1865 let first = raster_of(Brush::solid(Color::rgba(0.0, 0.0, 1.0, 1.0)));
1866 let second = raster_of(Brush::solid(Color::rgba(0.0, 0.0, 1.0, 1.0)));
1867 assert_eq!(first.id(), second.id());
1868
1869 let other_color = raster_of(Brush::solid(Color::rgba(1.0, 0.0, 0.0, 1.0)));
1870 assert_ne!(first.id(), other_color.id());
1871
1872 let wider = crate::VectorPath::parse("M 0 0 H 9 V 7 H 0 Z").expect("valid path");
1873 let mut scope = DrawScopeDefault::new(Size::new(16.0, 16.0));
1874 scope.draw_vector_path(&wider, Brush::solid(Color::rgba(0.0, 0.0, 1.0, 1.0)));
1875 let primitives = scope.into_primitives();
1876 let DrawPrimitive::Image { image, .. } = &primitives[0] else {
1877 panic!("expected image primitive");
1878 };
1879 assert_ne!(first.id(), image.id());
1880 }
1881
1882 #[test]
1883 fn draw_content_inserts_content_marker() {
1884 let mut scope = DrawScopeDefault::new(Size::new(8.0, 8.0));
1885 scope.draw_rect(Brush::solid(Color::WHITE));
1886 scope.draw_content();
1887 scope.draw_rect_blend(Brush::solid(Color::BLACK), BlendMode::DstOut);
1888
1889 let primitives = scope.into_primitives();
1890 assert!(matches!(primitives[1], DrawPrimitive::Content));
1891 assert!(matches!(
1892 primitives[2],
1893 DrawPrimitive::Blend {
1894 blend_mode: BlendMode::DstOut,
1895 ..
1896 }
1897 ));
1898 }
1899
1900 #[test]
1901 fn a_text_block_is_placed_by_its_alignment_inside_the_rect() {
1902 let rect = Rect {
1903 x: 10.0,
1904 y: 20.0,
1905 width: 100.0,
1906 height: 40.0,
1907 };
1908 let measurement = TextMeasurement {
1909 size: Size::new(60.0, 16.0),
1910 line_height: 16.0,
1911 first_baseline: 12.0,
1912 line_count: 1,
1913 };
1914 let style = |align, vertical| {
1915 DrawTextStyle::default()
1916 .with_align(align)
1917 .with_vertical_align(vertical)
1918 };
1919
1920 let left = align_text_block(
1921 rect,
1922 measurement,
1923 &style(TextAlign::Left, TextVerticalAlign::Top),
1924 );
1925 assert_eq!(left, Point::new(10.0, 20.0));
1926
1927 let centered = align_text_block(
1928 rect,
1929 measurement,
1930 &style(TextAlign::Center, TextVerticalAlign::Center),
1931 );
1932 assert_eq!(centered, Point::new(10.0 + 20.0, 20.0 + 12.0));
1933
1934 let right = align_text_block(
1935 rect,
1936 measurement,
1937 &style(TextAlign::Right, TextVerticalAlign::Bottom),
1938 );
1939 assert_eq!(right, Point::new(50.0, 44.0));
1940
1941 let baseline = align_text_block(
1942 rect,
1943 measurement,
1944 &style(TextAlign::Left, TextVerticalAlign::Baseline),
1945 );
1946 assert_eq!(baseline, Point::new(10.0, 20.0 - 12.0));
1947 }
1948
1949 #[test]
1950 fn draw_rect_blend_wraps_non_default_modes() {
1951 let mut scope = DrawScopeDefault::new(Size::new(10.0, 10.0));
1952 scope.draw_rect_blend(Brush::solid(Color::RED), BlendMode::DstOut);
1953
1954 let primitives = scope.into_primitives();
1955 assert_eq!(primitives.len(), 1);
1956 match &primitives[0] {
1957 DrawPrimitive::Blend {
1958 primitive,
1959 blend_mode,
1960 } => {
1961 assert_eq!(*blend_mode, BlendMode::DstOut);
1962 assert!(matches!(**primitive, DrawPrimitive::Rect { .. }));
1963 }
1964 other => panic!("expected blended primitive, got {other:?}"),
1965 }
1966 }
1967
1968 #[test]
1969 fn draw_circle_records_centered_round_rect() {
1970 let mut scope = DrawScopeDefault::new(Size::new(40.0, 40.0));
1971 scope.draw_circle(Brush::solid(Color::BLUE), Point::new(12.0, 16.0), 5.0);
1972
1973 let primitives = scope.into_primitives();
1974 assert_eq!(primitives.len(), 1);
1975 match &primitives[0] {
1976 DrawPrimitive::RoundRect { rect, radii, .. } => {
1977 assert_eq!(
1978 *rect,
1979 Rect {
1980 x: 7.0,
1981 y: 11.0,
1982 width: 10.0,
1983 height: 10.0,
1984 }
1985 );
1986 assert_eq!(*radii, CornerRadii::uniform(5.0));
1987 }
1988 other => panic!("expected circular round rect, got {other:?}"),
1989 }
1990 }
1991
1992 #[test]
1993 fn draw_circle_blend_wraps_non_default_modes() {
1994 let mut scope = DrawScopeDefault::new(Size::new(10.0, 10.0));
1995 scope.draw_circle_blend(
1996 Brush::solid(Color::RED),
1997 Point::new(5.0, 5.0),
1998 3.0,
1999 BlendMode::Plus,
2000 );
2001
2002 let primitives = scope.into_primitives();
2003 assert_eq!(primitives.len(), 1);
2004 match &primitives[0] {
2005 DrawPrimitive::Blend {
2006 primitive,
2007 blend_mode,
2008 } => {
2009 assert_eq!(*blend_mode, BlendMode::Plus);
2010 assert!(matches!(**primitive, DrawPrimitive::RoundRect { .. }));
2011 }
2012 other => panic!("expected blended circle primitive, got {other:?}"),
2013 }
2014 }
2015
2016 #[test]
2017 fn rect_is_empty_without_area() {
2018 assert!(Rect::EMPTY.is_empty());
2019 assert!(
2020 Rect {
2021 x: 4.0,
2022 y: 5.0,
2023 width: 0.0,
2024 height: 6.0,
2025 }
2026 .is_empty()
2027 );
2028 assert!(
2029 Rect {
2030 x: 4.0,
2031 y: 5.0,
2032 width: 6.0,
2033 height: -1.0,
2034 }
2035 .is_empty()
2036 );
2037 assert!(
2038 !Rect {
2039 x: 4.0,
2040 y: 5.0,
2041 width: 0.5,
2042 height: 0.5,
2043 }
2044 .is_empty()
2045 );
2046 }
2047
2048 #[test]
2049 fn rect_union_encloses_both_inputs() {
2050 let lhs = Rect {
2051 x: 10.0,
2052 y: 5.0,
2053 width: 8.0,
2054 height: 4.0,
2055 };
2056 let rhs = Rect {
2057 x: 4.0,
2058 y: 7.0,
2059 width: 10.0,
2060 height: 6.0,
2061 };
2062
2063 assert_eq!(
2064 lhs.union(rhs),
2065 Rect {
2066 x: 4.0,
2067 y: 5.0,
2068 width: 14.0,
2069 height: 8.0,
2070 }
2071 );
2072 }
2073
2074 #[test]
2075 fn draw_image_uses_scope_size_as_default_rect() {
2076 let mut scope = DrawScopeDefault::new(Size::new(40.0, 24.0));
2077 let image = ImageBitmap::from_rgba8(2, 2, vec![255; 16]).expect("image");
2078 scope.draw_image(image.clone());
2079 let primitives = scope.into_primitives();
2080 assert_eq!(primitives.len(), 1);
2081 match unwrap_image(&primitives[0]) {
2082 DrawPrimitive::Image {
2083 rect,
2084 image: actual,
2085 alpha,
2086 color_filter,
2087 sampling,
2088 src_rect,
2089 } => {
2090 assert_eq!(*rect, Rect::from_size(Size::new(40.0, 24.0)));
2091 assert_eq!(*actual, image);
2092 assert_eq!(*alpha, 1.0);
2093 assert!(color_filter.is_none());
2094 assert_eq!(*sampling, ImageSampling::Nearest);
2095 assert!(src_rect.is_none());
2096 }
2097 other => panic!("expected image primitive, got {other:?}"),
2098 }
2099 }
2100
2101 #[test]
2102 fn draw_image_src_stores_src_rect() {
2103 let mut scope = DrawScopeDefault::new(Size::new(100.0, 100.0));
2104 let image = ImageBitmap::from_rgba8(64, 64, vec![255; 64 * 64 * 4]).expect("image");
2105 let src = Rect {
2106 x: 10.0,
2107 y: 20.0,
2108 width: 30.0,
2109 height: 40.0,
2110 };
2111 let dst = Rect {
2112 x: 0.0,
2113 y: 0.0,
2114 width: 60.0,
2115 height: 80.0,
2116 };
2117 scope.draw_image_src(image.clone(), src, dst, 0.8, None);
2118 let primitives = scope.into_primitives();
2119 assert_eq!(primitives.len(), 1);
2120 match unwrap_image(&primitives[0]) {
2121 DrawPrimitive::Image {
2122 rect,
2123 image: actual,
2124 alpha,
2125 sampling,
2126 src_rect,
2127 ..
2128 } => {
2129 assert_eq!(*rect, dst);
2130 assert_eq!(*actual, image);
2131 assert!((alpha - 0.8).abs() < 1e-5);
2132 assert_eq!(*sampling, ImageSampling::Nearest);
2133 assert_eq!(*src_rect, Some(src));
2134 }
2135 other => panic!("expected image primitive, got {other:?}"),
2136 }
2137 }
2138
2139 #[test]
2140 fn draw_image_at_sampled_records_requested_sampling() {
2141 let mut scope = DrawScopeDefault::new(Size::new(100.0, 100.0));
2142 let image = ImageBitmap::from_rgba8(8, 8, vec![255; 8 * 8 * 4]).expect("image");
2143 let dst = Rect {
2144 x: 2.0,
2145 y: 3.0,
2146 width: 40.0,
2147 height: 30.0,
2148 };
2149
2150 scope.draw_image_at_sampled(dst, image.clone(), 0.7, None, ImageSampling::Linear);
2151
2152 let primitives = scope.into_primitives();
2153 assert_eq!(primitives.len(), 1);
2154 match unwrap_image(&primitives[0]) {
2155 DrawPrimitive::Image {
2156 rect,
2157 image: actual,
2158 alpha,
2159 sampling,
2160 src_rect,
2161 ..
2162 } => {
2163 assert_eq!(*rect, dst);
2164 assert_eq!(*actual, image);
2165 assert!((alpha - 0.7).abs() < 1e-5);
2166 assert_eq!(*sampling, ImageSampling::Linear);
2167 assert!(src_rect.is_none());
2168 }
2169 other => panic!("expected image primitive, got {other:?}"),
2170 }
2171 }
2172
2173 #[test]
2174 fn draw_image_src_sampled_records_requested_sampling() {
2175 let mut scope = DrawScopeDefault::new(Size::new(100.0, 100.0));
2176 let image = ImageBitmap::from_rgba8(64, 64, vec![255; 64 * 64 * 4]).expect("image");
2177 let src = Rect {
2178 x: 4.0,
2179 y: 6.0,
2180 width: 16.0,
2181 height: 20.0,
2182 };
2183 let dst = Rect {
2184 x: 8.0,
2185 y: 10.0,
2186 width: 32.0,
2187 height: 40.0,
2188 };
2189
2190 scope.draw_image_src_sampled(image.clone(), src, dst, 0.5, None, ImageSampling::Linear);
2191
2192 let primitives = scope.into_primitives();
2193 assert_eq!(primitives.len(), 1);
2194 match unwrap_image(&primitives[0]) {
2195 DrawPrimitive::Image {
2196 rect,
2197 image: actual,
2198 alpha,
2199 sampling,
2200 src_rect,
2201 ..
2202 } => {
2203 assert_eq!(*rect, dst);
2204 assert_eq!(*actual, image);
2205 assert!((alpha - 0.5).abs() < 1e-5);
2206 assert_eq!(*sampling, ImageSampling::Linear);
2207 assert_eq!(*src_rect, Some(src));
2208 }
2209 other => panic!("expected image primitive, got {other:?}"),
2210 }
2211 }
2212
2213 #[test]
2214 fn draw_image_at_clamps_alpha() {
2215 let mut scope = DrawScopeDefault::new(Size::new(10.0, 10.0));
2216 let image = ImageBitmap::from_rgba8(1, 1, vec![255, 255, 255, 255]).expect("image");
2217 scope.draw_image_at(
2218 Rect::from_origin_size(Point::new(2.0, 3.0), Size::new(5.0, 6.0)),
2219 image,
2220 3.0,
2221 Some(ColorFilter::Tint(Color::from_rgba_u8(128, 128, 255, 255))),
2222 );
2223 assert_image_alpha(&scope.into_primitives()[0], 1.0);
2224 }
2225
2226 #[test]
2227 fn graphics_layer_clone_with_render_effect() {
2228 let layer = GraphicsLayer {
2229 render_effect: Some(RenderEffect::blur(10.0)),
2230 backdrop_effect: Some(RenderEffect::blur(6.0)),
2231 color_filter: Some(ColorFilter::tint(Color::from_rgba_u8(128, 200, 255, 255))),
2232 alpha: 0.5,
2233 rotation_z: 12.0,
2234 shadow_elevation: 4.0,
2235 shape: LayerShape::Rounded(RoundedCornerShape::uniform(6.0)),
2236 clip: true,
2237 compositing_strategy: CompositingStrategy::Offscreen,
2238 blend_mode: BlendMode::SrcOver,
2239 ..Default::default()
2240 };
2241 let cloned = layer.clone();
2242 assert_eq!(cloned.alpha, 0.5);
2243 assert!(cloned.render_effect.is_some());
2244 assert!(cloned.backdrop_effect.is_some());
2245 assert_eq!(layer.color_filter, cloned.color_filter);
2246 assert_eq!(layer.render_effect, cloned.render_effect);
2247 assert_eq!(layer.backdrop_effect, cloned.backdrop_effect);
2248 assert!((cloned.rotation_z - 12.0).abs() < 1e-6);
2249 assert!((cloned.shadow_elevation - 4.0).abs() < 1e-6);
2250 assert_eq!(
2251 cloned.shape,
2252 LayerShape::Rounded(RoundedCornerShape::uniform(6.0))
2253 );
2254 assert!(cloned.clip);
2255 assert_eq!(cloned.compositing_strategy, CompositingStrategy::Offscreen);
2256 assert_eq!(cloned.blend_mode, BlendMode::SrcOver);
2257 }
2258
2259 #[test]
2260 fn graphics_layer_default_has_no_effect() {
2261 let layer = GraphicsLayer::default();
2262 assert!(layer.color_filter.is_none());
2263 assert!(layer.render_effect.is_none());
2264 assert!(layer.backdrop_effect.is_none());
2265 assert_eq!(layer.compositing_strategy, CompositingStrategy::Auto);
2266 assert_eq!(layer.blend_mode, BlendMode::SrcOver);
2267 assert_eq!(layer.alpha, 1.0);
2268 assert_eq!(layer.transform_origin, TransformOrigin::CENTER);
2269 assert!((layer.camera_distance - 8.0).abs() < 1e-6);
2270 assert_eq!(layer.shape, LayerShape::Rectangle);
2271 assert!(!layer.clip);
2272 assert_eq!(layer.ambient_shadow_color, Color::BLACK);
2273 assert_eq!(layer.spot_shadow_color, Color::BLACK);
2274 }
2275
2276 #[test]
2277 fn transform_origin_construction() {
2278 let origin = TransformOrigin::new(0.25, 0.75);
2279 assert!((origin.pivot_fraction_x - 0.25).abs() < 1e-6);
2280 assert!((origin.pivot_fraction_y - 0.75).abs() < 1e-6);
2281 }
2282
2283 #[test]
2284 fn layer_shape_default_is_rectangle() {
2285 assert_eq!(LayerShape::default(), LayerShape::Rectangle);
2286 }
2287
2288 use std::f32::consts::{FRAC_PI_2, PI};
2289
2290 use crate::{StrokeCap, StrokeJoin};
2291
2292 fn approx(a: f32, b: f32) -> bool {
2293 (a - b).abs() < 0.25
2294 }
2295
2296 fn scope(size: f32) -> DrawScopeDefault {
2297 DrawScopeDefault::new(Size::new(size, size))
2298 }
2299
2300 #[test]
2301 fn draw_rect_stroked_records_scope_rect_and_stroke() {
2302 let mut scope = scope(20.0);
2303 scope.draw_rect_stroked(
2304 Brush::solid(Color::RED),
2305 Stroke::new(3.0).with_join(StrokeJoin::Bevel),
2306 );
2307
2308 let primitives = scope.into_primitives();
2309 assert_eq!(primitives.len(), 1);
2310 match &primitives[0] {
2311 DrawPrimitive::Rect {
2312 rect,
2313 stroke: Some(stroke),
2314 ..
2315 } => {
2316 assert_eq!(*rect, Rect::from_size(Size::new(20.0, 20.0)));
2317 assert_eq!(stroke.width, 3.0);
2318 assert_eq!(stroke.join, StrokeJoin::Bevel);
2319 }
2320 other => panic!("expected stroked rect, got {other:?}"),
2321 }
2322 }
2323
2324 #[test]
2325 fn draw_rect_at_stroked_records_requested_rect() {
2326 let mut scope = scope(50.0);
2327 let rect = Rect {
2328 x: 4.0,
2329 y: 6.0,
2330 width: 12.0,
2331 height: 9.0,
2332 };
2333 scope.draw_rect_at_stroked(rect, Brush::solid(Color::BLUE), Stroke::new(2.0));
2334 match &scope.into_primitives()[0] {
2335 DrawPrimitive::Rect {
2336 rect: actual,
2337 stroke: Some(stroke),
2338 ..
2339 } => {
2340 assert_eq!(*actual, rect);
2341 assert_eq!(stroke.width, 2.0);
2342 }
2343 other => panic!("expected stroked rect, got {other:?}"),
2344 }
2345 }
2346
2347 #[test]
2348 fn draw_round_rect_stroked_keeps_radii_and_stroke() {
2349 let mut scope = scope(30.0);
2350 scope.draw_round_rect_stroked(
2351 Brush::solid(Color::GREEN),
2352 CornerRadii::uniform(5.0),
2353 Stroke::new(4.0).with_join(StrokeJoin::Round),
2354 );
2355 match &scope.into_primitives()[0] {
2356 DrawPrimitive::RoundRect {
2357 rect,
2358 radii,
2359 stroke: Some(stroke),
2360 ..
2361 } => {
2362 assert_eq!(*rect, Rect::from_size(Size::new(30.0, 30.0)));
2363 assert_eq!(*radii, CornerRadii::uniform(5.0));
2364 assert_eq!(stroke.width, 4.0);
2365 assert_eq!(stroke.join, StrokeJoin::Round);
2366 }
2367 other => panic!("expected stroked round rect, got {other:?}"),
2368 }
2369 }
2370
2371 #[test]
2372 fn draw_round_rect_at_stroked_records_requested_rect() {
2373 let mut scope = scope(60.0);
2374 let rect = Rect {
2375 x: 1.0,
2376 y: 2.0,
2377 width: 20.0,
2378 height: 10.0,
2379 };
2380 scope.draw_round_rect_at_stroked(
2381 rect,
2382 Brush::solid(Color::WHITE),
2383 CornerRadii::uniform(3.0),
2384 Stroke::new(1.5),
2385 );
2386 match &scope.into_primitives()[0] {
2387 DrawPrimitive::RoundRect {
2388 rect: actual,
2389 radii,
2390 stroke: Some(stroke),
2391 ..
2392 } => {
2393 assert_eq!(*actual, rect);
2394 assert_eq!(*radii, CornerRadii::uniform(3.0));
2395 assert_eq!(stroke.width, 1.5);
2396 }
2397 other => panic!("expected stroked round rect, got {other:?}"),
2398 }
2399 }
2400
2401 #[test]
2402 fn draw_circle_stroked_lowers_to_stroked_round_rect() {
2403 let mut scope = scope(40.0);
2404 scope.draw_circle_stroked(
2405 Brush::solid(Color::BLUE),
2406 Point::new(12.0, 16.0),
2407 5.0,
2408 Stroke::new(2.0),
2409 );
2410 match &scope.into_primitives()[0] {
2411 DrawPrimitive::RoundRect {
2412 rect,
2413 radii,
2414 stroke: Some(stroke),
2415 ..
2416 } => {
2417 assert_eq!(
2418 *rect,
2419 Rect {
2420 x: 7.0,
2421 y: 11.0,
2422 width: 10.0,
2423 height: 10.0,
2424 }
2425 );
2426 assert_eq!(*radii, CornerRadii::uniform(5.0));
2427 assert_eq!(stroke.width, 2.0);
2428 }
2429 other => panic!("expected stroked circular round rect, got {other:?}"),
2430 }
2431 }
2432
2433 #[test]
2434 fn draw_arc_records_arc_primitive_with_tight_bounds() {
2435 let mut scope = scope(200.0);
2436 scope.draw_arc(
2437 Brush::solid(Color::RED),
2438 Point::new(100.0, 100.0),
2439 50.0,
2440 0.0,
2441 FRAC_PI_2,
2442 Stroke::new(10.0),
2443 );
2444 let primitives = scope.into_primitives();
2445 assert_eq!(primitives.len(), 1);
2446 match &primitives[0] {
2447 DrawPrimitive::Arc {
2448 rect,
2449 center,
2450 radius,
2451 start_angle,
2452 sweep_angle,
2453 stroke: Some(stroke),
2454 inner_radius,
2455 ..
2456 } => {
2457 assert_eq!(*center, Point::new(100.0, 100.0));
2458 assert_eq!(*radius, 50.0);
2459 assert_eq!(*start_angle, 0.0);
2460 assert!(approx(*sweep_angle, FRAC_PI_2));
2461 assert_eq!(stroke.width, 10.0);
2462 assert_eq!(*inner_radius, 0.0);
2463 assert!(approx(rect.x, 100.0), "{rect:?}");
2464 assert!(approx(rect.y, 100.0), "{rect:?}");
2465 assert!(approx(rect.width, 55.0), "{rect:?}");
2466 assert!(approx(rect.height, 55.0), "{rect:?}");
2467 }
2468 other => panic!("expected arc primitive, got {other:?}"),
2469 }
2470 }
2471
2472 #[test]
2473 fn draw_arc_bounds_cover_a_quadrant_spanning_sweep() {
2474 let mut scope = scope(200.0);
2475 scope.draw_arc(
2476 Brush::solid(Color::RED),
2477 Point::new(100.0, 100.0),
2478 50.0,
2479 0.0,
2480 3.0 * FRAC_PI_2,
2481 Stroke::new(4.0),
2482 );
2483 let DrawPrimitive::Arc { rect, .. } = &scope.into_primitives()[0] else {
2484 panic!("expected arc primitive");
2485 };
2486 assert!(approx(rect.x, 48.0), "{rect:?}");
2487 assert!(approx(rect.y, 48.0), "{rect:?}");
2488 assert!(approx(rect.width, 104.0), "{rect:?}");
2489 assert!(approx(rect.height, 104.0), "{rect:?}");
2490 }
2491
2492 #[test]
2493 fn draw_annular_sector_records_inner_radius_and_no_stroke() {
2494 let mut scope = scope(200.0);
2495 scope.draw_annular_sector(
2496 Brush::solid(Color::WHITE),
2497 Point::new(100.0, 100.0),
2498 30.0,
2499 50.0,
2500 0.0,
2501 PI,
2502 );
2503 match &scope.into_primitives()[0] {
2504 DrawPrimitive::Arc {
2505 rect,
2506 center,
2507 radius,
2508 inner_radius,
2509 stroke,
2510 sweep_angle,
2511 ..
2512 } => {
2513 assert!(stroke.is_none(), "annular sectors are filled, not stroked");
2514 assert_eq!(*center, Point::new(100.0, 100.0));
2515 assert_eq!(*radius, 50.0);
2516 assert_eq!(*inner_radius, 30.0);
2517 assert!(approx(*sweep_angle, PI));
2518 assert!(approx(rect.x, 50.0), "{rect:?}");
2519 assert!(approx(rect.y, 100.0), "{rect:?}");
2520 assert!(approx(rect.width, 100.0), "{rect:?}");
2521 assert!(approx(rect.height, 50.0), "{rect:?}");
2522 }
2523 other => panic!("expected arc primitive, got {other:?}"),
2524 }
2525 }
2526
2527 #[test]
2528 fn draw_arc_blend_wraps_non_default_modes() {
2529 let mut scope = scope(100.0);
2530 scope.draw_arc_blend(
2531 Brush::solid(Color::RED),
2532 Point::new(50.0, 50.0),
2533 20.0,
2534 0.0,
2535 1.0,
2536 Stroke::new(2.0),
2537 BlendMode::DstOut,
2538 );
2539 match &scope.into_primitives()[0] {
2540 DrawPrimitive::Blend {
2541 primitive,
2542 blend_mode,
2543 } => {
2544 assert_eq!(*blend_mode, BlendMode::DstOut);
2545 assert!(matches!(**primitive, DrawPrimitive::Arc { .. }));
2546 }
2547 other => panic!("expected blended arc, got {other:?}"),
2548 }
2549 }
2550
2551 #[test]
2552 fn draw_annular_sector_blend_wraps_non_default_modes() {
2553 let mut scope = scope(100.0);
2554 scope.draw_annular_sector_blend(
2555 Brush::solid(Color::RED),
2556 Point::new(50.0, 50.0),
2557 5.0,
2558 20.0,
2559 0.0,
2560 1.0,
2561 BlendMode::Plus,
2562 );
2563 assert!(matches!(
2564 &scope.into_primitives()[0],
2565 DrawPrimitive::Blend {
2566 blend_mode: BlendMode::Plus,
2567 ..
2568 }
2569 ));
2570 }
2571
2572 #[test]
2573 fn stroked_blend_variants_wrap_non_default_modes() {
2574 let mut scope = scope(20.0);
2575 scope.draw_rect_stroked_blend(
2576 Brush::solid(Color::RED),
2577 Stroke::new(2.0),
2578 BlendMode::DstOut,
2579 );
2580 scope.draw_round_rect_stroked_blend(
2581 Brush::solid(Color::RED),
2582 CornerRadii::uniform(2.0),
2583 Stroke::new(2.0),
2584 BlendMode::DstOut,
2585 );
2586 scope.draw_circle_stroked_blend(
2587 Brush::solid(Color::RED),
2588 Point::new(10.0, 10.0),
2589 5.0,
2590 Stroke::new(2.0),
2591 BlendMode::DstOut,
2592 );
2593 let primitives = scope.into_primitives();
2594 assert_eq!(primitives.len(), 3);
2595 for primitive in &primitives {
2596 assert!(
2597 matches!(
2598 primitive,
2599 DrawPrimitive::Blend {
2600 blend_mode: BlendMode::DstOut,
2601 ..
2602 }
2603 ),
2604 "expected blended primitive, got {primitive:?}"
2605 );
2606 }
2607 }
2608
2609 #[test]
2610 fn negative_sweeps_and_overlong_sweeps_produce_finite_bounds() {
2611 let mut scope = scope(200.0);
2612 scope.draw_arc(
2613 Brush::solid(Color::RED),
2614 Point::new(100.0, 100.0),
2615 40.0,
2616 FRAC_PI_2,
2617 -FRAC_PI_2,
2618 Stroke::new(4.0),
2619 );
2620 scope.draw_arc(
2621 Brush::solid(Color::RED),
2622 Point::new(100.0, 100.0),
2623 40.0,
2624 0.3,
2625 crate::stroke::TAU * 4.0,
2626 Stroke::new(4.0),
2627 );
2628 let primitives = scope.into_primitives();
2629 assert_eq!(primitives.len(), 2);
2630
2631 let DrawPrimitive::Arc { rect: negative, .. } = &primitives[0] else {
2632 panic!("expected arc");
2633 };
2634 assert!(approx(negative.x, 100.0), "{negative:?}");
2635 assert!(approx(negative.y, 100.0), "{negative:?}");
2636 assert!(approx(negative.width, 42.0), "{negative:?}");
2637
2638 let DrawPrimitive::Arc { rect: full, .. } = &primitives[1] else {
2639 panic!("expected arc");
2640 };
2641 assert!(approx(full.x, 58.0), "{full:?}");
2642 assert!(approx(full.width, 84.0), "{full:?}");
2643 assert!(approx(full.height, 84.0), "{full:?}");
2644 }
2645
2646 #[test]
2647 fn degenerate_stroke_and_arc_inputs_emit_nothing_and_never_panic() {
2648 let mut scope = scope(50.0);
2649 let brush = Brush::solid(Color::RED);
2650 let center = Point::new(25.0, 25.0);
2651
2652 scope.draw_rect_stroked(brush.clone(), Stroke::new(0.0));
2653 scope.draw_rect_stroked(brush.clone(), Stroke::new(-4.0));
2654 scope.draw_rect_stroked(brush.clone(), Stroke::new(f32::NAN));
2655 scope.draw_round_rect_stroked(brush.clone(), CornerRadii::uniform(2.0), Stroke::new(0.0));
2656 scope.draw_circle_stroked(brush.clone(), center, 10.0, Stroke::new(0.0));
2657 scope.draw_circle_stroked(brush.clone(), center, f32::NAN, Stroke::new(2.0));
2658 scope.draw_arc(brush.clone(), center, 10.0, 0.0, 0.0, Stroke::new(2.0));
2659 scope.draw_arc(brush.clone(), center, 10.0, 0.0, f32::NAN, Stroke::new(2.0));
2660 scope.draw_arc(
2661 brush.clone(),
2662 center,
2663 f32::INFINITY,
2664 0.0,
2665 1.0,
2666 Stroke::new(2.0),
2667 );
2668 scope.draw_arc(brush.clone(), center, 10.0, 0.0, 1.0, Stroke::new(0.0));
2669 scope.draw_arc(brush.clone(), center, 0.0, 0.0, 1.0, Stroke::new(0.0));
2670 scope.draw_annular_sector(brush.clone(), center, 10.0, 10.0, 0.0, 1.0);
2671 scope.draw_annular_sector(brush.clone(), center, 20.0, 10.0, 0.0, 1.0);
2672 scope.draw_annular_sector(brush.clone(), center, 0.0, 0.0, 0.0, 1.0);
2673 scope.draw_annular_sector(brush.clone(), center, 0.0, 10.0, 0.0, 0.0);
2674 scope.draw_annular_sector(brush, center, f32::NAN, 10.0, 0.0, 1.0);
2675
2676 assert!(
2677 scope.into_primitives().is_empty(),
2678 "degenerate stroke/arc requests must not reach the renderer"
2679 );
2680 }
2681
2682 #[test]
2683 fn zero_radius_arc_with_positive_width_stays_finite() {
2684 let mut scope = scope(50.0);
2685 scope.draw_arc(
2686 Brush::solid(Color::RED),
2687 Point::new(25.0, 25.0),
2688 0.0,
2689 0.0,
2690 FRAC_PI_2,
2691 Stroke::new(6.0).with_cap(StrokeCap::Round),
2692 );
2693 let primitives = scope.into_primitives();
2694 assert_eq!(primitives.len(), 1);
2695 let DrawPrimitive::Arc { rect, .. } = &primitives[0] else {
2696 panic!("expected arc");
2697 };
2698 for value in [rect.x, rect.y, rect.width, rect.height] {
2699 assert!(value.is_finite(), "{rect:?}");
2700 }
2701 assert!(rect.width > 0.0 && rect.height > 0.0, "{rect:?}");
2702 }
2703
2704 struct FixedAdvanceTextMeasurer {
2705 advance: f32,
2706 line_height: f32,
2707 calls: std::cell::Cell<usize>,
2708 }
2709
2710 impl FixedAdvanceTextMeasurer {
2711 fn shared(advance: f32, line_height: f32) -> Rc<Self> {
2712 Rc::new(Self {
2713 advance,
2714 line_height,
2715 calls: std::cell::Cell::new(0),
2716 })
2717 }
2718 }
2719
2720 impl DrawTextMeasurer for FixedAdvanceTextMeasurer {
2721 fn measure_text(&self, text: &str, _style: &DrawTextStyle) -> TextMeasurement {
2722 self.calls.set(self.calls.get() + 1);
2723 let lines: Vec<&str> = text.split('\n').collect();
2724 let width = lines
2725 .iter()
2726 .map(|line| line.chars().count() as f32 * self.advance)
2727 .fold(0.0_f32, f32::max);
2728 TextMeasurement {
2729 size: Size::new(width, lines.len() as f32 * self.line_height),
2730 line_height: self.line_height,
2731 first_baseline: self.line_height * 0.75,
2732 line_count: lines.len(),
2733 }
2734 }
2735 }
2736
2737 fn text_scope(size: Size) -> (DrawScopeDefault, Rc<FixedAdvanceTextMeasurer>) {
2738 let measurer = FixedAdvanceTextMeasurer::shared(10.0, 20.0);
2739 (
2740 DrawScopeDefault::with_text_measurer(size, measurer.clone()),
2741 measurer,
2742 )
2743 }
2744
2745 fn unwrap_text(primitive: &DrawPrimitive) -> &TextPrimitive {
2746 match primitive {
2747 DrawPrimitive::Text(text) => text,
2748 other => panic!("expected text primitive, got {other:?}"),
2749 }
2750 }
2751
2752 #[test]
2753 fn drawn_text_occupies_exactly_the_box_measure_text_reported() {
2754 let (mut scope, _) = text_scope(Size::new(200.0, 100.0));
2755 let style = DrawTextStyle::new(16.0);
2756 let measured = scope.measure_text("ABCD", &style);
2757
2758 scope.draw_text_from(
2759 Point::new(7.0, 11.0),
2760 Brush::solid(Color::WHITE),
2761 "ABCD",
2762 &style,
2763 );
2764
2765 let primitives = scope.into_primitives();
2766 assert_eq!(primitives.len(), 1);
2767 let text = unwrap_text(&primitives[0]);
2768 assert_eq!(
2769 text.rect,
2770 Rect {
2771 x: 7.0,
2772 y: 11.0,
2773 width: measured.size.width,
2774 height: measured.size.height,
2775 },
2776 "the drawn block must be the measured block, or callers cannot center text"
2777 );
2778 assert_eq!(&*text.text, "ABCD");
2779 assert_eq!(text.color, Color::WHITE);
2780 }
2781
2782 #[test]
2783 fn text_alignment_positions_the_measured_block_inside_the_box() {
2784 let box_rect = Rect {
2785 x: 100.0,
2786 y: 50.0,
2787 width: 200.0,
2788 height: 80.0,
2789 };
2790 let cases = [
2791 (TextAlign::Left, TextVerticalAlign::Top, 100.0, 50.0),
2792 (TextAlign::Center, TextVerticalAlign::Center, 190.0, 80.0),
2793 (TextAlign::Right, TextVerticalAlign::Bottom, 280.0, 110.0),
2794 ];
2795 for (align, vertical_align, expected_x, expected_y) in cases {
2796 let (mut scope, _) = text_scope(Size::new(400.0, 400.0));
2797 let style = DrawTextStyle::new(16.0)
2798 .with_align(align)
2799 .with_vertical_align(vertical_align);
2800 scope.draw_text_at(box_rect, Brush::solid(Color::WHITE), "AB", &style);
2801 let primitives = scope.into_primitives();
2802 let text = unwrap_text(&primitives[0]);
2803 assert!(
2804 approx(text.rect.x, expected_x) && approx(text.rect.y, expected_y),
2805 "{align:?}/{vertical_align:?} placed the block at {:?}",
2806 text.rect
2807 );
2808 assert!(approx(text.rect.width, 20.0) && approx(text.rect.height, 20.0));
2809 }
2810 }
2811
2812 #[test]
2813 fn baseline_aligned_text_hangs_above_the_box_edge() {
2814 let (mut scope, _) = text_scope(Size::new(200.0, 200.0));
2815 let style = DrawTextStyle::new(16.0).with_vertical_align(TextVerticalAlign::Baseline);
2816 let measured = scope.measure_text("Ag", &style);
2817 scope.draw_text_at(
2818 Rect {
2819 x: 0.0,
2820 y: 100.0,
2821 width: 200.0,
2822 height: 0.0,
2823 },
2824 Brush::solid(Color::WHITE),
2825 "Ag",
2826 &style,
2827 );
2828 let primitives = scope.into_primitives();
2829 let text = unwrap_text(&primitives[0]);
2830 assert!(
2831 approx(text.rect.y, 100.0 - measured.first_baseline),
2832 "{:?}",
2833 text.rect
2834 );
2835 }
2836
2837 #[test]
2838 fn draw_text_fills_the_whole_scope_rect() {
2839 let (mut scope, _) = text_scope(Size::new(120.0, 60.0));
2840 let style = DrawTextStyle::new(16.0)
2841 .with_align(TextAlign::Right)
2842 .with_vertical_align(TextVerticalAlign::Bottom);
2843 scope.draw_text(Brush::solid(Color::WHITE), "AB", &style);
2844 let primitives = scope.into_primitives();
2845 let text = unwrap_text(&primitives[0]);
2846 assert!(
2847 approx(text.rect.x, 100.0) && approx(text.rect.y, 40.0),
2848 "{:?}",
2849 text.rect
2850 );
2851 }
2852
2853 #[test]
2854 fn draw_text_from_ignores_alignment_and_anchors_the_top_left() {
2855 let (mut scope, _) = text_scope(Size::new(400.0, 400.0));
2856 let style = DrawTextStyle::new(16.0)
2857 .with_align(TextAlign::Center)
2858 .with_vertical_align(TextVerticalAlign::Bottom);
2859 scope.draw_text_from(
2860 Point::new(30.0, 40.0),
2861 Brush::solid(Color::WHITE),
2862 "AB",
2863 &style,
2864 );
2865 let primitives = scope.into_primitives();
2866 let text = unwrap_text(&primitives[0]);
2867 assert!(
2868 approx(text.rect.x, 30.0) && approx(text.rect.y, 40.0),
2869 "{:?}",
2870 text.rect
2871 );
2872 }
2873
2874 #[test]
2875 fn multiline_text_measures_the_widest_line_and_stacks_the_lines() {
2876 let (mut scope, _) = text_scope(Size::new(400.0, 400.0));
2877 let style = DrawTextStyle::new(16.0);
2878 scope.draw_text_from(Point::ZERO, Brush::solid(Color::WHITE), "AB\nABCDE", &style);
2879 let primitives = scope.into_primitives();
2880 let text = unwrap_text(&primitives[0]);
2881 assert!(approx(text.rect.width, 50.0), "{:?}", text.rect);
2882 assert!(approx(text.rect.height, 40.0), "{:?}", text.rect);
2883 }
2884
2885 #[test]
2886 fn empty_text_draws_nothing_and_never_measures() {
2887 let (mut scope, measurer) = text_scope(Size::new(100.0, 100.0));
2888 scope.draw_text(Brush::solid(Color::WHITE), "", &DrawTextStyle::new(16.0));
2889 scope.draw_text_at(
2890 Rect::from_size(Size::new(10.0, 10.0)),
2891 Brush::solid(Color::WHITE),
2892 "",
2893 &DrawTextStyle::new(16.0),
2894 );
2895 scope.draw_text_from(
2896 Point::ZERO,
2897 Brush::solid(Color::WHITE),
2898 "",
2899 &DrawTextStyle::new(16.0),
2900 );
2901 assert!(scope.into_primitives().is_empty());
2902 assert_eq!(
2903 measurer.calls.get(),
2904 0,
2905 "an empty string must not cost a measurement"
2906 );
2907 }
2908
2909 #[test]
2910 fn invisible_text_draws_nothing() {
2911 let (mut scope, _) = text_scope(Size::new(100.0, 100.0));
2912 let style = DrawTextStyle::new(16.0);
2913 scope.draw_text(Brush::solid(Color(1.0, 1.0, 1.0, 0.0)), "AB", &style);
2914 scope.draw_text(
2915 Brush::LinearGradient {
2916 colors: Vec::new(),
2917 stops: None,
2918 start: Point::ZERO,
2919 end: Point::new(1.0, 1.0),
2920 tile_mode: crate::render_effect::TileMode::Clamp,
2921 },
2922 "AB",
2923 &style,
2924 );
2925 assert!(scope.into_primitives().is_empty());
2926 }
2927
2928 #[test]
2929 fn gradient_text_brushes_fall_back_to_their_first_stop() {
2930 let (mut scope, _) = text_scope(Size::new(100.0, 100.0));
2931 scope.draw_text(
2932 Brush::linear_gradient(vec![Color::RED, Color::BLUE]),
2933 "AB",
2934 &DrawTextStyle::new(16.0),
2935 );
2936 let primitives = scope.into_primitives();
2937 assert_eq!(unwrap_text(&primitives[0]).color, Color::RED);
2938 }
2939
2940 #[test]
2941 fn a_scope_without_a_measurer_falls_back_to_the_font_free_estimate() {
2942 let mut scope = DrawScopeDefault::new(Size::new(100.0, 100.0));
2943 let style = DrawTextStyle::new(16.0);
2944 assert_eq!(
2945 scope.measure_text("ABC", &style),
2946 crate::estimate_text_measurement("ABC", &style)
2947 );
2948 scope.draw_text_from(Point::ZERO, Brush::solid(Color::WHITE), "ABC", &style);
2949 let primitives = scope.into_primitives();
2950 let text = unwrap_text(&primitives[0]);
2951 assert!(text.rect.width > 0.0 && text.rect.height > 0.0);
2952 }
2953
2954 #[test]
2955 fn degenerate_text_geometry_emits_nothing_and_never_panics() {
2956 struct DegenerateTextMeasurer;
2957 impl DrawTextMeasurer for DegenerateTextMeasurer {
2958 fn measure_text(&self, _text: &str, _style: &DrawTextStyle) -> TextMeasurement {
2959 TextMeasurement {
2960 size: Size::new(f32::NAN, 0.0),
2961 line_height: f32::NAN,
2962 first_baseline: f32::NAN,
2963 line_count: 1,
2964 }
2965 }
2966 }
2967
2968 let mut scope = DrawScopeDefault::with_text_measurer(
2969 Size::new(50.0, 50.0),
2970 Rc::new(DegenerateTextMeasurer),
2971 );
2972 scope.draw_text(Brush::solid(Color::WHITE), "AB", &DrawTextStyle::new(16.0));
2973 scope.draw_text_at(
2974 Rect {
2975 x: f32::NAN,
2976 y: 0.0,
2977 width: 10.0,
2978 height: 10.0,
2979 },
2980 Brush::solid(Color::WHITE),
2981 "AB",
2982 &DrawTextStyle::new(16.0),
2983 );
2984 assert!(
2985 scope.into_primitives().is_empty(),
2986 "unmeasurable text must not reach the renderer"
2987 );
2988 }
2989
2990 #[test]
2991 fn text_style_survives_lowering_into_the_primitive() {
2992 let (mut scope, _) = text_scope(Size::new(100.0, 100.0));
2993 let style = DrawTextStyle::new(21.0)
2994 .with_font_family("Fira Sans")
2995 .with_weight(FontWeight::BOLD)
2996 .with_style(FontStyle::Italic)
2997 .with_letter_spacing(2.0)
2998 .with_line_height(26.0);
2999 scope.draw_text(Brush::solid(Color::WHITE), "AB", &style);
3000 let primitives = scope.into_primitives();
3001 assert_eq!(unwrap_text(&primitives[0]).style, style);
3002 }
3003
3004 #[test]
3005 fn a_layers_composite_alpha_is_a_truncated_byte() {
3006 for byte in 0..=255u32 {
3007 let exact = byte as f32 / 255.0;
3008 assert!(
3009 (GraphicsLayer::composite_alpha_8bit(exact) - exact).abs() < 1e-6,
3010 "byte {byte} moved"
3011 );
3012 if byte < 255 {
3013 let nearly_next = (byte as f32 + 0.999) / 255.0;
3014 assert!(
3015 (GraphicsLayer::composite_alpha_8bit(nearly_next) - exact).abs() < 1e-6,
3016 "byte {byte} + 0.999 did not truncate"
3017 );
3018 }
3019 }
3020 assert_eq!(GraphicsLayer::composite_alpha_8bit(1.0), 1.0);
3021 assert_eq!(GraphicsLayer::composite_alpha_8bit(0.0), 0.0);
3022 assert_eq!(GraphicsLayer::composite_alpha_8bit(-3.0), 0.0);
3023 assert_eq!(GraphicsLayer::composite_alpha_8bit(7.0), 1.0);
3024 }
3025}