1use std::path::PathBuf;
2use std::sync::Arc;
3
4use crate::{
5 point, size, Bounds, DevicePixels, Element, ElementContext, Hitbox, ImageData,
6 InteractiveElement, Interactivity, IntoElement, LayoutId, Pixels, SharedUri, Size,
7 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<Arc<ImageData>> for ImageSource {
54 fn from(value: Arc<ImageData>) -> Self {
55 Self::Data(value)
56 }
57}
58
59#[cfg(target_os = "macos")]
60impl From<CVImageBuffer> for ImageSource {
61 fn from(value: CVImageBuffer) -> Self {
62 Self::Surface(value)
63 }
64}
65
66/// An image element.
67pub struct Img {
68 interactivity: Interactivity,
69 source: ImageSource,
70 grayscale: bool,
71 object_fit: ObjectFit,
72}
73
74/// Create a new image element.
75pub fn img(source: impl Into<ImageSource>) -> Img {
76 Img {
77 interactivity: Interactivity::default(),
78 source: source.into(),
79 grayscale: false,
80 object_fit: ObjectFit::Contain,
81 }
82}
83
84/// How to fit the image into the bounds of the element.
85pub enum ObjectFit {
86 /// The image will be stretched to fill the bounds of the element.
87 Fill,
88 /// The image will be scaled to fit within the bounds of the element.
89 Contain,
90 /// The image will be scaled to cover the bounds of the element.
91 Cover,
92 /// The image will maintain its original size.
93 None,
94}
95
96impl ObjectFit {
97 /// Get the bounds of the image within the given bounds.
98 pub fn get_bounds(
99 &self,
100 bounds: Bounds<Pixels>,
101 image_size: Size<DevicePixels>,
102 ) -> Bounds<Pixels> {
103 let image_size = image_size.map(|dimension| Pixels::from(u32::from(dimension)));
104 let image_ratio = image_size.width / image_size.height;
105 let bounds_ratio = bounds.size.width / bounds.size.height;
106
107 match self {
108 ObjectFit::Fill => bounds,
109 ObjectFit::Contain => {
110 let new_size = if bounds_ratio > image_ratio {
111 size(
112 image_size.width * (bounds.size.height / image_size.height),
113 bounds.size.height,
114 )
115 } else {
116 size(
117 bounds.size.width,
118 image_size.height * (bounds.size.width / image_size.width),
119 )
120 };
121
122 Bounds {
123 origin: point(
124 bounds.origin.x + (bounds.size.width - new_size.width) / 2.0,
125 bounds.origin.y + (bounds.size.height - new_size.height) / 2.0,
126 ),
127 size: new_size,
128 }
129 }
130 ObjectFit::Cover => {
131 let new_size = if bounds_ratio > image_ratio {
132 size(
133 bounds.size.width,
134 image_size.height * (bounds.size.width / image_size.width),
135 )
136 } else {
137 size(
138 image_size.width * (bounds.size.height / image_size.height),
139 bounds.size.height,
140 )
141 };
142
143 Bounds {
144 origin: point(
145 bounds.origin.x + (bounds.size.width - new_size.width) / 2.0,
146 bounds.origin.y + (bounds.size.height - new_size.height) / 2.0,
147 ),
148 size: new_size,
149 }
150 }
151 ObjectFit::None => Bounds {
152 origin: bounds.origin,
153 size: image_size,
154 },
155 }
156 }
157}
158
159impl Img {
160 /// Set the image to be displayed in grayscale.
161 pub fn grayscale(mut self, grayscale: bool) -> Self {
162 self.grayscale = grayscale;
163 self
164 }
165 /// Set the object fit for the image.
166 pub fn object_fit(mut self, object_fit: ObjectFit) -> Self {
167 self.object_fit = object_fit;
168 self
169 }
170}
171
172impl Element for Img {
173 type BeforeLayout = ();
174 type AfterLayout = Option<Hitbox>;
175
176 fn before_layout(&mut self, cx: &mut ElementContext) -> (LayoutId, Self::BeforeLayout) {
177 let layout_id = self
178 .interactivity
179 .before_layout(cx, |style, cx| cx.request_layout(&style, []));
180 (layout_id, ())
181 }
182
183 fn after_layout(
184 &mut self,
185 bounds: Bounds<Pixels>,
186 _before_layout: &mut Self::BeforeLayout,
187 cx: &mut ElementContext,
188 ) -> Option<Hitbox> {
189 self.interactivity
190 .after_layout(bounds, bounds.size, cx, |_, _, hitbox, _| hitbox)
191 }
192
193 fn paint(
194 &mut self,
195 bounds: Bounds<Pixels>,
196 _: &mut Self::BeforeLayout,
197 hitbox: &mut Self::AfterLayout,
198 cx: &mut ElementContext,
199 ) {
200 let source = self.source.clone();
201 self.interactivity
202 .paint(bounds, hitbox.as_ref(), cx, |style, cx| {
203 let corner_radii = style.corner_radii.to_pixels(bounds.size, cx.rem_size());
204 match source {
205 ImageSource::Uri(_) | ImageSource::File(_) => {
206 let uri_or_path: UriOrPath = match source {
207 ImageSource::Uri(uri) => uri.into(),
208 ImageSource::File(path) => path.into(),
209 _ => unreachable!(),
210 };
211
212 let image_future = cx.image_cache.get(uri_or_path.clone(), cx);
213 if let Some(data) = image_future
214 .clone()
215 .now_or_never()
216 .and_then(|result| result.ok())
217 {
218 let new_bounds = self.object_fit.get_bounds(bounds, data.size());
219 cx.paint_image(new_bounds, corner_radii, data, self.grayscale)
220 .log_err();
221 } else {
222 cx.spawn(|mut cx| async move {
223 if image_future.await.ok().is_some() {
224 cx.on_next_frame(|cx| cx.refresh());
225 }
226 })
227 .detach();
228 }
229 }
230
231 ImageSource::Data(data) => {
232 let new_bounds = self.object_fit.get_bounds(bounds, data.size());
233 cx.paint_image(new_bounds, corner_radii, data, self.grayscale)
234 .log_err();
235 }
236
237 #[cfg(target_os = "macos")]
238 ImageSource::Surface(surface) => {
239 let size = size(surface.width().into(), surface.height().into());
240 let new_bounds = self.object_fit.get_bounds(bounds, size);
241 // TODO: Add support for corner_radii and grayscale.
242 cx.paint_surface(new_bounds, surface);
243 }
244 }
245 })
246 }
247}
248
249impl IntoElement for Img {
250 type Element = Self;
251
252 fn into_element(self) -> Self::Element {
253 self
254 }
255}
256
257impl Styled for Img {
258 fn style(&mut self) -> &mut StyleRefinement {
259 &mut self.interactivity.base_style
260 }
261}
262
263impl InteractiveElement for Img {
264 fn interactivity(&mut self) -> &mut Interactivity {
265 &mut self.interactivity
266 }
267}