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}