Detailed changes
@@ -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<HashMapImageCache>,
+ image_cache: Entity<RetainAllImageCache>,
}
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();
@@ -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<Box<dyn Fn(Vec<Bounds<Pixels>>, &mut Window, &mut App) + 'static>>,
+ image_cache: Option<Box<dyn ImageCacheProvider>>,
}
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::<SmallVec<_>>();
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);
+ }
+ },
+ )
+ });
}
}
@@ -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<T: ImageCache> ImageCacheProvider for Entity<T> {
}
/// An implementation of ImageCache, that uses an LRU caching strategy to unload images when the cache is full
-pub struct HashMapImageCache(HashMap<u64, ImageCacheItem>);
+pub struct RetainAllImageCache(HashMap<u64, ImageCacheItem>);
-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<Self> {
- 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<Result<Arc<RenderImage>, 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<ElementId>) -> 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::<Entity<RetainAllImageCache>, _>(
+ global_id,
+ |cache, _window| {
+ let mut cache = cache.unwrap_or_else(|| RetainAllImageCache::new(cx));
+ (cache.clone(), cache)
+ },
+ )
+ })
+ .into()
}
}
@@ -522,6 +522,37 @@ impl ImageSource {
ImageSource::Image(data) => window.use_asset::<AssetLogger<ImageDecoder>>(data, cx),
}
}
+
+ pub(crate) fn get_data(
+ &self,
+ cache: Option<AnyImageCache>,
+ window: &mut Window,
+ cx: &mut App,
+ ) -> Option<Result<Arc<RenderImage>, ImageCacheError>> {
+ match self {
+ ImageSource::Resource(resource) => {
+ if let Some(cache) = cache {
+ cache.load(resource, window, cx)
+ } else {
+ window.get_asset::<ImgResourceLoader>(resource, cx)
+ }
+ }
+ ImageSource::Custom(loading_fn) => loading_fn(window, cx),
+ ImageSource::Render(data) => Some(Ok(data.to_owned())),
+ ImageSource::Image(data) => window.get_asset::<AssetLogger<ImageDecoder>>(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::<ImgResourceLoader>(resource);
+ }
+ ImageSource::Custom(_) | ImageSource::Render(_) => {}
+ ImageSource::Image(data) => cx.remove_asset::<AssetLogger<ImageDecoder>>(data),
+ }
+ }
}
#[derive(Clone)]
@@ -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<Self>,
+ window: &mut Window,
+ cx: &mut App,
+ ) -> Option<Arc<RenderImage>> {
+ 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<Self>, 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<Arc<RenderImage>> {
fn frames_for_image(
@@ -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<A: Asset>(&mut self, source: &A::Source, cx: &mut App) -> Option<A::Output> {
+ let (task, _) = cx.fetch_asset::<A>(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<Pixels> {
@@ -2876,14 +2886,18 @@ impl Window {
}
/// 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
+ pub fn with_image_cache<F, R>(&mut self, image_cache: Option<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
+ 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
@@ -35,9 +35,19 @@ impl ImageView {
pub fn new(
image_item: Entity<ImageItem>,
project: Entity<Project>,
+ window: &mut Window,
cx: &mut Context<Self>,
) -> 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<Project>,
_: &Pane,
item: Entity<Self::Item>,
- _: &mut Window,
+ window: &mut Window,
cx: &mut Context<Self>,
) -> Self
where
Self: Sized,
{
- Self::new(item, project, cx)
+ Self::new(item, project, window, cx)
}
}
@@ -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<Workspace>,
+ image_cache: Entity<RetainAllImageCache>,
active_editor: Option<EditorState>,
focus_handle: FocusHandle,
contents: Option<ParsedMarkdown>,
@@ -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<Self>) -> 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))
@@ -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 {
@@ -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<RetainAllImageCache>,
source: String,
parsed_markdown: Option<markdown_preview::markdown_elements::ParsedMarkdown>,
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),
@@ -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<RetainAllImageCache>,
contents: Option<ParsedMarkdown>,
parsing_markdown_task: Option<Task<Result<()>>>,
}
@@ -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| {
@@ -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<F>(
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(),