diff --git a/src/primitives/held.rs b/src/primitives/held.rs index 09845cc..e5fbde7 100644 --- a/src/primitives/held.rs +++ b/src/primitives/held.rs @@ -1,26 +1,59 @@ -use std::sync::Arc; +use std::{ + any::TypeId, + collections::HashMap, + hash::{DefaultHasher, Hash, Hasher}, + sync::Arc, +}; -use gpui::{App, Asset, ElementId, RenderImage, Window}; +use gpui::{App, Asset, ElementId, Global, RenderImage, Window}; -/// An element's asset; freed on change or when it leaves. +/// Holders of each asset, keyed as gpui's cache keys it. +#[derive(Default)] +struct Holders(HashMap<(TypeId, u64), usize>); + +impl Global for Holders {} + +fn key(source: &A::Source) -> (TypeId, u64) { + let mut hasher = DefaultHasher::new(); + source.hash(&mut hasher); + (TypeId::of::(), hasher.finish()) +} + +/// An element's asset; the last holder to leave frees it. pub(crate) struct Held { source: Option, image: Option>, } impl Held { + fn take(&mut self, source: &A::Source, cx: &mut App) { + let holders = &mut cx.default_global::().0; + *holders.entry(key::(source)).or_default() += 1; + self.source = Some(source.clone()); + } + fn free(&mut self, cx: &mut App) { - if let Some(source) = self.source.take() { - cx.remove_asset::(&source); + let Some(source) = self.source.take() else { + return; + }; + let image = self.image.take(); + let holders = &mut cx.default_global::().0; + let key = key::(&source); + let count = holders.get_mut(&key).expect("a held asset is counted"); + *count -= 1; + if *count > 0 { + return; } - if let Some(image) = self.image.take() { + holders.remove(&key); + cx.remove_asset::(&source); + if let Some(image) = image { // The window may be drawing; free after it. cx.defer(move |cx| cx.drop_image(image, None)); } } } -/// `source` as `A` for element `id`; the previous one freed. +/// `source` as `A` for element `id`; the previous one let go. pub(crate) fn hold( id: impl Into, source: &A::Source, @@ -43,7 +76,7 @@ where held.update(cx, |held, cx| { if held.source.as_ref() != Some(source) { held.free(cx); - held.source = Some(source.clone()); + held.take(source, cx); } if let Some(loaded) = &loaded { held.image = image(loaded); diff --git a/src/typography/math/tests.rs b/src/typography/math/tests.rs index 10b5ec4..130da22 100644 --- a/src/typography/math/tests.rs +++ b/src/typography/math/tests.rs @@ -1,3 +1,5 @@ +use gpui::AppContext as _; + use super::*; fn formula(source: &str, display: bool) -> Formula { @@ -101,6 +103,12 @@ fn settle(cx: &mut gpui::VisualTestContext) { cx.run_until_parked(); } +/// Draws, then lets gpui release what the frame dropped. +fn flush(cx: &mut gpui::VisualTestContext) { + cx.run_until_parked(); + cx.update(|_, _| ()); +} + /// Whether `source`'s picture sits in the asset cache. fn cached(source: &str, cx: &mut gpui::VisualTestContext) -> bool { cx.update(|window, cx| { @@ -128,13 +136,44 @@ fn a_formula_frees_its_picture_when_it_changes_or_leaves(cx: &mut gpui::TestAppC assert!(!cached("b^2", cx), "gone with its element"); } +/// One formula in a view of its own. +struct One; + +impl gpui::Render for One { + fn render(&mut self, _: &mut Window, _: &mut gpui::Context) -> impl IntoElement { + Latex::parse("one", "c^2").expect("parses") + } +} + +/// Views of one formula, each drawn from gpui's view cache. +struct Pair(Vec>); + +impl gpui::Render for Pair { + fn render(&mut self, _: &mut Window, _: &mut gpui::Context) -> impl IntoElement { + div().children( + self.0.iter().map(|one| { + gpui::AnyView::from(one.clone()).cached(gpui::StyleRefinement::default()) + }), + ) + } +} + #[gpui::test] -fn a_shared_formula_draws_on_after_one_user_leaves(cx: &mut gpui::TestAppContext) { - let (view, cx) = open(vec!["c^2", "c^2"], cx); - view.update(cx, |view, cx| { - view.0.pop(); +fn a_shared_formula_stays_until_its_last_holder_leaves(cx: &mut gpui::TestAppContext) { + cx.update(crate::theme::Theme::init); + let (pair, cx) = cx.add_window_view(|_, cx| Pair(vec![cx.new(|_| One), cx.new(|_| One)])); + settle(cx); + assert!(cached("c^2", cx), "drawn"); + pair.update(cx, |pair, cx| { + pair.0.pop(); cx.notify(); }); - settle(cx); - assert!(cached("c^2", cx), "the one left loads it again"); + flush(cx); + assert!(cached("c^2", cx), "the cached survivor still holds it"); + pair.update(cx, |pair, cx| { + pair.0.clear(); + cx.notify(); + }); + flush(cx); + assert!(!cached("c^2", cx), "the last holder freed it"); }