This PR fixes several possible memory leaks due to loading images in markdown files and the image viewer, using the new image cache APIs TODO: - [x] Ensure this didn't break rendering in any of the affected components. Release Notes: - Fixed several image related memory leaks
346 lines
11 KiB
Rust
346 lines
11 KiB
Rust
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.
|
|
/// Note that this could as simple as passing an `Entity<T: ImageCache>`
|
|
pub fn image_cache(image_cache_provider: impl ImageCacheProvider) -> ImageCacheElement {
|
|
ImageCacheElement {
|
|
image_cache_provider: Box::new(image_cache_provider),
|
|
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_provider: Box<dyn ImageCacheProvider>,
|
|
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) {
|
|
let image_cache = self.image_cache_provider.provide(window, cx);
|
|
window.with_image_cache(Some(image_cache), |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,
|
|
) {
|
|
let image_cache = self.image_cache_provider.provide(window, cx);
|
|
window.with_image_cache(Some(image_cache), |window| {
|
|
for child in &mut self.children {
|
|
child.paint(window, cx);
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
/// An image loading task associated with an image cache.
|
|
pub type ImageLoadingTask = Shared<Task<Result<Arc<RenderImage>, ImageCacheError>>>;
|
|
|
|
/// An image cache item
|
|
pub enum ImageCacheItem {
|
|
/// The associated image is currently loading
|
|
Loading(ImageLoadingTask),
|
|
/// This item has loaded an image.
|
|
Loaded(Result<Arc<RenderImage>, ImageCacheError>),
|
|
}
|
|
|
|
impl std::fmt::Debug for ImageCacheItem {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
let status = match self {
|
|
ImageCacheItem::Loading(_) => &"Loading...".to_string(),
|
|
ImageCacheItem::Loaded(render_image) => &format!("{:?}", render_image),
|
|
};
|
|
f.debug_struct("ImageCacheItem")
|
|
.field("status", status)
|
|
.finish()
|
|
}
|
|
}
|
|
|
|
impl ImageCacheItem {
|
|
/// Attempt to get the image from the cache item.
|
|
pub fn get(&mut self) -> Option<Result<Arc<RenderImage>, ImageCacheError>> {
|
|
match self {
|
|
ImageCacheItem::Loading(task) => {
|
|
let res = task.now_or_never()?;
|
|
*self = ImageCacheItem::Loaded(res.clone());
|
|
Some(res)
|
|
}
|
|
ImageCacheItem::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 object that can create an ImageCache during the render phase.
|
|
/// See the ImageCache trait for more information.
|
|
pub trait ImageCacheProvider: 'static {
|
|
/// Called during the request_layout phase to create an ImageCache.
|
|
fn provide(&mut self, _window: &mut Window, _cx: &mut App) -> AnyImageCache;
|
|
}
|
|
|
|
impl<T: ImageCache> ImageCacheProvider for Entity<T> {
|
|
fn provide(&mut self, _window: &mut Window, _cx: &mut App) -> AnyImageCache {
|
|
self.clone().into()
|
|
}
|
|
}
|
|
|
|
/// An implementation of ImageCache, that uses an LRU caching strategy to unload images when the cache is full
|
|
pub struct RetainAllImageCache(HashMap<u64, ImageCacheItem>);
|
|
|
|
impl fmt::Debug for RetainAllImageCache {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
f.debug_struct("HashMapImageCache")
|
|
.field("num_images", &self.0.len())
|
|
.finish()
|
|
}
|
|
}
|
|
|
|
impl RetainAllImageCache {
|
|
/// Create a new image cache.
|
|
#[inline]
|
|
pub fn new(cx: &mut App) -> Entity<Self> {
|
|
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() {
|
|
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, ImageCacheItem::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 RetainAllImageCache {
|
|
fn load(
|
|
&mut self,
|
|
resource: &Resource,
|
|
window: &mut Window,
|
|
cx: &mut App,
|
|
) -> Option<Result<Arc<RenderImage>, ImageCacheError>> {
|
|
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()
|
|
}
|
|
}
|