Skip to main content

cranpose_ui/widgets/
image.rs

1//! Image composable and painter primitives.
2
3#![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
29/// Alpha value of a fully opaque image, the default for [`Image`].
30pub const DEFAULT_ALPHA: f32 = 1.0;
31#[cfg(feature = "svg")]
32const SVG_RASTER_CACHE_LIMIT: usize = 8;
33
34/// How an image is scaled to fit the bounds it is drawn into. Compose's `ContentScale`.
35#[derive(Clone, Copy, Debug, PartialEq)]
36pub enum ContentScale {
37    /// Scales both axes by one factor so the whole image fits inside the bounds, leaving space on
38    /// one axis.
39    Fit,
40    /// Scales both axes by one factor so the image covers the bounds, cropping what overflows.
41    Crop,
42    /// Scales each axis separately so the image fills the bounds, which distorts its aspect ratio.
43    FillBounds,
44    /// Scales both axes by one factor so the image width matches the bounds width.
45    FillWidth,
46    /// Scales both axes by one factor so the image height matches the bounds height.
47    FillHeight,
48    /// Like `Fit`, but never enlarges an image that already fits inside the bounds.
49    Inside,
50    /// Draws the image at its own size without scaling.
51    None,
52}
53
54impl ContentScale {
55    /// Returns the size of an image of `src_size` after this scale fits it to `dst_size`. For
56    /// `Crop` the result can exceed `dst_size`. Returns zero size if a side of either size is zero
57    /// or negative.
58    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/// The pixels that an [`Image`] draws: a bitmap, a region of a bitmap, a tiled or nine-patch
101/// region, or an SVG. Compose's `Painter`.
102#[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    /// A source repeated at its own size across whatever space it is given,
116    /// rather than scaled to fit it.
117    BitmapTiled {
118        bitmap: ImageBitmap,
119        source: Quad,
120        sampling: ImageSampling,
121    },
122    /// A source whose corners keep their size while its edges and middle grow.
123    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/// Four `f32`s carried as bits.
135///
136/// A painter is compared and hashed so a composable can skip when it has not
137/// changed, and floats are neither `Eq` nor `Hash`. Keeping the bits makes the
138/// comparison exact — two painters built from the same numbers are the same
139/// painter — without asking the caller to think about it.
140#[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    /// Creates a painter that draws the whole bitmap.
193    pub fn from_bitmap(bitmap: ImageBitmap) -> Self {
194        Self {
195            kind: PainterKind::Bitmap(bitmap),
196        }
197    }
198
199    /// Creates a painter for one source region of a bitmap atlas.
200    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    /// Creates a painter that repeats `source` at its own size across whatever
211    /// space it is given, instead of scaling it to fit.
212    ///
213    /// A texture, a hatch or a skin background covers an area of any size
214    /// without going soft. `source` is a region of `bitmap`, so an application
215    /// tiles one sprite out of an atlas and never the whole sheet.
216    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    /// Creates a painter whose corners keep their own size while its edges and
227    /// middle grow — one drawing of a button, panel or trough used at every
228    /// size.
229    ///
230    /// `center` and `edges` decide whether the growing parts are stretched or
231    /// tiled. Insets that leave no middle, or a destination with no room for
232    /// the corners, fall back to a plain scale rather than drawing corners over
233    /// each other.
234    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    /// Creates a painter that draws a parsed SVG.
255    pub fn from_svg(svg: SvgPainter) -> Self {
256        Self {
257            kind: PainterKind::Svg(svg),
258        }
259    }
260
261    /// Returns the size in pixels that the painter has without constraints: the bitmap size, the
262    /// source region size, or the size the SVG declares.
263    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    /// Returns the bitmap that the painter draws from, or `None` for an SVG painter. Region, tiled
277    /// and nine-patch painters return their whole source bitmap.
278    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    /// Returns the underlying bitmap when this painter is bitmap-backed.
289    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
306/// Creates a painter that draws the whole bitmap.
307pub fn BitmapPainter(bitmap: ImageBitmap) -> Painter {
308    Painter::from_bitmap(bitmap)
309}
310
311/// Creates a painter that draws one region from a bitmap atlas.
312pub fn BitmapRegionPainter(bitmap: ImageBitmap, source: Rect, sampling: ImageSampling) -> Painter {
313    Painter::from_bitmap_region(bitmap, source, sampling)
314}
315
316/// Creates a painter that repeats one region of a bitmap across its bounds.
317pub fn TiledPainter(bitmap: ImageBitmap, source: Rect, sampling: ImageSampling) -> Painter {
318    Painter::from_bitmap_tiled(bitmap, source, sampling)
319}
320
321/// Creates a painter whose corners stay put while its edges and middle grow.
322pub 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/// Errors returned while parsing or rasterizing an SVG painter.
334#[derive(Debug, Clone, PartialEq, Eq, Error)]
335pub enum SvgPainterError {
336    /// The crate was built without the `svg` feature.
337    #[error("SVG support is disabled; enable the cranpose-ui `svg` feature")]
338    SvgFeatureDisabled,
339    /// The SVG data could not be parsed. Holds the parser message.
340    #[error("failed to parse SVG: {0}")]
341    Parse(String),
342    /// A requested raster side was zero, negative or not a finite number.
343    #[error("SVG raster dimensions must be greater than zero")]
344    InvalidRasterDimensions,
345    /// The requested raster is too large to address.
346    #[error("SVG raster dimensions are too large")]
347    RasterDimensionsTooLarge,
348    /// Allocating the memory for the raster failed.
349    #[error("failed to allocate SVG raster {width}x{height}")]
350    RasterAllocationFailed {
351        /// Requested raster width in pixels.
352        width: u32,
353        /// Requested raster height in pixels.
354        height: u32,
355    },
356    /// The raster cache could not be locked.
357    #[error("SVG raster cache is unavailable")]
358    RasterCacheUnavailable,
359    /// Creating the bitmap from the rasterized pixels failed.
360    #[error(transparent)]
361    ImageBitmap(#[from] cranpose_ui_graphics::ImageBitmapError),
362}
363
364/// Parsed SVG image data that rasterizes on demand for the requested draw size.
365#[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    /// Parses SVG `bytes`. Fails with `SvgFeatureDisabled` when the `svg` feature is off and with
400    /// `Parse` when the data is not valid SVG.
401    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    /// Returns an id that clones of this painter share. Painters parsed separately have different
423    /// ids while both are alive.
424    pub fn id(&self) -> u64 {
425        Arc::as_ptr(&self.inner) as usize as u64
426    }
427
428    /// Returns the size in pixels that the SVG document declares.
429    pub fn intrinsic_size(&self) -> Size {
430        self.inner.intrinsic_size
431    }
432
433    /// Rasterizes the SVG into a bitmap of `pixel_size` pixels, rounded up to whole pixels. A few
434    /// recent sizes are cached.
435    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
550/// Parses `bytes` once for the calling composition position and returns the same painter on later
551/// recompositions. The cache key is the address and length of `bytes`.
552pub 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/// Measure policy for Image that preserves aspect ratio when constraints
564/// force the image smaller than its intrinsic size.
565///
566/// Unlike [`LeafMeasurePolicy`] which clamps width and height independently,
567/// this scales both dimensions by the same factor so the image is never
568/// distorted by layout constraints.
569#[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
641/// Where the painter draws in the container, as Compose's painter modifier
642/// places it: scaled, then aligned on sizes rounded to whole device pixels
643/// of `density`, overflowing the container where the scale makes it larger.
644fn 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
736// How an Image draws its painter: placed by its alignment and content
737// scale on whole device pixels of `density`, with its alpha and color filter.
738struct 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
808/// Draws a list of source-to-destination quads, clipped to the container.
809///
810/// Every fill that is not a plain scale — tiling, nine-patch — reduces to this,
811/// so the clipping rule and the sampling choice are stated once.
812fn 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
857// Draws a tiled or nine-patch painter across the whole container, in the
858// patches `quads_of` cuts for it. Content scale and alignment do not apply:
859// such a fill covers its bounds by construction.
860fn 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/// Displays a painter with an optional accessible description.
929///
930/// Images use the image accessibility role unless `modifier` supplies another role.
931/// Without a description or modifier semantics, the image is decorative.
932#[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    // The grid the image is aligned on: the composition's, as Text keeps it.
954    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;