diff --git a/src/primitives/mod.rs b/src/primitives/mod.rs index 0e7ad65..35964e7 100644 --- a/src/primitives/mod.rs +++ b/src/primitives/mod.rs @@ -9,6 +9,7 @@ mod image; mod layer; mod measure; mod pressable; +mod retired; mod severity; mod tooltip; @@ -31,5 +32,6 @@ pub(crate) use image::{checked_ratio, framed}; pub use layer::{Raised, raise}; pub use measure::{IntersectionObserver, Measure}; pub use pressable::Pressable; +pub use retired::Retired; pub use severity::Severity; pub use tooltip::{HelpTooltip, Tooltip, TooltipTrigger}; diff --git a/src/primitives/retired.rs b/src/primitives/retired.rs new file mode 100644 index 0000000..2e5badb --- /dev/null +++ b/src/primitives/retired.rs @@ -0,0 +1,30 @@ +//! Images a view stopped drawing, freed from the window's atlas. + +use std::sync::Arc; + +use gpui::{RenderImage, Window}; + +/// Images to free; GPUI keeps each painted image until told. +#[derive(Default)] +pub struct Retired(Vec>); + +impl Retired { + /// An image no longer drawn; freed at the next paint. + pub fn push(&mut self, image: Arc) { + self.0.push(image); + } + + /// Frees every retired image; call while painting. + pub fn release(&mut self, window: &mut Window) { + for image in self.0.drain(..) { + if let Err(error) = window.drop_image(image) { + log::error!("images: a retired image stayed: {error}"); + } + } + } + + /// Whether nothing waits to be freed. + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } +} diff --git a/src/primitives/tests.rs b/src/primitives/tests.rs index 5a79ad9..cfa70f9 100644 --- a/src/primitives/tests.rs +++ b/src/primitives/tests.rs @@ -217,8 +217,48 @@ fn f6_leaves_a_field_that_keeps_tab(cx: &mut TestAppContext) { let after = view.read_with(cx, |kept, _| kept.after.clone()); cx.simulate_keystrokes("f6"); assert_eq!(view.read_with(cx, |kept, _| kept.indents), 1, "Tab stayed in the field"); - cx.update(|window, cx| assert!(after.is_focused(window), "F6 moved on")); + cx.update(|window, _| assert!(after.is_focused(window), "F6 moved on")); cx.simulate_keystrokes("shift-f6"); let field = view.read_with(cx, |kept, _| kept.field.clone()); cx.update(|window, _| assert!(field.is_focused(window), "Shift-F6 came back")); } + +/// A view that paints one image until told to retire it. +struct Painted { + image: Option>, + retired: super::Retired, +} + +impl Render for Painted { + fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { + let view = cx.entity(); + gpui::canvas( + |_, _, _| {}, + move |bounds, _, window, cx| { + view.update(cx, |painted, _| { + painted.retired.release(window); + if let Some(image) = painted.image.clone() { + window.paint_image(bounds, bounds, Default::default(), image, 0, false).expect("paints"); + } + }) + }, + ) + .size(px(10.0)) + } +} + +#[gpui::test] +fn a_retired_image_is_freed_at_the_next_paint(cx: &mut TestAppContext) { + cx.update(Theme::init); + let pixels = image::RgbaImage::from_pixel(2, 2, image::Rgba([1, 2, 3, 255])); + let image = std::sync::Arc::new(gpui::RenderImage::new([image::Frame::new(pixels)])); + let (view, cx) = cx.add_window_view(|_, _| Painted { image: Some(image), retired: super::Retired::default() }); + cx.run_until_parked(); + view.update(cx, |painted, cx| { + let old = painted.image.take().expect("painted"); + painted.retired.push(old); + cx.notify(); + }); + cx.run_until_parked(); + assert!(view.read_with(cx, |painted, _| painted.retired.is_empty()), "the old image was freed"); +}