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