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