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