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