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}