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, Size, 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, Length::Auto) = (style.size.width, style.size.height)
191 {
192 style.size = Size {
193 width: Length::Definite(DefiniteLength::Absolute(
194 AbsoluteLength::Pixels(px(image_size.width.0 as f32)),
195 )),
196 height: Length::Definite(DefiniteLength::Absolute(
197 AbsoluteLength::Pixels(px(image_size.height.0 as f32)),
198 )),
199 }
200 }
201
202 if global_id.is_some() && data.frame_count() > 1 {
203 cx.request_animation_frame();
204 }
205 }
206
207 cx.request_layout(style, [])
208 });
209
210 ((layout_id, frame_index), state)
211 })
212 }
213
214 fn prepaint(
215 &mut self,
216 global_id: Option<&GlobalElementId>,
217 bounds: Bounds<Pixels>,
218 _request_layout: &mut Self::RequestLayoutState,
219 cx: &mut WindowContext,
220 ) -> Option<Hitbox> {
221 self.interactivity
222 .prepaint(global_id, bounds, bounds.size, cx, |_, _, hitbox, _| hitbox)
223 }
224
225 fn paint(
226 &mut self,
227 global_id: Option<&GlobalElementId>,
228 bounds: Bounds<Pixels>,
229 frame_index: &mut Self::RequestLayoutState,
230 hitbox: &mut Self::PrepaintState,
231 cx: &mut WindowContext,
232 ) {
233 let source = self.source.clone();
234 self.interactivity
235 .paint(global_id, bounds, hitbox.as_ref(), cx, |style, cx| {
236 let corner_radii = style.corner_radii.to_pixels(bounds.size, cx.rem_size());
237
238 if let Some(data) = source.use_data(cx) {
239 let new_bounds = self.object_fit.get_bounds(bounds, data.size(*frame_index));
240 cx.paint_image(
241 new_bounds,
242 corner_radii,
243 data.clone(),
244 *frame_index,
245 self.grayscale,
246 )
247 .log_err();
248 }
249 })
250 }
251}
252
253impl IntoElement for Img {
254 type Element = Self;
255
256 fn into_element(self) -> Self::Element {
257 self
258 }
259}
260
261impl Styled for Img {
262 fn style(&mut self) -> &mut StyleRefinement {
263 &mut self.interactivity.base_style
264 }
265}
266
267impl InteractiveElement for Img {
268 fn interactivity(&mut self) -> &mut Interactivity {
269 &mut self.interactivity
270 }
271}
272
273impl ImageSource {
274 pub(crate) fn use_data(&self, cx: &mut WindowContext) -> Option<Arc<RenderImage>> {
275 match self {
276 ImageSource::Uri(_) | ImageSource::Embedded(_) | ImageSource::File(_) => {
277 let uri_or_path: UriOrPath = match self {
278 ImageSource::Uri(uri) => uri.clone().into(),
279 ImageSource::File(path) => path.clone().into(),
280 ImageSource::Embedded(path) => UriOrPath::Embedded(path.clone()),
281 _ => unreachable!(),
282 };
283
284 cx.use_asset::<ImageAsset>(&uri_or_path)?.log_err()
285 }
286
287 ImageSource::Render(data) => Some(data.to_owned()),
288 ImageSource::Image(data) => cx.use_asset::<ImageDecoder>(data)?.log_err(),
289 }
290 }
291
292 /// Fetch the data associated with this source, using GPUI's asset caching
293 pub async fn data(&self, cx: &mut AppContext) -> Option<Arc<RenderImage>> {
294 match self {
295 ImageSource::Uri(_) | ImageSource::Embedded(_) | ImageSource::File(_) => {
296 let uri_or_path: UriOrPath = match self {
297 ImageSource::Uri(uri) => uri.clone().into(),
298 ImageSource::File(path) => path.clone().into(),
299 ImageSource::Embedded(path) => UriOrPath::Embedded(path.clone()),
300 _ => unreachable!(),
301 };
302
303 cx.fetch_asset::<ImageAsset>(&uri_or_path).0.await.log_err()
304 }
305
306 ImageSource::Render(data) => Some(data.to_owned()),
307 ImageSource::Image(data) => cx.fetch_asset::<ImageDecoder>(data).0.await.log_err(),
308 }
309 }
310}
311
312#[derive(Clone)]
313enum ImageDecoder {}
314
315impl Asset for ImageDecoder {
316 type Source = Arc<Image>;
317 type Output = Result<Arc<RenderImage>, Arc<anyhow::Error>>;
318
319 fn load(
320 source: Self::Source,
321 cx: &mut AppContext,
322 ) -> impl Future<Output = Self::Output> + Send + 'static {
323 let result = source.to_image_data(cx).map_err(Arc::new);
324 async { result }
325 }
326}
327
328#[derive(Clone)]
329enum ImageAsset {}
330
331impl Asset for ImageAsset {
332 type Source = UriOrPath;
333 type Output = Result<Arc<RenderImage>, ImageCacheError>;
334
335 fn load(
336 source: Self::Source,
337 cx: &mut AppContext,
338 ) -> impl Future<Output = Self::Output> + Send + 'static {
339 let client = cx.http_client();
340 // TODO: Can we make SVGs always rescale?
341 // let scale_factor = cx.scale_factor();
342 let svg_renderer = cx.svg_renderer();
343 let asset_source = cx.asset_source().clone();
344 async move {
345 let bytes = match source.clone() {
346 UriOrPath::Path(uri) => fs::read(uri.as_ref())?,
347 UriOrPath::Uri(uri) => {
348 let mut response = client
349 .get(uri.as_ref(), ().into(), true)
350 .await
351 .map_err(|e| ImageCacheError::Client(Arc::new(e)))?;
352 let mut body = Vec::new();
353 response.body_mut().read_to_end(&mut body).await?;
354 if !response.status().is_success() {
355 let mut body = String::from_utf8_lossy(&body).into_owned();
356 let first_line = body.lines().next().unwrap_or("").trim_end();
357 body.truncate(first_line.len());
358 return Err(ImageCacheError::BadStatus {
359 uri,
360 status: response.status(),
361 body,
362 });
363 }
364 body
365 }
366 UriOrPath::Embedded(path) => {
367 let data = asset_source.load(&path).ok().flatten();
368 if let Some(data) = data {
369 data.to_vec()
370 } else {
371 return Err(ImageCacheError::Asset(
372 format!("not found: {}", path).into(),
373 ));
374 }
375 }
376 };
377
378 let data = if let Ok(format) = image::guess_format(&bytes) {
379 let data = match format {
380 ImageFormat::Gif => {
381 let decoder = GifDecoder::new(Cursor::new(&bytes))?;
382 let mut frames = SmallVec::new();
383
384 for frame in decoder.into_frames() {
385 let mut frame = frame?;
386 // Convert from RGBA to BGRA.
387 for pixel in frame.buffer_mut().chunks_exact_mut(4) {
388 pixel.swap(0, 2);
389 }
390 frames.push(frame);
391 }
392
393 frames
394 }
395 _ => {
396 let mut data =
397 image::load_from_memory_with_format(&bytes, format)?.into_rgba8();
398
399 // Convert from RGBA to BGRA.
400 for pixel in data.chunks_exact_mut(4) {
401 pixel.swap(0, 2);
402 }
403
404 SmallVec::from_elem(Frame::new(data), 1)
405 }
406 };
407
408 RenderImage::new(data)
409 } else {
410 let pixmap =
411 // TODO: Can we make svgs always rescale?
412 svg_renderer.render_pixmap(&bytes, SvgSize::ScaleFactor(1.0))?;
413
414 let mut buffer =
415 ImageBuffer::from_raw(pixmap.width(), pixmap.height(), pixmap.take()).unwrap();
416
417 // Convert from RGBA to BGRA.
418 for pixel in buffer.chunks_exact_mut(4) {
419 pixel.swap(0, 2);
420 }
421
422 RenderImage::new(SmallVec::from_elem(Frame::new(buffer), 1))
423 };
424
425 Ok(Arc::new(data))
426 }
427 }
428}
429
430/// An error that can occur when interacting with the image cache.
431#[derive(Debug, Error, Clone)]
432pub enum ImageCacheError {
433 /// An error that occurred while fetching an image from a remote source.
434 #[error("http error: {0}")]
435 Client(#[from] Arc<anyhow::Error>),
436 /// An error that occurred while reading the image from disk.
437 #[error("IO error: {0}")]
438 Io(Arc<std::io::Error>),
439 /// An error that occurred while processing an image.
440 #[error("unexpected http status for {uri}: {status}, body: {body}")]
441 BadStatus {
442 /// The URI of the image.
443 uri: SharedUri,
444 /// The HTTP status code.
445 status: http_client::StatusCode,
446 /// The HTTP response body.
447 body: String,
448 },
449 /// An error that occurred while processing an asset.
450 #[error("asset error: {0}")]
451 Asset(SharedString),
452 /// An error that occurred while processing an image.
453 #[error("image error: {0}")]
454 Image(Arc<ImageError>),
455 /// An error that occurred while processing an SVG.
456 #[error("svg error: {0}")]
457 Usvg(Arc<usvg::Error>),
458}
459
460impl From<std::io::Error> for ImageCacheError {
461 fn from(error: std::io::Error) -> Self {
462 Self::Io(Arc::new(error))
463 }
464}
465
466impl From<ImageError> for ImageCacheError {
467 fn from(error: ImageError) -> Self {
468 Self::Image(Arc::new(error))
469 }
470}
471
472impl From<usvg::Error> for ImageCacheError {
473 fn from(error: usvg::Error) -> Self {
474 Self::Usvg(Arc::new(error))
475 }
476}