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