1#![allow(non_snake_case)]
4#![allow(clippy::too_many_arguments)] use crate::composable;
7use crate::layout::core::{Alignment, Measurable};
8use crate::modifier::{Modifier, Rect, Size};
9use crate::nine_patch::{nine_patch_quads, tile_quads, NinePatchInsets, PatchFill, PatchQuad};
10use crate::widgets::Layout;
11use cranpose_core::NodeId;
12use cranpose_ui_graphics::{ColorFilter, DrawScope, ImageBitmap, ImageSampling};
13use cranpose_ui_layout::{Constraints, MeasurePolicy, MeasureResult, Placement};
14use std::hash::{Hash, Hasher};
15use std::sync::Arc;
16#[cfg(feature = "svg")]
17use std::sync::{Mutex, MutexGuard};
18use thiserror::Error;
19
20#[cfg(feature = "svg")]
21#[path = "image_svg.rs"]
22mod image_svg;
23
24pub const DEFAULT_ALPHA: f32 = 1.0;
25#[cfg(feature = "svg")]
26const SVG_RASTER_CACHE_LIMIT: usize = 8;
27
28#[derive(Clone, Copy, Debug, PartialEq)]
29pub enum ContentScale {
30 Fit,
31 Crop,
32 FillBounds,
33 FillWidth,
34 FillHeight,
35 Inside,
36 None,
37}
38
39impl ContentScale {
40 pub fn scaled_size(self, src_size: Size, dst_size: Size) -> Size {
41 if src_size.width <= 0.0
42 || src_size.height <= 0.0
43 || dst_size.width <= 0.0
44 || dst_size.height <= 0.0
45 {
46 return Size::ZERO;
47 }
48
49 let scale_x = dst_size.width / src_size.width;
50 let scale_y = dst_size.height / src_size.height;
51
52 let (factor_x, factor_y) = match self {
53 Self::Fit => {
54 let factor = scale_x.min(scale_y);
55 (factor, factor)
56 }
57 Self::Crop => {
58 let factor = scale_x.max(scale_y);
59 (factor, factor)
60 }
61 Self::FillBounds => (scale_x, scale_y),
62 Self::FillWidth => (scale_x, scale_x),
63 Self::FillHeight => (scale_y, scale_y),
64 Self::Inside => {
65 if src_size.width <= dst_size.width && src_size.height <= dst_size.height {
66 (1.0, 1.0)
67 } else {
68 let factor = scale_x.min(scale_y);
69 (factor, factor)
70 }
71 }
72 Self::None => (1.0, 1.0),
73 };
74
75 Size {
76 width: src_size.width * factor_x,
77 height: src_size.height * factor_y,
78 }
79 }
80}
81
82#[derive(Clone, Debug, PartialEq, Eq, Hash)]
83pub struct Painter {
84 kind: PainterKind,
85}
86
87#[derive(Clone, Debug, PartialEq, Eq, Hash)]
88enum PainterKind {
89 Bitmap(ImageBitmap),
90 BitmapRegion {
91 bitmap: ImageBitmap,
92 source: Quad,
93 sampling: ImageSampling,
94 },
95 BitmapTiled {
98 bitmap: ImageBitmap,
99 source: Quad,
100 sampling: ImageSampling,
101 },
102 NinePatch {
104 bitmap: ImageBitmap,
105 source: Quad,
106 insets: Quad,
107 center: PatchFill,
108 edges: PatchFill,
109 sampling: ImageSampling,
110 },
111 Svg(SvgPainter),
112}
113
114#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
121struct Quad {
122 a: u32,
123 b: u32,
124 c: u32,
125 d: u32,
126}
127
128impl Quad {
129 fn of(a: f32, b: f32, c: f32, d: f32) -> Self {
130 Self {
131 a: a.to_bits(),
132 b: b.to_bits(),
133 c: c.to_bits(),
134 d: d.to_bits(),
135 }
136 }
137
138 fn values(self) -> (f32, f32, f32, f32) {
139 (
140 f32::from_bits(self.a),
141 f32::from_bits(self.b),
142 f32::from_bits(self.c),
143 f32::from_bits(self.d),
144 )
145 }
146
147 fn from_rect(source: Rect) -> Self {
148 Self::of(source.x, source.y, source.width, source.height)
149 }
150
151 fn rect(self) -> Rect {
152 let (x, y, width, height) = self.values();
153 Rect {
154 x,
155 y,
156 width,
157 height,
158 }
159 }
160
161 fn from_insets(insets: NinePatchInsets) -> Self {
162 Self::of(insets.left, insets.top, insets.right, insets.bottom)
163 }
164
165 fn insets(self) -> NinePatchInsets {
166 let (left, top, right, bottom) = self.values();
167 NinePatchInsets::new(left, top, right, bottom)
168 }
169}
170
171impl Painter {
172 pub fn from_bitmap(bitmap: ImageBitmap) -> Self {
173 Self {
174 kind: PainterKind::Bitmap(bitmap),
175 }
176 }
177
178 pub fn from_bitmap_region(bitmap: ImageBitmap, source: Rect, sampling: ImageSampling) -> Self {
180 Self {
181 kind: PainterKind::BitmapRegion {
182 bitmap,
183 source: Quad::from_rect(source),
184 sampling,
185 },
186 }
187 }
188
189 pub fn from_bitmap_tiled(bitmap: ImageBitmap, source: Rect, sampling: ImageSampling) -> Self {
196 Self {
197 kind: PainterKind::BitmapTiled {
198 bitmap,
199 source: Quad::from_rect(source),
200 sampling,
201 },
202 }
203 }
204
205 pub fn from_nine_patch(
214 bitmap: ImageBitmap,
215 source: Rect,
216 insets: NinePatchInsets,
217 center: PatchFill,
218 edges: PatchFill,
219 sampling: ImageSampling,
220 ) -> Self {
221 Self {
222 kind: PainterKind::NinePatch {
223 bitmap,
224 source: Quad::from_rect(source),
225 insets: Quad::from_insets(insets),
226 center,
227 edges,
228 sampling,
229 },
230 }
231 }
232
233 pub fn from_svg(svg: SvgPainter) -> Self {
234 Self {
235 kind: PainterKind::Svg(svg),
236 }
237 }
238
239 pub fn intrinsic_size(&self) -> Size {
240 match &self.kind {
241 PainterKind::Bitmap(bitmap) => bitmap.intrinsic_size(),
242 PainterKind::BitmapRegion { source, .. }
243 | PainterKind::BitmapTiled { source, .. }
244 | PainterKind::NinePatch { source, .. } => {
245 let source = source.rect();
246 Size::new(source.width.max(0.0), source.height.max(0.0))
247 }
248 PainterKind::Svg(svg) => svg.intrinsic_size(),
249 }
250 }
251
252 pub fn as_bitmap(&self) -> Option<&ImageBitmap> {
253 match &self.kind {
254 PainterKind::Bitmap(bitmap) => Some(bitmap),
255 PainterKind::BitmapRegion { bitmap, .. }
256 | PainterKind::BitmapTiled { bitmap, .. }
257 | PainterKind::NinePatch { bitmap, .. } => Some(bitmap),
258 PainterKind::Svg(_) => None,
259 }
260 }
261
262 pub fn bitmap(&self) -> Option<&ImageBitmap> {
264 self.as_bitmap()
265 }
266}
267
268impl From<ImageBitmap> for Painter {
269 fn from(value: ImageBitmap) -> Self {
270 Self::from_bitmap(value)
271 }
272}
273
274impl From<SvgPainter> for Painter {
275 fn from(value: SvgPainter) -> Self {
276 Self::from_svg(value)
277 }
278}
279
280pub fn BitmapPainter(bitmap: ImageBitmap) -> Painter {
281 Painter::from_bitmap(bitmap)
282}
283
284pub fn BitmapRegionPainter(bitmap: ImageBitmap, source: Rect, sampling: ImageSampling) -> Painter {
286 Painter::from_bitmap_region(bitmap, source, sampling)
287}
288
289pub fn TiledPainter(bitmap: ImageBitmap, source: Rect, sampling: ImageSampling) -> Painter {
291 Painter::from_bitmap_tiled(bitmap, source, sampling)
292}
293
294pub fn NinePatchPainter(
296 bitmap: ImageBitmap,
297 source: Rect,
298 insets: NinePatchInsets,
299 center: PatchFill,
300 edges: PatchFill,
301 sampling: ImageSampling,
302) -> Painter {
303 Painter::from_nine_patch(bitmap, source, insets, center, edges, sampling)
304}
305
306#[derive(Debug, Clone, PartialEq, Eq, Error)]
308pub enum SvgPainterError {
309 #[error("SVG support is disabled; enable the cranpose-ui `svg` feature")]
310 SvgFeatureDisabled,
311 #[error("failed to parse SVG: {0}")]
312 Parse(String),
313 #[error("SVG raster dimensions must be greater than zero")]
314 InvalidRasterDimensions,
315 #[error("SVG raster dimensions are too large")]
316 RasterDimensionsTooLarge,
317 #[error("failed to allocate SVG raster {width}x{height}")]
318 RasterAllocationFailed { width: u32, height: u32 },
319 #[error("SVG raster cache is unavailable")]
320 RasterCacheUnavailable,
321 #[error(transparent)]
322 ImageBitmap(#[from] cranpose_ui_graphics::ImageBitmapError),
323}
324
325#[derive(Clone)]
327pub struct SvgPainter {
328 inner: Arc<SvgPainterInner>,
329}
330
331struct SvgPainterInner {
332 #[cfg(feature = "svg")]
333 document: image_svg::SvgDocument,
334 intrinsic_size: Size,
335 #[cfg(feature = "svg")]
336 cache: Mutex<SvgRasterCache>,
337}
338
339#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
340#[cfg(feature = "svg")]
341struct SvgRasterKey {
342 width: u32,
343 height: u32,
344}
345
346#[derive(Clone, Debug)]
347#[cfg(feature = "svg")]
348struct SvgRasterEntry {
349 key: SvgRasterKey,
350 bitmap: ImageBitmap,
351}
352
353#[derive(Default, Debug)]
354#[cfg(feature = "svg")]
355struct SvgRasterCache {
356 entries: Vec<SvgRasterEntry>,
357}
358
359impl SvgPainter {
360 pub fn from_bytes(bytes: &[u8]) -> Result<Self, SvgPainterError> {
361 #[cfg(not(feature = "svg"))]
362 {
363 let _ = bytes;
364 Err(SvgPainterError::SvgFeatureDisabled)
365 }
366 #[cfg(feature = "svg")]
367 {
368 let document = image_svg::parse_svg_document(bytes)?;
369 let intrinsic_size = document.intrinsic_size();
370
371 Ok(Self {
372 inner: Arc::new(SvgPainterInner {
373 document,
374 intrinsic_size,
375 cache: Mutex::new(SvgRasterCache::default()),
376 }),
377 })
378 }
379 }
380
381 pub fn id(&self) -> u64 {
382 Arc::as_ptr(&self.inner) as usize as u64
383 }
384
385 pub fn intrinsic_size(&self) -> Size {
386 self.inner.intrinsic_size
387 }
388
389 pub fn rasterize(&self, pixel_size: Size) -> Result<ImageBitmap, SvgPainterError> {
390 #[cfg(not(feature = "svg"))]
391 {
392 let _ = pixel_size;
393 Err(SvgPainterError::SvgFeatureDisabled)
394 }
395 #[cfg(feature = "svg")]
396 {
397 let key = svg_raster_key(pixel_size)?;
398 if let Some(bitmap) = self.cached_bitmap(key)? {
399 return Ok(bitmap);
400 }
401
402 let bitmap = self.rasterize_uncached(key)?;
403 self.cache_bitmap(key, bitmap.clone())?;
404 Ok(bitmap)
405 }
406 }
407
408 #[cfg(feature = "svg")]
409 fn cached_bitmap(&self, key: SvgRasterKey) -> Result<Option<ImageBitmap>, SvgPainterError> {
410 let mut cache = self.lock_cache();
411 Ok(cache.get(key))
412 }
413
414 #[cfg(feature = "svg")]
415 fn cache_bitmap(&self, key: SvgRasterKey, bitmap: ImageBitmap) -> Result<(), SvgPainterError> {
416 let mut cache = self.lock_cache();
417 cache.insert(key, bitmap);
418 Ok(())
419 }
420
421 #[cfg(feature = "svg")]
422 fn lock_cache(&self) -> MutexGuard<'_, SvgRasterCache> {
423 self.inner
424 .cache
425 .lock()
426 .unwrap_or_else(|poisoned| poisoned.into_inner())
427 }
428
429 #[cfg(feature = "svg")]
430 fn rasterize_uncached(&self, key: SvgRasterKey) -> Result<ImageBitmap, SvgPainterError> {
431 (key.width as usize)
432 .checked_mul(key.height as usize)
433 .and_then(|value| value.checked_mul(4))
434 .ok_or(SvgPainterError::RasterDimensionsTooLarge)?;
435
436 let mut pixmap = tiny_skia::Pixmap::new(key.width, key.height).ok_or(
437 SvgPainterError::RasterAllocationFailed {
438 width: key.width,
439 height: key.height,
440 },
441 )?;
442 image_svg::rasterize_svg_document(&self.inner.document, &mut pixmap);
443 let pixels = image_svg::demultiplied_rgba_pixels(&pixmap);
444 Ok(ImageBitmap::from_rgba8(key.width, key.height, pixels)?)
445 }
446}
447
448impl std::fmt::Debug for SvgPainter {
449 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
450 f.debug_struct("SvgPainter")
451 .field("id", &self.id())
452 .field("intrinsic_size", &self.intrinsic_size())
453 .finish_non_exhaustive()
454 }
455}
456
457impl PartialEq for SvgPainter {
458 fn eq(&self, other: &Self) -> bool {
459 self.id() == other.id()
460 }
461}
462
463impl Eq for SvgPainter {}
464
465impl Hash for SvgPainter {
466 fn hash<H: Hasher>(&self, state: &mut H) {
467 self.id().hash(state);
468 }
469}
470
471#[cfg(feature = "svg")]
472impl SvgRasterCache {
473 fn get(&mut self, key: SvgRasterKey) -> Option<ImageBitmap> {
474 let position = self.entries.iter().position(|entry| entry.key == key)?;
475 let entry = self.entries.remove(position);
476 let bitmap = entry.bitmap.clone();
477 self.entries.push(entry);
478 Some(bitmap)
479 }
480
481 fn insert(&mut self, key: SvgRasterKey, bitmap: ImageBitmap) {
482 if let Some(position) = self.entries.iter().position(|entry| entry.key == key) {
483 self.entries.remove(position);
484 } else if self.entries.len() >= SVG_RASTER_CACHE_LIMIT {
485 self.entries.remove(0);
486 }
487 self.entries.push(SvgRasterEntry { key, bitmap });
488 }
489}
490
491#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
492struct SvgBytesKey {
493 ptr: usize,
494 len: usize,
495}
496
497impl SvgBytesKey {
498 fn new(bytes: &'static [u8]) -> Self {
499 Self {
500 ptr: bytes.as_ptr() as usize,
501 len: bytes.len(),
502 }
503 }
504}
505
506pub fn rememberSvg(bytes: &'static [u8]) -> Result<SvgPainter, SvgPainterError> {
507 let key = SvgBytesKey::new(bytes);
508 cranpose_core::withCurrentComposer(|composer| {
509 composer.with_key(&key, |composer| {
510 composer
511 .remember(|| SvgPainter::from_bytes(bytes))
512 .with(|result| result.clone())
513 })
514 })
515}
516
517#[derive(Clone, Debug, PartialEq)]
524struct ImageMeasurePolicy {
525 intrinsic_size: Size,
526}
527
528impl MeasurePolicy for ImageMeasurePolicy {
529 fn measure(
530 &self,
531 _measurables: &[Box<dyn Measurable>],
532 constraints: Constraints,
533 ) -> MeasureResult {
534 let mut placements = Vec::new();
535 let size = self.measure_into(&[], constraints, &mut placements);
536 MeasureResult::new(size, placements)
537 }
538
539 fn measure_into(
540 &self,
541 _measurables: &[Box<dyn Measurable>],
542 constraints: Constraints,
543 placements: &mut Vec<Placement>,
544 ) -> Size {
545 placements.clear();
546 let iw = self.intrinsic_size.width;
547 let ih = self.intrinsic_size.height;
548
549 if iw <= 0.0 || ih <= 0.0 {
550 let (w, h) = constraints.constrain(0.0, 0.0);
551 return Size {
552 width: w,
553 height: h,
554 };
555 }
556
557 let cw = iw.clamp(constraints.min_width, constraints.max_width);
559 let ch = ih.clamp(constraints.min_height, constraints.max_height);
560
561 let scale_x = cw / iw;
564 let scale_y = ch / ih;
565
566 let (width, height) = if scale_x < 1.0 || scale_y < 1.0 {
567 let factor = scale_x.min(scale_y);
568 let w = (iw * factor).clamp(constraints.min_width, constraints.max_width);
569 let h = (ih * factor).clamp(constraints.min_height, constraints.max_height);
570 (w, h)
571 } else {
572 (cw, ch)
573 };
574
575 Size { width, height }
576 }
577
578 fn min_intrinsic_width(&self, _measurables: &[Box<dyn Measurable>], _height: f32) -> f32 {
579 self.intrinsic_size.width
580 }
581
582 fn max_intrinsic_width(&self, _measurables: &[Box<dyn Measurable>], _height: f32) -> f32 {
583 self.intrinsic_size.width
584 }
585
586 fn min_intrinsic_height(&self, _measurables: &[Box<dyn Measurable>], _width: f32) -> f32 {
587 self.intrinsic_size.height
588 }
589
590 fn max_intrinsic_height(&self, _measurables: &[Box<dyn Measurable>], _width: f32) -> f32 {
591 self.intrinsic_size.height
592 }
593}
594
595fn destination_rect(
596 src_size: Size,
597 dst_size: Size,
598 alignment: Alignment,
599 content_scale: ContentScale,
600) -> Rect {
601 let draw_size = content_scale.scaled_size(src_size, dst_size);
602 let allows_overflow = content_scale == ContentScale::Crop;
603 let offset_x = aligned_x_offset(
604 alignment.horizontal,
605 dst_size.width,
606 draw_size.width,
607 allows_overflow,
608 );
609 let offset_y = aligned_y_offset(
610 alignment.vertical,
611 dst_size.height,
612 draw_size.height,
613 allows_overflow,
614 );
615 Rect {
616 x: offset_x,
617 y: offset_y,
618 width: draw_size.width,
619 height: draw_size.height,
620 }
621}
622
623fn aligned_x_offset(
624 alignment: cranpose_ui_layout::HorizontalAlignment,
625 available: f32,
626 child: f32,
627 allows_overflow: bool,
628) -> f32 {
629 if !allows_overflow {
630 return alignment.align(available, child);
631 }
632
633 match alignment {
634 cranpose_ui_layout::HorizontalAlignment::Start => 0.0,
635 cranpose_ui_layout::HorizontalAlignment::CenterHorizontally => (available - child) / 2.0,
636 cranpose_ui_layout::HorizontalAlignment::End => available - child,
637 }
638}
639
640fn aligned_y_offset(
641 alignment: cranpose_ui_layout::VerticalAlignment,
642 available: f32,
643 child: f32,
644 allows_overflow: bool,
645) -> f32 {
646 if !allows_overflow {
647 return alignment.align(available, child);
648 }
649
650 match alignment {
651 cranpose_ui_layout::VerticalAlignment::Top => 0.0,
652 cranpose_ui_layout::VerticalAlignment::CenterVertically => (available - child) / 2.0,
653 cranpose_ui_layout::VerticalAlignment::Bottom => available - child,
654 }
655}
656
657fn map_destination_clip_to_source(
658 src_rect: Rect,
659 dst_rect: Rect,
660 clipped_dst_rect: Rect,
661) -> Option<Rect> {
662 if src_rect.width <= 0.0
663 || src_rect.height <= 0.0
664 || dst_rect.width <= 0.0
665 || dst_rect.height <= 0.0
666 || clipped_dst_rect.width <= 0.0
667 || clipped_dst_rect.height <= 0.0
668 {
669 return None;
670 }
671
672 let scale_x = src_rect.width / dst_rect.width;
673 let scale_y = src_rect.height / dst_rect.height;
674
675 let src_min_x = src_rect.x;
676 let src_min_y = src_rect.y;
677 let src_max_x = src_rect.x + src_rect.width;
678 let src_max_y = src_rect.y + src_rect.height;
679
680 let raw_left = src_rect.x + (clipped_dst_rect.x - dst_rect.x) * scale_x;
681 let raw_top = src_rect.y + (clipped_dst_rect.y - dst_rect.y) * scale_y;
682 let raw_right =
683 src_rect.x + ((clipped_dst_rect.x + clipped_dst_rect.width) - dst_rect.x) * scale_x;
684 let raw_bottom =
685 src_rect.y + ((clipped_dst_rect.y + clipped_dst_rect.height) - dst_rect.y) * scale_y;
686
687 let left = raw_left.clamp(src_min_x, src_max_x);
688 let top = raw_top.clamp(src_min_y, src_max_y);
689 let right = raw_right.clamp(src_min_x, src_max_x);
690 let bottom = raw_bottom.clamp(src_min_y, src_max_y);
691 let width = right - left;
692 let height = bottom - top;
693
694 if width <= 0.0 || height <= 0.0 {
695 None
696 } else {
697 Some(Rect {
698 x: left,
699 y: top,
700 width,
701 height,
702 })
703 }
704}
705
706fn image_destination_clip(
707 src_size: Size,
708 container_size: Size,
709 alignment: Alignment,
710 content_scale: ContentScale,
711) -> Option<(Rect, Rect)> {
712 let dst_rect = destination_rect(src_size, container_size, alignment, content_scale);
713 if dst_rect.width <= 0.0 || dst_rect.height <= 0.0 {
714 return None;
715 }
716
717 let container_rect = Rect::from_size(container_size);
718 let clipped_dst_rect = dst_rect.intersect(container_rect)?;
719 Some((dst_rect, clipped_dst_rect))
720}
721
722fn draw_bitmap_painter(
723 scope: &mut dyn DrawScope,
724 bitmap: ImageBitmap,
725 intrinsic_size: Size,
726 alignment: Alignment,
727 content_scale: ContentScale,
728 alpha: f32,
729 color_filter: Option<ColorFilter>,
730) {
731 let container_size = scope.size();
732 let Some((dst_rect, clipped_dst_rect)) =
733 image_destination_clip(intrinsic_size, container_size, alignment, content_scale)
734 else {
735 return;
736 };
737 let full_src_rect = Rect::from_size(Size::new(bitmap.width() as f32, bitmap.height() as f32));
738 let Some(clipped_src_rect) =
739 map_destination_clip_to_source(full_src_rect, dst_rect, clipped_dst_rect)
740 else {
741 return;
742 };
743 scope.draw_image_src_sampled(
744 bitmap,
745 clipped_src_rect,
746 clipped_dst_rect,
747 alpha,
748 color_filter,
749 ImageSampling::Linear,
750 );
751}
752
753fn draw_bitmap_region_painter(
754 scope: &mut dyn DrawScope,
755 bitmap: ImageBitmap,
756 source: Rect,
757 alignment: Alignment,
758 content_scale: ContentScale,
759 alpha: f32,
760 color_filter: Option<ColorFilter>,
761 sampling: ImageSampling,
762) {
763 let source_size = Size::new(source.width.max(0.0), source.height.max(0.0));
764 let Some((dst_rect, clipped_dst_rect)) =
765 image_destination_clip(source_size, scope.size(), alignment, content_scale)
766 else {
767 return;
768 };
769 let Some(clipped_source) = map_destination_clip_to_source(source, dst_rect, clipped_dst_rect)
770 else {
771 return;
772 };
773 scope.draw_image_src_sampled(
774 bitmap,
775 clipped_source,
776 clipped_dst_rect,
777 alpha,
778 color_filter,
779 sampling,
780 );
781}
782
783fn draw_patch_quads(
788 scope: &mut dyn DrawScope,
789 bitmap: &ImageBitmap,
790 quads: &[PatchQuad],
791 container: Rect,
792 alpha: f32,
793 color_filter: Option<ColorFilter>,
794 sampling: ImageSampling,
795) {
796 for quad in quads {
797 let Some(clipped_destination) = intersect(quad.destination, container) else {
798 continue;
799 };
800 let Some(clipped_source) =
801 map_destination_clip_to_source(quad.source, quad.destination, clipped_destination)
802 else {
803 continue;
804 };
805 scope.draw_image_src_sampled(
806 bitmap.clone(),
807 clipped_source,
808 clipped_destination,
809 alpha,
810 color_filter,
811 sampling,
812 );
813 }
814}
815
816fn intersect(rect: Rect, bounds: Rect) -> Option<Rect> {
817 let left = rect.x.max(bounds.x);
818 let top = rect.y.max(bounds.y);
819 let right = (rect.x + rect.width).min(bounds.x + bounds.width);
820 let bottom = (rect.y + rect.height).min(bounds.y + bounds.height);
821 if right <= left || bottom <= top {
822 return None;
823 }
824 Some(Rect {
825 x: left,
826 y: top,
827 width: right - left,
828 height: bottom - top,
829 })
830}
831
832fn draw_bitmap_tiled_painter(
837 scope: &mut dyn DrawScope,
838 bitmap: ImageBitmap,
839 source: Rect,
840 alpha: f32,
841 color_filter: Option<ColorFilter>,
842 sampling: ImageSampling,
843) {
844 let container = Rect::from_size(scope.size());
845 let quads = tile_quads(source, container);
846 draw_patch_quads(
847 scope,
848 &bitmap,
849 &quads,
850 container,
851 alpha,
852 color_filter,
853 sampling,
854 );
855}
856
857#[allow(clippy::too_many_arguments)]
862fn draw_nine_patch_painter(
863 scope: &mut dyn DrawScope,
864 bitmap: ImageBitmap,
865 source: Rect,
866 insets: NinePatchInsets,
867 center: PatchFill,
868 edges: PatchFill,
869 alpha: f32,
870 color_filter: Option<ColorFilter>,
871 sampling: ImageSampling,
872) {
873 let container = Rect::from_size(scope.size());
874 let quads = nine_patch_quads(source, container, insets, center, edges);
875 draw_patch_quads(
876 scope,
877 &bitmap,
878 &quads,
879 container,
880 alpha,
881 color_filter,
882 sampling,
883 );
884}
885
886fn draw_svg_painter(
887 scope: &mut dyn DrawScope,
888 svg: SvgPainter,
889 intrinsic_size: Size,
890 alignment: Alignment,
891 content_scale: ContentScale,
892 alpha: f32,
893 color_filter: Option<ColorFilter>,
894) {
895 let container_size = scope.size();
896 let Some((dst_rect, clipped_dst_rect)) =
897 image_destination_clip(intrinsic_size, container_size, alignment, content_scale)
898 else {
899 return;
900 };
901 let density = crate::render_state::current_density();
902 let pixel_size = Size::new(dst_rect.width * density, dst_rect.height * density);
903 let bitmap = match svg.rasterize(pixel_size) {
904 Ok(bitmap) => bitmap,
905 Err(error) => {
906 log::warn!("failed to rasterize SVG painter: {error}");
907 return;
908 }
909 };
910 let full_src_rect = Rect::from_size(Size::new(bitmap.width() as f32, bitmap.height() as f32));
911 let Some(clipped_src_rect) =
912 map_destination_clip_to_source(full_src_rect, dst_rect, clipped_dst_rect)
913 else {
914 return;
915 };
916 scope.draw_image_src_sampled(
917 bitmap,
918 clipped_src_rect,
919 clipped_dst_rect,
920 alpha,
921 color_filter,
922 ImageSampling::Linear,
923 );
924}
925
926#[cfg(feature = "svg")]
927fn svg_raster_key(pixel_size: Size) -> Result<SvgRasterKey, SvgPainterError> {
928 let width = svg_raster_axis(pixel_size.width)?;
929 let height = svg_raster_axis(pixel_size.height)?;
930 width
931 .checked_mul(height)
932 .and_then(|value| value.checked_mul(4))
933 .ok_or(SvgPainterError::RasterDimensionsTooLarge)?;
934 Ok(SvgRasterKey { width, height })
935}
936
937#[cfg(feature = "svg")]
938fn svg_raster_axis(value: f32) -> Result<u32, SvgPainterError> {
939 if !value.is_finite() || value <= 0.0 {
940 return Err(SvgPainterError::InvalidRasterDimensions);
941 }
942
943 let rounded = value.ceil();
944 if rounded > u32::MAX as f32 {
945 return Err(SvgPainterError::RasterDimensionsTooLarge);
946 }
947 Ok(rounded as u32)
948}
949
950#[composable]
951pub fn Image<P>(
952 painter: P,
953 content_description: Option<String>,
954 modifier: Modifier,
955 alignment: Alignment,
956 content_scale: ContentScale,
957 alpha: f32,
958 color_filter: Option<ColorFilter>,
959) -> NodeId
960where
961 P: Into<Painter> + Clone + PartialEq + 'static,
962{
963 let painter = painter.into();
964 let intrinsic_dp = painter.intrinsic_size();
967 let draw_alpha = alpha.clamp(0.0, 1.0);
968 let draw_painter = painter.clone();
969
970 let semantics_modifier = if let Some(description) = content_description {
971 Modifier::empty().semantics(move |config| {
972 config.content_description = Some(description.clone());
973 })
974 } else {
975 Modifier::empty()
976 };
977
978 let image_modifier =
979 modifier
980 .then(semantics_modifier)
981 .draw_behind(move |scope: &mut dyn DrawScope| {
982 if draw_alpha <= 0.0 {
983 return;
984 }
985 let container_size = scope.size();
986 if container_size.width <= 0.0 || container_size.height <= 0.0 {
987 return;
988 }
989 match &draw_painter.kind {
990 PainterKind::Bitmap(bitmap) => draw_bitmap_painter(
991 scope,
992 bitmap.clone(),
993 intrinsic_dp,
994 alignment,
995 content_scale,
996 draw_alpha,
997 color_filter,
998 ),
999 PainterKind::BitmapRegion {
1000 bitmap,
1001 source,
1002 sampling,
1003 } => draw_bitmap_region_painter(
1004 scope,
1005 bitmap.clone(),
1006 source.rect(),
1007 alignment,
1008 content_scale,
1009 draw_alpha,
1010 color_filter,
1011 *sampling,
1012 ),
1013 PainterKind::BitmapTiled {
1014 bitmap,
1015 source,
1016 sampling,
1017 } => draw_bitmap_tiled_painter(
1018 scope,
1019 bitmap.clone(),
1020 source.rect(),
1021 draw_alpha,
1022 color_filter,
1023 *sampling,
1024 ),
1025 PainterKind::NinePatch {
1026 bitmap,
1027 source,
1028 insets,
1029 center,
1030 edges,
1031 sampling,
1032 } => draw_nine_patch_painter(
1033 scope,
1034 bitmap.clone(),
1035 source.rect(),
1036 insets.insets(),
1037 *center,
1038 *edges,
1039 draw_alpha,
1040 color_filter,
1041 *sampling,
1042 ),
1043 PainterKind::Svg(svg) => draw_svg_painter(
1044 scope,
1045 svg.clone(),
1046 intrinsic_dp,
1047 alignment,
1048 content_scale,
1049 draw_alpha,
1050 color_filter,
1051 ),
1052 }
1053 });
1054
1055 Layout(
1056 image_modifier,
1057 ImageMeasurePolicy {
1058 intrinsic_size: intrinsic_dp,
1059 },
1060 || {},
1061 )
1062}
1063
1064#[cfg(test)]
1065mod tests {
1066 use super::*;
1067 use crate::layout::core::Alignment;
1068
1069 #[cfg(feature = "svg")]
1070 const RED_RECT_SVG: &[u8] = br##"
1071 <svg xmlns="http://www.w3.org/2000/svg" width="10" height="20" viewBox="0 0 10 20">
1072 <rect x="0" y="0" width="10" height="20" fill="#ff0000"/>
1073 </svg>
1074 "##;
1075
1076 #[cfg(feature = "svg")]
1077 const TRANSPARENT_CENTER_SVG: &[u8] = br##"
1078 <svg xmlns="http://www.w3.org/2000/svg" width="4" height="4" viewBox="0 0 4 4">
1079 <rect x="1" y="1" width="2" height="2" fill="#00ff00"/>
1080 </svg>
1081 "##;
1082
1083 fn sample_bitmap() -> ImageBitmap {
1084 ImageBitmap::from_rgba8(4, 2, vec![255; 4 * 2 * 4]).expect("bitmap")
1085 }
1086
1087 #[test]
1088 fn svg_painter_ids_do_not_use_process_global_counter() {
1089 let source = include_str!("image.rs");
1090 let svg_counter = ["static ", "NEXT_SVG_PAINTER_ID"].concat();
1091
1092 assert!(
1093 !source.contains(&svg_counter),
1094 "SVG painter ids must come from retained painter identity"
1095 );
1096 }
1097
1098 #[cfg(feature = "svg")]
1099 fn cache_test_bitmap(width: u32) -> ImageBitmap {
1100 ImageBitmap::from_rgba8(width, 1, vec![255; width as usize * 4]).expect("bitmap")
1101 }
1102
1103 #[cfg(feature = "svg")]
1104 fn pixel_at(bitmap: &ImageBitmap, x: u32, y: u32) -> [u8; 4] {
1105 let offset = ((y * bitmap.width() + x) * 4) as usize;
1106 let pixels = bitmap.pixels();
1107 [
1108 pixels[offset],
1109 pixels[offset + 1],
1110 pixels[offset + 2],
1111 pixels[offset + 3],
1112 ]
1113 }
1114
1115 #[test]
1116 fn painter_reports_intrinsic_size_and_bitmap() {
1117 let bitmap = sample_bitmap();
1118 let painter = BitmapPainter(bitmap.clone());
1119 assert_eq!(painter.intrinsic_size(), Size::new(4.0, 2.0));
1120 assert_eq!(painter.bitmap(), Some(&bitmap));
1121 }
1122
1123 #[test]
1124 fn bitmap_region_painter_uses_region_as_intrinsic_size() {
1125 let bitmap = sample_bitmap();
1126 let source = Rect {
1127 x: 1.0,
1128 y: 0.0,
1129 width: 2.0,
1130 height: 1.0,
1131 };
1132 let painter = BitmapRegionPainter(bitmap.clone(), source, ImageSampling::Nearest);
1133 assert_eq!(painter.intrinsic_size(), Size::new(2.0, 1.0));
1134 assert_eq!(painter.bitmap(), Some(&bitmap));
1135 assert_eq!(
1136 painter,
1137 Painter::from_bitmap_region(bitmap, source, ImageSampling::Nearest)
1138 );
1139 }
1140
1141 fn atlas_bitmap() -> ImageBitmap {
1142 ImageBitmap::from_rgba8(64, 64, vec![255; 64 * 64 * 4]).expect("bitmap")
1143 }
1144
1145 fn source_rect(x: f32, y: f32, width: f32, height: f32) -> Rect {
1146 Rect {
1147 x,
1148 y,
1149 width,
1150 height,
1151 }
1152 }
1153
1154 fn drawn_images(
1155 size: Size,
1156 draw: impl FnOnce(&mut cranpose_ui_graphics::DrawScopeDefault),
1157 ) -> Vec<(Rect, Rect)> {
1158 let mut scope = cranpose_ui_graphics::DrawScopeDefault::new(size);
1159 draw(&mut scope);
1160 scope
1161 .into_primitives()
1162 .into_iter()
1163 .filter_map(|primitive| match primitive {
1164 cranpose_ui_graphics::DrawPrimitive::Image { src_rect, rect, .. } => {
1165 Some((src_rect?, rect))
1166 }
1167 _ => None,
1168 })
1169 .collect()
1170 }
1171
1172 #[test]
1173 fn a_tiled_painter_keeps_the_region_as_its_intrinsic_size() {
1174 let bitmap = atlas_bitmap();
1175 let source = source_rect(8.0, 8.0, 16.0, 4.0);
1176 let painter = TiledPainter(bitmap.clone(), source, ImageSampling::Nearest);
1177 assert_eq!(painter.intrinsic_size(), Size::new(16.0, 4.0));
1178 assert_eq!(painter.bitmap(), Some(&bitmap));
1179 assert_eq!(
1180 painter,
1181 Painter::from_bitmap_tiled(bitmap, source, ImageSampling::Nearest)
1182 );
1183 }
1184
1185 #[test]
1186 fn a_tiled_painter_repeats_its_region_at_its_own_size() {
1187 let bitmap = atlas_bitmap();
1188 let source = source_rect(8.0, 8.0, 16.0, 4.0);
1189 let drawn = drawn_images(Size::new(32.0, 8.0), |scope| {
1190 draw_bitmap_tiled_painter(
1191 scope,
1192 bitmap.clone(),
1193 source,
1194 1.0,
1195 None,
1196 ImageSampling::Nearest,
1197 );
1198 });
1199
1200 assert_eq!(drawn.len(), 4, "two columns by two rows");
1201 assert!(
1202 drawn
1203 .iter()
1204 .all(|(src, dst)| src.width == dst.width && src.height == dst.height),
1205 "a tile is drawn at 1:1, never scaled"
1206 );
1207 assert!(
1208 drawn.iter().all(|(src, _)| src.x >= 8.0
1209 && src.y >= 8.0
1210 && src.x + src.width <= 24.0
1211 && src.y + src.height <= 12.0),
1212 "a tile never reads outside the region it was given"
1213 );
1214 assert_eq!(drawn[0].1, source_rect(0.0, 0.0, 16.0, 4.0));
1215 assert_eq!(drawn[3].1, source_rect(16.0, 4.0, 16.0, 4.0));
1216 }
1217
1218 #[test]
1219 fn a_tiled_painter_clips_the_last_row_and_column() {
1220 let bitmap = atlas_bitmap();
1221 let source = source_rect(0.0, 0.0, 10.0, 10.0);
1222 let drawn = drawn_images(Size::new(25.0, 10.0), |scope| {
1223 draw_bitmap_tiled_painter(
1224 scope,
1225 bitmap.clone(),
1226 source,
1227 1.0,
1228 None,
1229 ImageSampling::Nearest,
1230 );
1231 });
1232 assert_eq!(drawn.len(), 3);
1233 let (src, dst) = drawn[2];
1234 assert_eq!(dst, source_rect(20.0, 0.0, 5.0, 10.0));
1235 assert_eq!(src, source_rect(0.0, 0.0, 5.0, 10.0));
1236 }
1237
1238 #[test]
1239 fn a_nine_patch_painter_keeps_the_region_as_its_intrinsic_size() {
1240 let bitmap = atlas_bitmap();
1241 let source = source_rect(0.0, 0.0, 30.0, 30.0);
1242 let painter = NinePatchPainter(
1243 bitmap.clone(),
1244 source,
1245 NinePatchInsets::uniform(10.0),
1246 PatchFill::Stretch,
1247 PatchFill::Stretch,
1248 ImageSampling::Nearest,
1249 );
1250 assert_eq!(painter.intrinsic_size(), Size::new(30.0, 30.0));
1251 assert_eq!(painter.bitmap(), Some(&bitmap));
1252 assert_eq!(
1253 painter,
1254 Painter::from_nine_patch(
1255 bitmap,
1256 source,
1257 NinePatchInsets::uniform(10.0),
1258 PatchFill::Stretch,
1259 PatchFill::Stretch,
1260 ImageSampling::Nearest,
1261 ),
1262 "two painters built from the same numbers are the same painter"
1263 );
1264 }
1265
1266 #[test]
1267 fn a_nine_patch_painter_draws_its_corners_at_their_own_size() {
1268 let bitmap = atlas_bitmap();
1269 let drawn = drawn_images(Size::new(100.0, 60.0), |scope| {
1270 draw_nine_patch_painter(
1271 scope,
1272 bitmap.clone(),
1273 source_rect(0.0, 0.0, 30.0, 30.0),
1274 NinePatchInsets::uniform(10.0),
1275 PatchFill::Stretch,
1276 PatchFill::Stretch,
1277 1.0,
1278 None,
1279 ImageSampling::Nearest,
1280 );
1281 });
1282
1283 assert_eq!(drawn.len(), 9);
1284 assert_eq!(
1285 drawn[0],
1286 (
1287 source_rect(0.0, 0.0, 10.0, 10.0),
1288 source_rect(0.0, 0.0, 10.0, 10.0),
1289 )
1290 );
1291 assert_eq!(
1292 drawn[8],
1293 (
1294 source_rect(20.0, 20.0, 10.0, 10.0),
1295 source_rect(90.0, 50.0, 10.0, 10.0),
1296 )
1297 );
1298 assert_eq!(
1299 drawn[4],
1300 (
1301 source_rect(10.0, 10.0, 10.0, 10.0),
1302 source_rect(10.0, 10.0, 80.0, 40.0),
1303 ),
1304 "the middle grows on both axes"
1305 );
1306 }
1307
1308 #[test]
1309 fn a_nine_patch_painter_reads_only_its_region_of_an_atlas() {
1310 let bitmap = atlas_bitmap();
1311 let drawn = drawn_images(Size::new(80.0, 40.0), |scope| {
1312 draw_nine_patch_painter(
1313 scope,
1314 bitmap.clone(),
1315 source_rect(32.0, 16.0, 24.0, 24.0),
1316 NinePatchInsets::uniform(8.0),
1317 PatchFill::Tile,
1318 PatchFill::Tile,
1319 1.0,
1320 None,
1321 ImageSampling::Nearest,
1322 );
1323 });
1324
1325 assert!(!drawn.is_empty());
1326 assert!(
1327 drawn.iter().all(|(src, _)| src.x >= 32.0
1328 && src.y >= 16.0
1329 && src.x + src.width <= 56.0
1330 && src.y + src.height <= 40.0),
1331 "no patch may read a neighbouring sprite out of the atlas"
1332 );
1333 }
1334
1335 #[test]
1336 #[cfg(feature = "svg")]
1337 fn svg_painter_reports_intrinsic_size() {
1338 let painter = SvgPainter::from_bytes(RED_RECT_SVG).expect("svg painter");
1339 let clone = painter.clone();
1340 assert_eq!(painter.intrinsic_size(), Size::new(10.0, 20.0));
1341 assert_eq!(painter.id(), clone.id());
1342 assert_eq!(Painter::from_svg(painter).bitmap(), None);
1343 }
1344
1345 #[test]
1346 #[cfg(feature = "svg")]
1347 fn svg_painter_rasterizes_requested_dimensions() {
1348 let painter = SvgPainter::from_bytes(RED_RECT_SVG).expect("svg painter");
1349 let bitmap = painter
1350 .rasterize(Size::new(5.0, 10.0))
1351 .expect("rasterized svg");
1352
1353 assert_eq!(bitmap.width(), 5);
1354 assert_eq!(bitmap.height(), 10);
1355 assert_eq!(pixel_at(&bitmap, 2, 5), [255, 0, 0, 255]);
1356 }
1357
1358 #[test]
1359 #[cfg(feature = "svg")]
1360 fn svg_painter_preserves_transparency() {
1361 let painter = SvgPainter::from_bytes(TRANSPARENT_CENTER_SVG).expect("svg painter");
1362 let bitmap = painter
1363 .rasterize(Size::new(4.0, 4.0))
1364 .expect("rasterized svg");
1365
1366 assert_eq!(pixel_at(&bitmap, 0, 0), [0, 0, 0, 0]);
1367 assert_eq!(pixel_at(&bitmap, 2, 2), [0, 255, 0, 255]);
1368 }
1369
1370 #[test]
1371 #[cfg(feature = "svg")]
1372 fn svg_painter_reuses_cached_raster_for_same_size() {
1373 let painter = SvgPainter::from_bytes(RED_RECT_SVG).expect("svg painter");
1374 let first = painter
1375 .rasterize(Size::new(8.0, 8.0))
1376 .expect("first raster");
1377 let second = painter
1378 .rasterize(Size::new(8.0, 8.0))
1379 .expect("second raster");
1380
1381 assert_eq!(first.id(), second.id());
1382 }
1383
1384 #[test]
1385 #[cfg(feature = "svg")]
1386 fn svg_raster_cache_evicts_least_recently_used_entry() {
1387 let mut cache = SvgRasterCache::default();
1388 let keys: Vec<SvgRasterKey> = (0..SVG_RASTER_CACHE_LIMIT)
1389 .map(|index| SvgRasterKey {
1390 width: index as u32 + 1,
1391 height: 1,
1392 })
1393 .collect();
1394
1395 for key in &keys {
1396 cache.insert(*key, cache_test_bitmap(key.width));
1397 }
1398
1399 let recent = cache.get(keys[0]).expect("cached raster");
1400 let new_key = SvgRasterKey {
1401 width: SVG_RASTER_CACHE_LIMIT as u32 + 1,
1402 height: 1,
1403 };
1404 cache.insert(new_key, cache_test_bitmap(new_key.width));
1405
1406 assert!(cache.get(keys[1]).is_none());
1407 assert_eq!(
1408 cache.get(keys[0]).expect("retained raster").id(),
1409 recent.id()
1410 );
1411 assert!(cache.get(new_key).is_some());
1412 }
1413
1414 #[test]
1415 #[cfg(feature = "svg")]
1416 fn svg_painter_rasterizes_distinct_sizes_separately() {
1417 let painter = SvgPainter::from_bytes(RED_RECT_SVG).expect("svg painter");
1418 let small = painter
1419 .rasterize(Size::new(8.0, 8.0))
1420 .expect("small raster");
1421 let large = painter
1422 .rasterize(Size::new(16.0, 16.0))
1423 .expect("large raster");
1424
1425 assert_ne!(small.id(), large.id());
1426 assert_eq!(large.width(), 16);
1427 assert_eq!(large.height(), 16);
1428 }
1429
1430 #[test]
1431 #[cfg(feature = "svg")]
1432 fn svg_painter_cache_recovers_after_poison() {
1433 let painter = SvgPainter::from_bytes(RED_RECT_SVG).expect("svg painter");
1434 let poison_result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1435 let _guard = painter
1436 .inner
1437 .cache
1438 .lock()
1439 .unwrap_or_else(|poisoned| poisoned.into_inner());
1440 panic!("poison svg raster cache for recovery test");
1441 }));
1442
1443 assert!(poison_result.is_err());
1444
1445 let bitmap = painter
1446 .rasterize(Size::new(8.0, 8.0))
1447 .expect("svg raster cache should recover after poisoning");
1448 assert_eq!(bitmap.width(), 8);
1449 assert_eq!(bitmap.height(), 8);
1450 }
1451
1452 #[test]
1453 fn svg_draw_density_has_no_outside_context_fallback() {
1454 let source = include_str!("image.rs");
1455 let fallback_call = ["try_current", "_density()", ".unwrap_or"].concat();
1456 assert!(
1457 !source.contains(&fallback_call),
1458 "SVG image drawing must use the active AppContext density"
1459 );
1460 }
1461
1462 #[test]
1463 #[cfg(feature = "svg")]
1464 fn svg_painter_rejects_invalid_bytes() {
1465 let err = SvgPainter::from_bytes(b"not svg").expect_err("invalid svg");
1466 assert!(matches!(err, SvgPainterError::Parse(_)));
1467 }
1468
1469 #[test]
1470 #[cfg(not(feature = "svg"))]
1471 fn svg_painter_reports_disabled_feature_without_resvg() {
1472 let err = SvgPainter::from_bytes(b"not svg").expect_err("svg feature disabled");
1473 assert!(matches!(err, SvgPainterError::SvgFeatureDisabled));
1474 }
1475
1476 #[test]
1477 fn fit_keeps_aspect_ratio() {
1478 let src = Size::new(200.0, 100.0);
1479 let dst = Size::new(300.0, 300.0);
1480 let result = ContentScale::Fit.scaled_size(src, dst);
1481 assert_eq!(result, Size::new(300.0, 150.0));
1482 }
1483
1484 #[test]
1485 fn crop_fills_bounds() {
1486 let src = Size::new(200.0, 100.0);
1487 let dst = Size::new(300.0, 300.0);
1488 let result = ContentScale::Crop.scaled_size(src, dst);
1489 assert_eq!(result, Size::new(600.0, 300.0));
1490 }
1491
1492 #[test]
1493 fn destination_rect_aligns_center() {
1494 let src = Size::new(200.0, 100.0);
1495 let dst = Size::new(300.0, 300.0);
1496 let rect = destination_rect(src, dst, Alignment::CENTER, ContentScale::Fit);
1497 assert_eq!(
1498 rect,
1499 Rect {
1500 x: 0.0,
1501 y: 75.0,
1502 width: 300.0,
1503 height: 150.0,
1504 }
1505 );
1506 }
1507
1508 #[test]
1509 fn crop_destination_clip_maps_centered_wide_source() {
1510 let src = Size::new(200.0, 100.0);
1511 let dst = Size::new(100.0, 100.0);
1512 let (dst_rect, clipped_dst_rect) =
1513 image_destination_clip(src, dst, Alignment::CENTER, ContentScale::Crop)
1514 .expect("destination clip");
1515 let rect = map_destination_clip_to_source(Rect::from_size(src), dst_rect, clipped_dst_rect)
1516 .expect("source clip");
1517 assert_eq!(
1518 rect,
1519 Rect {
1520 x: 50.0,
1521 y: 0.0,
1522 width: 100.0,
1523 height: 100.0,
1524 }
1525 );
1526 }
1527
1528 #[test]
1529 fn crop_destination_clip_honors_start_alignment() {
1530 let src = Size::new(200.0, 100.0);
1531 let dst = Size::new(100.0, 100.0);
1532 let (dst_rect, clipped_dst_rect) =
1533 image_destination_clip(src, dst, Alignment::TOP_START, ContentScale::Crop)
1534 .expect("destination clip");
1535 let rect = map_destination_clip_to_source(Rect::from_size(src), dst_rect, clipped_dst_rect)
1536 .expect("source clip");
1537 assert_eq!(
1538 rect,
1539 Rect {
1540 x: 0.0,
1541 y: 0.0,
1542 width: 100.0,
1543 height: 100.0,
1544 }
1545 );
1546 }
1547
1548 fn approx_eq(left: f32, right: f32) {
1549 assert!((left - right).abs() < 1e-4, "left={left}, right={right}");
1550 }
1551
1552 #[test]
1553 fn map_destination_clip_to_source_scales_proportionally() {
1554 let src = Rect {
1555 x: 0.0,
1556 y: 0.0,
1557 width: 100.0,
1558 height: 100.0,
1559 };
1560 let dst = Rect {
1561 x: -50.0,
1562 y: 0.0,
1563 width: 200.0,
1564 height: 100.0,
1565 };
1566 let clipped_dst = Rect {
1567 x: 0.0,
1568 y: 0.0,
1569 width: 100.0,
1570 height: 100.0,
1571 };
1572 let mapped = map_destination_clip_to_source(src, dst, clipped_dst).expect("mapped");
1573 approx_eq(mapped.x, 25.0);
1574 approx_eq(mapped.y, 0.0);
1575 approx_eq(mapped.width, 50.0);
1576 approx_eq(mapped.height, 100.0);
1577 }
1578
1579 #[test]
1580 fn map_destination_clip_to_source_returns_full_source_without_clipping() {
1581 let src = Rect {
1582 x: 0.0,
1583 y: 0.0,
1584 width: 120.0,
1585 height: 80.0,
1586 };
1587 let dst = Rect {
1588 x: 10.0,
1589 y: 5.0,
1590 width: 60.0,
1591 height: 40.0,
1592 };
1593 let mapped = map_destination_clip_to_source(src, dst, dst).expect("mapped");
1594 approx_eq(mapped.x, src.x);
1595 approx_eq(mapped.y, src.y);
1596 approx_eq(mapped.width, src.width);
1597 approx_eq(mapped.height, src.height);
1598 }
1599
1600 fn measure_image(intrinsic: Size, constraints: Constraints) -> Size {
1603 let policy = ImageMeasurePolicy {
1604 intrinsic_size: intrinsic,
1605 };
1606 policy.measure(&[], constraints).size
1607 }
1608
1609 #[test]
1610 fn image_measure_unconstrained() {
1611 let size = measure_image(
1612 Size::new(800.0, 600.0),
1613 Constraints::loose(f32::INFINITY, f32::INFINITY),
1614 );
1615 assert_eq!(size, Size::new(800.0, 600.0));
1616 }
1617
1618 #[test]
1619 fn image_measure_width_constrained_preserves_aspect_ratio() {
1620 let size = measure_image(
1622 Size::new(800.0, 600.0),
1623 Constraints::loose(400.0, f32::INFINITY),
1624 );
1625 assert_eq!(size, Size::new(400.0, 300.0));
1626 }
1627
1628 #[test]
1629 fn image_measure_height_constrained_preserves_aspect_ratio() {
1630 let size = measure_image(
1632 Size::new(800.0, 600.0),
1633 Constraints::loose(f32::INFINITY, 300.0),
1634 );
1635 assert_eq!(size, Size::new(400.0, 300.0));
1636 }
1637
1638 #[test]
1639 fn image_measure_both_constrained_uses_smaller_factor() {
1640 let size = measure_image(Size::new(800.0, 600.0), Constraints::loose(200.0, 400.0));
1643 assert_eq!(size, Size::new(200.0, 150.0));
1644 }
1645
1646 #[test]
1647 fn image_measure_fits_within_constraints() {
1648 let size = measure_image(Size::new(200.0, 100.0), Constraints::loose(400.0, 400.0));
1650 assert_eq!(size, Size::new(200.0, 100.0));
1651 }
1652
1653 #[test]
1654 fn image_measure_zero_intrinsic() {
1655 let size = measure_image(Size::ZERO, Constraints::loose(400.0, 400.0));
1656 assert_eq!(size, Size::new(0.0, 0.0));
1657 }
1658}