img.rs

  1use std::path::PathBuf;
  2use std::sync::Arc;
  3
  4use crate::{
  5    point, size, Bounds, DevicePixels, Element, ElementContext, ImageData, InteractiveElement,
  6    InteractiveElementState, 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}
 72
 73/// Create a new image element.
 74pub fn img(source: impl Into<ImageSource>) -> Img {
 75    Img {
 76        interactivity: Interactivity::default(),
 77        source: source.into(),
 78        grayscale: false,
 79    }
 80}
 81
 82impl Img {
 83    /// Set the image to be displayed in grayscale.
 84    pub fn grayscale(mut self, grayscale: bool) -> Self {
 85        self.grayscale = grayscale;
 86        self
 87    }
 88}
 89
 90impl Element for Img {
 91    type State = InteractiveElementState;
 92
 93    fn request_layout(
 94        &mut self,
 95        element_state: Option<Self::State>,
 96        cx: &mut ElementContext,
 97    ) -> (LayoutId, Self::State) {
 98        self.interactivity
 99            .layout(element_state, cx, |style, cx| cx.request_layout(&style, []))
100    }
101
102    fn paint(
103        &mut self,
104        bounds: Bounds<Pixels>,
105        element_state: &mut Self::State,
106        cx: &mut ElementContext,
107    ) {
108        let source = self.source.clone();
109        self.interactivity.paint(
110            bounds,
111            bounds.size,
112            element_state,
113            cx,
114            |style, _scroll_offset, cx| {
115                let corner_radii = style.corner_radii.to_pixels(bounds.size, cx.rem_size());
116                cx.with_z_index(1, |cx| {
117                    match source {
118                        ImageSource::Uri(_) | ImageSource::File(_) => {
119                            let uri_or_path: UriOrPath = match source {
120                                ImageSource::Uri(uri) => uri.into(),
121                                ImageSource::File(path) => path.into(),
122                                _ => unreachable!(),
123                            };
124
125                            let image_future = cx.image_cache.get(uri_or_path.clone(), cx);
126                            if let Some(data) = image_future
127                                .clone()
128                                .now_or_never()
129                                .and_then(|result| result.ok())
130                            {
131                                let new_bounds = preserve_aspect_ratio(bounds, data.size());
132                                cx.paint_image(new_bounds, corner_radii, data, self.grayscale)
133                                    .log_err();
134                            } else {
135                                cx.spawn(|mut cx| async move {
136                                    if image_future.await.ok().is_some() {
137                                        cx.on_next_frame(|cx| cx.refresh());
138                                    }
139                                })
140                                .detach();
141                            }
142                        }
143
144                        ImageSource::Data(data) => {
145                            let new_bounds = preserve_aspect_ratio(bounds, data.size());
146                            cx.paint_image(new_bounds, corner_radii, data, self.grayscale)
147                                .log_err();
148                        }
149
150                        #[cfg(target_os = "macos")]
151                        ImageSource::Surface(surface) => {
152                            let size = size(surface.width().into(), surface.height().into());
153                            let new_bounds = preserve_aspect_ratio(bounds, size);
154                            // TODO: Add support for corner_radii and grayscale.
155                            cx.paint_surface(new_bounds, surface);
156                        }
157                    };
158                });
159            },
160        )
161    }
162}
163
164impl IntoElement for Img {
165    type Element = Self;
166
167    fn element_id(&self) -> Option<crate::ElementId> {
168        self.interactivity.element_id.clone()
169    }
170
171    fn into_element(self) -> Self::Element {
172        self
173    }
174}
175
176impl Styled for Img {
177    fn style(&mut self) -> &mut StyleRefinement {
178        &mut self.interactivity.base_style
179    }
180}
181
182impl InteractiveElement for Img {
183    fn interactivity(&mut self) -> &mut Interactivity {
184        &mut self.interactivity
185    }
186}
187
188fn preserve_aspect_ratio(bounds: Bounds<Pixels>, image_size: Size<DevicePixels>) -> Bounds<Pixels> {
189    let image_size = image_size.map(|dimension| Pixels::from(u32::from(dimension)));
190    let image_ratio = image_size.width / image_size.height;
191    let bounds_ratio = bounds.size.width / bounds.size.height;
192
193    let new_size = if bounds_ratio > image_ratio {
194        size(
195            image_size.width * (bounds.size.height / image_size.height),
196            bounds.size.height,
197        )
198    } else {
199        size(
200            bounds.size.width,
201            image_size.height * (bounds.size.width / image_size.width),
202        )
203    };
204
205    Bounds {
206        origin: point(
207            bounds.origin.x + (bounds.size.width - new_size.width) / 2.0,
208            bounds.origin.y + (bounds.size.height - new_size.height) / 2.0,
209        ),
210        size: new_size,
211    }
212}