From b8db00dd3ca982d4c834267842495b0055d26460 Mon Sep 17 00:00:00 2001 From: Cyandev Date: Wed, 30 Jul 2025 09:55:24 +0800 Subject: [PATCH] scrollbar: Reduce unnecessary frame updates (#1104) The current scrollbar implementation calls `Window::request_animation_frame` during the delay phase, which causes unnecessary CPU usage. This PR updates the scrollbar to use a timer to wait before starting the fade-out animation, allowing the CPU to remain idle during this phase. There is the comparison with Instruments. **Before:** image **After:** image --- crates/ui/src/scroll/scrollbar.rs | 51 ++++++++++++++++++++----------- 1 file changed, 34 insertions(+), 17 deletions(-) diff --git a/crates/ui/src/scroll/scrollbar.rs b/crates/ui/src/scroll/scrollbar.rs index 63c08705..68a63254 100644 --- a/crates/ui/src/scroll/scrollbar.rs +++ b/crates/ui/src/scroll/scrollbar.rs @@ -10,7 +10,7 @@ use gpui::{ fill, point, px, relative, size, App, Axis, BorderStyle, Bounds, ContentMask, Corner, CursorStyle, Edges, Element, GlobalElementId, Hitbox, HitboxBehavior, Hsla, InspectorElementId, IntoElement, LayoutId, MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad, Pixels, Point, - Position, ScrollHandle, ScrollWheelEvent, Size, Style, UniformListScrollHandle, Window, + Position, ScrollHandle, ScrollWheelEvent, Size, Style, Timer, UniformListScrollHandle, Window, }; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -105,6 +105,7 @@ pub struct ScrollbarStateInner { last_scroll_time: Option, // Last update offset last_update: Instant, + idle_timer_scheduled: bool, } impl Default for ScrollbarState { @@ -117,6 +118,7 @@ impl Default for ScrollbarState { last_scroll_offset: point(px(0.), px(0.)), last_scroll_time: None, last_update: Instant::now(), + idle_timer_scheduled: false, }))) } } @@ -191,6 +193,12 @@ impl ScrollbarStateInner { state } + fn with_idle_timer_scheduled(&self, scheduled: bool) -> Self { + let mut state = *self; + state.idle_timer_scheduled = scheduled; + state + } + fn is_scrollbar_visible(&self) -> bool { // On drag if self.dragged_axis.is_some() { @@ -557,25 +565,34 @@ impl Element for Scrollbar { // Delay 2s to fade out the scrollbar thumb (in 1s) if let Some(last_time) = state.get().last_scroll_time { let elapsed = Instant::now().duration_since(last_time).as_secs_f32(); - if elapsed < FADE_OUT_DURATION { - if is_hovered_on_bar { - state.set(state.get().with_last_scroll_time(Some(Instant::now()))); - idle_state = if is_hovered_on_thumb { - Self::style_for_hovered_thumb(cx) - } else { - Self::style_for_hovered_bar(cx) - }; + if is_hovered_on_bar { + state.set(state.get().with_last_scroll_time(Some(Instant::now()))); + idle_state = if is_hovered_on_thumb { + Self::style_for_hovered_thumb(cx) } else { - if elapsed < FADE_OUT_DELAY { - idle_state.0 = cx.theme().scrollbar_thumb; - } else { - // opacity = 1 - (x - 2)^10 - let opacity = 1.0 - (elapsed - FADE_OUT_DELAY).powi(10); - idle_state.0 = cx.theme().scrollbar_thumb.opacity(opacity); - }; + Self::style_for_hovered_bar(cx) + }; + } else if elapsed < FADE_OUT_DELAY { + idle_state.0 = cx.theme().scrollbar_thumb; - window.request_animation_frame(); + if !state.get().idle_timer_scheduled { + let state = state.clone(); + state.set(state.get().with_idle_timer_scheduled(true)); + let current_view = window.current_view(); + let next_delay = Duration::from_secs_f32(FADE_OUT_DELAY - elapsed); + window + .spawn(cx, async move |cx| { + Timer::after(next_delay).await; + state.set(state.get().with_idle_timer_scheduled(false)); + cx.update(|_, cx| cx.notify(current_view)).ok(); + }) + .detach(); } + } else if elapsed < FADE_OUT_DURATION { + let opacity = 1.0 - (elapsed - FADE_OUT_DELAY).powi(10); + idle_state.0 = cx.theme().scrollbar_thumb.opacity(opacity); + + window.request_animation_frame(); } }