Skip to main content

freya_core/elements/
image.rs

1//! [image()] makes it possible to render a Skia image into the canvas.
2
3use std::{
4    any::Any,
5    borrow::Cow,
6    collections::HashMap,
7    rc::Rc,
8};
9
10use bytes::Bytes;
11use freya_engine::prelude::{
12    AlphaType,
13    ClipOp,
14    ColorType,
15    CubicResampler,
16    Data,
17    FilterMode,
18    ISize,
19    ImageInfo,
20    MipmapMode,
21    Paint,
22    SamplingOptions,
23    SkImage,
24    SkRect,
25    raster_from_data,
26};
27use rustc_hash::FxHashMap;
28use torin::prelude::Size2D;
29
30use crate::{
31    data::{
32        AccessibilityData,
33        EffectData,
34        LayoutData,
35        StyleState,
36        TextStyleData,
37    },
38    diff_key::DiffKey,
39    element::{
40        ClipContext,
41        Element,
42        ElementExt,
43        EventHandlerType,
44        LayoutContext,
45        RenderContext,
46    },
47    events::name::EventName,
48    layers::Layer,
49    prelude::{
50        AccessibilityExt,
51        ChildrenExt,
52        ContainerExt,
53        ContainerWithContentExt,
54        EffectExt,
55        EventHandlersExt,
56        ImageExt,
57        KeyExt,
58        LayerExt,
59        LayoutExt,
60        MaybeExt,
61    },
62    style::corner_radius::CornerRadius,
63    tree::DiffModifies,
64};
65
66/// [image] makes it possible to render a Skia image into the canvas.
67/// You most likely want to use a higher level than this, like the component `ImageViewer`.
68///
69/// See the available methods in [Image].
70pub fn image(image_handle: ImageHandle) -> Image {
71    let mut accessibility = AccessibilityData::default();
72    accessibility.builder.set_role(accesskit::Role::Image);
73    Image {
74        key: DiffKey::None,
75        element: ImageElement {
76            image_handle,
77            accessibility,
78            layout: LayoutData::default(),
79            event_handlers: HashMap::default(),
80            image_data: ImageData::default(),
81            relative_layer: Layer::default(),
82            effect: None,
83            corner_radius: None,
84        },
85        elements: Vec::new(),
86    }
87}
88
89/// How an image is positioned within its bounds once it has been scaled.
90#[derive(Default, Clone, Debug, PartialEq)]
91pub enum ImageCover {
92    /// Anchor the image to the top-left of the bounds. This is the default.
93    #[default]
94    Fill,
95    /// Center the image within the bounds.
96    Center,
97}
98
99/// How an image is scaled to fit its bounds while preserving its aspect ratio.
100#[derive(Default, Clone, Debug, PartialEq)]
101pub enum AspectRatio {
102    /// Scale so the whole image fits inside the bounds. This is the default.
103    #[default]
104    Min,
105    /// Scale so the image covers the whole bounds, cropping the overflow.
106    Max,
107    /// Keep the image at its natural size.
108    Fit,
109    /// Stretch the image to the bounds, ignoring its aspect ratio.
110    None,
111}
112
113/// The filtering algorithm used when an image is scaled.
114#[derive(Clone, Debug, PartialEq, Eq, Hash, Default)]
115pub enum SamplingMode {
116    /// Nearest-neighbor, fastest and sharpest, best for pixel art.
117    Nearest,
118    /// Bilinear filtering.
119    Bilinear,
120    /// Trilinear filtering with mipmaps. This is the default.
121    #[default]
122    Trilinear,
123    /// Mitchell-Netravali cubic resampling, a smooth high-quality filter.
124    Mitchell,
125    /// Catmull-Rom cubic resampling, a sharper high-quality filter.
126    CatmullRom,
127}
128
129impl SamplingMode {
130    /// The Skia [`SamplingOptions`] backing this filtering algorithm.
131    pub fn sampling_options(&self) -> SamplingOptions {
132        match self {
133            Self::Nearest => SamplingOptions::new(FilterMode::Nearest, MipmapMode::None),
134            Self::Bilinear => SamplingOptions::new(FilterMode::Linear, MipmapMode::None),
135            Self::Trilinear => SamplingOptions::new(FilterMode::Linear, MipmapMode::Linear),
136            Self::Mitchell => SamplingOptions::from(CubicResampler::mitchell()),
137            Self::CatmullRom => SamplingOptions::from(CubicResampler::catmull_rom()),
138        }
139    }
140}
141
142/// A decoded image shared by reference, ready to be rendered by an [`image()`].
143#[derive(Clone)]
144pub struct ImageHandle {
145    pub image: SkImage,
146    /// Backing data of the [`SkImage`], kept alive for as long as the image is used.
147    pub bytes: Bytes,
148}
149
150impl ImageHandle {
151    pub fn new(image: SkImage, bytes: Bytes) -> Self {
152        Self { image, bytes }
153    }
154
155    /// Build a handle from a raw `RGBA8888` pixel buffer, validating its length.
156    pub fn from_rgba(width: u32, height: u32, bytes: Bytes, alpha_type: AlphaType) -> Option<Self> {
157        let row_bytes = (width as usize).checked_mul(4)?;
158        if bytes.len() < row_bytes.checked_mul(height as usize)? {
159            return None;
160        }
161        let info = ImageInfo::new(
162            ISize::new(width as i32, height as i32),
163            ColorType::RGBA8888,
164            alpha_type,
165            None,
166        );
167        // Safety: `bytes` outlives the SkImage because the returned handle owns it.
168        let data = unsafe { Data::new_bytes(&bytes) };
169        let image = raster_from_data(&info, data, row_bytes)?;
170        Some(Self::new(image, bytes))
171    }
172}
173
174impl PartialEq for ImageHandle {
175    fn eq(&self, other: &Self) -> bool {
176        self.image.unique_id() == other.image.unique_id()
177    }
178}
179
180/// How an [`image()`] is scaled and sampled, grouping [`SamplingMode`], [`AspectRatio`] and [`ImageCover`].
181#[derive(Debug, Default, Clone, PartialEq)]
182pub struct ImageData {
183    pub sampling_mode: SamplingMode,
184    pub aspect_ratio: AspectRatio,
185    pub image_cover: ImageCover,
186}
187
188#[derive(PartialEq, Clone)]
189pub struct ImageElement {
190    pub accessibility: AccessibilityData,
191    pub layout: LayoutData,
192    pub event_handlers: FxHashMap<EventName, EventHandlerType>,
193    pub image_handle: ImageHandle,
194    pub image_data: ImageData,
195    pub relative_layer: Layer,
196    pub effect: Option<EffectData>,
197    pub corner_radius: Option<CornerRadius>,
198}
199
200impl ElementExt for ImageElement {
201    fn changed(&self, other: &Rc<dyn ElementExt>) -> bool {
202        let Some(image) = (other.as_ref() as &dyn Any).downcast_ref::<ImageElement>() else {
203            return false;
204        };
205        self != image
206    }
207
208    fn diff(&self, other: &Rc<dyn ElementExt>) -> DiffModifies {
209        let Some(image) = (other.as_ref() as &dyn Any).downcast_ref::<ImageElement>() else {
210            return DiffModifies::all();
211        };
212
213        let mut diff = DiffModifies::empty();
214
215        if self.accessibility != image.accessibility {
216            diff.insert(DiffModifies::ACCESSIBILITY);
217        }
218
219        if self.relative_layer != image.relative_layer {
220            diff.insert(DiffModifies::LAYER);
221        }
222
223        if self.layout != image.layout {
224            diff.insert(DiffModifies::LAYOUT);
225        }
226
227        if self.image_handle != image.image_handle {
228            diff.insert(DiffModifies::STYLE);
229
230            if self.image_handle.image.dimensions() != image.image_handle.image.dimensions() {
231                diff.insert(DiffModifies::LAYOUT);
232            }
233        }
234
235        if self.effect != image.effect {
236            diff.insert(DiffModifies::EFFECT);
237        }
238
239        if self.corner_radius != image.corner_radius {
240            diff.insert(DiffModifies::STYLE);
241        }
242
243        diff
244    }
245
246    fn layout(&'_ self) -> Cow<'_, LayoutData> {
247        Cow::Borrowed(&self.layout)
248    }
249
250    fn effect(&'_ self) -> Option<Cow<'_, EffectData>> {
251        self.effect.as_ref().map(Cow::Borrowed)
252    }
253
254    fn style(&'_ self) -> Cow<'_, StyleState> {
255        Cow::Owned(StyleState {
256            corner_radius: self.corner_radius.unwrap_or_default(),
257            ..StyleState::default()
258        })
259    }
260
261    fn text_style(&'_ self) -> Cow<'_, TextStyleData> {
262        Cow::Owned(TextStyleData::default())
263    }
264
265    fn accessibility(&'_ self) -> Cow<'_, AccessibilityData> {
266        Cow::Borrowed(&self.accessibility)
267    }
268
269    fn layer(&self) -> Layer {
270        self.relative_layer
271    }
272
273    fn should_measure_inner_children(&self) -> bool {
274        true
275    }
276
277    fn should_hook_measurement(&self) -> bool {
278        true
279    }
280
281    fn measure(&self, context: LayoutContext) -> Option<(Size2D, Rc<dyn Any>)> {
282        let image = &self.image_handle.image;
283
284        let image_width = image.width() as f32;
285        let image_height = image.height() as f32;
286
287        let width_ratio = context.area_size.width / image_width;
288        let height_ratio = context.area_size.height / image_height;
289
290        let size = match self.image_data.aspect_ratio {
291            AspectRatio::Max => {
292                let ratio = width_ratio.max(height_ratio);
293
294                Size2D::new(image_width * ratio, image_height * ratio)
295            }
296            AspectRatio::Min => {
297                let ratio = width_ratio.min(height_ratio);
298
299                Size2D::new(image_width * ratio, image_height * ratio)
300            }
301            AspectRatio::Fit => Size2D::new(image_width, image_height),
302            AspectRatio::None => *context.area_size,
303        };
304
305        Some((size, Rc::new(size)))
306    }
307
308    fn clip(&self, context: ClipContext) {
309        let rrect = self.render_rect(context.visible_area, context.scale_factor as f32);
310        context.canvas.clip_rrect(rrect, ClipOp::Intersect, true);
311    }
312
313    fn render(&self, context: RenderContext) {
314        let size = context
315            .layout_node
316            .data
317            .as_ref()
318            .unwrap()
319            .downcast_ref::<Size2D>()
320            .unwrap();
321
322        let area = context.layout_node.visible_area();
323
324        let mut rect = SkRect::new(
325            area.min_x(),
326            area.min_y(),
327            area.min_x() + size.width,
328            area.min_y() + size.height,
329        );
330        if self.image_data.image_cover == ImageCover::Center {
331            let width_offset = (size.width - area.width()) / 2.;
332            let height_offset = (size.height - area.height()) / 2.;
333
334            rect.left -= width_offset;
335            rect.right -= width_offset;
336            rect.top -= height_offset;
337            rect.bottom -= height_offset;
338        }
339
340        context.canvas.save();
341        let clip_rrect = self.render_rect(&area, context.scale_factor as f32);
342        context
343            .canvas
344            .clip_rrect(clip_rrect, ClipOp::Intersect, true);
345
346        let sampling = self.image_data.sampling_mode.sampling_options();
347
348        let mut paint = Paint::default();
349        paint.set_anti_alias(true);
350
351        context.canvas.draw_image_rect_with_sampling_options(
352            &self.image_handle.image,
353            None,
354            rect,
355            sampling,
356            &paint,
357        );
358
359        context.canvas.restore();
360    }
361}
362
363impl From<Image> for Element {
364    fn from(value: Image) -> Self {
365        Element::Element {
366            key: value.key,
367            element: Rc::new(value.element),
368            elements: value.elements,
369        }
370    }
371}
372
373impl KeyExt for Image {
374    fn write_key(&mut self) -> &mut DiffKey {
375        &mut self.key
376    }
377}
378
379impl EventHandlersExt for Image {
380    fn get_event_handlers(&mut self) -> &mut FxHashMap<EventName, EventHandlerType> {
381        &mut self.element.event_handlers
382    }
383}
384
385impl AccessibilityExt for Image {
386    fn get_accessibility_data(&mut self) -> &mut AccessibilityData {
387        &mut self.element.accessibility
388    }
389}
390impl MaybeExt for Image {}
391
392impl LayoutExt for Image {
393    fn get_layout(&mut self) -> &mut LayoutData {
394        &mut self.element.layout
395    }
396}
397
398impl ContainerExt for Image {}
399impl ContainerWithContentExt for Image {}
400
401impl ImageExt for Image {
402    fn get_image_data(&mut self) -> &mut ImageData {
403        &mut self.element.image_data
404    }
405}
406
407impl ChildrenExt for Image {
408    fn get_children(&mut self) -> &mut Vec<Element> {
409        &mut self.elements
410    }
411}
412
413impl LayerExt for Image {
414    fn get_layer(&mut self) -> &mut Layer {
415        &mut self.element.relative_layer
416    }
417}
418
419impl EffectExt for Image {
420    fn get_effect(&mut self) -> &mut EffectData {
421        self.element.effect.get_or_insert_with(EffectData::default)
422    }
423}
424
425pub struct Image {
426    key: DiffKey,
427    element: ImageElement,
428    elements: Vec<Element>,
429}
430
431impl Image {
432    pub fn try_downcast(element: &dyn ElementExt) -> Option<ImageElement> {
433        (element as &dyn Any)
434            .downcast_ref::<ImageElement>()
435            .cloned()
436    }
437
438    /// Round the image's corners, clipping it to the rounded shape. See [`CornerRadius`].
439    pub fn corner_radius(mut self, corner_radius: impl Into<CornerRadius>) -> Self {
440        self.element.corner_radius = Some(corner_radius.into());
441        self
442    }
443}