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}