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;
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(std::sync::PoisonError::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(Clone::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 measurement = self.measure_into(scope, &[], constraints, &mut placements);
543 MeasureResult::new(measurement, 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 ) -> cranpose_ui_layout::Measurement {
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 .into();
564 }
565
566 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;
570 let scale_y = ch / ih;
571
572 let (width, height) = if scale_x < 1.0 || scale_y < 1.0 {
573 let factor = scale_x.min(scale_y);
574 let w = (iw * factor).clamp(constraints.min_width, constraints.max_width);
575 let h = (ih * factor).clamp(constraints.min_height, constraints.max_height);
576 (w, h)
577 } else {
578 (cw, ch)
579 };
580
581 Size { width, height }.into()
582 }
583
584 fn min_intrinsic_width(&self, _measurables: &[Box<dyn Measurable>], _height: f32) -> f32 {
585 self.intrinsic_size.width
586 }
587
588 fn max_intrinsic_width(&self, _measurables: &[Box<dyn Measurable>], _height: f32) -> f32 {
589 self.intrinsic_size.width
590 }
591
592 fn min_intrinsic_height(&self, _measurables: &[Box<dyn Measurable>], _width: f32) -> f32 {
593 self.intrinsic_size.height
594 }
595
596 fn max_intrinsic_height(&self, _measurables: &[Box<dyn Measurable>], _width: f32) -> f32 {
597 self.intrinsic_size.height
598 }
599}
600
601fn destination_rect(
605 src_size: Size,
606 dst_size: Size,
607 alignment: Alignment,
608 content_scale: ContentScale,
609 density: f32,
610) -> Rect {
611 use cranpose_ui_layout::round_to_px;
612
613 let draw_size = content_scale.scaled_size(src_size, dst_size);
614 Rect {
615 x: alignment.horizontal.align(
616 round_to_px(dst_size.width, density),
617 round_to_px(draw_size.width, density),
618 density,
619 ),
620 y: alignment.vertical.align(
621 round_to_px(dst_size.height, density),
622 round_to_px(draw_size.height, density),
623 density,
624 ),
625 width: draw_size.width,
626 height: draw_size.height,
627 }
628}
629
630fn map_destination_clip_to_source(
631 src_rect: Rect,
632 dst_rect: Rect,
633 clipped_dst_rect: Rect,
634) -> Option<Rect> {
635 if src_rect.width <= 0.0
636 || src_rect.height <= 0.0
637 || dst_rect.width <= 0.0
638 || dst_rect.height <= 0.0
639 || clipped_dst_rect.width <= 0.0
640 || clipped_dst_rect.height <= 0.0
641 {
642 return None;
643 }
644
645 let scale_x = src_rect.width / dst_rect.width;
646 let scale_y = src_rect.height / dst_rect.height;
647
648 let src_min_x = src_rect.x;
649 let src_min_y = src_rect.y;
650 let src_max_x = src_rect.x + src_rect.width;
651 let src_max_y = src_rect.y + src_rect.height;
652
653 let raw_left = src_rect.x + (clipped_dst_rect.x - dst_rect.x) * scale_x;
654 let raw_top = src_rect.y + (clipped_dst_rect.y - dst_rect.y) * scale_y;
655 let raw_right =
656 src_rect.x + ((clipped_dst_rect.x + clipped_dst_rect.width) - dst_rect.x) * scale_x;
657 let raw_bottom =
658 src_rect.y + ((clipped_dst_rect.y + clipped_dst_rect.height) - dst_rect.y) * scale_y;
659
660 let left = raw_left.clamp(src_min_x, src_max_x);
661 let top = raw_top.clamp(src_min_y, src_max_y);
662 let right = raw_right.clamp(src_min_x, src_max_x);
663 let bottom = raw_bottom.clamp(src_min_y, src_max_y);
664 let width = right - left;
665 let height = bottom - top;
666
667 if width <= 0.0 || height <= 0.0 {
668 None
669 } else {
670 Some(Rect {
671 x: left,
672 y: top,
673 width,
674 height,
675 })
676 }
677}
678
679fn image_destination_clip(
680 src_size: Size,
681 container_size: Size,
682 alignment: Alignment,
683 content_scale: ContentScale,
684 density: f32,
685) -> Option<(Rect, Rect)> {
686 let dst_rect = destination_rect(src_size, container_size, alignment, content_scale, density);
687 if dst_rect.width <= 0.0 || dst_rect.height <= 0.0 {
688 return None;
689 }
690
691 let container_rect = Rect::from_size(container_size);
692 let clipped_dst_rect = dst_rect.intersect(container_rect)?;
693 Some((dst_rect, clipped_dst_rect))
694}
695
696fn draw_bitmap_painter(
697 scope: &mut dyn DrawScope,
698 bitmap: ImageBitmap,
699 intrinsic_size: Size,
700 alignment: Alignment,
701 content_scale: ContentScale,
702 alpha: f32,
703 color_filter: Option<ColorFilter>,
704 density: f32,
705) {
706 let container_size = scope.size();
707 let Some((dst_rect, clipped_dst_rect)) = image_destination_clip(
708 intrinsic_size,
709 container_size,
710 alignment,
711 content_scale,
712 density,
713 ) else {
714 return;
715 };
716 let full_src_rect = Rect::from_size(Size::new(bitmap.width() as f32, bitmap.height() as f32));
717 let Some(clipped_src_rect) =
718 map_destination_clip_to_source(full_src_rect, dst_rect, clipped_dst_rect)
719 else {
720 return;
721 };
722 scope.draw_image_src_sampled(
723 bitmap,
724 clipped_src_rect,
725 clipped_dst_rect,
726 alpha,
727 color_filter,
728 ImageSampling::Linear,
729 );
730}
731
732fn draw_bitmap_region_painter(
733 scope: &mut dyn DrawScope,
734 bitmap: ImageBitmap,
735 source: Rect,
736 alignment: Alignment,
737 content_scale: ContentScale,
738 alpha: f32,
739 color_filter: Option<ColorFilter>,
740 sampling: ImageSampling,
741 density: f32,
742) {
743 let source_size = Size::new(source.width.max(0.0), source.height.max(0.0));
744 let Some((dst_rect, clipped_dst_rect)) =
745 image_destination_clip(source_size, scope.size(), alignment, content_scale, density)
746 else {
747 return;
748 };
749 let Some(clipped_source) = map_destination_clip_to_source(source, dst_rect, clipped_dst_rect)
750 else {
751 return;
752 };
753 scope.draw_image_src_sampled(
754 bitmap,
755 clipped_source,
756 clipped_dst_rect,
757 alpha,
758 color_filter,
759 sampling,
760 );
761}
762
763fn draw_patch_quads(
768 scope: &mut dyn DrawScope,
769 bitmap: &ImageBitmap,
770 quads: &[PatchQuad],
771 container: Rect,
772 alpha: f32,
773 color_filter: Option<ColorFilter>,
774 sampling: ImageSampling,
775) {
776 for quad in quads {
777 let Some(clipped_destination) = intersect(quad.destination, container) else {
778 continue;
779 };
780 let Some(clipped_source) =
781 map_destination_clip_to_source(quad.source, quad.destination, clipped_destination)
782 else {
783 continue;
784 };
785 scope.draw_image_src_sampled(
786 bitmap.clone(),
787 clipped_source,
788 clipped_destination,
789 alpha,
790 color_filter,
791 sampling,
792 );
793 }
794}
795
796fn intersect(rect: Rect, bounds: Rect) -> Option<Rect> {
797 let left = rect.x.max(bounds.x);
798 let top = rect.y.max(bounds.y);
799 let right = (rect.x + rect.width).min(bounds.x + bounds.width);
800 let bottom = (rect.y + rect.height).min(bounds.y + bounds.height);
801 if right <= left || bottom <= top {
802 return None;
803 }
804 Some(Rect {
805 x: left,
806 y: top,
807 width: right - left,
808 height: bottom - top,
809 })
810}
811
812fn draw_bitmap_tiled_painter(
817 scope: &mut dyn DrawScope,
818 bitmap: ImageBitmap,
819 source: Rect,
820 alpha: f32,
821 color_filter: Option<ColorFilter>,
822 sampling: ImageSampling,
823) {
824 let container = Rect::from_size(scope.size());
825 let quads = tile_quads(source, container);
826 draw_patch_quads(
827 scope,
828 &bitmap,
829 &quads,
830 container,
831 alpha,
832 color_filter,
833 sampling,
834 );
835}
836
837#[expect(clippy::too_many_arguments)]
842fn draw_nine_patch_painter(
843 scope: &mut dyn DrawScope,
844 bitmap: ImageBitmap,
845 source: Rect,
846 insets: NinePatchInsets,
847 center: PatchFill,
848 edges: PatchFill,
849 alpha: f32,
850 color_filter: Option<ColorFilter>,
851 sampling: ImageSampling,
852) {
853 let container = Rect::from_size(scope.size());
854 let quads = nine_patch_quads(source, container, insets, center, edges);
855 draw_patch_quads(
856 scope,
857 &bitmap,
858 &quads,
859 container,
860 alpha,
861 color_filter,
862 sampling,
863 );
864}
865
866fn draw_svg_painter(
867 scope: &mut dyn DrawScope,
868 svg: SvgPainter,
869 intrinsic_size: Size,
870 alignment: Alignment,
871 content_scale: ContentScale,
872 alpha: f32,
873 color_filter: Option<ColorFilter>,
874 density: f32,
875) {
876 let container_size = scope.size();
877 let Some((dst_rect, clipped_dst_rect)) = image_destination_clip(
878 intrinsic_size,
879 container_size,
880 alignment,
881 content_scale,
882 density,
883 ) else {
884 return;
885 };
886 let pixel_size = Size::new(dst_rect.width * density, dst_rect.height * density);
887 let bitmap = match svg.rasterize(pixel_size) {
888 Ok(bitmap) => bitmap,
889 Err(error) => {
890 log::warn!("failed to rasterize SVG painter: {error}");
891 return;
892 }
893 };
894 let full_src_rect = Rect::from_size(Size::new(bitmap.width() as f32, bitmap.height() as f32));
895 let Some(clipped_src_rect) =
896 map_destination_clip_to_source(full_src_rect, dst_rect, clipped_dst_rect)
897 else {
898 return;
899 };
900 scope.draw_image_src_sampled(
901 bitmap,
902 clipped_src_rect,
903 clipped_dst_rect,
904 alpha,
905 color_filter,
906 ImageSampling::Linear,
907 );
908}
909
910#[cfg(feature = "svg")]
911fn svg_raster_key(pixel_size: Size) -> Result<SvgRasterKey, SvgPainterError> {
912 let width = svg_raster_axis(pixel_size.width)?;
913 let height = svg_raster_axis(pixel_size.height)?;
914 width
915 .checked_mul(height)
916 .and_then(|value| value.checked_mul(4))
917 .ok_or(SvgPainterError::RasterDimensionsTooLarge)?;
918 Ok(SvgRasterKey { width, height })
919}
920
921#[cfg(feature = "svg")]
922fn svg_raster_axis(value: f32) -> Result<u32, SvgPainterError> {
923 if !value.is_finite() || value <= 0.0 {
924 return Err(SvgPainterError::InvalidRasterDimensions);
925 }
926
927 let rounded = value.ceil();
928 if rounded > u32::MAX as f32 {
929 return Err(SvgPainterError::RasterDimensionsTooLarge);
930 }
931 Ok(rounded as u32)
932}
933
934#[composable]
939pub fn Image<P>(
940 painter: P,
941 content_description: Option<String>,
942 modifier: Modifier,
943 alignment: Alignment,
944 content_scale: ContentScale,
945 alpha: f32,
946 color_filter: Option<ColorFilter>,
947) -> NodeId
948where
949 P: Into<Painter> + Clone + PartialEq + 'static,
950{
951 let painter = painter.into();
952 let intrinsic_dp = painter.intrinsic_size();
953 let draw_alpha = alpha.clamp(0.0, 1.0);
954
955 let semantics_modifier = Modifier::empty().stable_semantics(move |config| {
956 config.content_description.clone_from(&content_description);
957 });
958
959 let density = crate::density::density().density();
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) => draw_bitmap_painter(
983 scope,
984 bitmap.clone(),
985 intrinsic_dp,
986 alignment,
987 content_scale,
988 draw_alpha,
989 color_filter,
990 density,
991 ),
992 PainterKind::BitmapRegion {
993 bitmap,
994 source,
995 sampling,
996 } => draw_bitmap_region_painter(
997 scope,
998 bitmap.clone(),
999 source.rect(),
1000 alignment,
1001 content_scale,
1002 draw_alpha,
1003 color_filter,
1004 *sampling,
1005 density,
1006 ),
1007 PainterKind::BitmapTiled {
1008 bitmap,
1009 source,
1010 sampling,
1011 } => draw_bitmap_tiled_painter(
1012 scope,
1013 bitmap.clone(),
1014 source.rect(),
1015 draw_alpha,
1016 color_filter,
1017 *sampling,
1018 ),
1019 PainterKind::NinePatch {
1020 bitmap,
1021 source,
1022 insets,
1023 center,
1024 edges,
1025 sampling,
1026 } => draw_nine_patch_painter(
1027 scope,
1028 bitmap.clone(),
1029 source.rect(),
1030 insets.insets(),
1031 *center,
1032 *edges,
1033 draw_alpha,
1034 color_filter,
1035 *sampling,
1036 ),
1037 PainterKind::Svg(svg) => draw_svg_painter(
1038 scope,
1039 svg.clone(),
1040 intrinsic_dp,
1041 alignment,
1042 content_scale,
1043 draw_alpha,
1044 color_filter,
1045 density,
1046 ),
1047 }
1048 });
1049
1050 Layout(
1051 image_modifier,
1052 ImageMeasurePolicy {
1053 intrinsic_size: intrinsic_dp,
1054 },
1055 || {},
1056 )
1057}
1058
1059#[cfg(test)]
1060#[path = "tests/image_tests.rs"]
1061mod tests;