img.rs

  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}