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