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:**
<img width="632" height="75" alt="image"
src="https://github.com/user-attachments/assets/b407f1e5-8337-4f8f-b308-2cc16f6bf7ba"
/>

**After:**
<img width="608" height="75" alt="image"
src="https://github.com/user-attachments/assets/965787cf-1a42-413a-9a5e-ad0910c5cbee"
/>
This commit is contained in:
Cyandev 2025-07-30 09:55:24 +08:00 committed by GitHub
parent 518004a014
commit b8db00dd3c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -10,7 +10,7 @@ use gpui::{
fill, point, px, relative, size, App, Axis, BorderStyle, Bounds, ContentMask, Corner, fill, point, px, relative, size, App, Axis, BorderStyle, Bounds, ContentMask, Corner,
CursorStyle, Edges, Element, GlobalElementId, Hitbox, HitboxBehavior, Hsla, InspectorElementId, CursorStyle, Edges, Element, GlobalElementId, Hitbox, HitboxBehavior, Hsla, InspectorElementId,
IntoElement, LayoutId, MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad, Pixels, Point, 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 schemars::JsonSchema;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -105,6 +105,7 @@ pub struct ScrollbarStateInner {
last_scroll_time: Option<Instant>, last_scroll_time: Option<Instant>,
// Last update offset // Last update offset
last_update: Instant, last_update: Instant,
idle_timer_scheduled: bool,
} }
impl Default for ScrollbarState { impl Default for ScrollbarState {
@ -117,6 +118,7 @@ impl Default for ScrollbarState {
last_scroll_offset: point(px(0.), px(0.)), last_scroll_offset: point(px(0.), px(0.)),
last_scroll_time: None, last_scroll_time: None,
last_update: Instant::now(), last_update: Instant::now(),
idle_timer_scheduled: false,
}))) })))
} }
} }
@ -191,6 +193,12 @@ impl ScrollbarStateInner {
state 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 { fn is_scrollbar_visible(&self) -> bool {
// On drag // On drag
if self.dragged_axis.is_some() { if self.dragged_axis.is_some() {
@ -557,25 +565,34 @@ impl Element for Scrollbar {
// Delay 2s to fade out the scrollbar thumb (in 1s) // Delay 2s to fade out the scrollbar thumb (in 1s)
if let Some(last_time) = state.get().last_scroll_time { if let Some(last_time) = state.get().last_scroll_time {
let elapsed = Instant::now().duration_since(last_time).as_secs_f32(); let elapsed = Instant::now().duration_since(last_time).as_secs_f32();
if elapsed < FADE_OUT_DURATION { if is_hovered_on_bar {
if is_hovered_on_bar { state.set(state.get().with_last_scroll_time(Some(Instant::now())));
state.set(state.get().with_last_scroll_time(Some(Instant::now()))); idle_state = if is_hovered_on_thumb {
idle_state = if is_hovered_on_thumb { Self::style_for_hovered_thumb(cx)
Self::style_for_hovered_thumb(cx)
} else {
Self::style_for_hovered_bar(cx)
};
} else { } else {
if elapsed < FADE_OUT_DELAY { Self::style_for_hovered_bar(cx)
idle_state.0 = cx.theme().scrollbar_thumb; };
} else { } else if elapsed < FADE_OUT_DELAY {
// opacity = 1 - (x - 2)^10 idle_state.0 = cx.theme().scrollbar_thumb;
let opacity = 1.0 - (elapsed - FADE_OUT_DELAY).powi(10);
idle_state.0 = cx.theme().scrollbar_thumb.opacity(opacity);
};
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();
} }
} }