floem/views/
img.rs

1//! Module defining image view and its properties: style, position and fit.
2#![deny(missing_docs)]
3use std::{path::PathBuf, sync::Arc};
4
5use floem_reactive::Effect;
6use peniko::{Blob, ImageAlphaType, ImageData};
7use sha2::{Digest, Sha256};
8use taffy::NodeId;
9
10use crate::{Renderer, style::Style, unit::UnitExt, view::View, view::ViewId};
11
12/// Holds information about image position and size inside container.
13pub struct ImageStyle {
14    fit: ObjectFit,
15    position: ObjectPosition,
16}
17
18/// How the content of a replaced element, such as an img or video, should be resized to fit its container.
19/// See <https://developer.mozilla.org/en-US/docs/Web/CSS/object-fit>.
20pub enum ObjectFit {
21    /// The replaced content is sized to fill the element's content box.
22    /// The entire object will completely fill the box.
23    /// If the object's aspect ratio does not match the aspect ratio of its box, then the object will be stretched to fit.
24    Fill,
25    /// The replaced content is scaled to maintain its aspect ratio while fitting within the element's content box.
26    /// The entire object is made to fill the box, while preserving its aspect ratio, so the object will be "letterboxed"
27    /// if its aspect ratio does not match the aspect ratio of the box.
28    Contain,
29    /// The content is sized to maintain its aspect ratio while filling the element's entire content box.
30    /// If the object's aspect ratio does not match the aspect ratio of its box, then the object will be clipped to fit.
31    Cover,
32    /// The content is sized as if none or contain were specified, whichever would result in a smaller concrete object size.
33    ScaleDown,
34    /// The replaced content is not resized.
35    None,
36}
37
38/// Specifies the alignment of the element's contents within the element's box.
39///
40/// Areas of the box which aren't covered by the replaced element's object will show the element's background.
41/// See <https://developer.mozilla.org/en-US/docs/Web/CSS/object-position>.
42pub struct ObjectPosition {
43    #[allow(unused)]
44    horiz: HorizPosition,
45    #[allow(unused)]
46    vert: VertPosition,
47}
48
49/// Specifies object position on horizontal axis inside the element's box.
50pub enum HorizPosition {
51    /// Top position inside the element's box on the horizontal axis.
52    Top,
53    /// Center position inside the element's box on the horizontal axis.
54    Center,
55    /// Bottom position inside the element's box on the horizontal axis.
56    Bot,
57    /// Horizontal position inside the element's box as **pixels**.
58    Px(f64),
59    /// Horizontal position inside the element's box as **percent**.
60    Pct(f64),
61}
62
63/// Specifies object position on vertical axis inside the element's box.
64pub enum VertPosition {
65    /// Left position inside the element's box on the vertical axis.
66    Left,
67    /// Center position inside the element's box on the vertical axis.
68    Center,
69    /// Right position inside the element's box on the vertical axis.
70    Right,
71    /// Vertical position inside the element's box as **pixels**.
72    Px(f64),
73    /// Vertical position inside the element's box as **percent**.
74    Pct(f64),
75}
76
77impl ImageStyle {
78    /// Default setting for the image position (center & fit)
79    pub const BASE: Self = ImageStyle {
80        position: ObjectPosition {
81            horiz: HorizPosition::Center,
82            vert: VertPosition::Center,
83        },
84        fit: ObjectFit::Fill,
85    };
86
87    /// How the content should be resized to fit its container.
88    pub fn fit(mut self, fit: ObjectFit) -> Self {
89        self.fit = fit;
90        self
91    }
92
93    /// Specifies the alignment of the element's contents within the element's box.
94    ///
95    /// Areas of the box which aren't covered by the replaced element's object will show the element's background.
96    pub fn object_pos(mut self, obj_pos: ObjectPosition) -> Self {
97        self.position = obj_pos;
98        self
99    }
100}
101
102/// Holds the data needed for [img] view fn to display images.
103pub struct Img {
104    id: ViewId,
105    img: Option<peniko::ImageBrush>,
106    img_hash: Option<Vec<u8>>,
107    content_node: Option<NodeId>,
108}
109
110/// A view that can display an image and controls its position.
111///
112/// It takes function that produce `Vec<u8>` and will convert it into [Image](peniko::Image).
113///
114/// ### Example:
115/// ```rust
116/// # use crate::floem::views::Decorators;
117/// # use floem::views::img;
118/// let ferris_png = include_bytes!("../../examples/widget-gallery/assets/ferris.png");
119/// // Create an image from the function returning Vec<u8>:
120/// img(move || ferris_png.to_vec())
121///     .style(|s| s.size(50.,50.));
122/// ```
123/// # Reactivity
124/// The `img` function is not reactive, so to make it change on event, wrap it
125/// with [`dyn_view`](crate::views::dyn_view::dyn_view).
126///
127/// ### Example with reactive updates:
128/// ```rust
129/// # use floem::prelude::*;
130/// # use crate::floem::views::Decorators;
131/// # use floem::views::img;
132/// # use floem::views::dyn_view;
133/// # use floem::reactive::RwSignal;
134///
135/// #[derive(Clone)]
136/// enum Image {
137///     ImageA,
138///     ImageB
139/// }
140///
141/// let ferris = include_bytes!("../../examples/widget-gallery/assets/ferris.png");
142/// let sunflower = include_bytes!("../../examples/widget-gallery/assets/sunflower.jpg");
143/// let switch_image = RwSignal::new(Image::ImageA);
144/// // Create an image from the function returning Vec<u8>:
145/// dyn_view(move || {
146///     let image = switch_image.get();
147///     img(move || {
148///         match image {
149///             Image::ImageA => ferris.to_vec(),
150///             Image::ImageB => sunflower.to_vec()
151///        }
152///     }).style(|s| s.size(50.,50.))
153/// });
154/// ```
155pub fn img(image: impl Fn() -> Vec<u8> + 'static) -> Img {
156    let image = image::load_from_memory(&image()).ok();
157    let width = image.as_ref().map_or(0, |img| img.width());
158    let height = image.as_ref().map_or(0, |img| img.height());
159    let data = Arc::new(image.map_or(Default::default(), |img| img.into_rgba8().into_vec()));
160    let blob = Blob::new(data);
161    let image = peniko::ImageBrush::new(ImageData {
162        data: blob,
163        format: peniko::ImageFormat::Rgba8,
164        alpha_type: ImageAlphaType::AlphaPremultiplied,
165        width,
166        height,
167    })
168    .with_quality(peniko::ImageQuality::High);
169    img_dynamic(move || image.clone())
170}
171
172/// A view that can display an image and controls its position.
173///
174/// It takes function that returns [`PathBuf`] and will convert it into [`Image`](peniko::Image).
175///
176/// ### Example:
177/// ```rust
178/// # use std::path::PathBuf;
179/// # use floem::views::Decorators;
180/// # use floem::views::img_from_path;
181///
182/// let path_to_ferris = PathBuf::from(r"../../examples/widget-gallery/assets/ferrig.png");
183/// // Create an image from the function returning PathBuf:
184/// img_from_path(move || path_to_ferris.clone())
185///     .style(|s| s.size(50.,50.));
186/// ```
187/// # Reactivity
188/// The `img` function is not reactive, so to make it change on event, wrap it
189/// with [`dyn_view`](crate::views::dyn_view::dyn_view).
190pub fn img_from_path(image: impl Fn() -> PathBuf + 'static) -> Img {
191    let image = image::open(image()).ok();
192    let width = image.as_ref().map_or(0, |img| img.width());
193    let height = image.as_ref().map_or(0, |img| img.height());
194    let data = Arc::new(image.map_or(Default::default(), |img| img.into_rgba8().into_vec()));
195    let blob = Blob::new(data);
196    let image = peniko::ImageBrush::new(ImageData {
197        data: blob,
198        format: peniko::ImageFormat::Rgba8,
199        alpha_type: ImageAlphaType::AlphaPremultiplied,
200        width,
201        height,
202    });
203    img_dynamic(move || image.clone())
204}
205
206pub(crate) fn img_dynamic(image: impl Fn() -> peniko::ImageBrush + 'static) -> Img {
207    let id = ViewId::new();
208    Effect::new(move |_| {
209        id.update_state(image());
210    });
211    Img {
212        id,
213        img: None,
214        img_hash: None,
215        content_node: None,
216    }
217}
218
219impl View for Img {
220    fn id(&self) -> ViewId {
221        self.id
222    }
223
224    fn debug_name(&self) -> std::borrow::Cow<'static, str> {
225        "Img".into()
226    }
227
228    fn update(&mut self, _cx: &mut crate::context::UpdateCx, state: Box<dyn std::any::Any>) {
229        if let Ok(img) = state.downcast::<peniko::ImageBrush>() {
230            let mut hasher = Sha256::new();
231            hasher.update(img.image.data.data());
232            self.img_hash = Some(hasher.finalize().to_vec());
233
234            self.img = Some(*img);
235            self.id.request_layout();
236        }
237    }
238
239    fn layout(&mut self, cx: &mut crate::context::LayoutCx) -> taffy::tree::NodeId {
240        cx.layout_node(self.id(), true, |_cx| {
241            if self.content_node.is_none() {
242                self.content_node = Some(
243                    self.id
244                        .taffy()
245                        .borrow_mut()
246                        .new_leaf(taffy::style::Style::DEFAULT)
247                        .unwrap(),
248                );
249            }
250            let content_node = self.content_node.unwrap();
251
252            let (width, height) = self
253                .img
254                .as_ref()
255                .map(|img| (img.image.width, img.image.height))
256                .unwrap_or((0, 0));
257
258            let style = Style::new()
259                .width((width as f64).px())
260                .height((height as f64).px())
261                .to_taffy_style();
262            let _ = self.id.taffy().borrow_mut().set_style(content_node, style);
263
264            vec![content_node]
265        })
266    }
267
268    fn paint(&mut self, cx: &mut crate::context::PaintCx) {
269        if let Some(ref img) = self.img {
270            let rect = self.id.get_content_rect();
271            cx.draw_img(
272                floem_renderer::Img {
273                    img: img.clone(),
274                    hash: self.img_hash.as_ref().unwrap(),
275                },
276                rect,
277            );
278        }
279    }
280}