chore: use gpui image cache (#47)

Reviewed-on: #47
This commit was merged in pull request #47.
This commit is contained in:
2026-09-16 02:21:12 +00:00
parent 42983383b0
commit bf7080654d
10 changed files with 364 additions and 912 deletions
+4 -4
View File
@@ -5,7 +5,7 @@ use std::sync::{Arc, LazyLock, RwLock};
pub use actions::*;
use anyhow::Error;
use chat::{ChatRegistry, Message, Room, RoomEvent, SendReport, SendStatus};
use common::{TimestampExt, coop_cache};
use common::TimestampExt;
use futures::lock::Mutex;
use gpui::prelude::FluentBuilder;
use gpui::{
@@ -13,8 +13,8 @@ use gpui::{
Focusable, InteractiveElement, IntoElement, ListAlignment, ListOffset, ListState, MouseButton,
ObjectFit, ParentElement, PathPromptOptions, Render, SharedString, SharedUri,
StatefulInteractiveElement, Styled, StyledImage, Subscription, SystemNotification,
SystemNotificationAction, Task, WeakEntity, Window, div, img, list, px, red, relative, svg,
white,
SystemNotificationAction, Task, WeakEntity, Window, div, img, list, px, red, relative,
retain_all, svg, white,
};
use itertools::Itertools;
use nostr_sdk::prelude::*;
@@ -2024,7 +2024,7 @@ impl Render for ChatPanel {
let pending_attachments = !self.encrypted_attachments.read(cx).is_empty();
v_flex()
.image_cache(coop_cache(self.id.clone(), 100))
.image_cache(retain_all(self.id.clone()))
.on_action(cx.listener(Self::on_command))
.size_full()
.when(*self.subject_bar.read(cx), |this| {
-135
View File
@@ -1,135 +0,0 @@
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<ElementId>, 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<u64>,
cache: HashMap<u64, (ImageCacheItem, Resource)>,
}
impl CoopImageCache {
pub fn new(max_items: usize, cx: &mut App) -> Entity<Self> {
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<Result<std::sync::Arc<gpui::RenderImage>, 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::<ImageAssetLoader>::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
}
}
-2
View File
@@ -1,4 +1,3 @@
pub use caching::*;
pub use debounced_delay::*;
pub use display::*;
pub use event::*;
@@ -7,7 +6,6 @@ pub use parser::*;
pub use paths::*;
pub use range::*;
mod caching;
mod debounced_delay;
mod display;
mod event;
-3
View File
@@ -14,9 +14,6 @@ pub const USER_KEYRING: &str = "Coop User Credential";
/// 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;
+18 -28
View File
@@ -4,18 +4,18 @@ use ::settings::AppSettings;
use anyhow::Error;
use auto_update::AutoUpdater;
use chat::{ChatEvent, ChatRegistry};
use common::{CoopImageCache, download_dir};
use common::download_dir;
use device::{DeviceEvent, DeviceRegistry};
use gpui::prelude::FluentBuilder;
use gpui::{
Action, App, AppContext, Axis, Context, Entity, InteractiveElement, IntoElement, ParentElement,
Render, SharedString, Styled, Subscription, Task, Window, div, image_cache, px,
Render, SharedString, Styled, Subscription, Task, Window, div, px,
};
use nostr_sdk::prelude::*;
use person::{PersonRegistry, shorten_pubkey};
use serde::Deserialize;
use smallvec::{SmallVec, smallvec};
use state::{IMAGE_CACHE_SIZE, NostrRegistry, StateEvent};
use state::{NostrRegistry, StateEvent};
use theme::{ActiveTheme, SIDEBAR_WIDTH, Theme, ThemeRegistry};
use ui::avatar::Avatar;
use ui::button::{Button, ButtonVariants};
@@ -64,9 +64,6 @@ pub struct Workspace {
/// App's Dock Area
dock: Entity<DockArea>,
/// App's Image Cache
image_cache: Entity<CoopImageCache>,
/// Async tasks
tasks: Vec<Task<Result<(), Error>>>,
@@ -82,7 +79,6 @@ impl Workspace {
let sidebar = cx.new(|cx| Sidebar::new(window, cx));
let dock = cx.new(|cx| DockArea::new(window, cx));
let image_cache = CoopImageCache::new(IMAGE_CACHE_SIZE, cx);
let mut subscriptions = smallvec![];
@@ -233,7 +229,6 @@ impl Workspace {
Self {
sidebar,
dock,
image_cache,
tasks: vec![],
_subscriptions: subscriptions,
}
@@ -778,31 +773,26 @@ impl Render for Workspace {
.relative()
.size_full()
.child(
image_cache(self.image_cache.clone())
.relative()
v_flex()
.size_full()
// Title Bar
.child(
v_flex()
TitleBar::new()
.child(self.titlebar_left(cx))
.child(self.titlebar_right(cx)),
)
// Main
.child(
h_flex()
.size_full()
// Title Bar
.child(
TitleBar::new()
.child(self.titlebar_left(cx))
.child(self.titlebar_right(cx)),
div()
.flex_shrink_0()
.h_full()
.w(SIDEBAR_WIDTH)
.child(self.sidebar.clone()),
)
// Main
.child(
h_flex()
.size_full()
.child(
div()
.flex_shrink_0()
.h_full()
.w(SIDEBAR_WIDTH)
.child(self.sidebar.clone()),
)
.child(self.dock.clone()),
),
.child(self.dock.clone()),
),
)
// Notifications
+2 -1
View File
@@ -5,7 +5,7 @@ use gpui::prelude::FluentBuilder;
use gpui::{
AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable,
InteractiveElement, IntoElement, ParentElement, Render, SharedString, Styled, Subscription,
Task, TextAlign, Window, div, rems,
Task, TextAlign, Window, div, rems, retain_all,
};
use instant::Duration;
use nostr_sdk::prelude::*;
@@ -297,6 +297,7 @@ impl Focusable for ContactListPanel {
impl Render for ContactListPanel {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
v_flex()
.image_cache(retain_all("contact-list-panel"))
.p_3()
.gap_3()
.w_full()
+2 -1
View File
@@ -4,7 +4,7 @@ use anyhow::Error;
use gpui::{
AnyElement, App, AppContext, ClipboardItem, Context, Entity, EventEmitter, FocusHandle,
Focusable, IntoElement, ParentElement, PathPromptOptions, Render, SharedString, Styled, Task,
Window, div,
Window, div, retain_all,
};
use instant::Duration;
use nostr_sdk::prelude::*;
@@ -321,6 +321,7 @@ impl Render for ProfilePanel {
let shorten_pkey = SharedString::from(shorten_pubkey(self.public_key, 8));
v_flex()
.image_cache(retain_all("profile-panel"))
.p_3()
.gap_3()
.w_full()
+4 -4
View File
@@ -3,19 +3,19 @@ use std::ops::Range;
use anyhow::Error;
use chat::{ChatEvent, ChatRegistry, Room, RoomKind};
use common::{DebouncedDelay, TimestampExt, coop_cache};
use common::{DebouncedDelay, TimestampExt};
use entry::RoomEntry;
use gpui::prelude::FluentBuilder;
use gpui::{
App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable, IntoElement,
ParentElement, Render, SharedString, Styled, Subscription, Task, UniformListScrollHandle,
Window, div, uniform_list,
Window, div, retain_all, uniform_list,
};
use instant::Duration;
use nostr_sdk::prelude::*;
use person::PersonRegistry;
use smallvec::{SmallVec, smallvec};
use state::{FIND_DELAY, IMAGE_CACHE_SIZE, NostrRegistry};
use state::{FIND_DELAY, NostrRegistry};
use theme::{ActiveTheme, SIDEBAR_WIDTH};
use ui::button::{Button, ButtonVariants};
use ui::dock::{Panel, PanelEvent};
@@ -521,7 +521,7 @@ impl Render for Sidebar {
};
v_flex()
.image_cache(coop_cache("sidebar", IMAGE_CACHE_SIZE))
.image_cache(retain_all("sidebar"))
.size_full()
.gap_2()
.child(