image_loading.rs

  1use std::{path::Path, sync::Arc, time::Duration};
  2
  3use gpui::{
  4    Animation, AnimationExt, App, Application, Asset, AssetLogger, AssetSource, Bounds, Context,
  5    Hsla, ImageAssetLoader, ImageCacheError, ImgResourceLoader, LOADING_DELAY, Length, RenderImage,
  6    Resource, SharedString, Window, WindowBounds, WindowOptions, black, div, img, prelude::*,
  7    pulsating_between, px, red, size,
  8};
  9
 10struct Assets {}
 11
 12impl AssetSource for Assets {
 13    fn load(&self, path: &str) -> anyhow::Result<Option<std::borrow::Cow<'static, [u8]>>> {
 14        std::fs::read(path)
 15            .map(Into::into)
 16            .map_err(Into::into)
 17            .map(Some)
 18    }
 19
 20    fn list(&self, path: &str) -> anyhow::Result<Vec<SharedString>> {
 21        Ok(std::fs::read_dir(path)?
 22            .filter_map(|entry| {
 23                Some(SharedString::from(
 24                    entry.ok()?.path().to_string_lossy().into_owned(),
 25                ))
 26            })
 27            .collect::<Vec<_>>())
 28    }
 29}
 30
 31const IMAGE: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/examples/image/app-icon.png");
 32
 33#[derive(Copy, Clone, Hash)]
 34struct LoadImageParameters {
 35    timeout: Duration,
 36    fail: bool,
 37}
 38
 39struct LoadImageWithParameters {}
 40
 41impl Asset for LoadImageWithParameters {
 42    type Source = LoadImageParameters;
 43
 44    type Output = Result<Arc<RenderImage>, ImageCacheError>;
 45
 46    fn load(
 47        parameters: Self::Source,
 48        cx: &mut App,
 49    ) -> impl std::future::Future<Output = Self::Output> + Send + 'static {
 50        let timer = cx.background_executor().timer(parameters.timeout);
 51        let data = AssetLogger::<ImageAssetLoader>::load(
 52            Resource::Path(Path::new(IMAGE).to_path_buf().into()),
 53            cx,
 54        );
 55        async move {
 56            timer.await;
 57            if parameters.fail {
 58                log::error!("Intentionally failed to load image");
 59                Err(anyhow::anyhow!("Failed to load image").into())
 60            } else {
 61                data.await
 62            }
 63        }
 64    }
 65}
 66
 67struct ImageLoadingExample {}
 68
 69impl ImageLoadingExample {
 70    fn loading_element() -> impl IntoElement {
 71        div().size_full().flex_none().p_0p5().rounded_xs().child(
 72            div().size_full().with_animation(
 73                "loading-bg",
 74                Animation::new(Duration::from_secs(3))
 75                    .repeat()
 76                    .with_easing(pulsating_between(0.04, 0.24)),
 77                move |this, delta| this.bg(black().opacity(delta)),
 78            ),
 79        )
 80    }
 81
 82    fn fallback_element() -> impl IntoElement {
 83        let fallback_color: Hsla = black().opacity(0.5);
 84
 85        div().size_full().flex_none().p_0p5().child(
 86            div()
 87                .size_full()
 88                .flex()
 89                .items_center()
 90                .justify_center()
 91                .rounded_xs()
 92                .text_sm()
 93                .text_color(fallback_color)
 94                .border_1()
 95                .border_color(fallback_color)
 96                .child("?"),
 97        )
 98    }
 99}
100
101impl Render for ImageLoadingExample {
102    fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
103        div().flex().flex_col().size_full().justify_around().child(
104            div().flex().flex_row().w_full().justify_around().child(
105                div()
106                    .flex()
107                    .bg(gpui::white())
108                    .size(Length::Definite(px(300.0).into()))
109                    .justify_center()
110                    .items_center()
111                    .child({
112                        let image_source = LoadImageParameters {
113                            timeout: LOADING_DELAY.saturating_sub(Duration::from_millis(25)),
114                            fail: false,
115                        };
116
117                        // Load within the 'loading delay', should not show loading fallback
118                        img(move |window: &mut Window, cx: &mut App| {
119                            window.use_asset::<LoadImageWithParameters>(&image_source, cx)
120                        })
121                        .id("image-1")
122                        .border_1()
123                        .size_12()
124                        .with_fallback(|| Self::fallback_element().into_any_element())
125                        .border_color(red())
126                        .with_loading(|| Self::loading_element().into_any_element())
127                        .on_click(move |_, _, cx| {
128                            cx.remove_asset::<LoadImageWithParameters>(&image_source);
129                        })
130                    })
131                    .child({
132                        // Load after a long delay
133                        let image_source = LoadImageParameters {
134                            timeout: Duration::from_secs(5),
135                            fail: false,
136                        };
137
138                        img(move |window: &mut Window, cx: &mut App| {
139                            window.use_asset::<LoadImageWithParameters>(&image_source, cx)
140                        })
141                        .id("image-2")
142                        .with_fallback(|| Self::fallback_element().into_any_element())
143                        .with_loading(|| Self::loading_element().into_any_element())
144                        .size_12()
145                        .border_1()
146                        .border_color(red())
147                        .on_click(move |_, _, cx| {
148                            cx.remove_asset::<LoadImageWithParameters>(&image_source);
149                        })
150                    })
151                    .child({
152                        // Fail to load image after a long delay
153                        let image_source = LoadImageParameters {
154                            timeout: Duration::from_secs(5),
155                            fail: true,
156                        };
157
158                        // Fail to load after a long delay
159                        img(move |window: &mut Window, cx: &mut App| {
160                            window.use_asset::<LoadImageWithParameters>(&image_source, cx)
161                        })
162                        .id("image-3")
163                        .with_fallback(|| Self::fallback_element().into_any_element())
164                        .with_loading(|| Self::loading_element().into_any_element())
165                        .size_12()
166                        .border_1()
167                        .border_color(red())
168                        .on_click(move |_, _, cx| {
169                            cx.remove_asset::<LoadImageWithParameters>(&image_source);
170                        })
171                    })
172                    .child({
173                        // Ensure that the normal image loader doesn't spam logs
174                        let image_source = Path::new(
175                            "this/file/really/shouldn't/exist/or/won't/be/an/image/I/hope",
176                        )
177                        .to_path_buf();
178                        img(image_source.clone())
179                            .id("image-4")
180                            .border_1()
181                            .size_12()
182                            .with_fallback(|| Self::fallback_element().into_any_element())
183                            .border_color(red())
184                            .with_loading(|| Self::loading_element().into_any_element())
185                            .on_click(move |_, _, cx| {
186                                cx.remove_asset::<ImgResourceLoader>(&image_source.clone().into());
187                            })
188                    }),
189            ),
190        )
191    }
192}
193
194fn main() {
195    env_logger::init();
196    Application::new()
197        .with_assets(Assets {})
198        .run(|cx: &mut App| {
199            let options = WindowOptions {
200                window_bounds: Some(WindowBounds::Windowed(Bounds::centered(
201                    None,
202                    size(px(300.), px(300.)),
203                    cx,
204                ))),
205                ..Default::default()
206            };
207            cx.open_window(options, |_, cx| {
208                cx.activate(false);
209                cx.new(|_| ImageLoadingExample {})
210            })
211            .unwrap();
212        });
213}