1#![cfg_attr(target_family = "wasm", no_main)]
2
3use futures::FutureExt;
4use gpui::{
5 App, AppContext, Asset as _, AssetLogger, Bounds, ClickEvent, Context, ElementId, Entity,
6 ImageAssetLoader, ImageCache, ImageCacheProvider, KeyBinding, Menu, MenuItem,
7 RetainAllImageCache, SharedString, TitlebarOptions, Window, WindowBounds, WindowOptions,
8 actions, div, hash, image_cache, img, prelude::*, px, rgb, size,
9};
10#[cfg(not(target_family = "wasm"))]
11use reqwest_client::ReqwestClient;
12use std::{collections::HashMap, sync::Arc};
13
14const IMAGES_IN_GALLERY: usize = 30;
15
16struct ImageGallery {
17 image_key: String,
18 items_count: usize,
19 total_count: usize,
20 image_cache: Entity<RetainAllImageCache>,
21}
22
23impl ImageGallery {
24 fn on_next_image(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
25 self.image_cache
26 .update(cx, |image_cache, cx| image_cache.clear(window, cx));
27
28 let t = std::time::SystemTime::now()
29 .duration_since(std::time::UNIX_EPOCH)
30 .unwrap()
31 .as_millis();
32
33 self.image_key = format!("{}", t);
34 self.total_count += self.items_count;
35 cx.notify();
36 }
37}
38
39impl Render for ImageGallery {
40 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
41 let image_url: SharedString =
42 format!("https://picsum.photos/400/200?t={}", self.image_key).into();
43
44 div()
45 .flex()
46 .flex_col()
47 .text_color(gpui::white())
48 .child("Manually managed image cache:")
49 .child(
50 div()
51 .image_cache(self.image_cache.clone())
52 .id("main")
53 .text_color(gpui::black())
54 .bg(rgb(0xE9E9E9))
55 .overflow_y_scroll()
56 .p_4()
57 .size_full()
58 .flex()
59 .flex_col()
60 .items_center()
61 .gap_2()
62 .child(
63 div()
64 .w_full()
65 .flex()
66 .flex_row()
67 .justify_between()
68 .child(format!(
69 "Example to show images and test memory usage (Rendered: {} images).",
70 self.total_count
71 ))
72 .child(
73 div()
74 .id("btn")
75 .py_1()
76 .px_4()
77 .bg(gpui::black())
78 .hover(|this| this.opacity(0.8))
79 .text_color(gpui::white())
80 .text_center()
81 .w_40()
82 .child("Next Photos")
83 .on_click(cx.listener(Self::on_next_image)),
84 ),
85 )
86 .child(
87 div()
88 .id("image-gallery")
89 .flex()
90 .flex_row()
91 .flex_wrap()
92 .gap_x_4()
93 .gap_y_2()
94 .justify_around()
95 .children(
96 (0..self.items_count)
97 .map(|ix| img(format!("{}-{}", image_url, ix)).size_20()),
98 ),
99 ),
100 )
101 .child(
102 "Automatically managed image cache:"
103 )
104 .child(image_cache(simple_lru_cache("lru-cache", IMAGES_IN_GALLERY)).child(
105 div()
106 .id("main")
107 .bg(rgb(0xE9E9E9))
108 .text_color(gpui::black())
109 .overflow_y_scroll()
110 .p_4()
111 .size_full()
112 .flex()
113 .flex_col()
114 .items_center()
115 .gap_2()
116 .child(
117 div()
118 .id("image-gallery")
119 .flex()
120 .flex_row()
121 .flex_wrap()
122 .gap_x_4()
123 .gap_y_2()
124 .justify_around()
125 .children(
126 (0..self.items_count)
127 .map(|ix| img(format!("{}-{}", image_url, ix)).size_20()),
128 ),
129 )
130 ))
131 }
132}
133
134fn simple_lru_cache(id: impl Into<ElementId>, max_items: usize) -> SimpleLruCacheProvider {
135 SimpleLruCacheProvider {
136 id: id.into(),
137 max_items,
138 }
139}
140
141struct SimpleLruCacheProvider {
142 id: ElementId,
143 max_items: usize,
144}
145
146impl ImageCacheProvider for SimpleLruCacheProvider {
147 fn provide(&mut self, window: &mut Window, cx: &mut App) -> gpui::AnyImageCache {
148 window
149 .with_global_id(self.id.clone(), |global_id, window| {
150 window.with_element_state::<Entity<SimpleLruCache>, _>(
151 global_id,
152 |lru_cache, _window| {
153 let mut lru_cache = lru_cache.unwrap_or_else(|| {
154 cx.new(|cx| SimpleLruCache::new(self.max_items, cx))
155 });
156 if lru_cache.read(cx).max_items != self.max_items {
157 lru_cache = cx.new(|cx| SimpleLruCache::new(self.max_items, cx));
158 }
159 (lru_cache.clone(), lru_cache)
160 },
161 )
162 })
163 .into()
164 }
165}
166
167struct SimpleLruCache {
168 max_items: usize,
169 usages: Vec<u64>,
170 cache: HashMap<u64, gpui::ImageCacheItem>,
171}
172
173impl SimpleLruCache {
174 fn new(max_items: usize, cx: &mut Context<Self>) -> Self {
175 cx.on_release(|simple_cache, cx| {
176 for (_, mut item) in std::mem::take(&mut simple_cache.cache) {
177 if let Some(Ok(image)) = item.get() {
178 cx.drop_image(image, None);
179 }
180 }
181 })
182 .detach();
183
184 Self {
185 max_items,
186 usages: Vec::with_capacity(max_items),
187 cache: HashMap::with_capacity(max_items),
188 }
189 }
190}
191
192impl ImageCache for SimpleLruCache {
193 fn load(
194 &mut self,
195 resource: &gpui::Resource,
196 window: &mut Window,
197 cx: &mut App,
198 ) -> Option<Result<Arc<gpui::RenderImage>, gpui::ImageCacheError>> {
199 assert_eq!(self.usages.len(), self.cache.len());
200 assert!(self.cache.len() <= self.max_items);
201
202 let hash = hash(resource);
203
204 if let Some(item) = self.cache.get_mut(&hash) {
205 let current_ix = self
206 .usages
207 .iter()
208 .position(|item| *item == hash)
209 .expect("cache and usages must stay in sync");
210 self.usages.remove(current_ix);
211 self.usages.insert(0, hash);
212
213 return item.get();
214 }
215
216 let fut = AssetLogger::<ImageAssetLoader>::load(resource.clone(), cx);
217 let task = cx.background_executor().spawn(fut).shared();
218 if self.usages.len() == self.max_items {
219 let oldest = self.usages.pop().unwrap();
220 let mut image = self
221 .cache
222 .remove(&oldest)
223 .expect("cache and usages must be in sync");
224 if let Some(Ok(image)) = image.get() {
225 cx.drop_image(image, Some(window));
226 }
227 }
228 self.cache
229 .insert(hash, gpui::ImageCacheItem::Loading(task.clone()));
230 self.usages.insert(0, hash);
231
232 let entity = window.current_view();
233 window
234 .spawn(cx, {
235 async move |cx| {
236 _ = task.await;
237 cx.on_next_frame(move |_, cx| {
238 cx.notify(entity);
239 });
240 }
241 })
242 .detach();
243
244 None
245 }
246}
247
248actions!(image, [Quit]);
249
250fn run_example() {
251 #[cfg(not(target_family = "wasm"))]
252 let app = gpui_platform::application();
253 #[cfg(target_family = "wasm")]
254 let app = gpui_platform::single_threaded_web();
255
256 app.run(move |cx: &mut App| {
257 #[cfg(not(target_family = "wasm"))]
258 {
259 let http_client = ReqwestClient::user_agent("gpui example").unwrap();
260 cx.set_http_client(Arc::new(http_client));
261 }
262 #[cfg(target_family = "wasm")]
263 {
264 // Safety: the web examples run single-threaded; the client is
265 // created and used exclusively on the main thread.
266 let http_client = unsafe {
267 gpui_web::FetchHttpClient::with_user_agent("gpui example")
268 .expect("failed to create FetchHttpClient")
269 };
270 cx.set_http_client(Arc::new(http_client));
271 }
272
273 cx.activate(true);
274 cx.on_action(|_: &Quit, cx| cx.quit());
275 cx.bind_keys([KeyBinding::new("cmd-q", Quit, None)]);
276 cx.set_menus(vec![Menu {
277 name: "Image Gallery".into(),
278 items: vec![MenuItem::action("Quit", Quit)],
279 }]);
280
281 let window_options = WindowOptions {
282 titlebar: Some(TitlebarOptions {
283 title: Some(SharedString::from("Image Gallery")),
284 appears_transparent: false,
285 ..Default::default()
286 }),
287
288 window_bounds: Some(WindowBounds::Windowed(Bounds::centered(
289 None,
290 size(px(1100.), px(860.)),
291 cx,
292 ))),
293
294 ..Default::default()
295 };
296
297 cx.open_window(window_options, |_, cx| {
298 cx.new(|ctx| ImageGallery {
299 image_key: "".into(),
300 items_count: IMAGES_IN_GALLERY,
301 total_count: 0,
302 image_cache: RetainAllImageCache::new(ctx),
303 })
304 })
305 .unwrap();
306 });
307}
308
309#[cfg(not(target_family = "wasm"))]
310fn main() {
311 env_logger::init();
312 run_example();
313}
314
315#[cfg(target_family = "wasm")]
316#[wasm_bindgen::prelude::wasm_bindgen(start)]
317pub fn start() {
318 gpui_platform::web_init();
319 run_example();
320}