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