image_loading.rs

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