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}
 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 BeforeLayout = ();
 92    type AfterLayout = Option<Hitbox>;
 93
 94    fn before_layout(&mut self, cx: &mut ElementContext) -> (LayoutId, Self::BeforeLayout) {
 95        let layout_id = self
 96            .interactivity
 97            .before_layout(cx, |style, cx| cx.request_layout(&style, []));
 98        (layout_id, ())
 99    }
100
101    fn after_layout(
102        &mut self,
103        bounds: Bounds<Pixels>,
104        _before_layout: &mut Self::BeforeLayout,
105        cx: &mut ElementContext,
106    ) -> Option<Hitbox> {
107        self.interactivity
108            .after_layout(bounds, bounds.size, cx, |_, _, hitbox, _| hitbox)
109    }
110
111    fn paint(
112        &mut self,
113        bounds: Bounds<Pixels>,
114        _: &mut Self::BeforeLayout,
115        hitbox: &mut Self::AfterLayout,
116        cx: &mut ElementContext,
117    ) {
118        let source = self.source.clone();
119        self.interactivity
120            .paint(bounds, hitbox.as_ref(), cx, |style, cx| {
121                let corner_radii = style.corner_radii.to_pixels(bounds.size, cx.rem_size());
122                match source {
123                    ImageSource::Uri(_) | ImageSource::File(_) => {
124                        let uri_or_path: UriOrPath = match source {
125                            ImageSource::Uri(uri) => uri.into(),
126                            ImageSource::File(path) => path.into(),
127                            _ => unreachable!(),
128                        };
129
130                        let image_future = cx.image_cache.get(uri_or_path.clone(), cx);
131                        if let Some(data) = image_future
132                            .clone()
133                            .now_or_never()
134                            .and_then(|result| result.ok())
135                        {
136                            let new_bounds = preserve_aspect_ratio(bounds, data.size());
137                            cx.paint_image(new_bounds, corner_radii, data, self.grayscale)
138                                .log_err();
139                        } else {
140                            cx.spawn(|mut cx| async move {
141                                if image_future.await.ok().is_some() {
142                                    cx.on_next_frame(|cx| cx.refresh());
143                                }
144                            })
145                            .detach();
146                        }
147                    }
148
149                    ImageSource::Data(data) => {
150                        let new_bounds = preserve_aspect_ratio(bounds, data.size());
151                        cx.paint_image(new_bounds, corner_radii, data, self.grayscale)
152                            .log_err();
153                    }
154
155                    #[cfg(target_os = "macos")]
156                    ImageSource::Surface(surface) => {
157                        let size = size(surface.width().into(), surface.height().into());
158                        let new_bounds = preserve_aspect_ratio(bounds, size);
159                        // TODO: Add support for corner_radii and grayscale.
160                        cx.paint_surface(new_bounds, surface);
161                    }
162                }
163            })
164    }
165}
166
167impl IntoElement for Img {
168    type Element = Self;
169
170    fn into_element(self) -> Self::Element {
171        self
172    }
173}
174
175impl Styled for Img {
176    fn style(&mut self) -> &mut StyleRefinement {
177        &mut self.interactivity.base_style
178    }
179}
180
181impl InteractiveElement for Img {
182    fn interactivity(&mut self) -> &mut Interactivity {
183        &mut self.interactivity
184    }
185}
186
187fn preserve_aspect_ratio(bounds: Bounds<Pixels>, image_size: Size<DevicePixels>) -> Bounds<Pixels> {
188    let image_size = image_size.map(|dimension| Pixels::from(u32::from(dimension)));
189    let image_ratio = image_size.width / image_size.height;
190    let bounds_ratio = bounds.size.width / bounds.size.height;
191
192    let new_size = if bounds_ratio > image_ratio {
193        size(
194            image_size.width * (bounds.size.height / image_size.height),
195            bounds.size.height,
196        )
197    } else {
198        size(
199            bounds.size.width,
200            image_size.height * (bounds.size.width / image_size.width),
201        )
202    };
203
204    Bounds {
205        origin: point(
206            bounds.origin.x + (bounds.size.width - new_size.width) / 2.0,
207            bounds.origin.y + (bounds.size.height - new_size.height) / 2.0,
208        ),
209        size: new_size,
210    }
211}