diff --git a/crates/chat_ui/src/lib.rs b/crates/chat_ui/src/lib.rs index 0c1a601..446cbe3 100644 --- a/crates/chat_ui/src/lib.rs +++ b/crates/chat_ui/src/lib.rs @@ -4,7 +4,7 @@ use std::sync::Arc; pub use actions::*; use anyhow::{Context as AnyhowContext, Error}; use chat::{ChatRegistry, Message, RenderedMessage, Room, RoomEvent, SendReport, SendStatus}; -use common::TimestampExt; +use common::{TimestampExt, coop_cache}; use gpui::prelude::FluentBuilder; use gpui::{ AnyElement, App, AppContext, ClipboardItem, Context, Entity, EventEmitter, FocusHandle, @@ -1485,6 +1485,7 @@ impl Focusable for ChatPanel { impl Render for ChatPanel { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { v_flex() + .image_cache(coop_cache(self.id.clone(), 100)) .on_action(cx.listener(Self::on_command)) .size_full() .when(*self.subject_bar.read(cx), |this| { diff --git a/crates/common/src/caching.rs b/crates/common/src/caching.rs new file mode 100644 index 0000000..c7ea380 --- /dev/null +++ b/crates/common/src/caching.rs @@ -0,0 +1,135 @@ +use std::collections::{HashMap, VecDeque}; +use std::mem::take; + +use futures::FutureExt; +use gpui::{ + App, AppContext, Asset, AssetLogger, ElementId, Entity, ImageAssetLoader, ImageCache, + ImageCacheItem, ImageCacheProvider, ImageSource, Resource, hash, +}; + +pub fn coop_cache(id: impl Into, max_items: usize) -> CoopImageCacheProvider { + CoopImageCacheProvider { + id: id.into(), + max_items, + } +} + +pub struct CoopImageCacheProvider { + id: ElementId, + max_items: usize, +} + +impl ImageCacheProvider for CoopImageCacheProvider { + fn provide(&mut self, window: &mut gpui::Window, cx: &mut App) -> gpui::AnyImageCache { + window + .with_global_id(self.id.clone(), |id, window| { + window.with_element_state(id, |cache, _| { + let cache = cache.unwrap_or_else(|| CoopImageCache::new(self.max_items, cx)); + (cache.clone(), cache) + }) + }) + .into() + } +} + +pub struct CoopImageCache { + max_items: usize, + usage_list: VecDeque, + cache: HashMap, +} + +impl CoopImageCache { + pub fn new(max_items: usize, cx: &mut App) -> Entity { + cx.new(|cx| { + log::info!("Creating CoopImageCache"); + cx.on_release(|this: &mut Self, cx| { + for (ix, (mut image, resource)) in take(&mut this.cache) { + if let Some(Ok(image)) = image.get() { + log::info!("Dropping image {ix}"); + cx.drop_image(image, None); + } + ImageSource::Resource(resource).remove_asset(cx); + } + }) + .detach(); + + CoopImageCache { + max_items, + usage_list: VecDeque::with_capacity(max_items), + cache: HashMap::with_capacity(max_items), + } + }) + } +} + +impl ImageCache for CoopImageCache { + fn load( + &mut self, + resource: &Resource, + window: &mut gpui::Window, + cx: &mut gpui::App, + ) -> Option, gpui::ImageCacheError>> { + let hash = hash(resource); + + if let Some(item) = self.cache.get_mut(&hash) { + let current_idx = self + .usage_list + .iter() + .position(|item| *item == hash) + .expect("cache has an item usage_list doesn't"); + + self.usage_list.remove(current_idx); + self.usage_list.push_front(hash); + + return item.0.get(); + } + + let load_future = AssetLogger::::load(resource.clone(), cx); + let task = cx.background_executor().spawn(load_future).shared(); + + if self.usage_list.len() >= self.max_items { + log::info!("Image cache is full, evicting oldest item"); + + if let Some(oldest) = self.usage_list.pop_back() { + let mut image = self + .cache + .remove(&oldest) + .expect("usage_list has an item cache doesn't"); + + if let Some(Ok(image)) = image.0.get() { + log::info!("requesting image to be dropped"); + cx.drop_image(image, Some(window)); + } + + ImageSource::Resource(image.1).remove_asset(cx); + } + } + + self.cache.insert( + hash, + ( + gpui::ImageCacheItem::Loading(task.clone()), + resource.clone(), + ), + ); + self.usage_list.push_front(hash); + + let entity = window.current_view(); + + window + .spawn(cx, async move |cx| { + let result = task.await; + + if let Err(err) = result { + log::error!("error loading image into cache: {:?}", err); + } + + cx.on_next_frame(move |_, cx| { + cx.notify(entity); + }); + }) + .detach(); + + None + } +} diff --git a/crates/common/src/lib.rs b/crates/common/src/lib.rs index 7d7720e..8f7bcdb 100644 --- a/crates/common/src/lib.rs +++ b/crates/common/src/lib.rs @@ -1,3 +1,4 @@ +pub use caching::*; pub use debounced_delay::*; pub use display::*; pub use event::*; @@ -6,6 +7,7 @@ pub use parser::*; pub use paths::*; pub use range::*; +mod caching; mod debounced_delay; mod display; mod event; diff --git a/crates/state/src/constants.rs b/crates/state/src/constants.rs index 296b498..56a1d74 100644 --- a/crates/state/src/constants.rs +++ b/crates/state/src/constants.rs @@ -15,6 +15,9 @@ pub const KEYRING: &str = "Coop Safe Storage"; /// Default timeout for subscription pub const TIMEOUT: u64 = 2; +/// Default image cache size +pub const IMAGE_CACHE_SIZE: usize = 20; + /// Default delay for searching pub const FIND_DELAY: u64 = 600; diff --git a/desktop/src/sidebar/mod.rs b/desktop/src/sidebar/mod.rs index 3cb3f9f..a9d1b99 100644 --- a/desktop/src/sidebar/mod.rs +++ b/desktop/src/sidebar/mod.rs @@ -4,18 +4,18 @@ use std::time::Duration; use anyhow::{Context as AnyhowContext, Error}; use chat::{ChatEvent, ChatRegistry, Room, RoomKind}; -use common::{DebouncedDelay, TimestampExt}; +use common::{DebouncedDelay, TimestampExt, coop_cache}; use entry::RoomEntry; use gpui::prelude::FluentBuilder; use gpui::{ App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable, IntoElement, - ParentElement, Render, RetainAllImageCache, SharedString, Styled, Subscription, Task, - UniformListScrollHandle, Window, div, uniform_list, + ParentElement, Render, SharedString, Styled, Subscription, Task, UniformListScrollHandle, + Window, div, uniform_list, }; use nostr_sdk::prelude::*; use person::PersonRegistry; use smallvec::{SmallVec, smallvec}; -use state::{FIND_DELAY, NostrRegistry}; +use state::{FIND_DELAY, IMAGE_CACHE_SIZE, NostrRegistry}; use theme::{ActiveTheme, SIDEBAR_WIDTH, TABBAR_HEIGHT}; use ui::button::{Button, ButtonVariants}; use ui::dock::{Panel, PanelEvent}; @@ -39,9 +39,6 @@ pub struct Sidebar { focus_handle: FocusHandle, scroll_handle: UniformListScrollHandle, - /// Image cache - image_cache: Entity, - /// Find input state find_input: Entity, @@ -141,7 +138,6 @@ impl Sidebar { name: "Sidebar".into(), focus_handle: cx.focus_handle(), scroll_handle: UniformListScrollHandle::new(), - image_cache: RetainAllImageCache::new(cx), find_input, find_debouncer: DebouncedDelay::new(), find_results, @@ -507,7 +503,7 @@ impl Render for Sidebar { }; v_flex() - .image_cache(self.image_cache.clone()) + .image_cache(coop_cache("sidebar", IMAGE_CACHE_SIZE)) .size_full() .gap_2() .child( diff --git a/desktop/src/workspace.rs b/desktop/src/workspace.rs index 090f91a..64f17f4 100644 --- a/desktop/src/workspace.rs +++ b/desktop/src/workspace.rs @@ -2,19 +2,19 @@ use std::sync::Arc; use ::settings::AppSettings; use chat::{ChatEvent, ChatRegistry}; -use common::download_dir; +use common::{CoopImageCache, download_dir}; use device::{DeviceEvent, DeviceRegistry}; use gpui::prelude::FluentBuilder; use gpui::{ Action, App, AppContext, Axis, Context, Entity, InteractiveElement, IntoElement, ParentElement, - Render, SharedString, StatefulInteractiveElement, Styled, Subscription, Window, div, px, - relative, + Render, SharedString, StatefulInteractiveElement, Styled, Subscription, Window, div, + image_cache, px, relative, }; use nostr_sdk::prelude::*; use person::{PersonRegistry, shorten_pubkey}; use serde::Deserialize; use smallvec::{SmallVec, smallvec}; -use state::{NostrRegistry, StateEvent}; +use state::{IMAGE_CACHE_SIZE, NostrRegistry, StateEvent}; use theme::{ActiveTheme, SIDEBAR_WIDTH, Theme, ThemeRegistry}; use title_bar::TitleBar; use ui::avatar::Avatar; @@ -70,6 +70,9 @@ pub struct Workspace { /// App's Dock Area dock: Entity, + /// App's Image Cache + image_cache: Entity, + /// Event subscriptions _subscriptions: SmallVec<[Subscription; 5]>, } @@ -82,6 +85,7 @@ impl Workspace { let titlebar = cx.new(|_| TitleBar::new()); let dock = cx.new(|cx| DockArea::new(window, cx)); + let image_cache = CoopImageCache::new(IMAGE_CACHE_SIZE, cx); let mut subscriptions = smallvec![]; @@ -231,6 +235,7 @@ impl Workspace { Self { titlebar, dock, + image_cache, _subscriptions: subscriptions, } } @@ -848,13 +853,17 @@ impl Render for Workspace { .relative() .size_full() .child( - v_flex() + image_cache(self.image_cache.clone()) .relative() .size_full() - // Title Bar - .child(self.titlebar.clone()) - // Dock - .child(self.dock.clone()), + .child( + v_flex() + .size_full() + // Title Bar + .child(self.titlebar.clone()) + // Dock + .child(self.dock.clone()), + ), ) // Notifications .children(notification_layer)