chore: reduce component overhead (#3)
Reviewed-on: #3
This commit was merged in pull request #3.
This commit is contained in:
+143
-28
@@ -1,18 +1,141 @@
|
||||
//! Overlay scrollbars for scroll surfaces.
|
||||
|
||||
use std::rc::Rc;
|
||||
use std::sync::Arc;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use gpui::{
|
||||
App, Bounds, DispatchPhase, ElementId, IntoElement, ListState, MouseButton, MouseDownEvent,
|
||||
MouseMoveEvent, MouseUpEvent, Pixels, Point, RenderOnce, ScrollEvent, ScrollHandle,
|
||||
Styled as _, UniformListScrollHandle, Window, canvas, fill, point,
|
||||
App, Bounds, DispatchPhase, ElementId, Entity, EntityId, IntoElement, ListState, MouseButton,
|
||||
MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, Point, RenderOnce, ScrollEvent,
|
||||
ScrollHandle, SharedString, Styled as _, Task, UniformListScrollHandle, Window, canvas, fill,
|
||||
point,
|
||||
};
|
||||
use gpui_native_theme::ActiveTheme as _;
|
||||
|
||||
use crate::scroll_area::{Autohide, scrollbar_state_key, thumb_geometry, thumb_style};
|
||||
/// When an overlay scrollbar shows.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub enum ScrollbarVisibility {
|
||||
/// The running platform's behavior, from the theme.
|
||||
#[default]
|
||||
Platform,
|
||||
Always,
|
||||
Autohide,
|
||||
}
|
||||
|
||||
/// Re-exported so callers only need the scrollbar module.
|
||||
pub use crate::scroll_area::ScrollbarVisibility;
|
||||
/// How long an autohide thumb stays fully visible before its fade begins.
|
||||
const AUTOHIDE_DELAY: Duration = Duration::from_millis(700);
|
||||
/// How long the autohide fade runs.
|
||||
const AUTOHIDE_FADE: Duration = Duration::from_millis(250);
|
||||
/// How often the fade repaints while it runs.
|
||||
const AUTOHIDE_FADE_STEP: Duration = Duration::from_millis(50);
|
||||
|
||||
/// The shared autohide timeline for an overlay scrollbar.
|
||||
#[derive(Clone)]
|
||||
struct Autohide {
|
||||
view: EntityId,
|
||||
state: Entity<AutohideState>,
|
||||
}
|
||||
|
||||
struct AutohideState {
|
||||
last_activity: Option<Instant>,
|
||||
hide_task: Option<Task<()>>,
|
||||
}
|
||||
|
||||
impl Autohide {
|
||||
fn new(key: ElementId, window: &mut Window, cx: &mut App) -> Self {
|
||||
let view = window.current_view();
|
||||
let state = window.use_keyed_state(key, cx, |_, _| AutohideState {
|
||||
last_activity: None,
|
||||
hide_task: None,
|
||||
});
|
||||
Self { view, state }
|
||||
}
|
||||
|
||||
/// Marks interaction.
|
||||
fn touch(&self, window: &mut Window, cx: &mut App) {
|
||||
let view = self.view;
|
||||
let reduce_motion = cx.theme().accessibility.reduce_motion;
|
||||
|
||||
let task = window.spawn(cx, async move |cx| {
|
||||
if reduce_motion {
|
||||
cx.background_executor()
|
||||
.timer(AUTOHIDE_DELAY + AUTOHIDE_FADE)
|
||||
.await;
|
||||
cx.update(|_, cx| cx.notify(view)).ok();
|
||||
return;
|
||||
}
|
||||
|
||||
let steps = (AUTOHIDE_FADE.as_millis() / AUTOHIDE_FADE_STEP.as_millis()).max(1);
|
||||
cx.background_executor().timer(AUTOHIDE_DELAY).await;
|
||||
|
||||
for _ in 0..steps {
|
||||
cx.update(|_, cx| cx.notify(view)).ok();
|
||||
cx.background_executor().timer(AUTOHIDE_FADE_STEP).await;
|
||||
}
|
||||
|
||||
cx.update(|_, cx| cx.notify(view)).ok();
|
||||
});
|
||||
|
||||
self.state.update(cx, |state, _| {
|
||||
state.last_activity = Some(Instant::now());
|
||||
state.hide_task = Some(task);
|
||||
});
|
||||
}
|
||||
|
||||
/// The thumb's opacity.
|
||||
fn alpha(&self, cx: &App, dragging: bool) -> f32 {
|
||||
if dragging {
|
||||
return 1.0;
|
||||
}
|
||||
|
||||
let Some(activity) = self.state.read(cx).last_activity else {
|
||||
return 0.0;
|
||||
};
|
||||
|
||||
let idle = activity.elapsed();
|
||||
if idle <= AUTOHIDE_DELAY {
|
||||
return 1.0;
|
||||
}
|
||||
|
||||
let fade = idle.saturating_sub(AUTOHIDE_DELAY);
|
||||
if fade >= AUTOHIDE_FADE {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
1.0 - fade.as_secs_f32() / AUTOHIDE_FADE.as_secs_f32()
|
||||
}
|
||||
}
|
||||
|
||||
fn scrollbar_state_key(name: &'static str, id: &ElementId) -> ElementId {
|
||||
ElementId::NamedChild(Arc::new(id.clone()), SharedString::from(name))
|
||||
}
|
||||
|
||||
/// Pure thumb geometry for an overlay scrollbar.
|
||||
fn thumb_geometry(
|
||||
viewport: Pixels,
|
||||
content: Pixels,
|
||||
offset: Pixels,
|
||||
track: Pixels,
|
||||
min_thumb: Pixels,
|
||||
) -> (Pixels, Pixels) {
|
||||
if content <= viewport || content <= Pixels::ZERO {
|
||||
return (track, Pixels::ZERO);
|
||||
}
|
||||
|
||||
let length = (viewport / content * track).max(min_thumb).min(track);
|
||||
let overflow = content - viewport;
|
||||
let progress = (-offset / overflow).clamp(0., 1.);
|
||||
let travel = track - length;
|
||||
(length, travel * progress)
|
||||
}
|
||||
|
||||
/// An overlay scrollbar thumb's style for one paint: `None` hides it.
|
||||
fn thumb_style(color: gpui::Hsla, alpha: f32) -> Option<gpui::Hsla> {
|
||||
(alpha > 0.0).then_some(gpui::Hsla {
|
||||
a: color.a * alpha,
|
||||
..color
|
||||
})
|
||||
}
|
||||
|
||||
/// The scroll state a scrollbar drives, implemented for gpui's scroll handles.
|
||||
pub trait ScrollbarHandle: 'static {
|
||||
@@ -128,33 +251,35 @@ pub struct Scrollbar {
|
||||
axis: Axis,
|
||||
handle: Rc<dyn ScrollbarHandle>,
|
||||
visibility: ScrollbarVisibility,
|
||||
key: ElementId,
|
||||
id: ElementId,
|
||||
}
|
||||
|
||||
impl Scrollbar {
|
||||
/// A vertical scrollbar driven by `handle`.
|
||||
pub fn vertical<H>(handle: H) -> Self
|
||||
/// A vertical scrollbar named by `id`, driven by `handle`.
|
||||
pub fn vertical<I, H>(id: I, handle: H) -> Self
|
||||
where
|
||||
I: Into<ElementId>,
|
||||
H: ScrollbarHandle,
|
||||
{
|
||||
Self {
|
||||
axis: Axis::Vertical,
|
||||
handle: Rc::new(handle),
|
||||
visibility: ScrollbarVisibility::default(),
|
||||
key: ElementId::Name("scrollbar".into()),
|
||||
id: id.into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// A horizontal scrollbar driven by `handle`.
|
||||
pub fn horizontal<H>(handle: H) -> Self
|
||||
/// A horizontal scrollbar named by `id`, driven by `handle`.
|
||||
pub fn horizontal<I, H>(id: I, handle: H) -> Self
|
||||
where
|
||||
I: Into<ElementId>,
|
||||
H: ScrollbarHandle,
|
||||
{
|
||||
Self {
|
||||
axis: Axis::Horizontal,
|
||||
handle: Rc::new(handle),
|
||||
visibility: ScrollbarVisibility::default(),
|
||||
key: ElementId::Name("scrollbar".into()),
|
||||
id: id.into(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,12 +288,6 @@ impl Scrollbar {
|
||||
self.visibility = visibility;
|
||||
self
|
||||
}
|
||||
|
||||
/// Names this scrollbar's state.
|
||||
pub fn key(mut self, key: impl Into<ElementId>) -> Self {
|
||||
self.key = key.into();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for Scrollbar {
|
||||
@@ -177,8 +296,8 @@ impl RenderOnce for Scrollbar {
|
||||
let handle = self.handle;
|
||||
let axis = self.axis;
|
||||
let visibility = self.visibility;
|
||||
let key = self.key;
|
||||
move |_, _, window, cx| paint_scrollbar(&handle, axis, visibility, &key, window, cx)
|
||||
let id = self.id;
|
||||
move |_, _, window, cx| paint_scrollbar(&handle, axis, visibility, &id, window, cx)
|
||||
})
|
||||
.absolute()
|
||||
.inset_0()
|
||||
@@ -192,7 +311,7 @@ fn paint_scrollbar(
|
||||
handle: &Rc<dyn ScrollbarHandle>,
|
||||
axis: Axis,
|
||||
visibility: ScrollbarVisibility,
|
||||
key: &ElementId,
|
||||
id: &ElementId,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) {
|
||||
@@ -217,12 +336,10 @@ fn paint_scrollbar(
|
||||
ScrollbarVisibility::Always => false,
|
||||
ScrollbarVisibility::Autohide => true,
|
||||
};
|
||||
|
||||
let autohide_state =
|
||||
autohide.then(|| Autohide::new(scrollbar_state_key("autohide", key), window, cx));
|
||||
autohide.then(|| Autohide::new(scrollbar_state_key("autohide", id), window, cx));
|
||||
|
||||
let track_length = viewport_along - inset * 2.0;
|
||||
|
||||
let (thumb_length, thumb_travel) = thumb_geometry(
|
||||
viewport_along,
|
||||
viewport_along + max_offset,
|
||||
@@ -267,9 +384,7 @@ fn paint_scrollbar(
|
||||
}
|
||||
};
|
||||
|
||||
let drag = window.use_keyed_state(scrollbar_state_key("thumb-drag", key), cx, |_, _| {
|
||||
Drag::None
|
||||
});
|
||||
let drag = window.use_keyed_state(scrollbar_state_key("thumb-drag", id), cx, |_, _| Drag::None);
|
||||
let dragging = drag.read(cx).is_some();
|
||||
|
||||
let alpha = match &autohide_state {
|
||||
|
||||
Reference in New Issue
Block a user