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::create_effect;
6use peniko::Blob;
7use sha2::{Digest, Sha256};
8use taffy::NodeId;
9
10use crate::{id::ViewId, style::Style, unit::UnitExt, view::View, Renderer};
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::Image>,
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::Image::new(blob, peniko::ImageFormat::Rgba8, width, height);
162    img_dynamic(move || image.clone())
163}
164
165/// A view that can display an image and controls its position.
166///
167/// It takes function that returns [`PathBuf`] and will convert it into [`Image`](peniko::Image).
168///
169/// ### Example:
170/// ```rust
171/// # use std::path::PathBuf;
172/// # use floem::views::Decorators;
173/// # use floem::views::img_from_path;
174///
175/// let path_to_ferris = PathBuf::from(r"../../examples/widget-gallery/assets/ferrig.png");
176/// // Create an image from the function returning PathBuf:
177/// img_from_path(move || path_to_ferris.clone())
178///     .style(|s| s.size(50.,50.));
179/// ```
180/// # Reactivity
181/// The `img` function is not reactive, so to make it change on event, wrap it
182/// with [`dyn_view`](crate::views::dyn_view::dyn_view).
183pub fn img_from_path(image: impl Fn() -> PathBuf + 'static) -> Img {
184    let image = image::open(image()).ok();
185    let width = image.as_ref().map_or(0, |img| img.width());
186    let height = image.as_ref().map_or(0, |img| img.height());
187    let data = Arc::new(image.map_or(Default::default(), |img| img.into_rgba8().into_vec()));
188    let blob = Blob::new(data);
189    let image = peniko::Image::new(blob, peniko::ImageFormat::Rgba8, width, height);
190    img_dynamic(move || image.clone())
191}
192
193pub(crate) fn img_dynamic(image: impl Fn() -> peniko::Image + 'static) -> Img {
194    let id = ViewId::new();
195    create_effect(move |_| {
196        id.update_state(image());
197    });
198    Img {
199        id,
200        img: None,
201        img_hash: None,
202        content_node: None,
203    }
204}
205
206impl View for Img {
207    fn id(&self) -> ViewId {
208        self.id
209    }
210
211    fn debug_name(&self) -> std::borrow::Cow<'static, str> {
212        "Img".into()
213    }
214
215    fn update(&mut self, _cx: &mut crate::context::UpdateCx, state: Box<dyn std::any::Any>) {
216        if let Ok(img) = state.downcast::<peniko::Image>() {
217            let mut hasher = Sha256::new();
218            hasher.update(img.data.data());
219            self.img_hash = Some(hasher.finalize().to_vec());
220
221            self.img = Some(*img);
222            self.id.request_layout();
223        }
224    }
225
226    fn layout(&mut self, cx: &mut crate::context::LayoutCx) -> taffy::tree::NodeId {
227        cx.layout_node(self.id(), true, |_cx| {
228            if self.content_node.is_none() {
229                self.content_node = Some(
230                    self.id
231                        .taffy()
232                        .borrow_mut()
233                        .new_leaf(taffy::style::Style::DEFAULT)
234                        .unwrap(),
235                );
236            }
237            let content_node = self.content_node.unwrap();
238
239            let (width, height) = self
240                .img
241                .as_ref()
242                .map(|img| (img.width, img.height))
243                .unwrap_or((0, 0));
244
245            let style = Style::new()
246                .width((width as f64).px())
247                .height((height as f64).px())
248                .to_taffy_style();
249            let _ = self.id.taffy().borrow_mut().set_style(content_node, style);
250
251            vec![content_node]
252        })
253    }
254
255    fn paint(&mut self, cx: &mut crate::context::PaintCx) {
256        if let Some(ref img) = self.img {
257            let rect = self.id.get_content_rect();
258            cx.draw_img(
259                floem_renderer::Img {
260                    img: img.clone(),
261                    hash: self.img_hash.as_ref().unwrap(),
262                },
263                rect,
264            );
265        }
266    }
267}