Detailed changes
@@ -0,0 +1,134 @@
+use gpui::{
+ App, AppContext, Application, Bounds, ClickEvent, Context, Entity, HashMapImageCache,
+ KeyBinding, Menu, MenuItem, SharedString, TitlebarOptions, Window, WindowBounds, WindowOptions,
+ actions, div, image_cache, img, prelude::*, px, rgb, size,
+};
+use reqwest_client::ReqwestClient;
+use std::sync::Arc;
+
+struct ImageGallery {
+ image_key: String,
+ items_count: usize,
+ total_count: usize,
+ image_cache: Entity<HashMapImageCache>,
+}
+
+impl ImageGallery {
+ fn on_next_image(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
+ self.image_cache
+ .update(cx, |image_cache, cx| image_cache.clear(window, cx));
+
+ let t = std::time::SystemTime::now()
+ .duration_since(std::time::UNIX_EPOCH)
+ .unwrap()
+ .as_millis();
+
+ self.image_key = format!("{}", t);
+ self.total_count += self.items_count;
+ cx.notify();
+ }
+}
+
+impl Render for ImageGallery {
+ fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+ let image_url: SharedString =
+ format!("https://picsum.photos/400/200?t={}", self.image_key).into();
+
+ image_cache(&self.image_cache).child(
+ div()
+ .id("main")
+ .font_family(".SystemUIFont")
+ .bg(rgb(0xE9E9E9))
+ .overflow_y_scroll()
+ .p_4()
+ .size_full()
+ .flex()
+ .flex_col()
+ .items_center()
+ .gap_2()
+ .child(
+ div()
+ .w_full()
+ .flex()
+ .flex_row()
+ .justify_between()
+ .child(format!(
+ "Example to show images and test memory usage (Rendered: {} images).",
+ self.total_count
+ ))
+ .child(
+ div()
+ .id("btn")
+ .py_1()
+ .px_4()
+ .bg(gpui::black())
+ .hover(|this| this.opacity(0.8))
+ .text_color(gpui::white())
+ .text_center()
+ .w_40()
+ .child("Next Photos")
+ .on_click(cx.listener(Self::on_next_image)),
+ ),
+ )
+ .child(
+ div()
+ .id("image-gallery")
+ .flex()
+ .flex_row()
+ .flex_wrap()
+ .gap_x_4()
+ .gap_y_2()
+ .justify_around()
+ .children(
+ (0..self.items_count)
+ .map(|ix| img(format!("{}-{}", image_url, ix)).size_20()),
+ ),
+ ),
+ )
+ }
+}
+
+actions!(image, [Quit]);
+
+fn main() {
+ env_logger::init();
+
+ Application::new().run(move |cx: &mut App| {
+ let http_client = ReqwestClient::user_agent("gpui example").unwrap();
+ cx.set_http_client(Arc::new(http_client));
+
+ cx.activate(true);
+ cx.on_action(|_: &Quit, cx| cx.quit());
+ cx.bind_keys([KeyBinding::new("cmd-q", Quit, None)]);
+ cx.set_menus(vec![Menu {
+ name: "Image Gallery".into(),
+ items: vec![MenuItem::action("Quit", Quit)],
+ }]);
+
+ let window_options = WindowOptions {
+ titlebar: Some(TitlebarOptions {
+ title: Some(SharedString::from("Image Gallery")),
+ appears_transparent: false,
+ ..Default::default()
+ }),
+
+ window_bounds: Some(WindowBounds::Windowed(Bounds::centered(
+ None,
+ size(px(1100.), px(860.)),
+ cx,
+ ))),
+
+ ..Default::default()
+ };
+
+ cx.open_window(window_options, |_, cx| {
+ cx.new(|ctx| ImageGallery {
+ image_key: "".into(),
+ items_count: 99,
+ total_count: 0,
+ image_cache: HashMapImageCache::new(ctx),
+ })
+ })
+ .unwrap();
+ });
+}
@@ -35,7 +35,7 @@ use crate::{
AssetSource, BackgroundExecutor, Bounds, ClipboardItem, CursorStyle, DispatchPhase, DisplayId,
EventEmitter, FocusHandle, FocusMap, ForegroundExecutor, Global, KeyBinding, Keymap, Keystroke,
LayoutId, Menu, MenuItem, OwnedMenu, PathPromptOptions, Pixels, Platform, PlatformDisplay,
- PlatformKeyboardLayout, Point, PromptBuilder, PromptHandle, PromptLevel, Render,
+ PlatformKeyboardLayout, Point, PromptBuilder, PromptHandle, PromptLevel, Render, RenderImage,
RenderablePromptHandle, Reservation, ScreenCaptureSource, SharedString, SubscriberSet,
Subscription, SvgRenderer, Task, TextSystem, Window, WindowAppearance, WindowHandle, WindowId,
WindowInvalidator, current_platform, hash, init_app_menus,
@@ -1616,6 +1616,22 @@ impl App {
pub fn can_select_mixed_files_and_dirs(&self) -> bool {
self.platform.can_select_mixed_files_and_dirs()
}
+
+ /// Removes an image from the sprite atlas on all windows.
+ ///
+ /// If the current window is being updated, it will be removed from `App.windows``, you can use `current_window` to specify the current window.
+ /// This is a no-op if the image is not in the sprite atlas.
+ pub fn drop_image(&mut self, image: Arc<RenderImage>, current_window: Option<&mut Window>) {
+ // remove the texture from all other windows
+ for window in self.windows.values_mut().flatten() {
+ _ = window.drop_image(image.clone());
+ }
+
+ // remove the texture from the current window
+ if let Some(window) = current_window {
+ _ = window.drop_image(image);
+ }
+ }
}
impl AppContext for App {
@@ -0,0 +1,286 @@
+use crate::{
+ AnyElement, AnyEntity, App, AppContext, Asset, AssetLogger, Bounds, Element, ElementId, Entity,
+ GlobalElementId, ImageAssetLoader, ImageCacheError, IntoElement, LayoutId, ParentElement,
+ Pixels, RenderImage, Resource, Style, StyleRefinement, Styled, Task, Window, hash,
+};
+
+use futures::{FutureExt, future::Shared};
+use refineable::Refineable;
+use smallvec::SmallVec;
+use std::{collections::HashMap, fmt, sync::Arc};
+
+/// An image cache element, all its child img elements will use the cache specified by this element.
+pub fn image_cache<I: ImageCache>(image_cache: &Entity<I>) -> ImageCacheElement {
+ ImageCacheElement {
+ image_cache: image_cache.clone().into(),
+ style: StyleRefinement::default(),
+ children: SmallVec::default(),
+ }
+}
+
+/// A dynamically typed image cache, which can be used to store any image cache
+#[derive(Clone)]
+pub struct AnyImageCache {
+ image_cache: AnyEntity,
+ load_fn: fn(
+ image_cache: &AnyEntity,
+ resource: &Resource,
+ window: &mut Window,
+ cx: &mut App,
+ ) -> Option<Result<Arc<RenderImage>, ImageCacheError>>,
+}
+
+impl<I: ImageCache> From<Entity<I>> for AnyImageCache {
+ fn from(image_cache: Entity<I>) -> Self {
+ Self {
+ image_cache: image_cache.into_any(),
+ load_fn: any_image_cache::load::<I>,
+ }
+ }
+}
+
+impl AnyImageCache {
+ /// Load an image given a resource
+ /// returns the result of loading the image if it has finished loading, or None if it is still loading
+ pub fn load(
+ &self,
+ resource: &Resource,
+ window: &mut Window,
+ cx: &mut App,
+ ) -> Option<Result<Arc<RenderImage>, ImageCacheError>> {
+ (self.load_fn)(&self.image_cache, resource, window, cx)
+ }
+}
+
+mod any_image_cache {
+ use super::*;
+
+ pub(crate) fn load<I: 'static + ImageCache>(
+ image_cache: &AnyEntity,
+ resource: &Resource,
+ window: &mut Window,
+ cx: &mut App,
+ ) -> Option<Result<Arc<RenderImage>, ImageCacheError>> {
+ let image_cache = image_cache.clone().downcast::<I>().unwrap();
+ return image_cache.update(cx, |image_cache, cx| image_cache.load(resource, window, cx));
+ }
+}
+
+/// An image cache element.
+pub struct ImageCacheElement {
+ image_cache: AnyImageCache,
+ style: StyleRefinement,
+ children: SmallVec<[AnyElement; 2]>,
+}
+
+impl ParentElement for ImageCacheElement {
+ fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
+ self.children.extend(elements)
+ }
+}
+
+impl Styled for ImageCacheElement {
+ fn style(&mut self) -> &mut StyleRefinement {
+ &mut self.style
+ }
+}
+
+impl IntoElement for ImageCacheElement {
+ type Element = Self;
+
+ fn into_element(self) -> Self::Element {
+ self
+ }
+}
+
+impl Element for ImageCacheElement {
+ type RequestLayoutState = SmallVec<[LayoutId; 4]>;
+ type PrepaintState = ();
+
+ fn id(&self) -> Option<ElementId> {
+ None
+ }
+
+ fn request_layout(
+ &mut self,
+ _id: Option<&GlobalElementId>,
+ window: &mut Window,
+ cx: &mut App,
+ ) -> (LayoutId, Self::RequestLayoutState) {
+ window.with_image_cache(self.image_cache.clone(), |window| {
+ let child_layout_ids = self
+ .children
+ .iter_mut()
+ .map(|child| child.request_layout(window, cx))
+ .collect::<SmallVec<_>>();
+ let mut style = Style::default();
+ style.refine(&self.style);
+ let layout_id = window.request_layout(style, child_layout_ids.iter().copied(), cx);
+ (layout_id, child_layout_ids)
+ })
+ }
+
+ fn prepaint(
+ &mut self,
+ _id: Option<&GlobalElementId>,
+ _bounds: Bounds<Pixels>,
+ _request_layout: &mut Self::RequestLayoutState,
+ window: &mut Window,
+ cx: &mut App,
+ ) -> Self::PrepaintState {
+ for child in &mut self.children {
+ child.prepaint(window, cx);
+ }
+ }
+
+ fn paint(
+ &mut self,
+ _id: Option<&GlobalElementId>,
+ _bounds: Bounds<Pixels>,
+ _request_layout: &mut Self::RequestLayoutState,
+ _prepaint: &mut Self::PrepaintState,
+ window: &mut Window,
+ cx: &mut App,
+ ) {
+ window.with_image_cache(self.image_cache.clone(), |window| {
+ for child in &mut self.children {
+ child.paint(window, cx);
+ }
+ })
+ }
+}
+
+type ImageLoadingTask = Shared<Task<Result<Arc<RenderImage>, ImageCacheError>>>;
+
+enum CacheItem {
+ Loading(ImageLoadingTask),
+ Loaded(Result<Arc<RenderImage>, ImageCacheError>),
+}
+
+impl CacheItem {
+ fn get(&mut self) -> Option<Result<Arc<RenderImage>, ImageCacheError>> {
+ match self {
+ CacheItem::Loading(task) => {
+ let res = task.now_or_never()?;
+ *self = CacheItem::Loaded(res.clone());
+ Some(res)
+ }
+ CacheItem::Loaded(res) => Some(res.clone()),
+ }
+ }
+}
+
+/// An object that can handle the caching and unloading of images.
+/// Implementations of this trait should ensure that images are removed from all windows when they are no longer needed.
+pub trait ImageCache: 'static {
+ /// Load an image given a resource
+ /// returns the result of loading the image if it has finished loading, or None if it is still loading
+ fn load(
+ &mut self,
+ resource: &Resource,
+ window: &mut Window,
+ cx: &mut App,
+ ) -> Option<Result<Arc<RenderImage>, ImageCacheError>>;
+}
+
+/// An implementation of ImageCache, that uses an LRU caching strategy to unload images when the cache is full
+pub struct HashMapImageCache(HashMap<u64, CacheItem>);
+
+impl fmt::Debug for HashMapImageCache {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ f.debug_struct("HashMapImageCache")
+ .field("num_images", &self.0.len())
+ .finish()
+ }
+}
+
+impl HashMapImageCache {
+ /// Create a new image cache.
+ #[inline]
+ pub fn new(cx: &mut App) -> Entity<Self> {
+ let e = cx.new(|_cx| HashMapImageCache(HashMap::new()));
+ cx.observe_release(&e, |image_cache, cx| {
+ for (_, mut item) in std::mem::replace(&mut image_cache.0, HashMap::new()) {
+ if let Some(Ok(image)) = item.get() {
+ cx.drop_image(image, None);
+ }
+ }
+ })
+ .detach();
+ e
+ }
+
+ /// Load an image from the given source.
+ ///
+ /// Returns `None` if the image is loading.
+ pub fn load(
+ &mut self,
+ source: &Resource,
+ window: &mut Window,
+ cx: &mut App,
+ ) -> Option<Result<Arc<RenderImage>, ImageCacheError>> {
+ let hash = hash(source);
+
+ if let Some(item) = self.0.get_mut(&hash) {
+ return item.get();
+ }
+
+ let fut = AssetLogger::<ImageAssetLoader>::load(source.clone(), cx);
+ let task = cx.background_executor().spawn(fut).shared();
+ self.0.insert(hash, CacheItem::Loading(task.clone()));
+
+ let entity = window.current_view();
+ window
+ .spawn(cx, {
+ async move |cx| {
+ _ = task.await;
+ cx.on_next_frame(move |_, cx| {
+ cx.notify(entity);
+ });
+ }
+ })
+ .detach();
+
+ None
+ }
+
+ /// Clear the image cache.
+ pub fn clear(&mut self, window: &mut Window, cx: &mut App) {
+ for (_, mut item) in std::mem::replace(&mut self.0, HashMap::new()) {
+ if let Some(Ok(image)) = item.get() {
+ cx.drop_image(image, Some(window));
+ }
+ }
+ }
+
+ /// Remove the image from the cache by the given source.
+ pub fn remove(&mut self, source: &Resource, window: &mut Window, cx: &mut App) {
+ let hash = hash(source);
+ if let Some(mut item) = self.0.remove(&hash) {
+ if let Some(Ok(image)) = item.get() {
+ cx.drop_image(image, Some(window));
+ }
+ }
+ }
+
+ /// Returns the number of images in the cache.
+ pub fn len(&self) -> usize {
+ self.0.len()
+ }
+
+ /// Returns true if the cache is empty.
+ pub fn is_empty(&self) -> bool {
+ self.0.is_empty()
+ }
+}
+
+impl ImageCache for HashMapImageCache {
+ fn load(
+ &mut self,
+ resource: &Resource,
+ window: &mut Window,
+ cx: &mut App,
+ ) -> Option<Result<Arc<RenderImage>, ImageCacheError>> {
+ HashMapImageCache::load(self, resource, window, cx)
+ }
+}
@@ -1,9 +1,9 @@
use crate::{
- AbsoluteLength, AnyElement, App, Asset, AssetLogger, Bounds, DefiniteLength, Element,
- ElementId, GlobalElementId, Hitbox, Image, InteractiveElement, Interactivity, IntoElement,
- LayoutId, Length, ObjectFit, Pixels, RenderImage, Resource, SMOOTH_SVG_SCALE_FACTOR,
- SharedString, SharedUri, StyleRefinement, Styled, SvgSize, Task, Window, px,
- swap_rgba_pa_to_bgra,
+ AbsoluteLength, AnyElement, AnyImageCache, App, Asset, AssetLogger, Bounds, DefiniteLength,
+ Element, ElementId, Entity, GlobalElementId, Hitbox, Image, ImageCache, InteractiveElement,
+ Interactivity, IntoElement, LayoutId, Length, ObjectFit, Pixels, RenderImage, Resource,
+ SMOOTH_SVG_SCALE_FACTOR, SharedString, SharedUri, StyleRefinement, Styled, SvgSize, Task,
+ Window, px, swap_rgba_pa_to_bgra,
};
use anyhow::{Result, anyhow};
@@ -190,6 +190,7 @@ pub struct Img {
interactivity: Interactivity,
source: ImageSource,
style: ImageStyle,
+ image_cache: Option<AnyImageCache>,
}
/// Create a new image element.
@@ -198,6 +199,7 @@ pub fn img(source: impl Into<ImageSource>) -> Img {
interactivity: Interactivity::default(),
source: source.into(),
style: ImageStyle::default(),
+ image_cache: None,
}
}
@@ -210,6 +212,23 @@ impl Img {
"hdr", "exr", "pbm", "pam", "ppm", "pgm", "ff", "farbfeld", "qoi", "svg",
]
}
+
+ /// Sets the image cache for the current node.
+ ///
+ /// If the `image_cache` is not explicitly provided, the function will determine the image cache by:
+ ///
+ /// 1. Checking if any ancestor node of the current node contains an `ImageCacheElement`, If such a node exists, the image cache specified by that ancestor will be used.
+ /// 2. If no ancestor node contains an `ImageCacheElement`, the global image cache will be used as a fallback.
+ ///
+ /// This mechanism provides a flexible way to manage image caching, allowing precise control when needed,
+ /// while ensuring a default behavior when no cache is explicitly specified.
+ #[inline]
+ pub fn image_cache<I: ImageCache>(self, image_cache: &Entity<I>) -> Self {
+ Self {
+ image_cache: Some(image_cache.clone().into()),
+ ..self
+ }
+ }
}
impl Deref for Stateful<Img> {
@@ -276,7 +295,13 @@ impl Element for Img {
|mut style, window, cx| {
let mut replacement_id = None;
- match self.source.use_data(window, cx) {
+ match self.source.use_data(
+ self.image_cache
+ .clone()
+ .or_else(|| window.image_cache_stack.last().cloned()),
+ window,
+ cx,
+ ) {
Some(Ok(data)) => {
if let Some(state) = &mut state {
let frame_count = data.frame_count();
@@ -421,7 +446,13 @@ impl Element for Img {
window,
cx,
|style, window, cx| {
- if let Some(Ok(data)) = source.use_data(window, cx) {
+ if let Some(Ok(data)) = source.use_data(
+ self.image_cache
+ .clone()
+ .or_else(|| window.image_cache_stack.last().cloned()),
+ window,
+ cx,
+ ) {
let new_bounds = self
.style
.object_fit
@@ -474,11 +505,18 @@ impl StatefulInteractiveElement for Img {}
impl ImageSource {
pub(crate) fn use_data(
&self,
+ cache: Option<AnyImageCache>,
window: &mut Window,
cx: &mut App,
) -> Option<Result<Arc<RenderImage>, ImageCacheError>> {
match self {
- ImageSource::Resource(resource) => window.use_asset::<ImgResourceLoader>(&resource, cx),
+ ImageSource::Resource(resource) => {
+ if let Some(cache) = cache {
+ cache.load(resource, window, cx)
+ } else {
+ window.use_asset::<ImgResourceLoader>(resource, cx)
+ }
+ }
ImageSource::Custom(loading_fn) => loading_fn(window, cx),
ImageSource::Render(data) => Some(Ok(data.to_owned())),
ImageSource::Image(data) => window.use_asset::<AssetLogger<ImageDecoder>>(data, cx),
@@ -4,6 +4,7 @@ mod canvas;
mod common;
mod deferred;
mod div;
+mod image_cache;
mod img;
mod list;
mod surface;
@@ -17,6 +18,7 @@ pub use canvas::*;
pub use common::*;
pub use deferred::*;
pub use div::*;
+pub use image_cache::*;
pub use img::*;
pub use list::*;
pub use surface::*;
@@ -1518,7 +1518,7 @@ impl Image {
cx: &mut App,
) -> Option<Arc<RenderImage>> {
ImageSource::Image(self)
- .use_data(window, cx)
+ .use_data(None, window, cx)
.and_then(|result| result.ok())
}
@@ -1,5 +1,5 @@
use crate::{
- Action, AnyDrag, AnyElement, AnyTooltip, AnyView, App, AppContext, Arena, Asset,
+ Action, AnyDrag, AnyElement, AnyImageCache, AnyTooltip, AnyView, App, AppContext, Arena, Asset,
AsyncWindowContext, AvailableSpace, Background, BorderStyle, Bounds, BoxShadow, Context,
Corners, CursorStyle, Decorations, DevicePixels, DispatchActionListener, DispatchNodeId,
DispatchTree, DisplayId, Edges, Effect, Entity, EntityId, EventEmitter, FileDropEvent, FontId,
@@ -617,6 +617,7 @@ pub struct Window {
pub(crate) element_opacity: Option<f32>,
pub(crate) content_mask_stack: Vec<ContentMask<Pixels>>,
pub(crate) requested_autoscroll: Option<Bounds<Pixels>>,
+ pub(crate) image_cache_stack: Vec<AnyImageCache>,
pub(crate) rendered_frame: Frame,
pub(crate) next_frame: Frame,
pub(crate) next_hitbox_id: HitboxId,
@@ -933,6 +934,7 @@ impl Window {
pending_input_observers: SubscriberSet::new(),
prompt: None,
client_inset: None,
+ image_cache_stack: Vec::new(),
})
}
@@ -2857,6 +2859,17 @@ impl Window {
result
}
+ /// Executes the provided function with the specified image cache.
+ pub(crate) fn with_image_cache<F, R>(&mut self, image_cache: AnyImageCache, f: F) -> R
+ where
+ F: FnOnce(&mut Self) -> R,
+ {
+ self.image_cache_stack.push(image_cache);
+ let result = f(self);
+ self.image_cache_stack.pop();
+ result
+ }
+
/// Sets an input handler, such as [`ElementInputHandler`][element_input_handler], which interfaces with the
/// platform to receive textual input with proper integration with concerns such
/// as IME interactions. This handler will be active for the upcoming frame until the following frame is