diff --git a/src/backend/render/cursor.rs b/src/backend/render/cursor.rs index 7a0861e5..8e14d1be 100644 --- a/src/backend/render/cursor.rs +++ b/src/backend/render/cursor.rs @@ -460,9 +460,7 @@ pub struct CursorStateInner { cursors: HashMap, current_image: Option, - /// Rasterized cursor frames keyed by `(shape, pixel size, frame index)` so - /// each SVG frame is rasterized (and uploaded) at most once per size. - image_cache: Vec<((CursorIcon, u32, usize), Image, MemoryRenderBuffer)>, + image_cache: Vec, hidden: bool, idle_timer: Option, @@ -476,6 +474,15 @@ pub struct CursorStateInner { magnification: f32, anim_from: f32, anim_start: Option, + rest_started: Option, +} + +/// A rasterized cursor frame, keyed by `(shape, pixel size, frame index)`. +struct CachedFrame { + key: (CursorIcon, u32, usize), + image: Image, + buffer: MemoryRenderBuffer, + unmagnified: bool, } /// One sampled pointer position on the recent motion path. @@ -485,6 +492,9 @@ struct PathSample { time: Instant, } +/// How long everything must stay unmagnified before the enlarged frames go. +const MAGNIFIED_FRAME_GRACE: Duration = Duration::from_secs(10); + /// How far back the motion path is considered when looking for a shake. const SHAKE_INTERVAL: Duration = Duration::from_millis(1000); /// Path-length / bounding-box-diagonal ratio required to count as a shake. @@ -529,6 +539,21 @@ impl CursorStateInner { self.cursor_size } + /// Drop the rasterizations only a magnified cursor needed, once nothing has + /// magnified it for [`MAGNIFIED_FRAME_GRACE`]. + pub fn drop_magnified_frames(&mut self, now: Instant, zoomed: bool) { + if zoomed || self.is_magnifying() { + self.rest_started = None; + return; + } + let rest_started = *self.rest_started.get_or_insert(now); + if now.duration_since(rest_started) < MAGNIFIED_FRAME_GRACE { + return; + } + + self.image_cache.retain(|frame| frame.unmagnified); + } + /// Feed one relative-motion event into the shake detector. pub fn detect_shake(&mut self, delta: Point, now: Instant) { // Drop samples that have aged out of the time window. @@ -680,6 +705,7 @@ impl Default for CursorStateInner { magnification: 1.0, anim_from: 1.0, anim_start: None, + rest_started: None, } } } @@ -748,34 +774,36 @@ pub fn draw_cursor( let key = (current_cursor, size_px, frame_idx); // Rasterize and upload this (shape, size, frame) only if not cached. - if !state - .image_cache - .iter() - .any(|(cached, _, _)| *cached == key) - { - let frame = { - let cursor = state.get_named_cursor(current_cursor); - cursor.render_frame(size_px, frame_idx) - }; - let actual_scale = (frame.size / state.size()).max(1); - let buffer = MemoryRenderBuffer::from_slice( - &frame.pixels_rgba, - Fourcc::Argb8888, - (frame.width as i32, frame.height as i32), - actual_scale as i32, - Transform::Normal, - None, - ); - state.image_cache.push((key, frame, buffer)); - } + let index = match state.image_cache.iter().position(|frame| frame.key == key) { + Some(index) => index, + None => { + let image = { + let cursor = state.get_named_cursor(current_cursor); + cursor.render_frame(size_px, frame_idx) + }; + let actual_scale = (image.size / state.size()).max(1); + let buffer = MemoryRenderBuffer::from_slice( + &image.pixels_rgba, + Fourcc::Argb8888, + (image.width as i32, image.height as i32), + actual_scale as i32, + Transform::Normal, + None, + ); + state.image_cache.push(CachedFrame { + key, + image, + buffer, + unmagnified: size_px == unmagnified_px, + }); + state.image_cache.len() - 1 + } + }; let (frame, pointer_image) = { - let (_, frame, buffer) = state - .image_cache - .iter() - .find(|(cached, _, _)| *cached == key) - .unwrap(); - (frame.clone(), buffer.clone()) + let entry = &mut state.image_cache[index]; + entry.unmagnified |= size_px == unmagnified_px; + (entry.image.clone(), entry.buffer.clone()) }; let actual_scale = (frame.size / state.size()).max(1); diff --git a/src/shell/mod.rs b/src/shell/mod.rs index 0baa3713..009c98dc 100644 --- a/src/shell/mod.rs +++ b/src/shell/mod.rs @@ -1614,6 +1614,29 @@ impl Common { a11y_keyboard_monitor.refresh(); } self.image_copy_capture_state.cleanup(); + self.cleanup_cursor_images(); + } + + /// Release the enlarged cursor frames a finished shake or zoom left behind. + fn cleanup_cursor_images(&mut self) { + let shell = self.shell.read(); + let zoomed = shell.zoom_state.as_ref().is_some_and(|zoom_state| { + shell + .outputs() + .any(|output| zoom_state.animating_level(output) > 1.0) + }); + let now = Instant::now(); + for seat in shell.seats.iter() { + if let Some(cursor_state) = seat + .user_data() + .get::() + { + cursor_state + .lock() + .unwrap() + .drop_magnified_frames(now, zoomed); + } + } } pub fn refresh_idle_inhibit(&mut self) {