image_gallery.rs

  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}