1use std::path::PathBuf;
2use std::sync::Arc;
3
4use crate::{
5 point, px, size, AbsoluteLength, Bounds, DefiniteLength, DevicePixels, Element, ElementContext,
6 Hitbox, ImageData, InteractiveElement, Interactivity, IntoElement, LayoutId, Length, Pixels,
7 SharedUri, Size, StyleRefinement, Styled, UriOrPath,
8};
9use futures::FutureExt;
10#[cfg(target_os = "macos")]
11use media::core_video::CVImageBuffer;
12use util::ResultExt;
13
14/// A source of image content.
15#[derive(Clone, Debug)]
16pub enum ImageSource {
17 /// Image content will be loaded from provided URI at render time.
18 Uri(SharedUri),
19 /// Image content will be loaded from the provided file at render time.
20 File(Arc<PathBuf>),
21 /// Cached image data
22 Data(Arc<ImageData>),
23 // TODO: move surface definitions into mac platform module
24 /// A CoreVideo image buffer
25 #[cfg(target_os = "macos")]
26 Surface(CVImageBuffer),
27}
28
29impl From<SharedUri> for ImageSource {
30 fn from(value: SharedUri) -> Self {
31 Self::Uri(value)
32 }
33}
34
35impl From<&'static str> for ImageSource {
36 fn from(uri: &'static str) -> Self {
37 Self::Uri(uri.into())
38 }
39}
40
41impl From<String> for ImageSource {
42 fn from(uri: String) -> Self {
43 Self::Uri(uri.into())
44 }
45}
46
47impl From<Arc<PathBuf>> for ImageSource {
48 fn from(value: Arc<PathBuf>) -> Self {
49 Self::File(value)
50 }
51}
52
53impl From<PathBuf> for ImageSource {
54 fn from(value: PathBuf) -> Self {
55 Self::File(value.into())
56 }
57}
58
59impl From<Arc<ImageData>> for ImageSource {
60 fn from(value: Arc<ImageData>) -> Self {
61 Self::Data(value)
62 }
63}
64
65#[cfg(target_os = "macos")]
66impl From<CVImageBuffer> for ImageSource {
67 fn from(value: CVImageBuffer) -> Self {
68 Self::Surface(value)
69 }
70}
71
72impl ImageSource {
73 fn data(&self, cx: &mut ElementContext) -> Option<Arc<ImageData>> {
74 match self {
75 ImageSource::Uri(_) | ImageSource::File(_) => {
76 let uri_or_path: UriOrPath = match self {
77 ImageSource::Uri(uri) => uri.clone().into(),
78 ImageSource::File(path) => path.clone().into(),
79 _ => unreachable!(),
80 };
81
82 let image_future = cx.image_cache.get(uri_or_path.clone(), cx);
83 if let Some(data) = image_future
84 .clone()
85 .now_or_never()
86 .and_then(|result| result.ok())
87 {
88 return Some(data);
89 } else {
90 cx.spawn(|mut cx| async move {
91 if image_future.await.ok().is_some() {
92 cx.on_next_frame(|cx| cx.refresh());
93 }
94 })
95 .detach();
96
97 return None;
98 }
99 }
100
101 ImageSource::Data(data) => {
102 return Some(data.clone());
103 }
104 #[cfg(target_os = "macos")]
105 ImageSource::Surface(_) => None,
106 }
107 }
108}
109
110/// An image element.
111pub struct Img {
112 interactivity: Interactivity,
113 source: ImageSource,
114 grayscale: bool,
115 object_fit: ObjectFit,
116}
117
118/// Create a new image element.
119pub fn img(source: impl Into<ImageSource>) -> Img {
120 Img {
121 interactivity: Interactivity::default(),
122 source: source.into(),
123 grayscale: false,
124 object_fit: ObjectFit::Contain,
125 }
126}
127
128/// How to fit the image into the bounds of the element.
129pub enum ObjectFit {
130 /// The image will be stretched to fill the bounds of the element.
131 Fill,
132 /// The image will be scaled to fit within the bounds of the element.
133 Contain,
134 /// The image will be scaled to cover the bounds of the element.
135 Cover,
136 /// The image will maintain its original size.
137 None,
138}
139
140impl ObjectFit {
141 /// Get the bounds of the image within the given bounds.
142 pub fn get_bounds(
143 &self,
144 bounds: Bounds<Pixels>,
145 image_size: Size<DevicePixels>,
146 ) -> Bounds<Pixels> {
147 let image_size = image_size.map(|dimension| Pixels::from(u32::from(dimension)));
148 let image_ratio = image_size.width / image_size.height;
149 let bounds_ratio = bounds.size.width / bounds.size.height;
150
151 match self {
152 ObjectFit::Fill => bounds,
153 ObjectFit::Contain => {
154 let new_size = if bounds_ratio > image_ratio {
155 size(
156 image_size.width * (bounds.size.height / image_size.height),
157 bounds.size.height,
158 )
159 } else {
160 size(
161 bounds.size.width,
162 image_size.height * (bounds.size.width / image_size.width),
163 )
164 };
165
166 Bounds {
167 origin: point(
168 bounds.origin.x + (bounds.size.width - new_size.width) / 2.0,
169 bounds.origin.y + (bounds.size.height - new_size.height) / 2.0,
170 ),
171 size: new_size,
172 }
173 }
174 ObjectFit::Cover => {
175 let new_size = if bounds_ratio > image_ratio {
176 size(
177 bounds.size.width,
178 image_size.height * (bounds.size.width / image_size.width),
179 )
180 } else {
181 size(
182 image_size.width * (bounds.size.height / image_size.height),
183 bounds.size.height,
184 )
185 };
186
187 Bounds {
188 origin: point(
189 bounds.origin.x + (bounds.size.width - new_size.width) / 2.0,
190 bounds.origin.y + (bounds.size.height - new_size.height) / 2.0,
191 ),
192 size: new_size,
193 }
194 }
195 ObjectFit::None => Bounds {
196 origin: bounds.origin,
197 size: image_size,
198 },
199 }
200 }
201}
202
203impl Img {
204 /// Set the image to be displayed in grayscale.
205 pub fn grayscale(mut self, grayscale: bool) -> Self {
206 self.grayscale = grayscale;
207 self
208 }
209 /// Set the object fit for the image.
210 pub fn object_fit(mut self, object_fit: ObjectFit) -> Self {
211 self.object_fit = object_fit;
212 self
213 }
214}
215
216impl Element for Img {
217 type BeforeLayout = ();
218 type AfterLayout = Option<Hitbox>;
219
220 fn before_layout(&mut self, cx: &mut ElementContext) -> (LayoutId, Self::BeforeLayout) {
221 let layout_id = self.interactivity.before_layout(cx, |mut style, cx| {
222 if let Some(data) = self.source.data(cx) {
223 let image_size = data.size();
224 match (style.size.width, style.size.height) {
225 (Length::Auto, Length::Auto) => {
226 style.size = Size {
227 width: Length::Definite(DefiniteLength::Absolute(
228 AbsoluteLength::Pixels(px(image_size.width.0 as f32)),
229 )),
230 height: Length::Definite(DefiniteLength::Absolute(
231 AbsoluteLength::Pixels(px(image_size.height.0 as f32)),
232 )),
233 }
234 }
235 _ => {}
236 }
237 }
238 cx.request_layout(&style, [])
239 });
240 (layout_id, ())
241 }
242
243 fn after_layout(
244 &mut self,
245 bounds: Bounds<Pixels>,
246 _before_layout: &mut Self::BeforeLayout,
247 cx: &mut ElementContext,
248 ) -> Option<Hitbox> {
249 self.interactivity
250 .after_layout(bounds, bounds.size, cx, |_, _, hitbox, _| hitbox)
251 }
252
253 fn paint(
254 &mut self,
255 bounds: Bounds<Pixels>,
256 _: &mut Self::BeforeLayout,
257 hitbox: &mut Self::AfterLayout,
258 cx: &mut ElementContext,
259 ) {
260 let source = self.source.clone();
261 self.interactivity
262 .paint(bounds, hitbox.as_ref(), cx, |style, cx| {
263 let corner_radii = style.corner_radii.to_pixels(bounds.size, cx.rem_size());
264
265 match source.data(cx) {
266 Some(data) => {
267 let bounds = self.object_fit.get_bounds(bounds, data.size());
268 cx.paint_image(bounds, corner_radii, data, self.grayscale)
269 .log_err();
270 }
271 #[cfg(not(target_os = "macos"))]
272 None => {
273 // No renderable image loaded yet. Do nothing.
274 }
275 #[cfg(target_os = "macos")]
276 None => match source {
277 ImageSource::Surface(surface) => {
278 let size = size(surface.width().into(), surface.height().into());
279 let new_bounds = self.object_fit.get_bounds(bounds, size);
280 // TODO: Add support for corner_radii and grayscale.
281 cx.paint_surface(new_bounds, surface);
282 }
283 _ => {
284 // No renderable image loaded yet. Do nothing.
285 }
286 },
287 }
288 })
289 }
290}
291
292impl IntoElement for Img {
293 type Element = Self;
294
295 fn into_element(self) -> Self::Element {
296 self
297 }
298}
299
300impl Styled for Img {
301 fn style(&mut self) -> &mut StyleRefinement {
302 &mut self.interactivity.base_style
303 }
304}
305
306impl InteractiveElement for Img {
307 fn interactivity(&mut self) -> &mut Interactivity {
308 &mut self.interactivity
309 }
310}