diff --git a/crates/gpui/examples/image_gallery.rs b/crates/gpui/examples/image_gallery.rs index ac10abcee0..e7abb196c7 100644 --- a/crates/gpui/examples/image_gallery.rs +++ b/crates/gpui/examples/image_gallery.rs @@ -1,9 +1,9 @@ use futures::FutureExt; use gpui::{ App, AppContext, Application, Asset as _, AssetLogger, Bounds, ClickEvent, Context, ElementId, - Entity, HashMapImageCache, ImageAssetLoader, ImageCache, ImageCacheProvider, KeyBinding, Menu, - MenuItem, SharedString, TitlebarOptions, Window, WindowBounds, WindowOptions, actions, div, - hash, image_cache, img, prelude::*, px, rgb, size, + Entity, ImageAssetLoader, ImageCache, ImageCacheProvider, KeyBinding, Menu, MenuItem, + RetainAllImageCache, SharedString, TitlebarOptions, Window, WindowBounds, WindowOptions, + actions, div, hash, image_cache, img, prelude::*, px, rgb, size, }; use reqwest_client::ReqwestClient; use std::{collections::HashMap, sync::Arc}; @@ -14,7 +14,7 @@ struct ImageGallery { image_key: String, items_count: usize, total_count: usize, - image_cache: Entity, + image_cache: Entity, } impl ImageGallery { @@ -44,8 +44,8 @@ impl Render for ImageGallery { .text_color(gpui::white()) .child("Manually managed image cache:") .child( - image_cache(self.image_cache.clone()).child( div() + .image_cache(self.image_cache.clone()) .id("main") .font_family(".SystemUIFont") .text_color(gpui::black()) @@ -95,7 +95,7 @@ impl Render for ImageGallery { .map(|ix| img(format!("{}-{}", image_url, ix)).size_20()), ), ), - )) + ) .child( "Automatically managed image cache:" ) @@ -282,7 +282,7 @@ fn main() { image_key: "".into(), items_count: IMAGES_IN_GALLERY, total_count: 0, - image_cache: HashMapImageCache::new(ctx), + image_cache: RetainAllImageCache::new(ctx), }) }) .unwrap(); diff --git a/crates/gpui/src/elements/div.rs b/crates/gpui/src/elements/div.rs index cf9cc45291..71dd82b88e 100644 --- a/crates/gpui/src/elements/div.rs +++ b/crates/gpui/src/elements/div.rs @@ -40,6 +40,8 @@ use std::{ use taffy::style::Overflow; use util::ResultExt; +use super::ImageCacheProvider; + const DRAG_THRESHOLD: f64 = 2.; const TOOLTIP_SHOW_DELAY: Duration = Duration::from_millis(500); const HOVERABLE_TOOLTIP_HIDE_DELAY: Duration = Duration::from_millis(500); @@ -1134,6 +1136,7 @@ pub fn div() -> Div { interactivity, children: SmallVec::default(), prepaint_listener: None, + image_cache: None, } } @@ -1142,6 +1145,7 @@ pub struct Div { interactivity: Interactivity, children: SmallVec<[AnyElement; 2]>, prepaint_listener: Option>, &mut Window, &mut App) + 'static>>, + image_cache: Option>, } impl Div { @@ -1154,6 +1158,12 @@ impl Div { self.prepaint_listener = Some(Box::new(listener)); self } + + /// Add an image cache at the location of this div in the element tree. + pub fn image_cache(mut self, cache: impl ImageCacheProvider) -> Self { + self.image_cache = Some(Box::new(cache)); + self + } } /// A frame state for a `Div` element, which contains layout IDs for its children. @@ -1199,7 +1209,12 @@ impl Element for Div { cx: &mut App, ) -> (LayoutId, Self::RequestLayoutState) { let mut child_layout_ids = SmallVec::new(); - let layout_id = + let image_cache = self + .image_cache + .as_mut() + .map(|provider| provider.provide(window, cx)); + + let layout_id = window.with_image_cache(image_cache, |window| { self.interactivity .request_layout(global_id, window, cx, |style, window, cx| { window.with_text_style(style.text_style().cloned(), |window| { @@ -1210,7 +1225,9 @@ impl Element for Div { .collect::>(); window.request_layout(style, child_layout_ids.iter().copied(), cx) }) - }); + }) + }); + (layout_id, DivFrameState { child_layout_ids }) } @@ -1291,18 +1308,25 @@ impl Element for Div { window: &mut Window, cx: &mut App, ) { - self.interactivity.paint( - global_id, - bounds, - hitbox.as_ref(), - window, - cx, - |_style, window, cx| { - for child in &mut self.children { - child.paint(window, cx); - } - }, - ); + let image_cache = self + .image_cache + .as_mut() + .map(|provider| provider.provide(window, cx)); + + window.with_image_cache(image_cache, |window| { + self.interactivity.paint( + global_id, + bounds, + hitbox.as_ref(), + window, + cx, + |_style, window, cx| { + for child in &mut self.children { + child.paint(window, cx); + } + }, + ) + }); } } diff --git a/crates/gpui/src/elements/image_cache.rs b/crates/gpui/src/elements/image_cache.rs index 677e611e5f..9c84810e3b 100644 --- a/crates/gpui/src/elements/image_cache.rs +++ b/crates/gpui/src/elements/image_cache.rs @@ -109,7 +109,7 @@ impl Element for ImageCacheElement { cx: &mut App, ) -> (LayoutId, Self::RequestLayoutState) { let image_cache = self.image_cache_provider.provide(window, cx); - window.with_image_cache(image_cache, |window| { + window.with_image_cache(Some(image_cache), |window| { let child_layout_ids = self .children .iter_mut() @@ -145,7 +145,7 @@ impl Element for ImageCacheElement { cx: &mut App, ) { let image_cache = self.image_cache_provider.provide(window, cx); - window.with_image_cache(image_cache, |window| { + window.with_image_cache(Some(image_cache), |window| { for child in &mut self.children { child.paint(window, cx); } @@ -217,9 +217,9 @@ impl ImageCacheProvider for Entity { } /// An implementation of ImageCache, that uses an LRU caching strategy to unload images when the cache is full -pub struct HashMapImageCache(HashMap); +pub struct RetainAllImageCache(HashMap); -impl fmt::Debug for HashMapImageCache { +impl fmt::Debug for RetainAllImageCache { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("HashMapImageCache") .field("num_images", &self.0.len()) @@ -227,11 +227,11 @@ impl fmt::Debug for HashMapImageCache { } } -impl HashMapImageCache { +impl RetainAllImageCache { /// Create a new image cache. #[inline] pub fn new(cx: &mut App) -> Entity { - let e = cx.new(|_cx| HashMapImageCache(HashMap::new())); + let e = cx.new(|_cx| RetainAllImageCache(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() { @@ -307,13 +307,39 @@ impl HashMapImageCache { } } -impl ImageCache for HashMapImageCache { +impl ImageCache for RetainAllImageCache { fn load( &mut self, resource: &Resource, window: &mut Window, cx: &mut App, ) -> Option, ImageCacheError>> { - HashMapImageCache::load(self, resource, window, cx) + RetainAllImageCache::load(self, resource, window, cx) + } +} + +/// Constructs a retain-all image cache that uses the element state associated with the given ID. +pub fn retain_all(id: impl Into) -> RetainAllImageCacheProvider { + RetainAllImageCacheProvider { id: id.into() } +} + +/// A provider struct for creating a retain-all image cache inline +pub struct RetainAllImageCacheProvider { + id: ElementId, +} + +impl ImageCacheProvider for RetainAllImageCacheProvider { + fn provide(&mut self, window: &mut Window, cx: &mut App) -> AnyImageCache { + window + .with_global_id(self.id.clone(), |global_id, window| { + window.with_element_state::, _>( + global_id, + |cache, _window| { + let mut cache = cache.unwrap_or_else(|| RetainAllImageCache::new(cx)); + (cache.clone(), cache) + }, + ) + }) + .into() } } diff --git a/crates/gpui/src/elements/img.rs b/crates/gpui/src/elements/img.rs index 9592f0ae4d..5d8b8daf45 100644 --- a/crates/gpui/src/elements/img.rs +++ b/crates/gpui/src/elements/img.rs @@ -522,6 +522,37 @@ impl ImageSource { ImageSource::Image(data) => window.use_asset::>(data, cx), } } + + pub(crate) fn get_data( + &self, + cache: Option, + window: &mut Window, + cx: &mut App, + ) -> Option, ImageCacheError>> { + match self { + ImageSource::Resource(resource) => { + if let Some(cache) = cache { + cache.load(resource, window, cx) + } else { + window.get_asset::(resource, cx) + } + } + ImageSource::Custom(loading_fn) => loading_fn(window, cx), + ImageSource::Render(data) => Some(Ok(data.to_owned())), + ImageSource::Image(data) => window.get_asset::>(data, cx), + } + } + + /// Remove this image source from the asset system + pub fn remove_asset(&self, cx: &mut App) { + match self { + ImageSource::Resource(resource) => { + cx.remove_asset::(resource); + } + ImageSource::Custom(_) | ImageSource::Render(_) => {} + ImageSource::Image(data) => cx.remove_asset::>(data), + } + } } #[derive(Clone)] diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index 5f3ac9ebbd..ea51bef366 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -1537,6 +1537,22 @@ impl Image { .and_then(|result| result.ok()) } + /// Use the GPUI `get_asset` API to make this image renderable + pub fn get_render_image( + self: Arc, + window: &mut Window, + cx: &mut App, + ) -> Option> { + ImageSource::Image(self) + .get_data(None, window, cx) + .and_then(|result| result.ok()) + } + + /// Use the GPUI `remove_asset` API to drop this image, if possible. + pub fn remove_asset(self: Arc, cx: &mut App) { + ImageSource::Image(self).remove_asset(cx); + } + /// Convert the clipboard image to an `ImageData` object. pub fn to_image_data(&self, svg_renderer: SvgRenderer) -> Result> { fn frames_for_image( diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index c9af06f7cc..6eac25cb6c 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -2117,6 +2117,16 @@ impl Window { None }) } + + /// Asynchronously load an asset, if the asset hasn't finished loading or doesn't exist this will return None. + /// Your view will not be re-drawn once the asset has finished loading. + /// + /// Note that the multiple calls to this method will only result in one `Asset::load` call at a + /// time. + pub fn get_asset(&mut self, source: &A::Source, cx: &mut App) -> Option { + let (task, _) = cx.fetch_asset::(source); + task.clone().now_or_never() + } /// Obtain the current element offset. This method should only be called during the /// prepaint phase of element drawing. pub fn element_offset(&self) -> Point { @@ -2876,14 +2886,18 @@ impl Window { } /// Executes the provided function with the specified image cache. - pub(crate) fn with_image_cache(&mut self, image_cache: AnyImageCache, f: F) -> R + pub fn with_image_cache(&mut self, image_cache: Option, 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 + if let Some(image_cache) = image_cache { + self.image_cache_stack.push(image_cache); + let result = f(self); + self.image_cache_stack.pop(); + result + } else { + f(self) + } } /// Sets an input handler, such as [`ElementInputHandler`][element_input_handler], which interfaces with the diff --git a/crates/image_viewer/src/image_viewer.rs b/crates/image_viewer/src/image_viewer.rs index 152d2074b4..ba735b356f 100644 --- a/crates/image_viewer/src/image_viewer.rs +++ b/crates/image_viewer/src/image_viewer.rs @@ -35,9 +35,19 @@ impl ImageView { pub fn new( image_item: Entity, project: Entity, + window: &mut Window, cx: &mut Context, ) -> Self { cx.subscribe(&image_item, Self::on_image_event).detach(); + cx.on_release_in(window, |this, window, cx| { + let image_data = this.image_item.read(cx).image.clone(); + if let Some(image) = image_data.clone().get_render_image(window, cx) { + cx.drop_image(image, None); + } + image_data.remove_asset(cx); + }) + .detach(); + Self { image_item, project, @@ -234,7 +244,9 @@ impl SerializableItem for ImageView { .update(cx, |project, cx| project.open_image(project_path, cx))? .await?; - cx.update(|_, cx| Ok(cx.new(|cx| ImageView::new(image_item, project, cx))))? + cx.update( + |window, cx| Ok(cx.new(|cx| ImageView::new(image_item, project, window, cx))), + )? }) } @@ -365,13 +377,13 @@ impl ProjectItem for ImageView { project: Entity, _: &Pane, item: Entity, - _: &mut Window, + window: &mut Window, cx: &mut Context, ) -> Self where Self: Sized, { - Self::new(item, project, cx) + Self::new(item, project, window, cx) } } diff --git a/crates/markdown_preview/src/markdown_preview_view.rs b/crates/markdown_preview/src/markdown_preview_view.rs index bbcb196b29..a60934283a 100644 --- a/crates/markdown_preview/src/markdown_preview_view.rs +++ b/crates/markdown_preview/src/markdown_preview_view.rs @@ -7,8 +7,8 @@ use editor::scroll::Autoscroll; use editor::{Editor, EditorEvent}; use gpui::{ App, ClickEvent, Context, Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement, - IntoElement, ListState, ParentElement, Render, Styled, Subscription, Task, WeakEntity, Window, - list, + IntoElement, ListState, ParentElement, Render, RetainAllImageCache, Styled, Subscription, Task, + WeakEntity, Window, list, }; use language::LanguageRegistry; use settings::Settings; @@ -30,6 +30,7 @@ const REPARSE_DEBOUNCE: Duration = Duration::from_millis(200); pub struct MarkdownPreviewView { workspace: WeakEntity, + image_cache: Entity, active_editor: Option, focus_handle: FocusHandle, contents: Option, @@ -264,6 +265,7 @@ impl MarkdownPreviewView { tab_content_text, language_registry, parsing_markdown_task: None, + image_cache: RetainAllImageCache::new(cx), }; this.set_editor(active_editor, window, cx); @@ -506,7 +508,9 @@ impl Render for MarkdownPreviewView { fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { let buffer_size = ThemeSettings::get_global(cx).buffer_font_size(cx); let buffer_line_height = ThemeSettings::get_global(cx).buffer_line_height; + v_flex() + .image_cache(self.image_cache.clone()) .id("MarkdownPreview") .key_context("MarkdownPreview") .track_focus(&self.focus_handle(cx)) diff --git a/crates/markdown_preview/src/markdown_renderer.rs b/crates/markdown_preview/src/markdown_renderer.rs index 5700f210f8..b1ec4dc09e 100644 --- a/crates/markdown_preview/src/markdown_renderer.rs +++ b/crates/markdown_preview/src/markdown_renderer.rs @@ -139,7 +139,6 @@ pub fn render_parsed_markdown( .map(|block| render_markdown_block(block, &mut cx)), ) } - pub fn render_markdown_block(block: &ParsedMarkdownElement, cx: &mut RenderContext) -> AnyElement { use ParsedMarkdownElement::*; match block { diff --git a/crates/repl/src/notebook/cell.rs b/crates/repl/src/notebook/cell.rs index 75fe44a191..b8071d4177 100644 --- a/crates/repl/src/notebook/cell.rs +++ b/crates/repl/src/notebook/cell.rs @@ -3,7 +3,9 @@ use std::sync::Arc; use editor::{Editor, EditorMode, MultiBuffer}; use futures::future::Shared; -use gpui::{App, Entity, Hsla, Task, TextStyleRefinement, prelude::*}; +use gpui::{ + App, Entity, Hsla, RetainAllImageCache, Task, TextStyleRefinement, image_cache, prelude::*, +}; use language::{Buffer, Language, LanguageRegistry}; use markdown_preview::{markdown_parser::parse_markdown, markdown_renderer::render_markdown_block}; use nbformat::v4::{CellId, CellMetadata, CellType}; @@ -148,6 +150,7 @@ impl Cell { MarkdownCell { markdown_parsing_task, + image_cache: RetainAllImageCache::new(cx), languages: languages.clone(), id: id.clone(), metadata: metadata.clone(), @@ -329,6 +332,7 @@ pub trait RunnableCell: RenderableCell { pub struct MarkdownCell { id: CellId, metadata: CellMetadata, + image_cache: Entity, source: String, parsed_markdown: Option, markdown_parsing_task: Task<()>, @@ -403,12 +407,12 @@ impl Render for MarkdownCell { .child(self.gutter(window, cx)) .child( v_flex() + .image_cache(self.image_cache.clone()) .size_full() .flex_1() .p_3() .font_ui(cx) .text_size(TextSize::Default.rems(cx)) - // .children(parsed.children.iter().map(|child| { div().relative().child(div().relative().child( render_markdown_block(child, &mut markdown_render_context), diff --git a/crates/repl/src/outputs/markdown.rs b/crates/repl/src/outputs/markdown.rs index fb04c9fd44..118260ae94 100644 --- a/crates/repl/src/outputs/markdown.rs +++ b/crates/repl/src/outputs/markdown.rs @@ -1,5 +1,7 @@ use anyhow::Result; -use gpui::{App, ClipboardItem, Context, Entity, Task, Window, div, prelude::*}; +use gpui::{ + App, ClipboardItem, Context, Entity, RetainAllImageCache, Task, Window, div, prelude::*, +}; use language::Buffer; use markdown_preview::{ markdown_elements::ParsedMarkdown, markdown_parser::parse_markdown, @@ -11,6 +13,7 @@ use crate::outputs::OutputContent; pub struct MarkdownView { raw_text: String, + image_cache: Entity, contents: Option, parsing_markdown_task: Option>>, } @@ -33,6 +36,7 @@ impl MarkdownView { Self { raw_text: text.clone(), + image_cache: RetainAllImageCache::new(cx), contents: None, parsing_markdown_task: Some(task), } @@ -74,6 +78,7 @@ impl Render for MarkdownView { markdown_preview::markdown_renderer::RenderContext::new(None, window, cx); v_flex() + .image_cache(self.image_cache.clone()) .gap_3() .py_4() .children(parsed.children.iter().map(|child| { diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index f8c778d01c..2b675ed9eb 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -26,8 +26,8 @@ use git_ui::project_diff::ProjectDiffToolbar; use gpui::{ Action, App, AppContext as _, AsyncWindowContext, Context, DismissEvent, Element, Entity, Focusable, KeyBinding, ParentElement, PathPromptOptions, PromptLevel, ReadGlobal, SharedString, - Styled, Task, TitlebarOptions, UpdateGlobal, Window, WindowKind, WindowOptions, actions, point, - px, + Styled, Task, TitlebarOptions, UpdateGlobal, Window, WindowKind, WindowOptions, actions, + image_cache, point, px, retain_all, }; use image_viewer::ImageInfo; use migrate::{MigrationBanner, MigrationEvent, MigrationNotification, MigrationType}; @@ -1336,7 +1336,7 @@ fn show_markdown_app_notification( let primary_button_on_click = primary_button_on_click.clone(); cx.new(move |cx| { MessageNotification::new_from_builder(cx, move |window, cx| { - gpui::div() + image_cache(retain_all("notification-cache")) .text_xs() .child(markdown_preview::markdown_renderer::render_parsed_markdown( &parsed_markdown.clone(),