1use std::sync::Arc;
2
3use crate::{
4 point, size, Bounds, DevicePixels, Element, ElementContext, ImageData, InteractiveElement,
5 InteractiveElementState, Interactivity, IntoElement, LayoutId, Pixels, SharedUrl, Size,
6 StyleRefinement, Styled,
7};
8use futures::FutureExt;
9use media::core_video::CVImageBuffer;
10use util::ResultExt;
11
12/// A source of image content.
13#[derive(Clone, Debug)]
14pub enum ImageSource {
15 /// Image content will be loaded from provided URI at render time.
16 Uri(SharedUrl),
17 /// Cached image data
18 Data(Arc<ImageData>),
19 // TODO: move surface definitions into mac platform module
20 /// A CoreVideo image buffer
21 Surface(CVImageBuffer),
22}
23
24impl From<SharedUrl> for ImageSource {
25 fn from(value: SharedUrl) -> Self {
26 Self::Uri(value)
27 }
28}
29
30impl From<&'static str> for ImageSource {
31 fn from(uri: &'static str) -> Self {
32 Self::Uri(uri.into())
33 }
34}
35
36impl From<String> for ImageSource {
37 fn from(uri: String) -> Self {
38 Self::Uri(uri.into())
39 }
40}
41
42impl From<Arc<ImageData>> for ImageSource {
43 fn from(value: Arc<ImageData>) -> Self {
44 Self::Data(value)
45 }
46}
47
48impl From<CVImageBuffer> for ImageSource {
49 fn from(value: CVImageBuffer) -> Self {
50 Self::Surface(value)
51 }
52}
53
54/// An image element.
55pub struct Img {
56 interactivity: Interactivity,
57 source: ImageSource,
58 grayscale: bool,
59}
60
61/// Create a new image element.
62pub fn img(source: impl Into<ImageSource>) -> Img {
63 Img {
64 interactivity: Interactivity::default(),
65 source: source.into(),
66 grayscale: false,
67 }
68}
69
70impl Img {
71 /// Set the image to be displayed in grayscale.
72 pub fn grayscale(mut self, grayscale: bool) -> Self {
73 self.grayscale = grayscale;
74 self
75 }
76}
77
78impl Element for Img {
79 type State = InteractiveElementState;
80
81 fn request_layout(
82 &mut self,
83 element_state: Option<Self::State>,
84 cx: &mut ElementContext,
85 ) -> (LayoutId, Self::State) {
86 self.interactivity
87 .layout(element_state, cx, |style, cx| cx.request_layout(&style, []))
88 }
89
90 fn paint(
91 &mut self,
92 bounds: Bounds<Pixels>,
93 element_state: &mut Self::State,
94 cx: &mut ElementContext,
95 ) {
96 let source = self.source.clone();
97 self.interactivity.paint(
98 bounds,
99 bounds.size,
100 element_state,
101 cx,
102 |style, _scroll_offset, cx| {
103 let corner_radii = style.corner_radii.to_pixels(bounds.size, cx.rem_size());
104 cx.with_z_index(1, |cx| {
105 match source {
106 ImageSource::Uri(uri) => {
107 let image_future = cx.image_cache.get(uri.clone(), cx);
108 if let Some(data) = image_future
109 .clone()
110 .now_or_never()
111 .and_then(|result| result.ok())
112 {
113 let new_bounds = preserve_aspect_ratio(bounds, data.size());
114 cx.paint_image(new_bounds, corner_radii, data, self.grayscale)
115 .log_err();
116 } else {
117 cx.spawn(|mut cx| async move {
118 if image_future.await.ok().is_some() {
119 cx.on_next_frame(|cx| cx.refresh());
120 }
121 })
122 .detach();
123 }
124 }
125
126 ImageSource::Data(data) => {
127 let new_bounds = preserve_aspect_ratio(bounds, data.size());
128 cx.paint_image(new_bounds, corner_radii, data, self.grayscale)
129 .log_err();
130 }
131
132 ImageSource::Surface(surface) => {
133 let size = size(surface.width().into(), surface.height().into());
134 let new_bounds = preserve_aspect_ratio(bounds, size);
135 // TODO: Add support for corner_radii and grayscale.
136 cx.paint_surface(new_bounds, surface);
137 }
138 };
139 });
140 },
141 )
142 }
143}
144
145impl IntoElement for Img {
146 type Element = Self;
147
148 fn element_id(&self) -> Option<crate::ElementId> {
149 self.interactivity.element_id.clone()
150 }
151
152 fn into_element(self) -> Self::Element {
153 self
154 }
155}
156
157impl Styled for Img {
158 fn style(&mut self) -> &mut StyleRefinement {
159 &mut self.interactivity.base_style
160 }
161}
162
163impl InteractiveElement for Img {
164 fn interactivity(&mut self) -> &mut Interactivity {
165 &mut self.interactivity
166 }
167}
168
169fn preserve_aspect_ratio(bounds: Bounds<Pixels>, image_size: Size<DevicePixels>) -> Bounds<Pixels> {
170 let image_size = image_size.map(|dimension| Pixels::from(u32::from(dimension)));
171 let image_ratio = image_size.width / image_size.height;
172 let bounds_ratio = bounds.size.width / bounds.size.height;
173
174 let new_size = if bounds_ratio > image_ratio {
175 size(
176 image_size.width * (bounds.size.height / image_size.height),
177 bounds.size.height,
178 )
179 } else {
180 size(
181 bounds.size.width,
182 image_size.height * (bounds.size.width / image_size.width),
183 )
184 };
185
186 Bounds {
187 origin: point(
188 bounds.origin.x + (bounds.size.width - new_size.width) / 2.0,
189 bounds.origin.y + (bounds.size.height - new_size.height) / 2.0,
190 ),
191 size: new_size,
192 }
193}