1use crate::{
2 px, AbsoluteLength, AppContext, Asset, Bounds, DefiniteLength, Element, ElementId,
3 GlobalElementId, Hitbox, Image, InteractiveElement, Interactivity, IntoElement, LayoutId,
4 Length, ObjectFit, Pixels, RenderImage, SharedString, SharedUri, StyleRefinement, Styled,
5 SvgSize, UriOrPath, WindowContext,
6};
7use futures::{AsyncReadExt, Future};
8use image::{
9 codecs::gif::GifDecoder, AnimationDecoder, Frame, ImageBuffer, ImageError, ImageFormat,
10};
11use smallvec::SmallVec;
12use std::{
13 fs,
14 io::Cursor,
15 path::PathBuf,
16 sync::Arc,
17 time::{Duration, Instant},
18};
19use thiserror::Error;
20use util::ResultExt;
21
22/// A source of image content.
23#[derive(Clone, Debug, PartialEq, Eq)]
24pub enum ImageSource {
25 /// Image content will be loaded from provided URI at render time.
26 Uri(SharedUri),
27 /// Image content will be loaded from the provided file at render time.
28 File(Arc<PathBuf>),
29 /// Cached image data
30 Render(Arc<RenderImage>),
31 /// Cached image data
32 Image(Arc<Image>),
33 /// Image content will be loaded from Asset at render time.
34 Embedded(SharedString),
35}
36
37fn is_uri(uri: &str) -> bool {
38 uri.contains("://")
39}
40
41impl From<SharedUri> for ImageSource {
42 fn from(value: SharedUri) -> Self {
43 Self::Uri(value)
44 }
45}
46
47impl From<&'static str> for ImageSource {
48 fn from(s: &'static str) -> Self {
49 if is_uri(s) {
50 Self::Uri(s.into())
51 } else {
52 Self::Embedded(s.into())
53 }
54 }
55}
56
57impl From<String> for ImageSource {
58 fn from(s: String) -> Self {
59 if is_uri(&s) {
60 Self::Uri(s.into())
61 } else {
62 Self::Embedded(s.into())
63 }
64 }
65}
66
67impl From<SharedString> for ImageSource {
68 fn from(s: SharedString) -> Self {
69 if is_uri(&s) {
70 Self::Uri(s.into())
71 } else {
72 Self::Embedded(s)
73 }
74 }
75}
76
77impl From<Arc<PathBuf>> for ImageSource {
78 fn from(value: Arc<PathBuf>) -> Self {
79 Self::File(value)
80 }
81}
82
83impl From<PathBuf> for ImageSource {
84 fn from(value: PathBuf) -> Self {
85 Self::File(value.into())
86 }
87}
88
89impl From<Arc<RenderImage>> for ImageSource {
90 fn from(value: Arc<RenderImage>) -> Self {
91 Self::Render(value)
92 }
93}
94
95/// An image element.
96pub struct Img {
97 interactivity: Interactivity,
98 source: ImageSource,
99 grayscale: bool,
100 object_fit: ObjectFit,
101}
102
103/// Create a new image element.
104pub fn img(source: impl Into<ImageSource>) -> Img {
105 Img {
106 interactivity: Interactivity::default(),
107 source: source.into(),
108 grayscale: false,
109 object_fit: ObjectFit::Contain,
110 }
111}
112
113impl Img {
114 /// A list of all format extensions currently supported by this img element
115 pub fn extensions() -> &'static [&'static str] {
116 // This is the list in [image::ImageFormat::from_extension] + `svg`
117 &[
118 "avif", "jpg", "jpeg", "png", "gif", "webp", "tif", "tiff", "tga", "dds", "bmp", "ico",
119 "hdr", "exr", "pbm", "pam", "ppm", "pgm", "ff", "farbfeld", "qoi", "svg",
120 ]
121 }
122
123 /// Set the image to be displayed in grayscale.
124 pub fn grayscale(mut self, grayscale: bool) -> Self {
125 self.grayscale = grayscale;
126 self
127 }
128 /// Set the object fit for the image.
129 pub fn object_fit(mut self, object_fit: ObjectFit) -> Self {
130 self.object_fit = object_fit;
131 self
132 }
133}
134
135/// The image state between frames
136struct ImgState {
137 frame_index: usize,
138 last_frame_time: Option<Instant>,
139}
140
141impl Element for Img {
142 type RequestLayoutState = usize;
143 type PrepaintState = Option<Hitbox>;
144
145 fn id(&self) -> Option<ElementId> {
146 self.interactivity.element_id.clone()
147 }
148
149 fn request_layout(
150 &mut self,
151 global_id: Option<&GlobalElementId>,
152 cx: &mut WindowContext,
153 ) -> (LayoutId, Self::RequestLayoutState) {
154 cx.with_optional_element_state(global_id, |state, cx| {
155 let mut state = state.map(|state| {
156 state.unwrap_or(ImgState {
157 frame_index: 0,
158 last_frame_time: None,
159 })
160 });
161
162 let frame_index = state.as_ref().map(|state| state.frame_index).unwrap_or(0);
163
164 let layout_id = self
165 .interactivity
166 .request_layout(global_id, cx, |mut style, cx| {
167 if let Some(data) = self.source.use_data(cx) {
168 if let Some(state) = &mut state {
169 let frame_count = data.frame_count();
170 if frame_count > 1 {
171 let current_time = Instant::now();
172 if let Some(last_frame_time) = state.last_frame_time {
173 let elapsed = current_time - last_frame_time;
174 let frame_duration =
175 Duration::from(data.delay(state.frame_index));
176
177 if elapsed >= frame_duration {
178 state.frame_index = (state.frame_index + 1) % frame_count;
179 state.last_frame_time =
180 Some(current_time - (elapsed - frame_duration));
181 }
182 } else {
183 state.last_frame_time = Some(current_time);
184 }
185 }
186 }
187
188 let image_size = data.size(frame_index);
189
190 if let Length::Auto = style.size.width {
191 style.size.width = match style.size.height {
192 Length::Definite(DefiniteLength::Absolute(
193 AbsoluteLength::Pixels(height),
194 )) => Length::Definite(
195 px(image_size.width.0 as f32 * height.0
196 / image_size.height.0 as f32)
197 .into(),
198 ),
199 _ => Length::Definite(px(image_size.width.0 as f32).into()),
200 };
201 }
202
203 if let Length::Auto = style.size.height {
204 style.size.height = match style.size.width {
205 Length::Definite(DefiniteLength::Absolute(
206 AbsoluteLength::Pixels(width),
207 )) => Length::Definite(
208 px(image_size.height.0 as f32 * width.0
209 / image_size.width.0 as f32)
210 .into(),
211 ),
212 _ => Length::Definite(px(image_size.height.0 as f32).into()),
213 };
214 }
215
216 if global_id.is_some() && data.frame_count() > 1 {
217 cx.request_animation_frame();
218 }
219 }
220
221 cx.request_layout(style, [])
222 });
223
224 ((layout_id, frame_index), state)
225 })
226 }
227
228 fn prepaint(
229 &mut self,
230 global_id: Option<&GlobalElementId>,
231 bounds: Bounds<Pixels>,
232 _request_layout: &mut Self::RequestLayoutState,
233 cx: &mut WindowContext,
234 ) -> Option<Hitbox> {
235 self.interactivity
236 .prepaint(global_id, bounds, bounds.size, cx, |_, _, hitbox, _| hitbox)
237 }
238
239 fn paint(
240 &mut self,
241 global_id: Option<&GlobalElementId>,
242 bounds: Bounds<Pixels>,
243 frame_index: &mut Self::RequestLayoutState,
244 hitbox: &mut Self::PrepaintState,
245 cx: &mut WindowContext,
246 ) {
247 let source = self.source.clone();
248 self.interactivity
249 .paint(global_id, bounds, hitbox.as_ref(), cx, |style, cx| {
250 let corner_radii = style.corner_radii.to_pixels(bounds.size, cx.rem_size());
251
252 if let Some(data) = source.use_data(cx) {
253 let new_bounds = self.object_fit.get_bounds(bounds, data.size(*frame_index));
254 cx.paint_image(
255 new_bounds,
256 corner_radii,
257 data.clone(),
258 *frame_index,
259 self.grayscale,
260 )
261 .log_err();
262 }
263 })
264 }
265}
266
267impl IntoElement for Img {
268 type Element = Self;
269
270 fn into_element(self) -> Self::Element {
271 self
272 }
273}
274
275impl Styled for Img {
276 fn style(&mut self) -> &mut StyleRefinement {
277 &mut self.interactivity.base_style
278 }
279}
280
281impl InteractiveElement for Img {
282 fn interactivity(&mut self) -> &mut Interactivity {
283 &mut self.interactivity
284 }
285}
286
287impl ImageSource {
288 pub(crate) fn use_data(&self, cx: &mut WindowContext) -> Option<Arc<RenderImage>> {
289 match self {
290 ImageSource::Uri(_) | ImageSource::Embedded(_) | ImageSource::File(_) => {
291 let uri_or_path: UriOrPath = match self {
292 ImageSource::Uri(uri) => uri.clone().into(),
293 ImageSource::File(path) => path.clone().into(),
294 ImageSource::Embedded(path) => UriOrPath::Embedded(path.clone()),
295 _ => unreachable!(),
296 };
297
298 cx.use_asset::<ImageAsset>(&uri_or_path)?.log_err()
299 }
300
301 ImageSource::Render(data) => Some(data.to_owned()),
302 ImageSource::Image(data) => cx.use_asset::<ImageDecoder>(data)?.log_err(),
303 }
304 }
305
306 /// Fetch the data associated with this source, using GPUI's asset caching
307 pub async fn data(&self, cx: &mut AppContext) -> Option<Arc<RenderImage>> {
308 match self {
309 ImageSource::Uri(_) | ImageSource::Embedded(_) | ImageSource::File(_) => {
310 let uri_or_path: UriOrPath = match self {
311 ImageSource::Uri(uri) => uri.clone().into(),
312 ImageSource::File(path) => path.clone().into(),
313 ImageSource::Embedded(path) => UriOrPath::Embedded(path.clone()),
314 _ => unreachable!(),
315 };
316
317 cx.fetch_asset::<ImageAsset>(&uri_or_path).0.await.log_err()
318 }
319
320 ImageSource::Render(data) => Some(data.to_owned()),
321 ImageSource::Image(data) => cx.fetch_asset::<ImageDecoder>(data).0.await.log_err(),
322 }
323 }
324}
325
326#[derive(Clone)]
327enum ImageDecoder {}
328
329impl Asset for ImageDecoder {
330 type Source = Arc<Image>;
331 type Output = Result<Arc<RenderImage>, Arc<anyhow::Error>>;
332
333 fn load(
334 source: Self::Source,
335 cx: &mut AppContext,
336 ) -> impl Future<Output = Self::Output> + Send + 'static {
337 let result = source.to_image_data(cx).map_err(Arc::new);
338 async { result }
339 }
340}
341
342#[derive(Clone)]
343enum ImageAsset {}
344
345impl Asset for ImageAsset {
346 type Source = UriOrPath;
347 type Output = Result<Arc<RenderImage>, ImageCacheError>;
348
349 fn load(
350 source: Self::Source,
351 cx: &mut AppContext,
352 ) -> impl Future<Output = Self::Output> + Send + 'static {
353 let client = cx.http_client();
354 // TODO: Can we make SVGs always rescale?
355 // let scale_factor = cx.scale_factor();
356 let svg_renderer = cx.svg_renderer();
357 let asset_source = cx.asset_source().clone();
358 async move {
359 let bytes = match source.clone() {
360 UriOrPath::Path(uri) => fs::read(uri.as_ref())?,
361 UriOrPath::Uri(uri) => {
362 let mut response = client
363 .get(uri.as_ref(), ().into(), true)
364 .await
365 .map_err(|e| ImageCacheError::Client(Arc::new(e)))?;
366 let mut body = Vec::new();
367 response.body_mut().read_to_end(&mut body).await?;
368 if !response.status().is_success() {
369 let mut body = String::from_utf8_lossy(&body).into_owned();
370 let first_line = body.lines().next().unwrap_or("").trim_end();
371 body.truncate(first_line.len());
372 return Err(ImageCacheError::BadStatus {
373 uri,
374 status: response.status(),
375 body,
376 });
377 }
378 body
379 }
380 UriOrPath::Embedded(path) => {
381 let data = asset_source.load(&path).ok().flatten();
382 if let Some(data) = data {
383 data.to_vec()
384 } else {
385 return Err(ImageCacheError::Asset(
386 format!("not found: {}", path).into(),
387 ));
388 }
389 }
390 };
391
392 let data = if let Ok(format) = image::guess_format(&bytes) {
393 let data = match format {
394 ImageFormat::Gif => {
395 let decoder = GifDecoder::new(Cursor::new(&bytes))?;
396 let mut frames = SmallVec::new();
397
398 for frame in decoder.into_frames() {
399 let mut frame = frame?;
400 // Convert from RGBA to BGRA.
401 for pixel in frame.buffer_mut().chunks_exact_mut(4) {
402 pixel.swap(0, 2);
403 }
404 frames.push(frame);
405 }
406
407 frames
408 }
409 _ => {
410 let mut data =
411 image::load_from_memory_with_format(&bytes, format)?.into_rgba8();
412
413 // Convert from RGBA to BGRA.
414 for pixel in data.chunks_exact_mut(4) {
415 pixel.swap(0, 2);
416 }
417
418 SmallVec::from_elem(Frame::new(data), 1)
419 }
420 };
421
422 RenderImage::new(data)
423 } else {
424 let pixmap =
425 // TODO: Can we make svgs always rescale?
426 svg_renderer.render_pixmap(&bytes, SvgSize::ScaleFactor(1.0))?;
427
428 let mut buffer =
429 ImageBuffer::from_raw(pixmap.width(), pixmap.height(), pixmap.take()).unwrap();
430
431 // Convert from RGBA to BGRA.
432 for pixel in buffer.chunks_exact_mut(4) {
433 pixel.swap(0, 2);
434 }
435
436 RenderImage::new(SmallVec::from_elem(Frame::new(buffer), 1))
437 };
438
439 Ok(Arc::new(data))
440 }
441 }
442}
443
444/// An error that can occur when interacting with the image cache.
445#[derive(Debug, Error, Clone)]
446pub enum ImageCacheError {
447 /// An error that occurred while fetching an image from a remote source.
448 #[error("http error: {0}")]
449 Client(#[from] Arc<anyhow::Error>),
450 /// An error that occurred while reading the image from disk.
451 #[error("IO error: {0}")]
452 Io(Arc<std::io::Error>),
453 /// An error that occurred while processing an image.
454 #[error("unexpected http status for {uri}: {status}, body: {body}")]
455 BadStatus {
456 /// The URI of the image.
457 uri: SharedUri,
458 /// The HTTP status code.
459 status: http_client::StatusCode,
460 /// The HTTP response body.
461 body: String,
462 },
463 /// An error that occurred while processing an asset.
464 #[error("asset error: {0}")]
465 Asset(SharedString),
466 /// An error that occurred while processing an image.
467 #[error("image error: {0}")]
468 Image(Arc<ImageError>),
469 /// An error that occurred while processing an SVG.
470 #[error("svg error: {0}")]
471 Usvg(Arc<usvg::Error>),
472}
473
474impl From<std::io::Error> for ImageCacheError {
475 fn from(error: std::io::Error) -> Self {
476 Self::Io(Arc::new(error))
477 }
478}
479
480impl From<ImageError> for ImageCacheError {
481 fn from(error: ImageError) -> Self {
482 Self::Image(Arc::new(error))
483 }
484}
485
486impl From<usvg::Error> for ImageCacheError {
487 fn from(error: usvg::Error) -> Self {
488 Self::Usvg(Arc::new(error))
489 }
490}