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, MeasureScope, 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 scope: &dyn MeasureScope,
532 _measurables: &[Box<dyn Measurable>],
533 constraints: Constraints,
534 ) -> MeasureResult {
535 let mut placements = Vec::new();
536 let size = self.measure_into(scope, &[], constraints, &mut placements);
537 MeasureResult::new(size, placements)
538 }
539
540 fn measure_into(
541 &self,
542 _scope: &dyn MeasureScope,
543 _measurables: &[Box<dyn Measurable>],
544 constraints: Constraints,
545 placements: &mut Vec<Placement>,
546 ) -> Size {
547 placements.clear();
548 let iw = self.intrinsic_size.width;
549 let ih = self.intrinsic_size.height;
550
551 if iw <= 0.0 || ih <= 0.0 {
552 let (w, h) = constraints.constrain(0.0, 0.0);
553 return Size {
554 width: w,
555 height: h,
556 };
557 }
558
559 let cw = iw.clamp(constraints.min_width, constraints.max_width);
561 let ch = ih.clamp(constraints.min_height, constraints.max_height);
562
563 let scale_x = cw / iw;
566 let scale_y = ch / ih;
567
568 let (width, height) = if scale_x < 1.0 || scale_y < 1.0 {
569 let factor = scale_x.min(scale_y);
570 let w = (iw * factor).clamp(constraints.min_width, constraints.max_width);
571 let h = (ih * factor).clamp(constraints.min_height, constraints.max_height);
572 (w, h)
573 } else {
574 (cw, ch)
575 };
576
577 Size { width, height }
578 }
579
580 fn min_intrinsic_width(&self, _measurables: &[Box<dyn Measurable>], _height: f32) -> f32 {
581 self.intrinsic_size.width
582 }
583
584 fn max_intrinsic_width(&self, _measurables: &[Box<dyn Measurable>], _height: f32) -> f32 {
585 self.intrinsic_size.width
586 }
587
588 fn min_intrinsic_height(&self, _measurables: &[Box<dyn Measurable>], _width: f32) -> f32 {
589 self.intrinsic_size.height
590 }
591
592 fn max_intrinsic_height(&self, _measurables: &[Box<dyn Measurable>], _width: f32) -> f32 {
593 self.intrinsic_size.height
594 }
595}
596
597fn destination_rect(
598 src_size: Size,
599 dst_size: Size,
600 alignment: Alignment,
601 content_scale: ContentScale,
602) -> Rect {
603 let draw_size = content_scale.scaled_size(src_size, dst_size);
604 let allows_overflow = content_scale == ContentScale::Crop;
605 let offset_x = aligned_x_offset(
606 alignment.horizontal,
607 dst_size.width,
608 draw_size.width,
609 allows_overflow,
610 );
611 let offset_y = aligned_y_offset(
612 alignment.vertical,
613 dst_size.height,
614 draw_size.height,
615 allows_overflow,
616 );
617 Rect {
618 x: offset_x,
619 y: offset_y,
620 width: draw_size.width,
621 height: draw_size.height,
622 }
623}
624
625fn aligned_x_offset(
626 alignment: cranpose_ui_layout::HorizontalAlignment,
627 available: f32,
628 child: f32,
629 allows_overflow: bool,
630) -> f32 {
631 if !allows_overflow {
632 return alignment.align(available, child);
633 }
634
635 match alignment {
636 cranpose_ui_layout::HorizontalAlignment::Start => 0.0,
637 cranpose_ui_layout::HorizontalAlignment::CenterHorizontally => (available - child) / 2.0,
638 cranpose_ui_layout::HorizontalAlignment::End => available - child,
639 }
640}
641
642fn aligned_y_offset(
643 alignment: cranpose_ui_layout::VerticalAlignment,
644 available: f32,
645 child: f32,
646 allows_overflow: bool,
647) -> f32 {
648 if !allows_overflow {
649 return alignment.align(available, child);
650 }
651
652 match alignment {
653 cranpose_ui_layout::VerticalAlignment::Top => 0.0,
654 cranpose_ui_layout::VerticalAlignment::CenterVertically => (available - child) / 2.0,
655 cranpose_ui_layout::VerticalAlignment::Bottom => available - child,
656 }
657}
658
659fn map_destination_clip_to_source(
660 src_rect: Rect,
661 dst_rect: Rect,
662 clipped_dst_rect: Rect,
663) -> Option<Rect> {
664 if src_rect.width <= 0.0
665 || src_rect.height <= 0.0
666 || dst_rect.width <= 0.0
667 || dst_rect.height <= 0.0
668 || clipped_dst_rect.width <= 0.0
669 || clipped_dst_rect.height <= 0.0
670 {
671 return None;
672 }
673
674 let scale_x = src_rect.width / dst_rect.width;
675 let scale_y = src_rect.height / dst_rect.height;
676
677 let src_min_x = src_rect.x;
678 let src_min_y = src_rect.y;
679 let src_max_x = src_rect.x + src_rect.width;
680 let src_max_y = src_rect.y + src_rect.height;
681
682 let raw_left = src_rect.x + (clipped_dst_rect.x - dst_rect.x) * scale_x;
683 let raw_top = src_rect.y + (clipped_dst_rect.y - dst_rect.y) * scale_y;
684 let raw_right =
685 src_rect.x + ((clipped_dst_rect.x + clipped_dst_rect.width) - dst_rect.x) * scale_x;
686 let raw_bottom =
687 src_rect.y + ((clipped_dst_rect.y + clipped_dst_rect.height) - dst_rect.y) * scale_y;
688
689 let left = raw_left.clamp(src_min_x, src_max_x);
690 let top = raw_top.clamp(src_min_y, src_max_y);
691 let right = raw_right.clamp(src_min_x, src_max_x);
692 let bottom = raw_bottom.clamp(src_min_y, src_max_y);
693 let width = right - left;
694 let height = bottom - top;
695
696 if width <= 0.0 || height <= 0.0 {
697 None
698 } else {
699 Some(Rect {
700 x: left,
701 y: top,
702 width,
703 height,
704 })
705 }
706}
707
708fn image_destination_clip(
709 src_size: Size,
710 container_size: Size,
711 alignment: Alignment,
712 content_scale: ContentScale,
713) -> Option<(Rect, Rect)> {
714 let dst_rect = destination_rect(src_size, container_size, alignment, content_scale);
715 if dst_rect.width <= 0.0 || dst_rect.height <= 0.0 {
716 return None;
717 }
718
719 let container_rect = Rect::from_size(container_size);
720 let clipped_dst_rect = dst_rect.intersect(container_rect)?;
721 Some((dst_rect, clipped_dst_rect))
722}
723
724fn draw_bitmap_painter(
725 scope: &mut dyn DrawScope,
726 bitmap: ImageBitmap,
727 intrinsic_size: Size,
728 alignment: Alignment,
729 content_scale: ContentScale,
730 alpha: f32,
731 color_filter: Option<ColorFilter>,
732) {
733 let container_size = scope.size();
734 let Some((dst_rect, clipped_dst_rect)) =
735 image_destination_clip(intrinsic_size, container_size, alignment, content_scale)
736 else {
737 return;
738 };
739 let full_src_rect = Rect::from_size(Size::new(bitmap.width() as f32, bitmap.height() as f32));
740 let Some(clipped_src_rect) =
741 map_destination_clip_to_source(full_src_rect, dst_rect, clipped_dst_rect)
742 else {
743 return;
744 };
745 scope.draw_image_src_sampled(
746 bitmap,
747 clipped_src_rect,
748 clipped_dst_rect,
749 alpha,
750 color_filter,
751 ImageSampling::Linear,
752 );
753}
754
755fn draw_bitmap_region_painter(
756 scope: &mut dyn DrawScope,
757 bitmap: ImageBitmap,
758 source: Rect,
759 alignment: Alignment,
760 content_scale: ContentScale,
761 alpha: f32,
762 color_filter: Option<ColorFilter>,
763 sampling: ImageSampling,
764) {
765 let source_size = Size::new(source.width.max(0.0), source.height.max(0.0));
766 let Some((dst_rect, clipped_dst_rect)) =
767 image_destination_clip(source_size, scope.size(), alignment, content_scale)
768 else {
769 return;
770 };
771 let Some(clipped_source) = map_destination_clip_to_source(source, dst_rect, clipped_dst_rect)
772 else {
773 return;
774 };
775 scope.draw_image_src_sampled(
776 bitmap,
777 clipped_source,
778 clipped_dst_rect,
779 alpha,
780 color_filter,
781 sampling,
782 );
783}
784
785fn draw_patch_quads(
790 scope: &mut dyn DrawScope,
791 bitmap: &ImageBitmap,
792 quads: &[PatchQuad],
793 container: Rect,
794 alpha: f32,
795 color_filter: Option<ColorFilter>,
796 sampling: ImageSampling,
797) {
798 for quad in quads {
799 let Some(clipped_destination) = intersect(quad.destination, container) else {
800 continue;
801 };
802 let Some(clipped_source) =
803 map_destination_clip_to_source(quad.source, quad.destination, clipped_destination)
804 else {
805 continue;
806 };
807 scope.draw_image_src_sampled(
808 bitmap.clone(),
809 clipped_source,
810 clipped_destination,
811 alpha,
812 color_filter,
813 sampling,
814 );
815 }
816}
817
818fn intersect(rect: Rect, bounds: Rect) -> Option<Rect> {
819 let left = rect.x.max(bounds.x);
820 let top = rect.y.max(bounds.y);
821 let right = (rect.x + rect.width).min(bounds.x + bounds.width);
822 let bottom = (rect.y + rect.height).min(bounds.y + bounds.height);
823 if right <= left || bottom <= top {
824 return None;
825 }
826 Some(Rect {
827 x: left,
828 y: top,
829 width: right - left,
830 height: bottom - top,
831 })
832}
833
834fn draw_bitmap_tiled_painter(
839 scope: &mut dyn DrawScope,
840 bitmap: ImageBitmap,
841 source: Rect,
842 alpha: f32,
843 color_filter: Option<ColorFilter>,
844 sampling: ImageSampling,
845) {
846 let container = Rect::from_size(scope.size());
847 let quads = tile_quads(source, container);
848 draw_patch_quads(
849 scope,
850 &bitmap,
851 &quads,
852 container,
853 alpha,
854 color_filter,
855 sampling,
856 );
857}
858
859#[allow(clippy::too_many_arguments)]
864fn draw_nine_patch_painter(
865 scope: &mut dyn DrawScope,
866 bitmap: ImageBitmap,
867 source: Rect,
868 insets: NinePatchInsets,
869 center: PatchFill,
870 edges: PatchFill,
871 alpha: f32,
872 color_filter: Option<ColorFilter>,
873 sampling: ImageSampling,
874) {
875 let container = Rect::from_size(scope.size());
876 let quads = nine_patch_quads(source, container, insets, center, edges);
877 draw_patch_quads(
878 scope,
879 &bitmap,
880 &quads,
881 container,
882 alpha,
883 color_filter,
884 sampling,
885 );
886}
887
888fn draw_svg_painter(
889 scope: &mut dyn DrawScope,
890 svg: SvgPainter,
891 intrinsic_size: Size,
892 alignment: Alignment,
893 content_scale: ContentScale,
894 alpha: f32,
895 color_filter: Option<ColorFilter>,
896) {
897 let container_size = scope.size();
898 let Some((dst_rect, clipped_dst_rect)) =
899 image_destination_clip(intrinsic_size, container_size, alignment, content_scale)
900 else {
901 return;
902 };
903 let density = crate::render_state::current_density();
904 let pixel_size = Size::new(dst_rect.width * density, dst_rect.height * density);
905 let bitmap = match svg.rasterize(pixel_size) {
906 Ok(bitmap) => bitmap,
907 Err(error) => {
908 log::warn!("failed to rasterize SVG painter: {error}");
909 return;
910 }
911 };
912 let full_src_rect = Rect::from_size(Size::new(bitmap.width() as f32, bitmap.height() as f32));
913 let Some(clipped_src_rect) =
914 map_destination_clip_to_source(full_src_rect, dst_rect, clipped_dst_rect)
915 else {
916 return;
917 };
918 scope.draw_image_src_sampled(
919 bitmap,
920 clipped_src_rect,
921 clipped_dst_rect,
922 alpha,
923 color_filter,
924 ImageSampling::Linear,
925 );
926}
927
928#[cfg(feature = "svg")]
929fn svg_raster_key(pixel_size: Size) -> Result<SvgRasterKey, SvgPainterError> {
930 let width = svg_raster_axis(pixel_size.width)?;
931 let height = svg_raster_axis(pixel_size.height)?;
932 width
933 .checked_mul(height)
934 .and_then(|value| value.checked_mul(4))
935 .ok_or(SvgPainterError::RasterDimensionsTooLarge)?;
936 Ok(SvgRasterKey { width, height })
937}
938
939#[cfg(feature = "svg")]
940fn svg_raster_axis(value: f32) -> Result<u32, SvgPainterError> {
941 if !value.is_finite() || value <= 0.0 {
942 return Err(SvgPainterError::InvalidRasterDimensions);
943 }
944
945 let rounded = value.ceil();
946 if rounded > u32::MAX as f32 {
947 return Err(SvgPainterError::RasterDimensionsTooLarge);
948 }
949 Ok(rounded as u32)
950}
951
952#[composable]
953pub fn Image<P>(
954 painter: P,
955 content_description: Option<String>,
956 modifier: Modifier,
957 alignment: Alignment,
958 content_scale: ContentScale,
959 alpha: f32,
960 color_filter: Option<ColorFilter>,
961) -> NodeId
962where
963 P: Into<Painter> + Clone + PartialEq + 'static,
964{
965 let painter = painter.into();
966 let intrinsic_dp = painter.intrinsic_size();
969 let draw_alpha = alpha.clamp(0.0, 1.0);
970 let draw_painter = painter.clone();
971
972 let semantics_modifier = if let Some(description) = content_description {
973 Modifier::empty().semantics(move |config| {
974 config.content_description = Some(description.clone());
975 })
976 } else {
977 Modifier::empty()
978 };
979
980 let image_modifier =
981 modifier
982 .then(semantics_modifier)
983 .draw_behind(move |scope: &mut dyn DrawScope| {
984 if draw_alpha <= 0.0 {
985 return;
986 }
987 let container_size = scope.size();
988 if container_size.width <= 0.0 || container_size.height <= 0.0 {
989 return;
990 }
991 match &draw_painter.kind {
992 PainterKind::Bitmap(bitmap) => draw_bitmap_painter(
993 scope,
994 bitmap.clone(),
995 intrinsic_dp,
996 alignment,
997 content_scale,
998 draw_alpha,
999 color_filter,
1000 ),
1001 PainterKind::BitmapRegion {
1002 bitmap,
1003 source,
1004 sampling,
1005 } => draw_bitmap_region_painter(
1006 scope,
1007 bitmap.clone(),
1008 source.rect(),
1009 alignment,
1010 content_scale,
1011 draw_alpha,
1012 color_filter,
1013 *sampling,
1014 ),
1015 PainterKind::BitmapTiled {
1016 bitmap,
1017 source,
1018 sampling,
1019 } => draw_bitmap_tiled_painter(
1020 scope,
1021 bitmap.clone(),
1022 source.rect(),
1023 draw_alpha,
1024 color_filter,
1025 *sampling,
1026 ),
1027 PainterKind::NinePatch {
1028 bitmap,
1029 source,
1030 insets,
1031 center,
1032 edges,
1033 sampling,
1034 } => draw_nine_patch_painter(
1035 scope,
1036 bitmap.clone(),
1037 source.rect(),
1038 insets.insets(),
1039 *center,
1040 *edges,
1041 draw_alpha,
1042 color_filter,
1043 *sampling,
1044 ),
1045 PainterKind::Svg(svg) => draw_svg_painter(
1046 scope,
1047 svg.clone(),
1048 intrinsic_dp,
1049 alignment,
1050 content_scale,
1051 draw_alpha,
1052 color_filter,
1053 ),
1054 }
1055 });
1056
1057 Layout(
1058 image_modifier,
1059 ImageMeasurePolicy {
1060 intrinsic_size: intrinsic_dp,
1061 },
1062 || {},
1063 )
1064}
1065
1066#[cfg(test)]
1067mod tests {
1068 use super::*;
1069 use crate::layout::core::Alignment;
1070
1071 #[cfg(feature = "svg")]
1072 const RED_RECT_SVG: &[u8] = br##"
1073 <svg xmlns="http://www.w3.org/2000/svg" width="10" height="20" viewBox="0 0 10 20">
1074 <rect x="0" y="0" width="10" height="20" fill="#ff0000"/>
1075 </svg>
1076 "##;
1077
1078 #[cfg(feature = "svg")]
1079 const TRANSPARENT_CENTER_SVG: &[u8] = br##"
1080 <svg xmlns="http://www.w3.org/2000/svg" width="4" height="4" viewBox="0 0 4 4">
1081 <rect x="1" y="1" width="2" height="2" fill="#00ff00"/>
1082 </svg>
1083 "##;
1084
1085 fn sample_bitmap() -> ImageBitmap {
1086 ImageBitmap::from_rgba8(4, 2, vec![255; 4 * 2 * 4]).expect("bitmap")
1087 }
1088
1089 #[test]
1090 fn svg_painter_ids_do_not_use_process_global_counter() {
1091 let source = include_str!("image.rs");
1092 let svg_counter = ["static ", "NEXT_SVG_PAINTER_ID"].concat();
1093
1094 assert!(
1095 !source.contains(&svg_counter),
1096 "SVG painter ids must come from retained painter identity"
1097 );
1098 }
1099
1100 #[cfg(feature = "svg")]
1101 fn cache_test_bitmap(width: u32) -> ImageBitmap {
1102 ImageBitmap::from_rgba8(width, 1, vec![255; width as usize * 4]).expect("bitmap")
1103 }
1104
1105 #[cfg(feature = "svg")]
1106 fn pixel_at(bitmap: &ImageBitmap, x: u32, y: u32) -> [u8; 4] {
1107 let offset = ((y * bitmap.width() + x) * 4) as usize;
1108 let pixels = bitmap.pixels();
1109 [
1110 pixels[offset],
1111 pixels[offset + 1],
1112 pixels[offset + 2],
1113 pixels[offset + 3],
1114 ]
1115 }
1116
1117 #[test]
1118 fn painter_reports_intrinsic_size_and_bitmap() {
1119 let bitmap = sample_bitmap();
1120 let painter = BitmapPainter(bitmap.clone());
1121 assert_eq!(painter.intrinsic_size(), Size::new(4.0, 2.0));
1122 assert_eq!(painter.bitmap(), Some(&bitmap));
1123 }
1124
1125 #[test]
1126 fn bitmap_region_painter_uses_region_as_intrinsic_size() {
1127 let bitmap = sample_bitmap();
1128 let source = Rect {
1129 x: 1.0,
1130 y: 0.0,
1131 width: 2.0,
1132 height: 1.0,
1133 };
1134 let painter = BitmapRegionPainter(bitmap.clone(), source, ImageSampling::Nearest);
1135 assert_eq!(painter.intrinsic_size(), Size::new(2.0, 1.0));
1136 assert_eq!(painter.bitmap(), Some(&bitmap));
1137 assert_eq!(
1138 painter,
1139 Painter::from_bitmap_region(bitmap, source, ImageSampling::Nearest)
1140 );
1141 }
1142
1143 fn atlas_bitmap() -> ImageBitmap {
1144 ImageBitmap::from_rgba8(64, 64, vec![255; 64 * 64 * 4]).expect("bitmap")
1145 }
1146
1147 fn source_rect(x: f32, y: f32, width: f32, height: f32) -> Rect {
1148 Rect {
1149 x,
1150 y,
1151 width,
1152 height,
1153 }
1154 }
1155
1156 fn drawn_images(
1157 size: Size,
1158 draw: impl FnOnce(&mut cranpose_ui_graphics::DrawScopeDefault),
1159 ) -> Vec<(Rect, Rect)> {
1160 let mut scope = cranpose_ui_graphics::DrawScopeDefault::new(size);
1161 draw(&mut scope);
1162 scope
1163 .into_primitives()
1164 .into_iter()
1165 .filter_map(|primitive| match primitive {
1166 cranpose_ui_graphics::DrawPrimitive::Image { src_rect, rect, .. } => {
1167 Some((src_rect?, rect))
1168 }
1169 _ => None,
1170 })
1171 .collect()
1172 }
1173
1174 #[test]
1175 fn a_tiled_painter_keeps_the_region_as_its_intrinsic_size() {
1176 let bitmap = atlas_bitmap();
1177 let source = source_rect(8.0, 8.0, 16.0, 4.0);
1178 let painter = TiledPainter(bitmap.clone(), source, ImageSampling::Nearest);
1179 assert_eq!(painter.intrinsic_size(), Size::new(16.0, 4.0));
1180 assert_eq!(painter.bitmap(), Some(&bitmap));
1181 assert_eq!(
1182 painter,
1183 Painter::from_bitmap_tiled(bitmap, source, ImageSampling::Nearest)
1184 );
1185 }
1186
1187 #[test]
1188 fn a_tiled_painter_repeats_its_region_at_its_own_size() {
1189 let bitmap = atlas_bitmap();
1190 let source = source_rect(8.0, 8.0, 16.0, 4.0);
1191 let drawn = drawn_images(Size::new(32.0, 8.0), |scope| {
1192 draw_bitmap_tiled_painter(
1193 scope,
1194 bitmap.clone(),
1195 source,
1196 1.0,
1197 None,
1198 ImageSampling::Nearest,
1199 );
1200 });
1201
1202 assert_eq!(drawn.len(), 4, "two columns by two rows");
1203 assert!(
1204 drawn
1205 .iter()
1206 .all(|(src, dst)| src.width == dst.width && src.height == dst.height),
1207 "a tile is drawn at 1:1, never scaled"
1208 );
1209 assert!(
1210 drawn.iter().all(|(src, _)| src.x >= 8.0
1211 && src.y >= 8.0
1212 && src.x + src.width <= 24.0
1213 && src.y + src.height <= 12.0),
1214 "a tile never reads outside the region it was given"
1215 );
1216 assert_eq!(drawn[0].1, source_rect(0.0, 0.0, 16.0, 4.0));
1217 assert_eq!(drawn[3].1, source_rect(16.0, 4.0, 16.0, 4.0));
1218 }
1219
1220 #[test]
1221 fn a_tiled_painter_clips_the_last_row_and_column() {
1222 let bitmap = atlas_bitmap();
1223 let source = source_rect(0.0, 0.0, 10.0, 10.0);
1224 let drawn = drawn_images(Size::new(25.0, 10.0), |scope| {
1225 draw_bitmap_tiled_painter(
1226 scope,
1227 bitmap.clone(),
1228 source,
1229 1.0,
1230 None,
1231 ImageSampling::Nearest,
1232 );
1233 });
1234 assert_eq!(drawn.len(), 3);
1235 let (src, dst) = drawn[2];
1236 assert_eq!(dst, source_rect(20.0, 0.0, 5.0, 10.0));
1237 assert_eq!(src, source_rect(0.0, 0.0, 5.0, 10.0));
1238 }
1239
1240 #[test]
1241 fn a_nine_patch_painter_keeps_the_region_as_its_intrinsic_size() {
1242 let bitmap = atlas_bitmap();
1243 let source = source_rect(0.0, 0.0, 30.0, 30.0);
1244 let painter = NinePatchPainter(
1245 bitmap.clone(),
1246 source,
1247 NinePatchInsets::uniform(10.0),
1248 PatchFill::Stretch,
1249 PatchFill::Stretch,
1250 ImageSampling::Nearest,
1251 );
1252 assert_eq!(painter.intrinsic_size(), Size::new(30.0, 30.0));
1253 assert_eq!(painter.bitmap(), Some(&bitmap));
1254 assert_eq!(
1255 painter,
1256 Painter::from_nine_patch(
1257 bitmap,
1258 source,
1259 NinePatchInsets::uniform(10.0),
1260 PatchFill::Stretch,
1261 PatchFill::Stretch,
1262 ImageSampling::Nearest,
1263 ),
1264 "two painters built from the same numbers are the same painter"
1265 );
1266 }
1267
1268 #[test]
1269 fn a_nine_patch_painter_draws_its_corners_at_their_own_size() {
1270 let bitmap = atlas_bitmap();
1271 let drawn = drawn_images(Size::new(100.0, 60.0), |scope| {
1272 draw_nine_patch_painter(
1273 scope,
1274 bitmap.clone(),
1275 source_rect(0.0, 0.0, 30.0, 30.0),
1276 NinePatchInsets::uniform(10.0),
1277 PatchFill::Stretch,
1278 PatchFill::Stretch,
1279 1.0,
1280 None,
1281 ImageSampling::Nearest,
1282 );
1283 });
1284
1285 assert_eq!(drawn.len(), 9);
1286 assert_eq!(
1287 drawn[0],
1288 (
1289 source_rect(0.0, 0.0, 10.0, 10.0),
1290 source_rect(0.0, 0.0, 10.0, 10.0),
1291 )
1292 );
1293 assert_eq!(
1294 drawn[8],
1295 (
1296 source_rect(20.0, 20.0, 10.0, 10.0),
1297 source_rect(90.0, 50.0, 10.0, 10.0),
1298 )
1299 );
1300 assert_eq!(
1301 drawn[4],
1302 (
1303 source_rect(10.0, 10.0, 10.0, 10.0),
1304 source_rect(10.0, 10.0, 80.0, 40.0),
1305 ),
1306 "the middle grows on both axes"
1307 );
1308 }
1309
1310 #[test]
1311 fn a_nine_patch_painter_reads_only_its_region_of_an_atlas() {
1312 let bitmap = atlas_bitmap();
1313 let drawn = drawn_images(Size::new(80.0, 40.0), |scope| {
1314 draw_nine_patch_painter(
1315 scope,
1316 bitmap.clone(),
1317 source_rect(32.0, 16.0, 24.0, 24.0),
1318 NinePatchInsets::uniform(8.0),
1319 PatchFill::Tile,
1320 PatchFill::Tile,
1321 1.0,
1322 None,
1323 ImageSampling::Nearest,
1324 );
1325 });
1326
1327 assert!(!drawn.is_empty());
1328 assert!(
1329 drawn.iter().all(|(src, _)| src.x >= 32.0
1330 && src.y >= 16.0
1331 && src.x + src.width <= 56.0
1332 && src.y + src.height <= 40.0),
1333 "no patch may read a neighbouring sprite out of the atlas"
1334 );
1335 }
1336
1337 #[test]
1338 #[cfg(feature = "svg")]
1339 fn svg_painter_reports_intrinsic_size() {
1340 let painter = SvgPainter::from_bytes(RED_RECT_SVG).expect("svg painter");
1341 let clone = painter.clone();
1342 assert_eq!(painter.intrinsic_size(), Size::new(10.0, 20.0));
1343 assert_eq!(painter.id(), clone.id());
1344 assert_eq!(Painter::from_svg(painter).bitmap(), None);
1345 }
1346
1347 #[test]
1348 #[cfg(feature = "svg")]
1349 fn svg_painter_rasterizes_requested_dimensions() {
1350 let painter = SvgPainter::from_bytes(RED_RECT_SVG).expect("svg painter");
1351 let bitmap = painter
1352 .rasterize(Size::new(5.0, 10.0))
1353 .expect("rasterized svg");
1354
1355 assert_eq!(bitmap.width(), 5);
1356 assert_eq!(bitmap.height(), 10);
1357 assert_eq!(pixel_at(&bitmap, 2, 5), [255, 0, 0, 255]);
1358 }
1359
1360 #[test]
1361 #[cfg(feature = "svg")]
1362 fn svg_painter_preserves_transparency() {
1363 let painter = SvgPainter::from_bytes(TRANSPARENT_CENTER_SVG).expect("svg painter");
1364 let bitmap = painter
1365 .rasterize(Size::new(4.0, 4.0))
1366 .expect("rasterized svg");
1367
1368 assert_eq!(pixel_at(&bitmap, 0, 0), [0, 0, 0, 0]);
1369 assert_eq!(pixel_at(&bitmap, 2, 2), [0, 255, 0, 255]);
1370 }
1371
1372 #[test]
1373 #[cfg(feature = "svg")]
1374 fn svg_painter_reuses_cached_raster_for_same_size() {
1375 let painter = SvgPainter::from_bytes(RED_RECT_SVG).expect("svg painter");
1376 let first = painter
1377 .rasterize(Size::new(8.0, 8.0))
1378 .expect("first raster");
1379 let second = painter
1380 .rasterize(Size::new(8.0, 8.0))
1381 .expect("second raster");
1382
1383 assert_eq!(first.id(), second.id());
1384 }
1385
1386 #[test]
1387 #[cfg(feature = "svg")]
1388 fn svg_raster_cache_evicts_least_recently_used_entry() {
1389 let mut cache = SvgRasterCache::default();
1390 let keys: Vec<SvgRasterKey> = (0..SVG_RASTER_CACHE_LIMIT)
1391 .map(|index| SvgRasterKey {
1392 width: index as u32 + 1,
1393 height: 1,
1394 })
1395 .collect();
1396
1397 for key in &keys {
1398 cache.insert(*key, cache_test_bitmap(key.width));
1399 }
1400
1401 let recent = cache.get(keys[0]).expect("cached raster");
1402 let new_key = SvgRasterKey {
1403 width: SVG_RASTER_CACHE_LIMIT as u32 + 1,
1404 height: 1,
1405 };
1406 cache.insert(new_key, cache_test_bitmap(new_key.width));
1407
1408 assert!(cache.get(keys[1]).is_none());
1409 assert_eq!(
1410 cache.get(keys[0]).expect("retained raster").id(),
1411 recent.id()
1412 );
1413 assert!(cache.get(new_key).is_some());
1414 }
1415
1416 #[test]
1417 #[cfg(feature = "svg")]
1418 fn svg_painter_rasterizes_distinct_sizes_separately() {
1419 let painter = SvgPainter::from_bytes(RED_RECT_SVG).expect("svg painter");
1420 let small = painter
1421 .rasterize(Size::new(8.0, 8.0))
1422 .expect("small raster");
1423 let large = painter
1424 .rasterize(Size::new(16.0, 16.0))
1425 .expect("large raster");
1426
1427 assert_ne!(small.id(), large.id());
1428 assert_eq!(large.width(), 16);
1429 assert_eq!(large.height(), 16);
1430 }
1431
1432 #[test]
1433 #[cfg(feature = "svg")]
1434 fn svg_painter_cache_recovers_after_poison() {
1435 let painter = SvgPainter::from_bytes(RED_RECT_SVG).expect("svg painter");
1436 let poison_result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1437 let _guard = painter
1438 .inner
1439 .cache
1440 .lock()
1441 .unwrap_or_else(|poisoned| poisoned.into_inner());
1442 panic!("poison svg raster cache for recovery test");
1443 }));
1444
1445 assert!(poison_result.is_err());
1446
1447 let bitmap = painter
1448 .rasterize(Size::new(8.0, 8.0))
1449 .expect("svg raster cache should recover after poisoning");
1450 assert_eq!(bitmap.width(), 8);
1451 assert_eq!(bitmap.height(), 8);
1452 }
1453
1454 #[test]
1455 fn svg_draw_density_has_no_outside_context_fallback() {
1456 let source = include_str!("image.rs");
1457 let fallback_call = ["try_current", "_density()", ".unwrap_or"].concat();
1458 assert!(
1459 !source.contains(&fallback_call),
1460 "SVG image drawing must use the active AppContext density"
1461 );
1462 }
1463
1464 #[test]
1465 #[cfg(feature = "svg")]
1466 fn svg_painter_rejects_invalid_bytes() {
1467 let err = SvgPainter::from_bytes(b"not svg").expect_err("invalid svg");
1468 assert!(matches!(err, SvgPainterError::Parse(_)));
1469 }
1470
1471 #[test]
1472 #[cfg(not(feature = "svg"))]
1473 fn svg_painter_reports_disabled_feature_without_resvg() {
1474 let err = SvgPainter::from_bytes(b"not svg").expect_err("svg feature disabled");
1475 assert!(matches!(err, SvgPainterError::SvgFeatureDisabled));
1476 }
1477
1478 #[test]
1479 fn fit_keeps_aspect_ratio() {
1480 let src = Size::new(200.0, 100.0);
1481 let dst = Size::new(300.0, 300.0);
1482 let result = ContentScale::Fit.scaled_size(src, dst);
1483 assert_eq!(result, Size::new(300.0, 150.0));
1484 }
1485
1486 #[test]
1487 fn crop_fills_bounds() {
1488 let src = Size::new(200.0, 100.0);
1489 let dst = Size::new(300.0, 300.0);
1490 let result = ContentScale::Crop.scaled_size(src, dst);
1491 assert_eq!(result, Size::new(600.0, 300.0));
1492 }
1493
1494 #[test]
1495 fn destination_rect_aligns_center() {
1496 let src = Size::new(200.0, 100.0);
1497 let dst = Size::new(300.0, 300.0);
1498 let rect = destination_rect(src, dst, Alignment::CENTER, ContentScale::Fit);
1499 assert_eq!(
1500 rect,
1501 Rect {
1502 x: 0.0,
1503 y: 75.0,
1504 width: 300.0,
1505 height: 150.0,
1506 }
1507 );
1508 }
1509
1510 #[test]
1511 fn crop_destination_clip_maps_centered_wide_source() {
1512 let src = Size::new(200.0, 100.0);
1513 let dst = Size::new(100.0, 100.0);
1514 let (dst_rect, clipped_dst_rect) =
1515 image_destination_clip(src, dst, Alignment::CENTER, ContentScale::Crop)
1516 .expect("destination clip");
1517 let rect = map_destination_clip_to_source(Rect::from_size(src), dst_rect, clipped_dst_rect)
1518 .expect("source clip");
1519 assert_eq!(
1520 rect,
1521 Rect {
1522 x: 50.0,
1523 y: 0.0,
1524 width: 100.0,
1525 height: 100.0,
1526 }
1527 );
1528 }
1529
1530 #[test]
1531 fn crop_destination_clip_honors_start_alignment() {
1532 let src = Size::new(200.0, 100.0);
1533 let dst = Size::new(100.0, 100.0);
1534 let (dst_rect, clipped_dst_rect) =
1535 image_destination_clip(src, dst, Alignment::TOP_START, ContentScale::Crop)
1536 .expect("destination clip");
1537 let rect = map_destination_clip_to_source(Rect::from_size(src), dst_rect, clipped_dst_rect)
1538 .expect("source clip");
1539 assert_eq!(
1540 rect,
1541 Rect {
1542 x: 0.0,
1543 y: 0.0,
1544 width: 100.0,
1545 height: 100.0,
1546 }
1547 );
1548 }
1549
1550 fn approx_eq(left: f32, right: f32) {
1551 assert!((left - right).abs() < 1e-4, "left={left}, right={right}");
1552 }
1553
1554 #[test]
1555 fn map_destination_clip_to_source_scales_proportionally() {
1556 let src = Rect {
1557 x: 0.0,
1558 y: 0.0,
1559 width: 100.0,
1560 height: 100.0,
1561 };
1562 let dst = Rect {
1563 x: -50.0,
1564 y: 0.0,
1565 width: 200.0,
1566 height: 100.0,
1567 };
1568 let clipped_dst = Rect {
1569 x: 0.0,
1570 y: 0.0,
1571 width: 100.0,
1572 height: 100.0,
1573 };
1574 let mapped = map_destination_clip_to_source(src, dst, clipped_dst).expect("mapped");
1575 approx_eq(mapped.x, 25.0);
1576 approx_eq(mapped.y, 0.0);
1577 approx_eq(mapped.width, 50.0);
1578 approx_eq(mapped.height, 100.0);
1579 }
1580
1581 #[test]
1582 fn map_destination_clip_to_source_returns_full_source_without_clipping() {
1583 let src = Rect {
1584 x: 0.0,
1585 y: 0.0,
1586 width: 120.0,
1587 height: 80.0,
1588 };
1589 let dst = Rect {
1590 x: 10.0,
1591 y: 5.0,
1592 width: 60.0,
1593 height: 40.0,
1594 };
1595 let mapped = map_destination_clip_to_source(src, dst, dst).expect("mapped");
1596 approx_eq(mapped.x, src.x);
1597 approx_eq(mapped.y, src.y);
1598 approx_eq(mapped.width, src.width);
1599 approx_eq(mapped.height, src.height);
1600 }
1601
1602 fn measure_image(intrinsic: Size, constraints: Constraints) -> Size {
1605 let policy = ImageMeasurePolicy {
1606 intrinsic_size: intrinsic,
1607 };
1608 let scope = crate::density::DensityMeasureScope::new(crate::density::Density::default());
1609 policy.measure(&scope, &[], constraints).size
1610 }
1611
1612 #[test]
1613 fn image_measure_unconstrained() {
1614 let size = measure_image(
1615 Size::new(800.0, 600.0),
1616 Constraints::loose(f32::INFINITY, f32::INFINITY),
1617 );
1618 assert_eq!(size, Size::new(800.0, 600.0));
1619 }
1620
1621 #[test]
1622 fn image_measure_width_constrained_preserves_aspect_ratio() {
1623 let size = measure_image(
1625 Size::new(800.0, 600.0),
1626 Constraints::loose(400.0, f32::INFINITY),
1627 );
1628 assert_eq!(size, Size::new(400.0, 300.0));
1629 }
1630
1631 #[test]
1632 fn image_measure_height_constrained_preserves_aspect_ratio() {
1633 let size = measure_image(
1635 Size::new(800.0, 600.0),
1636 Constraints::loose(f32::INFINITY, 300.0),
1637 );
1638 assert_eq!(size, Size::new(400.0, 300.0));
1639 }
1640
1641 #[test]
1642 fn image_measure_both_constrained_uses_smaller_factor() {
1643 let size = measure_image(Size::new(800.0, 600.0), Constraints::loose(200.0, 400.0));
1646 assert_eq!(size, Size::new(200.0, 150.0));
1647 }
1648
1649 #[test]
1650 fn image_measure_fits_within_constraints() {
1651 let size = measure_image(Size::new(200.0, 100.0), Constraints::loose(400.0, 400.0));
1653 assert_eq!(size, Size::new(200.0, 100.0));
1654 }
1655
1656 #[test]
1657 fn image_measure_zero_intrinsic() {
1658 let size = measure_image(Size::ZERO, Constraints::loose(400.0, 400.0));
1659 assert_eq!(size, Size::new(0.0, 0.0));
1660 }
1661}