1#![expect(non_snake_case, reason = "names follow Compose's casing")]
4
5#[cfg(feature = "svg")]
6use std::sync::{Mutex, MutexGuard};
7use std::{
8 hash::{Hash, Hasher},
9 sync::Arc,
10};
11
12use cranpose_core::NodeId;
13use cranpose_ui_graphics::{ColorFilter, DrawScope, ImageBitmap, ImageSampling};
14use cranpose_ui_layout::{Constraints, MeasurePolicy, MeasureResult, MeasureScope, Placement};
15use thiserror::Error;
16
17use crate::{
18 composable,
19 layout::core::{Alignment, Measurable},
20 modifier::{Modifier, Rect, Size},
21 nine_patch::{NinePatchInsets, PatchFill, PatchQuad, nine_patch_quads, tile_quads},
22 widgets::Layout,
23};
24
25#[cfg(feature = "svg")]
26#[path = "image_svg.rs"]
27mod image_svg;
28
29pub const DEFAULT_ALPHA: f32 = 1.0;
31#[cfg(feature = "svg")]
32const SVG_RASTER_CACHE_LIMIT: usize = 8;
33
34#[derive(Clone, Copy, Debug, PartialEq)]
36pub enum ContentScale {
37 Fit,
40 Crop,
42 FillBounds,
44 FillWidth,
46 FillHeight,
48 Inside,
50 None,
52}
53
54impl ContentScale {
55 pub fn scaled_size(self, src_size: Size, dst_size: Size) -> Size {
59 if src_size.width <= 0.0
60 || src_size.height <= 0.0
61 || dst_size.width <= 0.0
62 || dst_size.height <= 0.0
63 {
64 return Size::ZERO;
65 }
66
67 let scale_x = dst_size.width / src_size.width;
68 let scale_y = dst_size.height / src_size.height;
69
70 let (factor_x, factor_y) = match self {
71 Self::Fit => {
72 let factor = scale_x.min(scale_y);
73 (factor, factor)
74 }
75 Self::Crop => {
76 let factor = scale_x.max(scale_y);
77 (factor, factor)
78 }
79 Self::FillBounds => (scale_x, scale_y),
80 Self::FillWidth => (scale_x, scale_x),
81 Self::FillHeight => (scale_y, scale_y),
82 Self::Inside => {
83 if src_size.width <= dst_size.width && src_size.height <= dst_size.height {
84 (1.0, 1.0)
85 } else {
86 let factor = scale_x.min(scale_y);
87 (factor, factor)
88 }
89 }
90 Self::None => (1.0, 1.0),
91 };
92
93 Size {
94 width: src_size.width * factor_x,
95 height: src_size.height * factor_y,
96 }
97 }
98}
99
100#[derive(Clone, Debug, PartialEq, Eq, Hash)]
103pub struct Painter {
104 kind: PainterKind,
105}
106
107#[derive(Clone, Debug, PartialEq, Eq, Hash)]
108enum PainterKind {
109 Bitmap(ImageBitmap),
110 BitmapRegion {
111 bitmap: ImageBitmap,
112 source: Quad,
113 sampling: ImageSampling,
114 },
115 BitmapTiled {
118 bitmap: ImageBitmap,
119 source: Quad,
120 sampling: ImageSampling,
121 },
122 NinePatch {
124 bitmap: ImageBitmap,
125 source: Quad,
126 insets: Quad,
127 center: PatchFill,
128 edges: PatchFill,
129 sampling: ImageSampling,
130 },
131 Svg(SvgPainter),
132}
133
134#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
141struct Quad {
142 a: u32,
143 b: u32,
144 c: u32,
145 d: u32,
146}
147
148impl Quad {
149 fn of(a: f32, b: f32, c: f32, d: f32) -> Self {
150 Self {
151 a: a.to_bits(),
152 b: b.to_bits(),
153 c: c.to_bits(),
154 d: d.to_bits(),
155 }
156 }
157
158 fn values(self) -> (f32, f32, f32, f32) {
159 (
160 f32::from_bits(self.a),
161 f32::from_bits(self.b),
162 f32::from_bits(self.c),
163 f32::from_bits(self.d),
164 )
165 }
166
167 fn from_rect(source: Rect) -> Self {
168 Self::of(source.x, source.y, source.width, source.height)
169 }
170
171 fn rect(self) -> Rect {
172 let (x, y, width, height) = self.values();
173 Rect {
174 x,
175 y,
176 width,
177 height,
178 }
179 }
180
181 fn from_insets(insets: NinePatchInsets) -> Self {
182 Self::of(insets.left, insets.top, insets.right, insets.bottom)
183 }
184
185 fn insets(self) -> NinePatchInsets {
186 let (left, top, right, bottom) = self.values();
187 NinePatchInsets::new(left, top, right, bottom)
188 }
189}
190
191impl Painter {
192 pub fn from_bitmap(bitmap: ImageBitmap) -> Self {
194 Self {
195 kind: PainterKind::Bitmap(bitmap),
196 }
197 }
198
199 pub fn from_bitmap_region(bitmap: ImageBitmap, source: Rect, sampling: ImageSampling) -> Self {
201 Self {
202 kind: PainterKind::BitmapRegion {
203 bitmap,
204 source: Quad::from_rect(source),
205 sampling,
206 },
207 }
208 }
209
210 pub fn from_bitmap_tiled(bitmap: ImageBitmap, source: Rect, sampling: ImageSampling) -> Self {
217 Self {
218 kind: PainterKind::BitmapTiled {
219 bitmap,
220 source: Quad::from_rect(source),
221 sampling,
222 },
223 }
224 }
225
226 pub fn from_nine_patch(
235 bitmap: ImageBitmap,
236 source: Rect,
237 insets: NinePatchInsets,
238 center: PatchFill,
239 edges: PatchFill,
240 sampling: ImageSampling,
241 ) -> Self {
242 Self {
243 kind: PainterKind::NinePatch {
244 bitmap,
245 source: Quad::from_rect(source),
246 insets: Quad::from_insets(insets),
247 center,
248 edges,
249 sampling,
250 },
251 }
252 }
253
254 pub fn from_svg(svg: SvgPainter) -> Self {
256 Self {
257 kind: PainterKind::Svg(svg),
258 }
259 }
260
261 pub fn intrinsic_size(&self) -> Size {
264 match &self.kind {
265 PainterKind::Bitmap(bitmap) => bitmap.intrinsic_size(),
266 PainterKind::BitmapRegion { source, .. }
267 | PainterKind::BitmapTiled { source, .. }
268 | PainterKind::NinePatch { source, .. } => {
269 let source = source.rect();
270 Size::new(source.width.max(0.0), source.height.max(0.0))
271 }
272 PainterKind::Svg(svg) => svg.intrinsic_size(),
273 }
274 }
275
276 pub fn as_bitmap(&self) -> Option<&ImageBitmap> {
279 match &self.kind {
280 PainterKind::Bitmap(bitmap) => Some(bitmap),
281 PainterKind::BitmapRegion { bitmap, .. }
282 | PainterKind::BitmapTiled { bitmap, .. }
283 | PainterKind::NinePatch { bitmap, .. } => Some(bitmap),
284 PainterKind::Svg(_) => None,
285 }
286 }
287
288 pub fn bitmap(&self) -> Option<&ImageBitmap> {
290 self.as_bitmap()
291 }
292}
293
294impl From<ImageBitmap> for Painter {
295 fn from(value: ImageBitmap) -> Self {
296 Self::from_bitmap(value)
297 }
298}
299
300impl From<SvgPainter> for Painter {
301 fn from(value: SvgPainter) -> Self {
302 Self::from_svg(value)
303 }
304}
305
306pub fn BitmapPainter(bitmap: ImageBitmap) -> Painter {
308 Painter::from_bitmap(bitmap)
309}
310
311pub fn BitmapRegionPainter(bitmap: ImageBitmap, source: Rect, sampling: ImageSampling) -> Painter {
313 Painter::from_bitmap_region(bitmap, source, sampling)
314}
315
316pub fn TiledPainter(bitmap: ImageBitmap, source: Rect, sampling: ImageSampling) -> Painter {
318 Painter::from_bitmap_tiled(bitmap, source, sampling)
319}
320
321pub fn NinePatchPainter(
323 bitmap: ImageBitmap,
324 source: Rect,
325 insets: NinePatchInsets,
326 center: PatchFill,
327 edges: PatchFill,
328 sampling: ImageSampling,
329) -> Painter {
330 Painter::from_nine_patch(bitmap, source, insets, center, edges, sampling)
331}
332
333#[derive(Debug, Clone, PartialEq, Eq, Error)]
335pub enum SvgPainterError {
336 #[error("SVG support is disabled; enable the cranpose-ui `svg` feature")]
338 SvgFeatureDisabled,
339 #[error("failed to parse SVG: {0}")]
341 Parse(String),
342 #[error("SVG raster dimensions must be greater than zero")]
344 InvalidRasterDimensions,
345 #[error("SVG raster dimensions are too large")]
347 RasterDimensionsTooLarge,
348 #[error("failed to allocate SVG raster {width}x{height}")]
350 RasterAllocationFailed {
351 width: u32,
353 height: u32,
355 },
356 #[error("SVG raster cache is unavailable")]
358 RasterCacheUnavailable,
359 #[error(transparent)]
361 ImageBitmap(#[from] cranpose_ui_graphics::ImageBitmapError),
362}
363
364#[derive(Clone)]
366pub struct SvgPainter {
367 inner: Arc<SvgPainterInner>,
368}
369
370struct SvgPainterInner {
371 #[cfg(feature = "svg")]
372 document: image_svg::SvgDocument,
373 intrinsic_size: Size,
374 #[cfg(feature = "svg")]
375 cache: Mutex<SvgRasterCache>,
376}
377
378#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
379#[cfg(feature = "svg")]
380struct SvgRasterKey {
381 width: u32,
382 height: u32,
383}
384
385#[derive(Clone, Debug)]
386#[cfg(feature = "svg")]
387struct SvgRasterEntry {
388 key: SvgRasterKey,
389 bitmap: ImageBitmap,
390}
391
392#[derive(Default, Debug)]
393#[cfg(feature = "svg")]
394struct SvgRasterCache {
395 entries: Vec<SvgRasterEntry>,
396}
397
398impl SvgPainter {
399 pub fn from_bytes(bytes: &[u8]) -> Result<Self, SvgPainterError> {
402 #[cfg(not(feature = "svg"))]
403 {
404 let _ = bytes;
405 Err(SvgPainterError::SvgFeatureDisabled)
406 }
407 #[cfg(feature = "svg")]
408 {
409 let document = image_svg::parse_svg_document(bytes)?;
410 let intrinsic_size = document.intrinsic_size();
411
412 Ok(Self {
413 inner: Arc::new(SvgPainterInner {
414 document,
415 intrinsic_size,
416 cache: Mutex::new(SvgRasterCache::default()),
417 }),
418 })
419 }
420 }
421
422 pub fn id(&self) -> u64 {
425 Arc::as_ptr(&self.inner) as usize as u64
426 }
427
428 pub fn intrinsic_size(&self) -> Size {
430 self.inner.intrinsic_size
431 }
432
433 pub fn rasterize(&self, pixel_size: Size) -> Result<ImageBitmap, SvgPainterError> {
436 #[cfg(not(feature = "svg"))]
437 {
438 let _ = pixel_size;
439 Err(SvgPainterError::SvgFeatureDisabled)
440 }
441 #[cfg(feature = "svg")]
442 {
443 let key = svg_raster_key(pixel_size)?;
444 if let Some(bitmap) = self.cached_bitmap(key)? {
445 return Ok(bitmap);
446 }
447
448 let bitmap = self.rasterize_uncached(key)?;
449 self.cache_bitmap(key, bitmap.clone())?;
450 Ok(bitmap)
451 }
452 }
453
454 #[cfg(feature = "svg")]
455 fn cached_bitmap(&self, key: SvgRasterKey) -> Result<Option<ImageBitmap>, SvgPainterError> {
456 Ok(self.lock_cache().get(key))
457 }
458
459 #[cfg(feature = "svg")]
460 fn cache_bitmap(&self, key: SvgRasterKey, bitmap: ImageBitmap) -> Result<(), SvgPainterError> {
461 self.lock_cache().insert(key, bitmap);
462 Ok(())
463 }
464
465 #[cfg(feature = "svg")]
466 fn lock_cache(&self) -> MutexGuard<'_, SvgRasterCache> {
467 self.inner
468 .cache
469 .lock()
470 .unwrap_or_else(std::sync::PoisonError::into_inner)
471 }
472
473 #[cfg(feature = "svg")]
474 fn rasterize_uncached(&self, key: SvgRasterKey) -> Result<ImageBitmap, SvgPainterError> {
475 (key.width as usize)
476 .checked_mul(key.height as usize)
477 .and_then(|value| value.checked_mul(4))
478 .ok_or(SvgPainterError::RasterDimensionsTooLarge)?;
479
480 let mut pixmap = tiny_skia::Pixmap::new(key.width, key.height).ok_or(
481 SvgPainterError::RasterAllocationFailed {
482 width: key.width,
483 height: key.height,
484 },
485 )?;
486 image_svg::rasterize_svg_document(&self.inner.document, &mut pixmap);
487 let pixels = image_svg::demultiplied_rgba_pixels(&pixmap);
488 Ok(ImageBitmap::from_rgba8(key.width, key.height, pixels)?)
489 }
490}
491
492impl std::fmt::Debug for SvgPainter {
493 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
494 f.debug_struct("SvgPainter")
495 .field("id", &self.id())
496 .field("intrinsic_size", &self.intrinsic_size())
497 .finish_non_exhaustive()
498 }
499}
500
501impl PartialEq for SvgPainter {
502 fn eq(&self, other: &Self) -> bool {
503 self.id() == other.id()
504 }
505}
506
507impl Eq for SvgPainter {}
508
509impl Hash for SvgPainter {
510 fn hash<H: Hasher>(&self, state: &mut H) {
511 self.id().hash(state);
512 }
513}
514
515#[cfg(feature = "svg")]
516impl SvgRasterCache {
517 fn get(&mut self, key: SvgRasterKey) -> Option<ImageBitmap> {
518 let position = self.entries.iter().position(|entry| entry.key == key)?;
519 let entry = self.entries.remove(position);
520 let bitmap = entry.bitmap.clone();
521 self.entries.push(entry);
522 Some(bitmap)
523 }
524
525 fn insert(&mut self, key: SvgRasterKey, bitmap: ImageBitmap) {
526 if let Some(position) = self.entries.iter().position(|entry| entry.key == key) {
527 self.entries.remove(position);
528 } else if self.entries.len() >= SVG_RASTER_CACHE_LIMIT {
529 self.entries.remove(0);
530 }
531 self.entries.push(SvgRasterEntry { key, bitmap });
532 }
533}
534
535#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
536struct SvgBytesKey {
537 ptr: usize,
538 len: usize,
539}
540
541impl SvgBytesKey {
542 fn new(bytes: &'static [u8]) -> Self {
543 Self {
544 ptr: bytes.as_ptr() as usize,
545 len: bytes.len(),
546 }
547 }
548}
549
550pub fn rememberSvg(bytes: &'static [u8]) -> Result<SvgPainter, SvgPainterError> {
553 let key = SvgBytesKey::new(bytes);
554 cranpose_core::withCurrentComposer(|composer| {
555 composer.with_key(&key, |composer| {
556 composer
557 .remember(|| SvgPainter::from_bytes(bytes))
558 .with(Clone::clone)
559 })
560 })
561}
562
563#[derive(Clone, Debug, PartialEq)]
570struct ImageMeasurePolicy {
571 intrinsic_size: Size,
572}
573
574impl MeasurePolicy for ImageMeasurePolicy {
575 fn measure(
576 &self,
577 scope: &dyn MeasureScope,
578 _measurables: &[Box<dyn Measurable>],
579 constraints: Constraints,
580 ) -> MeasureResult {
581 let mut placements = Vec::new();
582 let measurement = self.measure_into(scope, &[], constraints, &mut placements);
583 MeasureResult::new(measurement, placements)
584 }
585
586 fn measure_into(
587 &self,
588 _scope: &dyn MeasureScope,
589 _measurables: &[Box<dyn Measurable>],
590 constraints: Constraints,
591 placements: &mut Vec<Placement>,
592 ) -> cranpose_ui_layout::Measurement {
593 placements.clear();
594 let iw = self.intrinsic_size.width;
595 let ih = self.intrinsic_size.height;
596
597 if iw <= 0.0 || ih <= 0.0 {
598 let (w, h) = constraints.constrain(0.0, 0.0);
599 return Size {
600 width: w,
601 height: h,
602 }
603 .into();
604 }
605
606 let cw = iw.clamp(constraints.min_width, constraints.max_width);
607 let ch = ih.clamp(constraints.min_height, constraints.max_height);
608
609 let scale_x = cw / iw;
610 let scale_y = ch / ih;
611
612 let (width, height) = if scale_x < 1.0 || scale_y < 1.0 {
613 let factor = scale_x.min(scale_y);
614 let w = (iw * factor).clamp(constraints.min_width, constraints.max_width);
615 let h = (ih * factor).clamp(constraints.min_height, constraints.max_height);
616 (w, h)
617 } else {
618 (cw, ch)
619 };
620
621 Size { width, height }.into()
622 }
623
624 fn min_intrinsic_width(&self, _measurables: &[Box<dyn Measurable>], _height: f32) -> f32 {
625 self.intrinsic_size.width
626 }
627
628 fn max_intrinsic_width(&self, _measurables: &[Box<dyn Measurable>], _height: f32) -> f32 {
629 self.intrinsic_size.width
630 }
631
632 fn min_intrinsic_height(&self, _measurables: &[Box<dyn Measurable>], _width: f32) -> f32 {
633 self.intrinsic_size.height
634 }
635
636 fn max_intrinsic_height(&self, _measurables: &[Box<dyn Measurable>], _width: f32) -> f32 {
637 self.intrinsic_size.height
638 }
639}
640
641fn destination_rect(
645 src_size: Size,
646 dst_size: Size,
647 alignment: Alignment,
648 content_scale: ContentScale,
649 density: f32,
650) -> Rect {
651 use cranpose_ui_layout::round_to_px;
652
653 let draw_size = content_scale.scaled_size(src_size, dst_size);
654 Rect {
655 x: alignment.horizontal.align(
656 round_to_px(dst_size.width, density),
657 round_to_px(draw_size.width, density),
658 density,
659 ),
660 y: alignment.vertical.align(
661 round_to_px(dst_size.height, density),
662 round_to_px(draw_size.height, density),
663 density,
664 ),
665 width: draw_size.width,
666 height: draw_size.height,
667 }
668}
669
670fn map_destination_clip_to_source(
671 src_rect: Rect,
672 dst_rect: Rect,
673 clipped_dst_rect: Rect,
674) -> Option<Rect> {
675 if src_rect.width <= 0.0
676 || src_rect.height <= 0.0
677 || dst_rect.width <= 0.0
678 || dst_rect.height <= 0.0
679 || clipped_dst_rect.width <= 0.0
680 || clipped_dst_rect.height <= 0.0
681 {
682 return None;
683 }
684
685 let scale_x = src_rect.width / dst_rect.width;
686 let scale_y = src_rect.height / dst_rect.height;
687
688 let src_min_x = src_rect.x;
689 let src_min_y = src_rect.y;
690 let src_max_x = src_rect.x + src_rect.width;
691 let src_max_y = src_rect.y + src_rect.height;
692
693 let raw_left = src_rect.x + (clipped_dst_rect.x - dst_rect.x) * scale_x;
694 let raw_top = src_rect.y + (clipped_dst_rect.y - dst_rect.y) * scale_y;
695 let raw_right =
696 src_rect.x + ((clipped_dst_rect.x + clipped_dst_rect.width) - dst_rect.x) * scale_x;
697 let raw_bottom =
698 src_rect.y + ((clipped_dst_rect.y + clipped_dst_rect.height) - dst_rect.y) * scale_y;
699
700 let left = raw_left.clamp(src_min_x, src_max_x);
701 let top = raw_top.clamp(src_min_y, src_max_y);
702 let right = raw_right.clamp(src_min_x, src_max_x);
703 let bottom = raw_bottom.clamp(src_min_y, src_max_y);
704 let width = right - left;
705 let height = bottom - top;
706
707 if width <= 0.0 || height <= 0.0 {
708 None
709 } else {
710 Some(Rect {
711 x: left,
712 y: top,
713 width,
714 height,
715 })
716 }
717}
718
719fn image_destination_clip(
720 src_size: Size,
721 container_size: Size,
722 alignment: Alignment,
723 content_scale: ContentScale,
724 density: f32,
725) -> Option<(Rect, Rect)> {
726 let dst_rect = destination_rect(src_size, container_size, alignment, content_scale, density);
727 if dst_rect.width <= 0.0 || dst_rect.height <= 0.0 {
728 return None;
729 }
730
731 let container_rect = Rect::from_size(container_size);
732 let clipped_dst_rect = dst_rect.intersect(container_rect)?;
733 Some((dst_rect, clipped_dst_rect))
734}
735
736struct ImageDraw {
739 alignment: Alignment,
740 content_scale: ContentScale,
741 alpha: f32,
742 color_filter: Option<ColorFilter>,
743 density: f32,
744}
745
746impl ImageDraw {
747 fn clip(&self, src_size: Size, container_size: Size) -> Option<(Rect, Rect)> {
748 image_destination_clip(
749 src_size,
750 container_size,
751 self.alignment,
752 self.content_scale,
753 self.density,
754 )
755 }
756}
757
758fn draw_bitmap_painter(
759 scope: &mut dyn DrawScope,
760 bitmap: ImageBitmap,
761 intrinsic_size: Size,
762 draw: &ImageDraw,
763) {
764 let Some((dst_rect, clipped_dst_rect)) = draw.clip(intrinsic_size, scope.size()) else {
765 return;
766 };
767 let full_src_rect = Rect::from_size(Size::new(bitmap.width() as f32, bitmap.height() as f32));
768 let Some(clipped_src_rect) =
769 map_destination_clip_to_source(full_src_rect, dst_rect, clipped_dst_rect)
770 else {
771 return;
772 };
773 scope.draw_image_src_sampled(
774 bitmap,
775 clipped_src_rect,
776 clipped_dst_rect,
777 draw.alpha,
778 draw.color_filter.clone(),
779 ImageSampling::Linear,
780 );
781}
782
783fn draw_bitmap_region_painter(
784 scope: &mut dyn DrawScope,
785 bitmap: ImageBitmap,
786 source: Rect,
787 sampling: ImageSampling,
788 draw: &ImageDraw,
789) {
790 let source_size = Size::new(source.width.max(0.0), source.height.max(0.0));
791 let Some((dst_rect, clipped_dst_rect)) = draw.clip(source_size, scope.size()) else {
792 return;
793 };
794 let Some(clipped_source) = map_destination_clip_to_source(source, dst_rect, clipped_dst_rect)
795 else {
796 return;
797 };
798 scope.draw_image_src_sampled(
799 bitmap,
800 clipped_source,
801 clipped_dst_rect,
802 draw.alpha,
803 draw.color_filter.clone(),
804 sampling,
805 );
806}
807
808fn draw_patch_quads(
813 scope: &mut dyn DrawScope,
814 bitmap: &ImageBitmap,
815 quads: &[PatchQuad],
816 container: Rect,
817 alpha: f32,
818 color_filter: Option<ColorFilter>,
819 sampling: ImageSampling,
820) {
821 for quad in quads {
822 let Some(clipped_destination) = intersect(quad.destination, container) else {
823 continue;
824 };
825 let Some(clipped_source) =
826 map_destination_clip_to_source(quad.source, quad.destination, clipped_destination)
827 else {
828 continue;
829 };
830 scope.draw_image_src_sampled(
831 bitmap.clone(),
832 clipped_source,
833 clipped_destination,
834 alpha,
835 color_filter.clone(),
836 sampling,
837 );
838 }
839}
840
841fn intersect(rect: Rect, bounds: Rect) -> Option<Rect> {
842 let left = rect.x.max(bounds.x);
843 let top = rect.y.max(bounds.y);
844 let right = (rect.x + rect.width).min(bounds.x + bounds.width);
845 let bottom = (rect.y + rect.height).min(bounds.y + bounds.height);
846 if right <= left || bottom <= top {
847 return None;
848 }
849 Some(Rect {
850 x: left,
851 y: top,
852 width: right - left,
853 height: bottom - top,
854 })
855}
856
857fn draw_filled_painter(
861 scope: &mut dyn DrawScope,
862 bitmap: &ImageBitmap,
863 quads_of: impl FnOnce(Rect) -> Vec<PatchQuad>,
864 alpha: f32,
865 color_filter: Option<ColorFilter>,
866 sampling: ImageSampling,
867) {
868 let container = Rect::from_size(scope.size());
869 let quads = quads_of(container);
870 draw_patch_quads(
871 scope,
872 bitmap,
873 &quads,
874 container,
875 alpha,
876 color_filter,
877 sampling,
878 );
879}
880
881fn draw_svg_painter(
882 scope: &mut dyn DrawScope,
883 svg: SvgPainter,
884 intrinsic_size: Size,
885 draw: &ImageDraw,
886) {
887 let Some((dst_rect, clipped_dst_rect)) = draw.clip(intrinsic_size, scope.size()) else {
888 return;
889 };
890 let pixel_size = Size::new(
891 dst_rect.width * draw.density,
892 dst_rect.height * draw.density,
893 );
894 let bitmap = match svg.rasterize(pixel_size) {
895 Ok(bitmap) => bitmap,
896 Err(error) => {
897 log::warn!("failed to rasterize SVG painter: {error}");
898 return;
899 }
900 };
901 let full_src_rect = Rect::from_size(Size::new(bitmap.width() as f32, bitmap.height() as f32));
902 let Some(clipped_src_rect) =
903 map_destination_clip_to_source(full_src_rect, dst_rect, clipped_dst_rect)
904 else {
905 return;
906 };
907 scope.draw_image_src_sampled(
908 bitmap,
909 clipped_src_rect,
910 clipped_dst_rect,
911 draw.alpha,
912 draw.color_filter.clone(),
913 ImageSampling::Linear,
914 );
915}
916
917#[cfg(feature = "svg")]
918fn svg_raster_key(pixel_size: Size) -> Result<SvgRasterKey, SvgPainterError> {
919 let width = image_svg::raster_axis(pixel_size.width)?;
920 let height = image_svg::raster_axis(pixel_size.height)?;
921 width
922 .checked_mul(height)
923 .and_then(|value| value.checked_mul(4))
924 .ok_or(SvgPainterError::RasterDimensionsTooLarge)?;
925 Ok(SvgRasterKey { width, height })
926}
927
928#[composable]
933pub fn Image<P>(
934 painter: P,
935 content_description: Option<String>,
936 modifier: Modifier,
937 alignment: Alignment,
938 content_scale: ContentScale,
939 alpha: f32,
940 color_filter: Option<ColorFilter>,
941) -> NodeId
942where
943 P: Into<Painter> + Clone + PartialEq + 'static,
944{
945 let painter = painter.into();
946 let intrinsic_dp = painter.intrinsic_size();
947 let draw_alpha = alpha.clamp(0.0, 1.0);
948
949 let semantics_modifier = Modifier::empty().stable_semantics(move |config| {
950 config.content_description.clone_from(&content_description);
951 });
952
953 let draw = ImageDraw {
955 alignment,
956 content_scale,
957 alpha: draw_alpha,
958 color_filter,
959 density: crate::density::density().density(),
960 };
961 let image_modifier = semantics_modifier
962 .then(modifier)
963 .stable_semantics(|config| {
964 if config.role.is_none()
965 && config
966 .content_description
967 .as_ref()
968 .is_some_and(|label| !label.trim().is_empty())
969 {
970 config.role = Some(crate::SemanticsWidgetRole::Image);
971 }
972 })
973 .draw_behind(move |scope: &mut dyn DrawScope| {
974 if draw.alpha <= 0.0 {
975 return;
976 }
977 let container_size = scope.size();
978 if container_size.width <= 0.0 || container_size.height <= 0.0 {
979 return;
980 }
981 match &painter.kind {
982 PainterKind::Bitmap(bitmap) => {
983 draw_bitmap_painter(scope, bitmap.clone(), intrinsic_dp, &draw);
984 }
985 PainterKind::BitmapRegion {
986 bitmap,
987 source,
988 sampling,
989 } => draw_bitmap_region_painter(
990 scope,
991 bitmap.clone(),
992 source.rect(),
993 *sampling,
994 &draw,
995 ),
996 PainterKind::BitmapTiled {
997 bitmap,
998 source,
999 sampling,
1000 } => draw_filled_painter(
1001 scope,
1002 bitmap,
1003 |container| tile_quads(source.rect(), container),
1004 draw.alpha,
1005 draw.color_filter.clone(),
1006 *sampling,
1007 ),
1008 PainterKind::NinePatch {
1009 bitmap,
1010 source,
1011 insets,
1012 center,
1013 edges,
1014 sampling,
1015 } => draw_filled_painter(
1016 scope,
1017 bitmap,
1018 |container| {
1019 nine_patch_quads(source.rect(), container, insets.insets(), *center, *edges)
1020 },
1021 draw.alpha,
1022 draw.color_filter.clone(),
1023 *sampling,
1024 ),
1025 PainterKind::Svg(svg) => {
1026 draw_svg_painter(scope, svg.clone(), intrinsic_dp, &draw);
1027 }
1028 }
1029 });
1030
1031 Layout(
1032 image_modifier,
1033 ImageMeasurePolicy {
1034 intrinsic_size: intrinsic_dp,
1035 },
1036 || {},
1037 )
1038}
1039
1040#[cfg(test)]
1041#[path = "tests/image_tests.rs"]
1042mod tests;