Skip to repository content849 lines · 30.4 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T06:50:35.794Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
img.rs
1use crate::{
2 AnyElement, AnyImageCache, App, Asset, AssetLogger, Bounds, DefiniteLength, Element, ElementId,
3 Entity, GlobalElementId, Hitbox, Image, ImageCache, InspectorElementId, InteractiveElement,
4 Interactivity, IntoElement, LayoutId, Length, ObjectFit, Pixels, RenderImage, Resource,
5 SharedString, SharedUri, StyleRefinement, Styled, Task, Window, decode_static_image,
6 decode_static_image_from_decoder, px,
7};
8use anyhow::Result;
9
10use futures::Future;
11use gpui_util::ResultExt;
12use image::{
13 AnimationDecoder, ImageError, ImageFormat, Rgba,
14 codecs::{gif::GifDecoder, webp::WebPDecoder},
15};
16use scheduler::Instant;
17use smallvec::SmallVec;
18use std::{
19 fs,
20 io::{self, Cursor},
21 ops::{Deref, DerefMut},
22 path::{Path, PathBuf},
23 str::FromStr,
24 sync::Arc,
25 time::Duration,
26};
27use thiserror::Error;
28
29use super::{Stateful, StatefulInteractiveElement};
30
31/// The delay before showing the loading state.
32pub const LOADING_DELAY: Duration = Duration::from_millis(200);
33
34/// A type alias to the resource loader that the `img()` element uses.
35///
36/// Note: that this is only for Resources, like URLs or file paths.
37/// Custom loaders, or external images will not use this asset loader
38pub type ImgResourceLoader = AssetLogger<ImageAssetLoader>;
39
40/// A source of image content.
41#[derive(Clone)]
42pub enum ImageSource {
43 /// The image content will be loaded from some resource location
44 Resource(Resource),
45 /// Cached image data
46 Render(Arc<RenderImage>),
47 /// Cached image data
48 Image(Arc<Image>),
49 /// A custom loading function to use
50 Custom(Arc<dyn Fn(&mut Window, &mut App) -> Option<Result<Arc<RenderImage>, ImageCacheError>>>),
51}
52
53fn is_uri(uri: &str) -> bool {
54 url::Url::from_str(uri).is_ok()
55}
56
57impl From<SharedUri> for ImageSource {
58 fn from(value: SharedUri) -> Self {
59 Self::Resource(Resource::Uri(value))
60 }
61}
62
63impl<'a> From<&'a str> for ImageSource {
64 fn from(s: &'a str) -> Self {
65 if is_uri(s) {
66 Self::Resource(Resource::Uri(s.to_string().into()))
67 } else {
68 Self::Resource(Resource::Embedded(s.to_string().into()))
69 }
70 }
71}
72
73impl From<String> for ImageSource {
74 fn from(s: String) -> Self {
75 if is_uri(&s) {
76 Self::Resource(Resource::Uri(s.into()))
77 } else {
78 Self::Resource(Resource::Embedded(s.into()))
79 }
80 }
81}
82
83impl From<SharedString> for ImageSource {
84 fn from(s: SharedString) -> Self {
85 s.as_ref().into()
86 }
87}
88
89impl From<&Path> for ImageSource {
90 fn from(value: &Path) -> Self {
91 Self::Resource(value.to_path_buf().into())
92 }
93}
94
95impl From<Arc<Path>> for ImageSource {
96 fn from(value: Arc<Path>) -> Self {
97 Self::Resource(value.into())
98 }
99}
100
101impl From<PathBuf> for ImageSource {
102 fn from(value: PathBuf) -> Self {
103 Self::Resource(value.into())
104 }
105}
106
107impl From<Arc<RenderImage>> for ImageSource {
108 fn from(value: Arc<RenderImage>) -> Self {
109 Self::Render(value)
110 }
111}
112
113impl From<Arc<Image>> for ImageSource {
114 fn from(value: Arc<Image>) -> Self {
115 Self::Image(value)
116 }
117}
118
119impl<F> From<F> for ImageSource
120where
121 F: Fn(&mut Window, &mut App) -> Option<Result<Arc<RenderImage>, ImageCacheError>> + 'static,
122{
123 fn from(value: F) -> Self {
124 Self::Custom(Arc::new(value))
125 }
126}
127
128/// The style of an image element.
129pub struct ImageStyle {
130 grayscale: bool,
131 object_fit: ObjectFit,
132 loading: Option<Box<dyn Fn() -> AnyElement>>,
133 fallback: Option<Box<dyn Fn() -> AnyElement>>,
134}
135
136impl Default for ImageStyle {
137 fn default() -> Self {
138 Self {
139 grayscale: false,
140 object_fit: ObjectFit::Contain,
141 loading: None,
142 fallback: None,
143 }
144 }
145}
146
147/// Style an image element.
148pub trait StyledImage: Sized {
149 /// Get a mutable [ImageStyle] from the element.
150 fn image_style(&mut self) -> &mut ImageStyle;
151
152 /// Set the image to be displayed in grayscale.
153 fn grayscale(mut self, grayscale: bool) -> Self {
154 self.image_style().grayscale = grayscale;
155 self
156 }
157
158 /// Set the object fit for the image.
159 fn object_fit(mut self, object_fit: ObjectFit) -> Self {
160 self.image_style().object_fit = object_fit;
161 self
162 }
163
164 /// Set a fallback function that will be invoked to render an error view should
165 /// the image fail to load.
166 fn with_fallback(mut self, fallback: impl Fn() -> AnyElement + 'static) -> Self {
167 self.image_style().fallback = Some(Box::new(fallback));
168 self
169 }
170
171 /// Set a fallback function that will be invoked to render a view while the image
172 /// is still being loaded.
173 fn with_loading(mut self, loading: impl Fn() -> AnyElement + 'static) -> Self {
174 self.image_style().loading = Some(Box::new(loading));
175 self
176 }
177}
178
179impl StyledImage for Img {
180 fn image_style(&mut self) -> &mut ImageStyle {
181 &mut self.style
182 }
183}
184
185impl StyledImage for Stateful<Img> {
186 fn image_style(&mut self) -> &mut ImageStyle {
187 &mut self.element.style
188 }
189}
190
191/// An image element.
192pub struct Img {
193 interactivity: Interactivity,
194 source: ImageSource,
195 style: ImageStyle,
196 image_cache: Option<AnyImageCache>,
197}
198
199/// Create a new image element.
200#[track_caller]
201pub fn img(source: impl Into<ImageSource>) -> Img {
202 Img {
203 interactivity: Interactivity::new(),
204 source: source.into(),
205 style: ImageStyle::default(),
206 image_cache: None,
207 }
208}
209
210impl Img {
211 /// A list of all format extensions currently supported by this img element
212 pub fn extensions() -> &'static [&'static str] {
213 // This is the list in [image::ImageFormat::from_extension] + `svg`
214 &[
215 "avif", "jpg", "jpeg", "png", "gif", "webp", "tif", "tiff", "tga", "dds", "bmp", "ico",
216 "hdr", "exr", "pbm", "pam", "ppm", "pgm", "ff", "farbfeld", "qoi", "svg",
217 ]
218 }
219
220 /// Sets the image cache for the current node.
221 ///
222 /// If the `image_cache` is not explicitly provided, the function will determine the image cache by:
223 ///
224 /// 1. Checking if any ancestor node of the current node contains an `ImageCacheElement`, If such a node exists, the image cache specified by that ancestor will be used.
225 /// 2. If no ancestor node contains an `ImageCacheElement`, the global image cache will be used as a fallback.
226 ///
227 /// This mechanism provides a flexible way to manage image caching, allowing precise control when needed,
228 /// while ensuring a default behavior when no cache is explicitly specified.
229 #[inline]
230 pub fn image_cache<I: ImageCache>(self, image_cache: &Entity<I>) -> Self {
231 Self {
232 image_cache: Some(image_cache.clone().into()),
233 ..self
234 }
235 }
236}
237
238impl Deref for Stateful<Img> {
239 type Target = Img;
240
241 fn deref(&self) -> &Self::Target {
242 &self.element
243 }
244}
245
246impl DerefMut for Stateful<Img> {
247 fn deref_mut(&mut self) -> &mut Self::Target {
248 &mut self.element
249 }
250}
251
252/// The image state between frames
253struct ImgState {
254 frame_index: usize,
255 last_frame_time: Option<Instant>,
256 started_loading: Option<(Instant, Task<()>)>,
257}
258
259/// The image layout state between frames
260pub struct ImgLayoutState {
261 frame_index: usize,
262 replacement: Option<AnyElement>,
263}
264
265impl Element for Img {
266 type RequestLayoutState = ImgLayoutState;
267 type PrepaintState = Option<Hitbox>;
268
269 fn id(&self) -> Option<ElementId> {
270 self.interactivity.element_id.clone()
271 }
272
273 fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
274 self.interactivity.source_location()
275 }
276
277 fn request_layout(
278 &mut self,
279 global_id: Option<&GlobalElementId>,
280 inspector_id: Option<&InspectorElementId>,
281 window: &mut Window,
282 cx: &mut App,
283 ) -> (LayoutId, Self::RequestLayoutState) {
284 let mut layout_state = ImgLayoutState {
285 frame_index: 0,
286 replacement: None,
287 };
288
289 window.with_optional_element_state(global_id, |state, window| {
290 let mut state = state.map(|state| {
291 state.unwrap_or(ImgState {
292 frame_index: 0,
293 last_frame_time: None,
294 started_loading: None,
295 })
296 });
297
298 let mut frame_index = state.as_ref().map(|state| state.frame_index).unwrap_or(0);
299
300 let layout_id = self.interactivity.request_layout(
301 global_id,
302 inspector_id,
303 window,
304 cx,
305 |mut style, window, cx| {
306 let mut replacement_id = None;
307
308 match self.source.use_data(
309 self.image_cache
310 .clone()
311 .or_else(|| window.image_cache_stack.last().cloned()),
312 window,
313 cx,
314 ) {
315 Some(Ok(data)) => {
316 let frame_count = data.frame_count();
317 let max_frame_index = frame_count.saturating_sub(1);
318
319 if let Some(state) = &mut state {
320 state.frame_index = state.frame_index.min(max_frame_index);
321 if frame_count > 1 && !cx.reduce_motion() {
322 if window.is_window_active() {
323 let current_time = Instant::now();
324 if let Some(last_frame_time) = state.last_frame_time {
325 let elapsed = current_time - last_frame_time;
326 let frame_duration =
327 Duration::from(data.delay(state.frame_index));
328
329 if elapsed >= frame_duration {
330 state.frame_index =
331 (state.frame_index + 1) % frame_count;
332 state.last_frame_time =
333 Some(current_time - (elapsed - frame_duration));
334 }
335 } else {
336 state.last_frame_time = Some(current_time);
337 }
338 } else {
339 state.last_frame_time = None;
340 }
341 } else {
342 state.last_frame_time = None;
343 }
344 state.started_loading = None;
345 frame_index = state.frame_index;
346 }
347
348 let image_size = data.render_size(frame_index);
349 style.aspect_ratio = Some(image_size.width / image_size.height);
350
351 if let Length::Auto = style.size.width {
352 style.size.width = match style.size.height {
353 Length::Definite(DefiniteLength::Absolute(abs_length)) => {
354 let height_px = abs_length.to_pixels(window.rem_size());
355 Length::Definite(
356 px(image_size.width.0 * height_px.0
357 / image_size.height.0)
358 .into(),
359 )
360 }
361 _ => Length::Definite(image_size.width.into()),
362 };
363 }
364
365 if let Length::Auto = style.size.height {
366 style.size.height = match style.size.width {
367 Length::Definite(DefiniteLength::Absolute(abs_length)) => {
368 let width_px = abs_length.to_pixels(window.rem_size());
369 Length::Definite(
370 px(image_size.height.0 * width_px.0
371 / image_size.width.0)
372 .into(),
373 )
374 }
375 _ => Length::Definite(image_size.height.into()),
376 };
377 }
378
379 if global_id.is_some()
380 && data.frame_count() > 1
381 && window.is_window_active()
382 && !cx.reduce_motion()
383 {
384 window.request_animation_frame();
385 }
386 }
387 Some(_err) => {
388 if let Some(fallback) = self.style.fallback.as_ref() {
389 let mut element = fallback();
390 replacement_id = Some(element.request_layout(window, cx));
391 layout_state.replacement = Some(element);
392 }
393 if let Some(state) = &mut state {
394 state.started_loading = None;
395 }
396 }
397 None => {
398 if let Some(state) = &mut state {
399 if let Some((started_loading, _)) = state.started_loading {
400 if started_loading.elapsed() > LOADING_DELAY
401 && let Some(loading) = self.style.loading.as_ref()
402 {
403 let mut element = loading();
404 replacement_id = Some(element.request_layout(window, cx));
405 layout_state.replacement = Some(element);
406 }
407 } else {
408 let current_view = window.current_view();
409 let task = window.spawn(cx, async move |cx| {
410 cx.background_executor().timer(LOADING_DELAY).await;
411 cx.update(move |_, cx| {
412 cx.notify(current_view);
413 })
414 .ok();
415 });
416 state.started_loading = Some((Instant::now(), task));
417 }
418 }
419 }
420 }
421
422 window.request_layout(style, replacement_id, cx)
423 },
424 );
425
426 layout_state.frame_index = frame_index;
427
428 ((layout_id, layout_state), state)
429 })
430 }
431
432 fn prepaint(
433 &mut self,
434 global_id: Option<&GlobalElementId>,
435 inspector_id: Option<&InspectorElementId>,
436 bounds: Bounds<Pixels>,
437 request_layout: &mut Self::RequestLayoutState,
438 window: &mut Window,
439 cx: &mut App,
440 ) -> Self::PrepaintState {
441 self.interactivity.prepaint(
442 global_id,
443 inspector_id,
444 bounds,
445 bounds.size,
446 window,
447 cx,
448 |_, _, hitbox, window, cx| {
449 if let Some(replacement) = &mut request_layout.replacement {
450 replacement.prepaint(window, cx);
451 }
452
453 hitbox
454 },
455 )
456 }
457
458 fn paint(
459 &mut self,
460 global_id: Option<&GlobalElementId>,
461 inspector_id: Option<&InspectorElementId>,
462 bounds: Bounds<Pixels>,
463 layout_state: &mut Self::RequestLayoutState,
464 hitbox: &mut Self::PrepaintState,
465 window: &mut Window,
466 cx: &mut App,
467 ) {
468 let source = self.source.clone();
469 self.interactivity.paint(
470 global_id,
471 inspector_id,
472 bounds,
473 hitbox.as_ref(),
474 window,
475 cx,
476 |style, window, cx| {
477 if let Some(Ok(data)) = source.use_data(
478 self.image_cache
479 .clone()
480 .or_else(|| window.image_cache_stack.last().cloned()),
481 window,
482 cx,
483 ) {
484 if data.frame_count() == 0 {
485 return;
486 }
487 let new_bounds = self
488 .style
489 .object_fit
490 .get_bounds(bounds, data.size(layout_state.frame_index));
491 let corner_radii = style
492 .corner_radii
493 .to_pixels(window.rem_size())
494 .clamp_radii_for_quad_size(new_bounds.size);
495 window
496 .paint_image(
497 new_bounds,
498 corner_radii,
499 data,
500 layout_state.frame_index,
501 self.style.grayscale,
502 )
503 .log_err();
504 } else if let Some(replacement) = &mut layout_state.replacement {
505 replacement.paint(window, cx);
506 }
507 },
508 )
509 }
510}
511
512impl Styled for Img {
513 fn style(&mut self) -> &mut StyleRefinement {
514 &mut self.interactivity.base_style
515 }
516}
517
518impl InteractiveElement for Img {
519 fn interactivity(&mut self) -> &mut Interactivity {
520 &mut self.interactivity
521 }
522}
523
524impl IntoElement for Img {
525 type Element = Self;
526
527 fn into_element(self) -> Self::Element {
528 self
529 }
530}
531
532impl StatefulInteractiveElement for Img {}
533
534impl ImageSource {
535 pub(crate) fn use_data(
536 &self,
537 cache: Option<AnyImageCache>,
538 window: &mut Window,
539 cx: &mut App,
540 ) -> Option<Result<Arc<RenderImage>, ImageCacheError>> {
541 match self {
542 ImageSource::Resource(resource) => {
543 if let Some(cache) = cache {
544 cache.load(resource, window, cx)
545 } else {
546 window.use_asset::<ImgResourceLoader>(resource, cx)
547 }
548 }
549 ImageSource::Custom(loading_fn) => loading_fn(window, cx),
550 ImageSource::Render(data) => Some(Ok(data.to_owned())),
551 ImageSource::Image(data) => window.use_asset::<AssetLogger<ImageDecoder>>(data, cx),
552 }
553 }
554
555 pub(crate) fn get_data(
556 &self,
557 cache: Option<AnyImageCache>,
558 window: &mut Window,
559 cx: &mut App,
560 ) -> Option<Result<Arc<RenderImage>, ImageCacheError>> {
561 match self {
562 ImageSource::Resource(resource) => {
563 if let Some(cache) = cache {
564 cache.load(resource, window, cx)
565 } else {
566 window.get_asset::<ImgResourceLoader>(resource, cx)
567 }
568 }
569 ImageSource::Custom(loading_fn) => loading_fn(window, cx),
570 ImageSource::Render(data) => Some(Ok(data.to_owned())),
571 ImageSource::Image(data) => window.get_asset::<AssetLogger<ImageDecoder>>(data, cx),
572 }
573 }
574
575 /// Remove this image source from the asset system
576 pub fn remove_asset(&self, cx: &mut App) {
577 match self {
578 ImageSource::Resource(resource) => {
579 cx.remove_asset::<ImgResourceLoader>(resource);
580 }
581 ImageSource::Custom(_) | ImageSource::Render(_) => {}
582 ImageSource::Image(data) => cx.remove_asset::<AssetLogger<ImageDecoder>>(data),
583 }
584 }
585}
586
587#[derive(Clone)]
588enum ImageDecoder {}
589
590impl Asset for ImageDecoder {
591 type Source = Arc<Image>;
592 type Output = Result<Arc<RenderImage>, ImageCacheError>;
593
594 fn load(
595 source: Self::Source,
596 cx: &mut App,
597 ) -> impl Future<Output = Self::Output> + Send + 'static {
598 let renderer = cx.svg_renderer();
599 async move { source.to_image_data(renderer).map_err(Into::into) }
600 }
601}
602
603/// An image loader for the GPUI asset system
604#[derive(Clone)]
605pub enum ImageAssetLoader {}
606
607impl Asset for ImageAssetLoader {
608 type Source = Resource;
609 type Output = Result<Arc<RenderImage>, ImageCacheError>;
610
611 fn load(
612 source: Self::Source,
613 cx: &mut App,
614 ) -> impl Future<Output = Self::Output> + Send + 'static {
615 let client = cx.http_client();
616 // TODO: Can we make SVGs always rescale?
617 // let scale_factor = cx.scale_factor();
618 let svg_renderer = cx.svg_renderer();
619 let asset_source = cx.asset_source().clone();
620 async move {
621 let bytes = match source.clone() {
622 Resource::Path(uri) => fs::read(uri.as_ref())?,
623 Resource::Uri(uri) => {
624 use anyhow::Context as _;
625 use futures::AsyncReadExt as _;
626
627 let mut response = client
628 .get(uri.as_ref(), ().into(), true)
629 .await
630 .with_context(|| format!("loading image asset from {uri:?}"))?;
631 let mut body = Vec::new();
632 response.body_mut().read_to_end(&mut body).await?;
633 if !response.status().is_success() {
634 let mut body = String::from_utf8_lossy(&body).into_owned();
635 let first_line = body.lines().next().unwrap_or("").trim_end();
636 body.truncate(first_line.len());
637 return Err(ImageCacheError::BadStatus {
638 uri,
639 status: response.status(),
640 body,
641 });
642 }
643 body
644 }
645 Resource::Embedded(path) => {
646 let data = asset_source.load(&path).ok().flatten();
647 if let Some(data) = data {
648 data.to_vec()
649 } else {
650 return Err(ImageCacheError::Asset(
651 format!("Embedded resource not found: {}", path).into(),
652 ));
653 }
654 }
655 };
656
657 if let Ok(format) = image::guess_format(&bytes) {
658 let data = match format {
659 ImageFormat::Gif => {
660 let decoder = GifDecoder::new(Cursor::new(&bytes))?;
661 let mut frames = SmallVec::new();
662
663 for frame in decoder.into_frames() {
664 match frame {
665 Ok(mut frame) => {
666 // Convert from RGBA to BGRA.
667 for pixel in frame.buffer_mut().chunks_exact_mut(4) {
668 pixel.swap(0, 2);
669 }
670 frames.push(frame);
671 }
672 Err(err) => {
673 log::debug!(
674 "Skipping GIF frame in {source:?} due to decode error: {err}"
675 );
676 }
677 }
678 }
679
680 if frames.is_empty() {
681 return Err(ImageCacheError::Other(Arc::new(anyhow::anyhow!(
682 "GIF could not be decoded: all frames failed ({source:?})"
683 ))));
684 }
685
686 frames
687 }
688 ImageFormat::WebP => {
689 let mut decoder = WebPDecoder::new(Cursor::new(&bytes))?;
690
691 if decoder.has_animation() {
692 let _ = decoder.set_background_color(Rgba([0, 0, 0, 0]));
693 let mut frames = SmallVec::new();
694
695 for frame in decoder.into_frames() {
696 match frame {
697 Ok(mut frame) => {
698 // Convert from RGBA to BGRA.
699 for pixel in frame.buffer_mut().chunks_exact_mut(4) {
700 pixel.swap(0, 2);
701 }
702 frames.push(frame);
703 }
704 Err(err) => {
705 log::debug!(
706 "Skipping WebP frame in {source:?} due to decode error: {err}"
707 );
708 }
709 }
710 }
711
712 if frames.is_empty() {
713 return Err(ImageCacheError::Other(Arc::new(anyhow::anyhow!(
714 "WebP could not be decoded: all frames failed ({source:?})"
715 ))));
716 }
717
718 frames
719 } else {
720 decode_static_image_from_decoder(decoder)?
721 }
722 }
723 _ => decode_static_image(&bytes, format)?,
724 };
725
726 Ok(Arc::new(RenderImage::new(data)))
727 } else {
728 svg_renderer
729 .render_single_frame(&bytes, 1.0)
730 .map_err(Into::into)
731 }
732 }
733 }
734}
735
736/// An error that can occur when interacting with the image cache.
737#[derive(Debug, Error, Clone)]
738pub enum ImageCacheError {
739 /// Some other kind of error occurred
740 #[error("error: {0}")]
741 Other(#[from] Arc<anyhow::Error>),
742 /// An error that occurred while reading the image from disk.
743 #[error("IO error: {0}")]
744 Io(Arc<std::io::Error>),
745 /// An error that occurred while processing an image.
746 #[error("unexpected http status for {uri}: {status}, body: {body}")]
747 BadStatus {
748 /// The URI of the image.
749 uri: SharedUri,
750 /// The HTTP status code.
751 status: http_client::StatusCode,
752 /// The HTTP response body.
753 body: String,
754 },
755 /// An error that occurred while processing an asset.
756 #[error("asset error: {0}")]
757 Asset(SharedString),
758 /// An error that occurred while processing an image.
759 #[error("image error: {0}")]
760 Image(Arc<ImageError>),
761 /// An error that occurred while processing an SVG.
762 #[error("svg error: {0}")]
763 Usvg(Arc<usvg::Error>),
764}
765
766impl From<anyhow::Error> for ImageCacheError {
767 fn from(value: anyhow::Error) -> Self {
768 Self::Other(Arc::new(value))
769 }
770}
771
772impl From<io::Error> for ImageCacheError {
773 fn from(value: io::Error) -> Self {
774 Self::Io(Arc::new(value))
775 }
776}
777
778impl From<usvg::Error> for ImageCacheError {
779 fn from(value: usvg::Error) -> Self {
780 Self::Usvg(Arc::new(value))
781 }
782}
783
784impl From<image::ImageError> for ImageCacheError {
785 fn from(value: image::ImageError) -> Self {
786 Self::Image(Arc::new(value))
787 }
788}
789
790#[cfg(test)]
791mod tests {
792 use super::*;
793 use crate::{ParentElement as _, TestAppContext, canvas, div, point, px, size};
794 use image::{Frame, ImageBuffer, Rgba};
795
796 const TEST_IMG_ID: &str = "test-img";
797
798 fn test_image(frame_count: usize) -> Arc<RenderImage> {
799 let frame = Frame::new(ImageBuffer::from_pixel(1, 1, Rgba([0, 0, 0, 0])));
800 Arc::new(RenderImage::new(SmallVec::from_iter(
801 (0..frame_count).map(|_| frame.clone()),
802 )))
803 }
804
805 /// Overwrites the cached `frame_index` of the sibling `img` during paint.
806 fn seed_frame_index(frame_index: usize) -> impl IntoElement {
807 canvas(
808 |_, _, _| (),
809 move |_, _, window, _| {
810 window.with_global_id(TEST_IMG_ID.into(), |id, window| {
811 window.with_element_state::<ImgState, _>(id, |state, _| {
812 let mut state = state.expect("img state should be initialized");
813 state.frame_index = frame_index;
814 ((), state)
815 });
816 });
817 },
818 )
819 }
820
821 #[gpui::test]
822 fn zero_frame_image_does_not_panic_on_paint(cx: &mut TestAppContext) {
823 cx.add_empty_window()
824 .draw(point(px(0.), px(0.)), size(px(100.), px(100.)), |_, _| {
825 img(ImageSource::Render(test_image(0))).into_any_element()
826 });
827 }
828
829 #[gpui::test]
830 fn stale_frame_index_is_clamped_when_image_changes(cx: &mut TestAppContext) {
831 let window = cx.add_empty_window();
832
833 // Assert that a cached frame_index from a previous multi-frame image
834 // does not cause an out-of-bounds panic when the image is replaced
835 // with one that has fewer frames.
836 window.draw(point(px(0.), px(0.)), size(px(100.), px(100.)), |_, _| {
837 div()
838 .child(img(ImageSource::Render(test_image(5))).id(TEST_IMG_ID))
839 .child(seed_frame_index(4))
840 .into_any_element()
841 });
842 window.draw(point(px(0.), px(0.)), size(px(100.), px(100.)), |_, _| {
843 img(ImageSource::Render(test_image(1)))
844 .id(TEST_IMG_ID)
845 .into_any_element()
846 });
847 }
848}
849