scrollbar: Improve scrollbar behaviour. (#481)
- Only show scroll when recent scrolled. - Keep showing scrollbar when is visible and hovered on. - Delay 3s to fade out. - If not visible, do not handle mouse down event. - Add `cx.theme().scrollbar_show` option to control scrollbar show `[Hover, Scrolling]`, default: `Scrolling`. Close #472 
This commit is contained in:
parent
26024ba2b5
commit
2836540260
4 changed files with 198 additions and 83 deletions
|
|
@ -14,6 +14,7 @@ use ui::{
|
|||
dock::{DockArea, DockAreaState, DockEvent, DockItem, DockPlacement},
|
||||
h_flex,
|
||||
popup_menu::PopupMenuExt,
|
||||
scroll::ScrollbarShow,
|
||||
theme::{ActiveTheme, Theme},
|
||||
ContextModal, IconName, Root, Sizable, TitleBar,
|
||||
};
|
||||
|
|
@ -25,6 +26,9 @@ const MAIN_DOCK_AREA: DockAreaTab = DockAreaTab {
|
|||
version: 5,
|
||||
};
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, Deserialize)]
|
||||
struct SelectScrollbarShow(ScrollbarShow);
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, Deserialize)]
|
||||
struct SelectLocale(SharedString);
|
||||
|
||||
|
|
@ -34,7 +38,10 @@ struct SelectFont(usize);
|
|||
#[derive(Clone, PartialEq, Eq, Deserialize)]
|
||||
struct AddPanel(DockPlacement);
|
||||
|
||||
impl_actions!(story, [SelectLocale, SelectFont, AddPanel]);
|
||||
impl_actions!(
|
||||
story,
|
||||
[SelectLocale, SelectFont, AddPanel, SelectScrollbarShow]
|
||||
);
|
||||
|
||||
actions!(workspace, [Open, CloseWindow]);
|
||||
|
||||
|
|
@ -573,30 +580,52 @@ impl FontSizeSelector {
|
|||
}
|
||||
}
|
||||
|
||||
fn on_select(&mut self, font_size: &SelectFont, cx: &mut ViewContext<Self>) {
|
||||
fn on_select_font(&mut self, font_size: &SelectFont, cx: &mut ViewContext<Self>) {
|
||||
Theme::global_mut(cx).font_size = font_size.0 as f32;
|
||||
cx.refresh();
|
||||
}
|
||||
|
||||
fn on_select_scrollbar_show(&mut self, show: &SelectScrollbarShow, cx: &mut ViewContext<Self>) {
|
||||
Theme::global_mut(cx).scrollbar_show = show.0;
|
||||
cx.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for FontSizeSelector {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let focus_handle = self.focus_handle.clone();
|
||||
let font_size = cx.theme().font_size as i32;
|
||||
let scroll_show = cx.theme().scrollbar_show;
|
||||
|
||||
div()
|
||||
.id("font-size-selector")
|
||||
.track_focus(&focus_handle)
|
||||
.on_action(cx.listener(Self::on_select))
|
||||
.on_action(cx.listener(Self::on_select_font))
|
||||
.on_action(cx.listener(Self::on_select_scrollbar_show))
|
||||
.child(
|
||||
Button::new("btn")
|
||||
.small()
|
||||
.ghost()
|
||||
.icon(IconName::ALargeSmall)
|
||||
.icon(IconName::Settings2)
|
||||
.popup_menu(move |this, _| {
|
||||
this.menu_with_check("Large", font_size == 18, Box::new(SelectFont(18)))
|
||||
.menu_with_check("Default", font_size == 16, Box::new(SelectFont(16)))
|
||||
.menu_with_check("Small", font_size == 14, Box::new(SelectFont(14)))
|
||||
this.menu_with_check(
|
||||
"Font Large",
|
||||
font_size == 18,
|
||||
Box::new(SelectFont(18)),
|
||||
)
|
||||
.menu_with_check("Font Default", font_size == 16, Box::new(SelectFont(16)))
|
||||
.menu_with_check("Font Small", font_size == 14, Box::new(SelectFont(14)))
|
||||
.separator()
|
||||
.menu_with_check(
|
||||
"Scrolling to show Scrollbar",
|
||||
scroll_show == ScrollbarShow::Scrolling,
|
||||
Box::new(SelectScrollbarShow(ScrollbarShow::Scrolling)),
|
||||
)
|
||||
.menu_with_check(
|
||||
"Hover to show Scrollbar",
|
||||
scroll_show == ScrollbarShow::Hover,
|
||||
Box::new(SelectScrollbarShow(ScrollbarShow::Hover)),
|
||||
)
|
||||
})
|
||||
.anchor(AnchorCorner::TopRight),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1249,7 +1249,7 @@ impl Render for TextInput {
|
|||
.absolute()
|
||||
.top_0()
|
||||
.left_0()
|
||||
.right_0()
|
||||
.right(px(1.))
|
||||
.bottom_0()
|
||||
.child(
|
||||
Scrollbar::vertical(
|
||||
|
|
|
|||
|
|
@ -2,14 +2,32 @@ use std::{cell::Cell, rc::Rc, time::Instant};
|
|||
|
||||
use crate::theme::ActiveTheme;
|
||||
use gpui::{
|
||||
fill, point, px, relative, Bounds, ContentMask, CursorStyle, Edges, Element, EntityId, Hitbox,
|
||||
Hsla, IntoElement, MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad, Pixels, Point,
|
||||
Position, ScrollHandle, ScrollWheelEvent, Style, UniformListScrollHandle,
|
||||
fill, point, px, relative, AppContext, Bounds, ContentMask, CursorStyle, Edges, Element,
|
||||
EntityId, Hitbox, Hsla, IntoElement, MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad,
|
||||
Pixels, Point, Position, ScrollHandle, ScrollWheelEvent, Style, UniformListScrollHandle,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Scrollbar show mode.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Hash, Default)]
|
||||
pub enum ScrollbarShow {
|
||||
#[default]
|
||||
Scrolling,
|
||||
Hover,
|
||||
}
|
||||
|
||||
impl ScrollbarShow {
|
||||
fn is_hover(&self) -> bool {
|
||||
matches!(self, Self::Hover)
|
||||
}
|
||||
}
|
||||
|
||||
const MIN_THUMB_SIZE: f32 = 80.;
|
||||
const THUMB_RADIUS: Pixels = Pixels(3.0);
|
||||
const THUMB_INSET: Pixels = Pixels(4.);
|
||||
const FADE_OUT_DURATION: f32 = 3.0;
|
||||
const FADE_OUT_DELAY: f32 = 2.0;
|
||||
const NORMAL_OPACITY: f32 = 0.6;
|
||||
|
||||
pub trait ScrollHandleOffsetable {
|
||||
fn offset(&self) -> Point<Pixels>;
|
||||
|
|
@ -92,6 +110,9 @@ impl ScrollbarState {
|
|||
fn with_hovered(&self, axis: Option<ScrollbarAxis>) -> Self {
|
||||
let mut state = *self;
|
||||
state.hovered_axis = axis;
|
||||
if self.is_scrollbar_visible() {
|
||||
state.last_scroll_time = Some(Instant::now());
|
||||
}
|
||||
state
|
||||
}
|
||||
|
||||
|
|
@ -111,6 +132,21 @@ impl ScrollbarState {
|
|||
state.last_scroll_time = last_scroll_time;
|
||||
state
|
||||
}
|
||||
|
||||
fn with_last_scroll_time(&self, t: Option<Instant>) -> Self {
|
||||
let mut state = *self;
|
||||
state.last_scroll_time = t;
|
||||
state
|
||||
}
|
||||
|
||||
fn is_scrollbar_visible(&self) -> bool {
|
||||
if let Some(last_time) = self.last_scroll_time {
|
||||
let elapsed = Instant::now().duration_since(last_time).as_secs_f32();
|
||||
elapsed < FADE_OUT_DURATION
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
|
|
@ -257,6 +293,52 @@ impl Scrollbar {
|
|||
self.axis = axis;
|
||||
self
|
||||
}
|
||||
|
||||
fn style_for_default(cx: &AppContext) -> (Hsla, Hsla, Hsla, Pixels, Pixels) {
|
||||
(
|
||||
cx.theme().scrollbar_thumb,
|
||||
cx.theme().scrollbar,
|
||||
cx.theme().border,
|
||||
THUMB_INSET - px(1.),
|
||||
THUMB_RADIUS,
|
||||
)
|
||||
}
|
||||
|
||||
fn style_for_hovered_thumb(cx: &AppContext) -> (Hsla, Hsla, Hsla, Pixels, Pixels) {
|
||||
(
|
||||
cx.theme().scrollbar_thumb,
|
||||
cx.theme().scrollbar,
|
||||
cx.theme().border,
|
||||
THUMB_INSET - px(1.),
|
||||
THUMB_RADIUS,
|
||||
)
|
||||
}
|
||||
|
||||
fn style_for_hovered_bar(cx: &AppContext) -> (Hsla, Hsla, Hsla, Pixels, Pixels) {
|
||||
let (inset, radius) = if cx.theme().scrollbar_show.is_hover() {
|
||||
(THUMB_INSET, THUMB_RADIUS - px(1.))
|
||||
} else {
|
||||
(THUMB_INSET - px(1.), THUMB_RADIUS)
|
||||
};
|
||||
|
||||
(
|
||||
cx.theme().scrollbar_thumb.opacity(NORMAL_OPACITY),
|
||||
gpui::transparent_black(),
|
||||
gpui::transparent_black(),
|
||||
inset,
|
||||
radius,
|
||||
)
|
||||
}
|
||||
|
||||
fn style_for_idle(_: &AppContext) -> (Hsla, Hsla, Hsla, Pixels, Pixels) {
|
||||
(
|
||||
gpui::transparent_black(),
|
||||
gpui::transparent_black(),
|
||||
gpui::transparent_black(),
|
||||
THUMB_INSET,
|
||||
THUMB_RADIUS - px(1.),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoElement for Scrollbar {
|
||||
|
|
@ -326,7 +408,6 @@ impl Element for Scrollbar {
|
|||
let mut states = vec![];
|
||||
|
||||
let mut has_both = self.axis.is_both();
|
||||
const NORMAL_OPACITY: f32 = 0.6;
|
||||
|
||||
for axis in self.axis.all().into_iter() {
|
||||
let is_vertical = axis.is_vertical();
|
||||
|
|
@ -390,49 +471,45 @@ impl Element for Scrollbar {
|
|||
};
|
||||
|
||||
let state = self.state.clone();
|
||||
let is_hover_to_show = cx.theme().scrollbar_show.is_hover();
|
||||
let is_hovered_on_bar = state.get().hovered_axis == Some(axis);
|
||||
let is_hovered_on_thumb = state.get().hovered_on_thumb == Some(axis);
|
||||
|
||||
let (thumb_bg, bar_bg, bar_border, inset, radius) =
|
||||
if state.get().dragged_axis == Some(axis) {
|
||||
(
|
||||
cx.theme().scrollbar_thumb,
|
||||
cx.theme().scrollbar,
|
||||
cx.theme().border,
|
||||
THUMB_INSET - px(1.),
|
||||
THUMB_RADIUS,
|
||||
)
|
||||
} else if state.get().hovered_axis == Some(axis) {
|
||||
if state.get().hovered_on_thumb == Some(axis) {
|
||||
(
|
||||
cx.theme().scrollbar_thumb,
|
||||
cx.theme().scrollbar,
|
||||
cx.theme().border,
|
||||
THUMB_INSET - px(1.),
|
||||
THUMB_RADIUS,
|
||||
)
|
||||
Self::style_for_default(cx)
|
||||
} else if is_hover_to_show && is_hovered_on_bar {
|
||||
if is_hovered_on_thumb {
|
||||
Self::style_for_hovered_thumb(cx)
|
||||
} else {
|
||||
(
|
||||
cx.theme().scrollbar_thumb.opacity(NORMAL_OPACITY),
|
||||
gpui::transparent_black(),
|
||||
gpui::transparent_black(),
|
||||
THUMB_INSET,
|
||||
THUMB_RADIUS,
|
||||
)
|
||||
Self::style_for_hovered_bar(cx)
|
||||
}
|
||||
} else {
|
||||
let mut idle_state = (
|
||||
gpui::transparent_black(),
|
||||
gpui::transparent_black(),
|
||||
gpui::transparent_black(),
|
||||
THUMB_INSET,
|
||||
THUMB_RADIUS - px(1.),
|
||||
);
|
||||
let mut idle_state = Self::style_for_idle(cx);
|
||||
// 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 < 1.0 {
|
||||
let y_value = NORMAL_OPACITY - elapsed.powi(10); // y = 1 - x^10
|
||||
idle_state.0 = cx.theme().scrollbar_thumb.opacity(y_value);
|
||||
cx.request_animation_frame();
|
||||
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)
|
||||
};
|
||||
} else {
|
||||
let y_value = if elapsed < FADE_OUT_DELAY {
|
||||
NORMAL_OPACITY
|
||||
} else {
|
||||
// y = 1 - (x - 2)^10
|
||||
NORMAL_OPACITY - (elapsed - FADE_OUT_DELAY).powi(10)
|
||||
};
|
||||
idle_state.0 = cx.theme().scrollbar_thumb.opacity(y_value);
|
||||
cx.request_animation_frame();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
idle_state
|
||||
};
|
||||
|
||||
|
|
@ -494,6 +571,8 @@ impl Element for Scrollbar {
|
|||
cx: &mut gpui::WindowContext,
|
||||
) {
|
||||
let hitbox_bounds = prepaint.hitbox.bounds;
|
||||
let is_visible = self.state.get().is_scrollbar_visible();
|
||||
let is_hover_to_show = cx.theme().scrollbar_show.is_hover();
|
||||
|
||||
for state in prepaint.states.iter() {
|
||||
let axis = state.axis;
|
||||
|
|
@ -557,52 +636,54 @@ impl Element for Scrollbar {
|
|||
|
||||
let safe_range = (-scroll_area_size + container_size)..px(0.);
|
||||
|
||||
cx.on_mouse_event({
|
||||
let state = self.state.clone();
|
||||
let view_id = self.view_id;
|
||||
let scroll_handle = self.scroll_handle.clone();
|
||||
if is_hover_to_show || is_visible {
|
||||
cx.on_mouse_event({
|
||||
let state = self.state.clone();
|
||||
let view_id = self.view_id;
|
||||
let scroll_handle = self.scroll_handle.clone();
|
||||
|
||||
move |event: &MouseDownEvent, phase, cx| {
|
||||
if phase.bubble() && bounds.contains(&event.position) {
|
||||
cx.stop_propagation();
|
||||
move |event: &MouseDownEvent, phase, cx| {
|
||||
if phase.bubble() && bounds.contains(&event.position) {
|
||||
cx.stop_propagation();
|
||||
|
||||
if thumb_bounds.contains(&event.position) {
|
||||
// click on the thumb bar, set the drag position
|
||||
let pos = event.position - thumb_bounds.origin;
|
||||
if thumb_bounds.contains(&event.position) {
|
||||
// click on the thumb bar, set the drag position
|
||||
let pos = event.position - thumb_bounds.origin;
|
||||
|
||||
state.set(state.get().with_drag_pos(axis, pos));
|
||||
state.set(state.get().with_drag_pos(axis, pos));
|
||||
|
||||
cx.notify(Some(view_id));
|
||||
} else {
|
||||
// click on the scrollbar, jump to the position
|
||||
// Set the thumb bar center to the click position
|
||||
let offset = scroll_handle.offset();
|
||||
let percentage = if is_vertical {
|
||||
(event.position.y - thumb_size / 2. - bounds.origin.y)
|
||||
/ (bounds.size.height - thumb_size)
|
||||
cx.notify(Some(view_id));
|
||||
} else {
|
||||
(event.position.x - thumb_size / 2. - bounds.origin.x)
|
||||
/ (bounds.size.width - thumb_size)
|
||||
}
|
||||
.min(1.);
|
||||
// click on the scrollbar, jump to the position
|
||||
// Set the thumb bar center to the click position
|
||||
let offset = scroll_handle.offset();
|
||||
let percentage = if is_vertical {
|
||||
(event.position.y - thumb_size / 2. - bounds.origin.y)
|
||||
/ (bounds.size.height - thumb_size)
|
||||
} else {
|
||||
(event.position.x - thumb_size / 2. - bounds.origin.x)
|
||||
/ (bounds.size.width - thumb_size)
|
||||
}
|
||||
.min(1.);
|
||||
|
||||
if is_vertical {
|
||||
scroll_handle.set_offset(point(
|
||||
offset.x,
|
||||
(-scroll_area_size * percentage)
|
||||
.clamp(safe_range.start, safe_range.end),
|
||||
));
|
||||
} else {
|
||||
scroll_handle.set_offset(point(
|
||||
(-scroll_area_size * percentage)
|
||||
.clamp(safe_range.start, safe_range.end),
|
||||
offset.y,
|
||||
));
|
||||
if is_vertical {
|
||||
scroll_handle.set_offset(point(
|
||||
offset.x,
|
||||
(-scroll_area_size * percentage)
|
||||
.clamp(safe_range.start, safe_range.end),
|
||||
));
|
||||
} else {
|
||||
scroll_handle.set_offset(point(
|
||||
(-scroll_area_size * percentage)
|
||||
.clamp(safe_range.start, safe_range.end),
|
||||
offset.y,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
cx.on_mouse_event({
|
||||
let scroll_handle = self.scroll_handle.clone();
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ use gpui::{
|
|||
ViewContext, WindowAppearance, WindowContext,
|
||||
};
|
||||
|
||||
use crate::scroll::ScrollbarShow;
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
Theme::sync_system_appearance(cx)
|
||||
}
|
||||
|
|
@ -373,6 +375,8 @@ pub struct Theme {
|
|||
pub radius: f32,
|
||||
pub shadow: bool,
|
||||
pub transparent: Hsla,
|
||||
/// Show the scrollbar mode, default: Scrolling
|
||||
pub scrollbar_show: ScrollbarShow,
|
||||
}
|
||||
|
||||
impl Deref for Theme {
|
||||
|
|
@ -518,6 +522,7 @@ impl From<ThemeColor> for Theme {
|
|||
},
|
||||
radius: 4.0,
|
||||
shadow: true,
|
||||
scrollbar_show: ScrollbarShow::default(),
|
||||
colors,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue