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, Pixels,
6 RenderImage, Resource, SharedString, Window, WindowBounds, WindowOptions, black, div, img,
7 prelude::*, 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(Pixels(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.), Pixels(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}