Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 19 additions & 4 deletions apps/desktop-gpui/src/editor_crop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1081,6 +1081,15 @@ pub struct CropState {
}

impl CropState {
/// The display frame is a full-resolution decode and the window's atlas
/// keeps every painted image until it is told otherwise, so a closed crop
/// would otherwise stay resident until the editor window closes.
pub fn release_frame(&mut self, window: &mut Window) {
if let Some(frame) = self.frame.take() {
let _ = window.drop_image(frame);
}
}

/// `computeInitialBounds` (`:483-517`) at open, with the container the
/// viewport gives.
pub fn new(target: (u32, u32), box_size: (f32, f32), initial: CropBounds) -> Self {
Expand Down Expand Up @@ -1606,9 +1615,10 @@ impl EditorWindow {
window: &mut Window,
cx: &mut Context<Self>,
) {
if self.crop.take().is_none() {
let Some(mut state) = self.crop.take() else {
return;
}
};
state.release_frame(window);
tracing::info!(reason, "crop cancelled");
self.publish_project();
cx.notify();
Expand All @@ -1630,6 +1640,7 @@ impl EditorWindow {
cx.notify();
return;
}
state.release_frame(window);
let bounds = state.real();
let crop = Crop {
position: XY::new(bounds.x.max(0.) as u32, bounds.y.max(0.) as u32),
Expand Down Expand Up @@ -2154,12 +2165,16 @@ impl EditorWindow {
Some(task) => task.await.ok().flatten(),
None => None,
};
this.update(cx, |this: &mut Self, cx| {
this.update_in(cx, |this: &mut Self, window, cx| {
let Some(state) = this.crop.as_mut() else {
return;
};
match image {
Some(image) => state.frame = Some(image),
Some(image) => {
if let Some(previous) = state.frame.replace(image) {
let _ = window.drop_image(previous);
}
}
None => {
tracing::warn!("crop: display frame decode failed");
state.frame_failed = true;
Expand Down
83 changes: 66 additions & 17 deletions apps/desktop-gpui/src/editor_sidebar.rs
Original file line number Diff line number Diff line change
Expand Up @@ -596,6 +596,36 @@ fn noise_texture(width: u32, height: u32, base_frequency: f32) -> Arc<RenderImag
)]))
}

#[derive(Default)]
struct NoiseCache {
current: Option<(i32, Arc<RenderImage>)>,
retired: Vec<Arc<RenderImage>>,
}

impl NoiseCache {
fn image(&mut self, scale: f32) -> Arc<RenderImage> {
let key = scale.round() as i32;
if let Some((cached, image)) = &self.current
&& *cached == key
{
return image.clone();
}
let image = noise_texture(
GRADIENT_PREVIEW_WIDTH as u32,
GRADIENT_PREVIEW_HEIGHT as u32,
noise_base_frequency(scale),
);
if let Some((_, previous)) = self.current.replace((key, image.clone())) {
self.retired.push(previous);
}
image
}

fn take_retired(&mut self) -> Vec<Arc<RenderImage>> {
std::mem::take(&mut self.retired)
}
}

// ---------------------------------------------------------------------------
// State
// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -787,7 +817,7 @@ pub struct SidebarState {
/// The gradient preview's grain, cached by the grain-scale step it was
/// generated for -- regenerating 42k pixels of noise on every frame of a
/// slider drag would be the one expensive thing in this panel.
noise: std::cell::RefCell<Option<(i32, Arc<RenderImage>)>>,
noise: std::cell::RefCell<NoiseCache>,

/// The open `KSelect`'s menu, if any -- one slot for every select in the
/// sidebar, the settings window's `Menu.popup()` stand-in.
Expand Down Expand Up @@ -876,7 +906,7 @@ impl SidebarState {
cursor_previews: std::cell::RefCell::new(BTreeMap::new()),
cursor_scale: 2.,
panel_sections: std::cell::RefCell::new(HashMap::new()),
noise: std::cell::RefCell::new(None),
noise: std::cell::RefCell::default(),
menu: None,
caption_model: "best",
caption_language: "auto",
Expand Down Expand Up @@ -905,19 +935,16 @@ impl SidebarState {

/// The grain texture for a grain-scale value, generated once per step.
fn noise_image(&self, scale: f32) -> Arc<RenderImage> {
let key = scale.round() as i32;
if let Some((cached, image)) = self.noise.borrow().as_ref()
&& *cached == key
{
return image.clone();
self.noise.borrow_mut().image(scale)
}

/// Evicts the grain textures earlier steps of a slider drag left in the
/// window's atlas. The panel is built without a `&mut Window`, so they
/// are queued there and released from `render` on the next frame.
pub(crate) fn release_retired_images(&self, window: &mut Window) {
for image in self.noise.borrow_mut().take_retired() {
let _ = window.drop_image(image);
}
let image = noise_texture(
GRADIENT_PREVIEW_WIDTH as u32,
GRADIENT_PREVIEW_HEIGHT as u32,
noise_base_frequency(scale),
);
*self.noise.borrow_mut() = Some((key, image.clone()));
image
}

/// The stored track rect, without creating one -- the pointer maths reads
Expand Down Expand Up @@ -2137,7 +2164,9 @@ impl EditorWindow {
/// selected path.
fn ensure_preview(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let Some(path) = self.preview_path() else {
self.sidebar.preview = None;
if let Some((_, previous)) = self.sidebar.preview.take() {
let _ = window.drop_image(previous);
}
return;
};
if self
Expand All @@ -2157,8 +2186,10 @@ impl EditorWindow {
.await
};
let Some(image) = decoded else { return };
this.update(cx, |this, cx| {
this.sidebar.preview = Some((target, image));
this.update_in(cx, |this, window, cx| {
if let Some((_, previous)) = this.sidebar.preview.replace((target, image)) {
let _ = window.drop_image(previous);
}
cx.notify();
})
.ok();
Expand Down Expand Up @@ -4070,6 +4101,24 @@ impl EditorWindow {
mod tests {
use super::*;

#[test]
fn a_new_grain_step_retires_the_old_texture_for_atlas_eviction() {
let mut noise = NoiseCache::default();
let first = noise.image(20.);
assert!(Arc::ptr_eq(&noise.image(20.2), &first));
assert!(noise.take_retired().is_empty());

let second = noise.image(35.);
let third = noise.image(50.);
assert!(!Arc::ptr_eq(&second, &first));
let retired = noise.take_retired();
assert_eq!(retired.len(), 2);
assert!(Arc::ptr_eq(&retired[0], &first));
assert!(Arc::ptr_eq(&retired[1], &second));
assert!(noise.take_retired().is_empty());
assert!(Arc::ptr_eq(&noise.image(50.), &third));
}

#[test]
fn the_catalogue_matches_the_source() {
assert_eq!(WALLPAPER_NAMES.len(), 53);
Expand Down
1 change: 1 addition & 0 deletions apps/desktop-gpui/src/editor_window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11241,6 +11241,7 @@ impl Render for EditorWindow {
});
self.prepare_frame_fields(window, cx);
self.sync_crop_container(window);
self.sidebar.release_retired_images(window);
let theme = self.theme;
// The timeline's own bounds are what `secsPerPixel` divides by, and
// this window is resizable, so read them off the viewport rather than
Expand Down
33 changes: 26 additions & 7 deletions apps/desktop-gpui/src/screenshot_crop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,18 @@ pub struct ScreenshotCropDialog {
_field_subscriptions: Vec<gpui::Subscription>,
}

impl ScreenshotCropDialog {
/// The original is decoded at full resolution (about 59 MB at 5K) and the
/// window's atlas keeps every painted image until it is told otherwise, so
/// each closed dialog would otherwise stay resident until the editor
/// window closes.
fn release_image(&mut self, window: &mut Window) {
if let Some(image) = self.image.take() {
let _ = window.drop_image(image);
}
}
}

impl ScreenshotEditorWindow {
/// The header's crop button (`Header.tsx:84-93`): open the dialog seeded
/// with the crop in force, or the whole image. Gated on `image_size` the
Expand Down Expand Up @@ -163,21 +175,28 @@ impl ScreenshotEditorWindow {

/// Close without writing -- Escape or the backdrop. The project was never
/// touched, so there is nothing to roll back.
pub(crate) fn cancel_crop_dialog(&mut self, reason: &'static str, cx: &mut Context<Self>) {
if self.crop.take().is_none() {
pub(crate) fn cancel_crop_dialog(
&mut self,
reason: &'static str,
window: &mut Window,
cx: &mut Context<Self>,
) {
let Some(mut dialog) = self.crop.take() else {
return;
}
};
dialog.release_image(window);
tracing::info!(reason, "screenshot crop cancelled");
cx.notify();
}

/// The footer's Save (`Editor.tsx:463-482`): one `setProject` write of
/// `background.crop`, so one history entry, then close.
fn save_crop_dialog(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let Some(dialog) = self.crop.take() else {
let Some(mut dialog) = self.crop.take() else {
return;
};
let bounds = dialog.state.real();
dialog.release_image(window);
let crop = Crop {
position: XY::new(bounds.x.max(0.) as u32, bounds.y.max(0.) as u32),
size: XY::new(bounds.width.max(0.) as u32, bounds.height.max(0.) as u32),
Expand Down Expand Up @@ -470,7 +489,7 @@ impl ScreenshotEditorWindow {
return true;
}
if key == "escape" {
self.cancel_crop_dialog("escape", cx);
self.cancel_crop_dialog("escape", window, cx);
return true;
}
let Some(nudge) = engine::is_nudge_key(key) else {
Expand Down Expand Up @@ -668,8 +687,8 @@ impl ScreenshotEditorWindow {
.absolute()
.inset_0()
.bg(gpui::hsla(0., 0., 0., 0.8))
.on_click(cx.listener(|this, _, _window, cx| {
this.cancel_crop_dialog("backdrop", cx)
.on_click(cx.listener(|this, _, window, cx| {
this.cancel_crop_dialog("backdrop", window, cx)
})),
)
.child(
Expand Down
25 changes: 15 additions & 10 deletions apps/desktop-gpui/src/screenshot_editor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1107,7 +1107,7 @@ impl ScreenshotEditorWindow {
if self.bg_tab == BgTab::Wallpaper {
self.ensure_wallpapers(cx);
}
self.ensure_background_image(cx);
self.ensure_background_image(window, cx);
if let Ok(dest) = std::env::var("CAP_GPUI_AUTO_SCREENSHOT_EXPORT")
&& !dest.trim().is_empty()
{
Expand Down Expand Up @@ -1271,7 +1271,7 @@ impl ScreenshotEditorWindow {
{
self.selected_annotation = None;
}
self.ensure_background_image(cx);
self.ensure_background_image(window, cx);
self.publish();
self.schedule_save(window, cx);
self.refresh_mask_overlays(window, cx);
Expand Down Expand Up @@ -1505,9 +1505,11 @@ impl ScreenshotEditorWindow {

/// The Image tab's `h-48` preview. Decoded off-thread and cached by path,
/// so switching tabs does not re-read the file.
fn ensure_background_image(&mut self, cx: &mut Context<Self>) {
fn ensure_background_image(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let BackgroundSource::Image { path: Some(path) } = &self.project.background.source else {
self.bg_image = None;
if let Some((_, previous)) = self.bg_image.take() {
let _ = window.drop_image(previous);
}
return;
};
if self
Expand All @@ -1518,16 +1520,19 @@ impl ScreenshotEditorWindow {
return;
}
let path = path.clone();
self.bg_image_task = Some(cx.spawn(async move |this, cx| {
self.bg_image_task = Some(cx.spawn_in(window, async move |this, cx| {
let decoded = cx
.background_executor()
.spawn({
let path = path.clone();
async move { decode_preview_image(Path::new(&path)) }
})
.await;
this.update(cx, |this, cx| {
this.bg_image = decoded.map(|image| (path, image));
this.update_in(cx, |this, window, cx| {
let next = decoded.map(|image| (path, image));
if let Some((_, previous)) = std::mem::replace(&mut this.bg_image, next) {
let _ = window.drop_image(previous);
}
cx.notify();
})
.ok();
Expand All @@ -1550,7 +1555,7 @@ impl ScreenshotEditorWindow {
window,
cx,
);
this.ensure_background_image(cx);
this.ensure_background_image(window, cx);
})
.ok();
})
Expand Down Expand Up @@ -3284,7 +3289,7 @@ impl ScreenshotEditorWindow {
window,
cx,
);
self.ensure_background_image(cx);
self.ensure_background_image(window, cx);
}

fn render_wallpaper_tab(&self, cx: &mut Context<Self>) -> AnyElement {
Expand Down Expand Up @@ -3484,7 +3489,7 @@ impl ScreenshotEditorWindow {
window,
cx,
);
this.ensure_background_image(cx);
this.ensure_background_image(window, cx);
})),
)
.into_any_element()
Expand Down
Loading