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