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}