Thread view scrollbar (#35655)

This also adds a convenient `Scrollbar:auto_hide` function so that we
don't have to handle that at the callsite.

Release Notes:

- N/A

---------

Co-authored-by: David Kleingeld <davidsk@zed.dev>
This commit is contained in:
Agus Zubiaga 2025-08-06 11:01:34 -03:00 committed by GitHub
parent 3c602fecbf
commit 33f198fef1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 238 additions and 233 deletions

View file

@ -21,10 +21,10 @@ use editor::{
use file_icons::FileIcons; use file_icons::FileIcons;
use gpui::{ use gpui::{
Action, Animation, AnimationExt, App, BorderStyle, EdgesRefinement, Empty, Entity, EntityId, Action, Animation, AnimationExt, App, BorderStyle, EdgesRefinement, Empty, Entity, EntityId,
FocusHandle, Focusable, Hsla, Length, ListOffset, ListState, PlatformDisplay, SharedString, FocusHandle, Focusable, Hsla, Length, ListOffset, ListState, MouseButton, PlatformDisplay,
StyleRefinement, Subscription, Task, TextStyle, TextStyleRefinement, Transformation, SharedString, Stateful, StyleRefinement, Subscription, Task, TextStyle, TextStyleRefinement,
UnderlineStyle, WeakEntity, Window, WindowHandle, div, linear_color_stop, linear_gradient, Transformation, UnderlineStyle, WeakEntity, Window, WindowHandle, div, linear_color_stop,
list, percentage, point, prelude::*, pulsating_between, linear_gradient, list, percentage, point, prelude::*, pulsating_between,
}; };
use language::language_settings::SoftWrap; use language::language_settings::SoftWrap;
use language::{Buffer, Language}; use language::{Buffer, Language};
@ -34,7 +34,9 @@ use project::Project;
use settings::Settings as _; use settings::Settings as _;
use text::{Anchor, BufferSnapshot}; use text::{Anchor, BufferSnapshot};
use theme::ThemeSettings; use theme::ThemeSettings;
use ui::{Disclosure, Divider, DividerColor, KeyBinding, Tooltip, prelude::*}; use ui::{
Disclosure, Divider, DividerColor, KeyBinding, Scrollbar, ScrollbarState, Tooltip, prelude::*,
};
use util::ResultExt; use util::ResultExt;
use workspace::{CollaboratorId, Workspace}; use workspace::{CollaboratorId, Workspace};
use zed_actions::agent::{Chat, NextHistoryMessage, PreviousHistoryMessage}; use zed_actions::agent::{Chat, NextHistoryMessage, PreviousHistoryMessage};
@ -69,6 +71,7 @@ pub struct AcpThreadView {
notification_subscriptions: HashMap<WindowHandle<AgentNotification>, Vec<Subscription>>, notification_subscriptions: HashMap<WindowHandle<AgentNotification>, Vec<Subscription>>,
last_error: Option<Entity<Markdown>>, last_error: Option<Entity<Markdown>>,
list_state: ListState, list_state: ListState,
scrollbar_state: ScrollbarState,
auth_task: Option<Task<()>>, auth_task: Option<Task<()>>,
expanded_tool_calls: HashSet<acp::ToolCallId>, expanded_tool_calls: HashSet<acp::ToolCallId>,
expanded_thinking_blocks: HashSet<(usize, usize)>, expanded_thinking_blocks: HashSet<(usize, usize)>,
@ -187,7 +190,8 @@ impl AcpThreadView {
notifications: Vec::new(), notifications: Vec::new(),
notification_subscriptions: HashMap::default(), notification_subscriptions: HashMap::default(),
diff_editors: Default::default(), diff_editors: Default::default(),
list_state: list_state, list_state: list_state.clone(),
scrollbar_state: ScrollbarState::new(list_state).parent_entity(&cx.entity()),
last_error: None, last_error: None,
auth_task: None, auth_task: None,
expanded_tool_calls: HashSet::default(), expanded_tool_calls: HashSet::default(),
@ -2479,6 +2483,39 @@ impl AcpThreadView {
.child(open_as_markdown) .child(open_as_markdown)
.child(scroll_to_top) .child(scroll_to_top)
} }
fn render_vertical_scrollbar(&self, cx: &mut Context<Self>) -> Stateful<Div> {
div()
.id("acp-thread-scrollbar")
.occlude()
.on_mouse_move(cx.listener(|_, _, _, cx| {
cx.notify();
cx.stop_propagation()
}))
.on_hover(|_, _, cx| {
cx.stop_propagation();
})
.on_any_mouse_down(|_, _, cx| {
cx.stop_propagation();
})
.on_mouse_up(
MouseButton::Left,
cx.listener(|_, _, _, cx| {
cx.stop_propagation();
}),
)
.on_scroll_wheel(cx.listener(|_, _, _, cx| {
cx.notify();
}))
.h_full()
.absolute()
.right_1()
.top_1()
.bottom_0()
.w(px(12.))
.cursor_default()
.children(Scrollbar::vertical(self.scrollbar_state.clone()).map(|s| s.auto_hide(cx)))
}
} }
impl Focusable for AcpThreadView { impl Focusable for AcpThreadView {
@ -2553,6 +2590,7 @@ impl Render for AcpThreadView {
.flex_grow() .flex_grow()
.into_any(), .into_any(),
) )
.child(self.render_vertical_scrollbar(cx))
.children(match thread_clone.read(cx).status() { .children(match thread_clone.read(cx).status() {
ThreadStatus::Idle | ThreadStatus::WaitingForToolConfirmation => { ThreadStatus::Idle | ThreadStatus::WaitingForToolConfirmation => {
None None

View file

@ -69,8 +69,6 @@ pub struct ActiveThread {
messages: Vec<MessageId>, messages: Vec<MessageId>,
list_state: ListState, list_state: ListState,
scrollbar_state: ScrollbarState, scrollbar_state: ScrollbarState,
show_scrollbar: bool,
hide_scrollbar_task: Option<Task<()>>,
rendered_messages_by_id: HashMap<MessageId, RenderedMessage>, rendered_messages_by_id: HashMap<MessageId, RenderedMessage>,
rendered_tool_uses: HashMap<LanguageModelToolUseId, RenderedToolUse>, rendered_tool_uses: HashMap<LanguageModelToolUseId, RenderedToolUse>,
editing_message: Option<(MessageId, EditingMessageState)>, editing_message: Option<(MessageId, EditingMessageState)>,
@ -805,9 +803,7 @@ impl ActiveThread {
expanded_thinking_segments: HashMap::default(), expanded_thinking_segments: HashMap::default(),
expanded_code_blocks: HashMap::default(), expanded_code_blocks: HashMap::default(),
list_state: list_state.clone(), list_state: list_state.clone(),
scrollbar_state: ScrollbarState::new(list_state), scrollbar_state: ScrollbarState::new(list_state).parent_entity(&cx.entity()),
show_scrollbar: false,
hide_scrollbar_task: None,
editing_message: None, editing_message: None,
last_error: None, last_error: None,
copied_code_block_ids: HashSet::default(), copied_code_block_ids: HashSet::default(),
@ -3502,60 +3498,37 @@ impl ActiveThread {
} }
} }
fn render_vertical_scrollbar(&self, cx: &mut Context<Self>) -> Option<Stateful<Div>> { fn render_vertical_scrollbar(&self, cx: &mut Context<Self>) -> Stateful<Div> {
if !self.show_scrollbar && !self.scrollbar_state.is_dragging() { div()
return None; .occlude()
} .id("active-thread-scrollbar")
.on_mouse_move(cx.listener(|_, _, _, cx| {
Some( cx.notify();
div() cx.stop_propagation()
.occlude() }))
.id("active-thread-scrollbar") .on_hover(|_, _, cx| {
.on_mouse_move(cx.listener(|_, _, _, cx| { cx.stop_propagation();
cx.notify(); })
cx.stop_propagation() .on_any_mouse_down(|_, _, cx| {
})) cx.stop_propagation();
.on_hover(|_, _, cx| { })
.on_mouse_up(
MouseButton::Left,
cx.listener(|_, _, _, cx| {
cx.stop_propagation(); cx.stop_propagation();
}) }),
.on_any_mouse_down(|_, _, cx| { )
cx.stop_propagation(); .on_scroll_wheel(cx.listener(|_, _, _, cx| {
}) cx.notify();
.on_mouse_up( }))
MouseButton::Left, .h_full()
cx.listener(|_, _, _, cx| { .absolute()
cx.stop_propagation(); .right_1()
}), .top_1()
) .bottom_0()
.on_scroll_wheel(cx.listener(|_, _, _, cx| { .w(px(12.))
cx.notify(); .cursor_default()
})) .children(Scrollbar::vertical(self.scrollbar_state.clone()).map(|s| s.auto_hide(cx)))
.h_full()
.absolute()
.right_1()
.top_1()
.bottom_0()
.w(px(12.))
.cursor_default()
.children(Scrollbar::vertical(self.scrollbar_state.clone())),
)
}
fn hide_scrollbar_later(&mut self, cx: &mut Context<Self>) {
const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1);
self.hide_scrollbar_task = Some(cx.spawn(async move |thread, cx| {
cx.background_executor()
.timer(SCROLLBAR_SHOW_INTERVAL)
.await;
thread
.update(cx, |thread, cx| {
if !thread.scrollbar_state.is_dragging() {
thread.show_scrollbar = false;
cx.notify();
}
})
.log_err();
}))
} }
pub fn is_codeblock_expanded(&self, message_id: MessageId, ix: usize) -> bool { pub fn is_codeblock_expanded(&self, message_id: MessageId, ix: usize) -> bool {
@ -3596,26 +3569,8 @@ impl Render for ActiveThread {
.size_full() .size_full()
.relative() .relative()
.bg(cx.theme().colors().panel_background) .bg(cx.theme().colors().panel_background)
.on_mouse_move(cx.listener(|this, _, _, cx| {
this.show_scrollbar = true;
this.hide_scrollbar_later(cx);
cx.notify();
}))
.on_scroll_wheel(cx.listener(|this, _, _, cx| {
this.show_scrollbar = true;
this.hide_scrollbar_later(cx);
cx.notify();
}))
.on_mouse_up(
MouseButton::Left,
cx.listener(|this, _, _, cx| {
this.hide_scrollbar_later(cx);
}),
)
.child(list(self.list_state.clone(), cx.processor(Self::render_message)).flex_grow()) .child(list(self.list_state.clone(), cx.processor(Self::render_message)).flex_grow())
.when_some(self.render_vertical_scrollbar(cx), |this, scrollbar| { .child(self.render_vertical_scrollbar(cx))
this.child(scrollbar)
})
} }
} }

View file

@ -29,7 +29,6 @@ use ui::{
Scrollbar, ScrollbarState, SharedString, StatefulInteractiveElement, Styled, Toggleable, Scrollbar, ScrollbarState, SharedString, StatefulInteractiveElement, Styled, Toggleable,
Tooltip, Window, div, h_flex, px, v_flex, Tooltip, Window, div, h_flex, px, v_flex,
}; };
use util::ResultExt;
use workspace::Workspace; use workspace::Workspace;
use zed_actions::{ToggleEnableBreakpoint, UnsetBreakpoint}; use zed_actions::{ToggleEnableBreakpoint, UnsetBreakpoint};
@ -56,8 +55,6 @@ pub(crate) struct BreakpointList {
scrollbar_state: ScrollbarState, scrollbar_state: ScrollbarState,
breakpoints: Vec<BreakpointEntry>, breakpoints: Vec<BreakpointEntry>,
session: Option<Entity<Session>>, session: Option<Entity<Session>>,
hide_scrollbar_task: Option<Task<()>>,
show_scrollbar: bool,
focus_handle: FocusHandle, focus_handle: FocusHandle,
scroll_handle: UniformListScrollHandle, scroll_handle: UniformListScrollHandle,
selected_ix: Option<usize>, selected_ix: Option<usize>,
@ -103,8 +100,6 @@ impl BreakpointList {
worktree_store, worktree_store,
scrollbar_state, scrollbar_state,
breakpoints: Default::default(), breakpoints: Default::default(),
hide_scrollbar_task: None,
show_scrollbar: false,
workspace, workspace,
session, session,
focus_handle, focus_handle,
@ -565,21 +560,6 @@ impl BreakpointList {
Ok(()) Ok(())
} }
fn hide_scrollbar(&mut self, window: &mut Window, cx: &mut Context<Self>) {
const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1);
self.hide_scrollbar_task = Some(cx.spawn_in(window, async move |panel, cx| {
cx.background_executor()
.timer(SCROLLBAR_SHOW_INTERVAL)
.await;
panel
.update(cx, |panel, cx| {
panel.show_scrollbar = false;
cx.notify();
})
.log_err();
}))
}
fn render_list(&mut self, cx: &mut Context<Self>) -> impl IntoElement { fn render_list(&mut self, cx: &mut Context<Self>) -> impl IntoElement {
let selected_ix = self.selected_ix; let selected_ix = self.selected_ix;
let focus_handle = self.focus_handle.clone(); let focus_handle = self.focus_handle.clone();
@ -614,43 +594,39 @@ impl BreakpointList {
.flex_grow() .flex_grow()
} }
fn render_vertical_scrollbar(&self, cx: &mut Context<Self>) -> Option<Stateful<Div>> { fn render_vertical_scrollbar(&self, cx: &mut Context<Self>) -> Stateful<Div> {
if !(self.show_scrollbar || self.scrollbar_state.is_dragging()) { div()
return None; .occlude()
} .id("breakpoint-list-vertical-scrollbar")
Some( .on_mouse_move(cx.listener(|_, _, _, cx| {
div() cx.notify();
.occlude() cx.stop_propagation()
.id("breakpoint-list-vertical-scrollbar") }))
.on_mouse_move(cx.listener(|_, _, _, cx| { .on_hover(|_, _, cx| {
cx.notify(); cx.stop_propagation();
cx.stop_propagation() })
})) .on_any_mouse_down(|_, _, cx| {
.on_hover(|_, _, cx| { cx.stop_propagation();
})
.on_mouse_up(
MouseButton::Left,
cx.listener(|_, _, _, cx| {
cx.stop_propagation(); cx.stop_propagation();
}) }),
.on_any_mouse_down(|_, _, cx| { )
cx.stop_propagation(); .on_scroll_wheel(cx.listener(|_, _, _, cx| {
}) cx.notify();
.on_mouse_up( }))
MouseButton::Left, .h_full()
cx.listener(|_, _, _, cx| { .absolute()
cx.stop_propagation(); .right_1()
}), .top_1()
) .bottom_0()
.on_scroll_wheel(cx.listener(|_, _, _, cx| { .w(px(12.))
cx.notify(); .cursor_default()
})) .children(Scrollbar::vertical(self.scrollbar_state.clone()).map(|s| s.auto_hide(cx)))
.h_full()
.absolute()
.right_1()
.top_1()
.bottom_0()
.w(px(12.))
.cursor_default()
.children(Scrollbar::vertical(self.scrollbar_state.clone())),
)
} }
pub(crate) fn render_control_strip(&self) -> AnyElement { pub(crate) fn render_control_strip(&self) -> AnyElement {
let selection_kind = self.selection_kind(); let selection_kind = self.selection_kind();
let focus_handle = self.focus_handle.clone(); let focus_handle = self.focus_handle.clone();
@ -819,15 +795,6 @@ impl Render for BreakpointList {
.id("breakpoint-list") .id("breakpoint-list")
.key_context("BreakpointList") .key_context("BreakpointList")
.track_focus(&self.focus_handle) .track_focus(&self.focus_handle)
.on_hover(cx.listener(|this, hovered, window, cx| {
if *hovered {
this.show_scrollbar = true;
this.hide_scrollbar_task.take();
cx.notify();
} else if !this.focus_handle.contains_focused(window, cx) {
this.hide_scrollbar(window, cx);
}
}))
.on_action(cx.listener(Self::select_next)) .on_action(cx.listener(Self::select_next))
.on_action(cx.listener(Self::select_previous)) .on_action(cx.listener(Self::select_previous))
.on_action(cx.listener(Self::select_first)) .on_action(cx.listener(Self::select_first))
@ -844,7 +811,7 @@ impl Render for BreakpointList {
v_flex() v_flex()
.size_full() .size_full()
.child(self.render_list(cx)) .child(self.render_list(cx))
.children(self.render_vertical_scrollbar(cx)), .child(self.render_vertical_scrollbar(cx)),
) )
.when_some(self.strip_mode, |this, _| { .when_some(self.strip_mode, |this, _| {
this.child(Divider::horizontal()).child( this.child(Divider::horizontal()).child(

View file

@ -23,7 +23,6 @@ use ui::{
ParentElement, Pixels, PopoverMenuHandle, Render, Scrollbar, ScrollbarState, SharedString, ParentElement, Pixels, PopoverMenuHandle, Render, Scrollbar, ScrollbarState, SharedString,
StatefulInteractiveElement, Styled, TextSize, Tooltip, Window, div, h_flex, px, v_flex, StatefulInteractiveElement, Styled, TextSize, Tooltip, Window, div, h_flex, px, v_flex,
}; };
use util::ResultExt;
use workspace::Workspace; use workspace::Workspace;
use crate::{ToggleDataBreakpoint, session::running::stack_frame_list::StackFrameList}; use crate::{ToggleDataBreakpoint, session::running::stack_frame_list::StackFrameList};
@ -34,9 +33,7 @@ pub(crate) struct MemoryView {
workspace: WeakEntity<Workspace>, workspace: WeakEntity<Workspace>,
scroll_handle: UniformListScrollHandle, scroll_handle: UniformListScrollHandle,
scroll_state: ScrollbarState, scroll_state: ScrollbarState,
show_scrollbar: bool,
stack_frame_list: WeakEntity<StackFrameList>, stack_frame_list: WeakEntity<StackFrameList>,
hide_scrollbar_task: Option<Task<()>>,
focus_handle: FocusHandle, focus_handle: FocusHandle,
view_state: ViewState, view_state: ViewState,
query_editor: Entity<Editor>, query_editor: Entity<Editor>,
@ -150,8 +147,6 @@ impl MemoryView {
scroll_state, scroll_state,
scroll_handle, scroll_handle,
stack_frame_list, stack_frame_list,
show_scrollbar: false,
hide_scrollbar_task: None,
focus_handle: cx.focus_handle(), focus_handle: cx.focus_handle(),
view_state, view_state,
query_editor, query_editor,
@ -168,61 +163,42 @@ impl MemoryView {
.detach(); .detach();
this this
} }
fn hide_scrollbar(&mut self, window: &mut Window, cx: &mut Context<Self>) {
const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1);
self.hide_scrollbar_task = Some(cx.spawn_in(window, async move |panel, cx| {
cx.background_executor()
.timer(SCROLLBAR_SHOW_INTERVAL)
.await;
panel
.update(cx, |panel, cx| {
panel.show_scrollbar = false;
cx.notify();
})
.log_err();
}))
}
fn render_vertical_scrollbar(&self, cx: &mut Context<Self>) -> Option<Stateful<Div>> { fn render_vertical_scrollbar(&self, cx: &mut Context<Self>) -> Stateful<Div> {
if !(self.show_scrollbar || self.scroll_state.is_dragging()) { div()
return None; .occlude()
} .id("memory-view-vertical-scrollbar")
Some( .on_drag_move(cx.listener(|this, evt, _, cx| {
div() let did_handle = this.handle_scroll_drag(evt);
.occlude() cx.notify();
.id("memory-view-vertical-scrollbar") if did_handle {
.on_drag_move(cx.listener(|this, evt, _, cx| { cx.stop_propagation()
let did_handle = this.handle_scroll_drag(evt); }
cx.notify(); }))
if did_handle { .on_drag(ScrollbarDragging, |_, _, _, cx| cx.new(|_| Empty))
cx.stop_propagation() .on_hover(|_, _, cx| {
} cx.stop_propagation();
})) })
.on_drag(ScrollbarDragging, |_, _, _, cx| cx.new(|_| Empty)) .on_any_mouse_down(|_, _, cx| {
.on_hover(|_, _, cx| { cx.stop_propagation();
})
.on_mouse_up(
MouseButton::Left,
cx.listener(|_, _, _, cx| {
cx.stop_propagation(); cx.stop_propagation();
}) }),
.on_any_mouse_down(|_, _, cx| { )
cx.stop_propagation(); .on_scroll_wheel(cx.listener(|_, _, _, cx| {
}) cx.notify();
.on_mouse_up( }))
MouseButton::Left, .h_full()
cx.listener(|_, _, _, cx| { .absolute()
cx.stop_propagation(); .right_1()
}), .top_1()
) .bottom_0()
.on_scroll_wheel(cx.listener(|_, _, _, cx| { .w(px(12.))
cx.notify(); .cursor_default()
})) .children(Scrollbar::vertical(self.scroll_state.clone()).map(|s| s.auto_hide(cx)))
.h_full()
.absolute()
.right_1()
.top_1()
.bottom_0()
.w(px(12.))
.cursor_default()
.children(Scrollbar::vertical(self.scroll_state.clone())),
)
} }
fn render_memory(&self, cx: &mut Context<Self>) -> UniformList { fn render_memory(&self, cx: &mut Context<Self>) -> UniformList {
@ -920,15 +896,6 @@ impl Render for MemoryView {
.on_action(cx.listener(Self::page_up)) .on_action(cx.listener(Self::page_up))
.size_full() .size_full()
.track_focus(&self.focus_handle) .track_focus(&self.focus_handle)
.on_hover(cx.listener(|this, hovered, window, cx| {
if *hovered {
this.show_scrollbar = true;
this.hide_scrollbar_task.take();
cx.notify();
} else if !this.focus_handle.contains_focused(window, cx) {
this.hide_scrollbar(window, cx);
}
}))
.child( .child(
h_flex() h_flex()
.w_full() .w_full()
@ -978,7 +945,7 @@ impl Render for MemoryView {
) )
.with_priority(1) .with_priority(1)
})) }))
.children(self.render_vertical_scrollbar(cx)), .child(self.render_vertical_scrollbar(cx)),
) )
} }
} }

View file

@ -1,11 +1,20 @@
use std::{any::Any, cell::Cell, fmt::Debug, ops::Range, rc::Rc, sync::Arc}; use std::{
any::Any,
cell::{Cell, RefCell},
fmt::Debug,
ops::Range,
rc::Rc,
sync::Arc,
time::Duration,
};
use crate::{IntoElement, prelude::*, px, relative}; use crate::{IntoElement, prelude::*, px, relative};
use gpui::{ use gpui::{
Along, App, Axis as ScrollbarAxis, BorderStyle, Bounds, ContentMask, Corners, CursorStyle, Along, App, Axis as ScrollbarAxis, BorderStyle, Bounds, ContentMask, Corners, CursorStyle,
Edges, Element, ElementId, Entity, EntityId, GlobalElementId, Hitbox, HitboxBehavior, Hsla, Edges, Element, ElementId, Entity, EntityId, GlobalElementId, Hitbox, HitboxBehavior, Hsla,
IsZero, LayoutId, ListState, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, IsZero, LayoutId, ListState, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels,
Point, ScrollHandle, ScrollWheelEvent, Size, Style, UniformListScrollHandle, Window, quad, Point, ScrollHandle, ScrollWheelEvent, Size, Style, Task, UniformListScrollHandle, Window,
quad,
}; };
pub struct Scrollbar { pub struct Scrollbar {
@ -108,6 +117,25 @@ pub struct ScrollbarState {
thumb_state: Rc<Cell<ThumbState>>, thumb_state: Rc<Cell<ThumbState>>,
parent_id: Option<EntityId>, parent_id: Option<EntityId>,
scroll_handle: Arc<dyn ScrollableHandle>, scroll_handle: Arc<dyn ScrollableHandle>,
auto_hide: Rc<RefCell<AutoHide>>,
}
#[derive(Debug)]
enum AutoHide {
Disabled,
Hidden {
parent_id: EntityId,
},
Visible {
parent_id: EntityId,
_task: Task<()>,
},
}
impl AutoHide {
fn is_hidden(&self) -> bool {
matches!(self, AutoHide::Hidden { .. })
}
} }
impl ScrollbarState { impl ScrollbarState {
@ -116,6 +144,7 @@ impl ScrollbarState {
thumb_state: Default::default(), thumb_state: Default::default(),
parent_id: None, parent_id: None,
scroll_handle: Arc::new(scroll), scroll_handle: Arc::new(scroll),
auto_hide: Rc::new(RefCell::new(AutoHide::Disabled)),
} }
} }
@ -174,6 +203,38 @@ impl ScrollbarState {
let thumb_percentage_end = (start_offset + thumb_size) / viewport_size; let thumb_percentage_end = (start_offset + thumb_size) / viewport_size;
Some(thumb_percentage_start..thumb_percentage_end) Some(thumb_percentage_start..thumb_percentage_end)
} }
fn show_temporarily(&self, parent_id: EntityId, cx: &mut App) {
const SHOW_INTERVAL: Duration = Duration::from_secs(1);
let auto_hide = self.auto_hide.clone();
auto_hide.replace(AutoHide::Visible {
parent_id,
_task: cx.spawn({
let this = auto_hide.clone();
async move |cx| {
cx.background_executor().timer(SHOW_INTERVAL).await;
this.replace(AutoHide::Hidden { parent_id });
cx.update(|cx| {
cx.notify(parent_id);
})
.ok();
}
}),
});
}
fn unhide(&self, position: &Point<Pixels>, cx: &mut App) {
let parent_id = match &*self.auto_hide.borrow() {
AutoHide::Disabled => return,
AutoHide::Hidden { parent_id } => *parent_id,
AutoHide::Visible { parent_id, _task } => *parent_id,
};
if self.scroll_handle().viewport().contains(position) {
self.show_temporarily(parent_id, cx);
}
}
} }
impl Scrollbar { impl Scrollbar {
@ -189,6 +250,14 @@ impl Scrollbar {
let thumb = state.thumb_range(kind)?; let thumb = state.thumb_range(kind)?;
Some(Self { thumb, state, kind }) Some(Self { thumb, state, kind })
} }
/// Automatically hide the scrollbar when idle
pub fn auto_hide<V: 'static>(self, cx: &mut Context<V>) -> Self {
if matches!(*self.state.auto_hide.borrow(), AutoHide::Disabled) {
self.state.show_temporarily(cx.entity_id(), cx);
}
self
}
} }
impl Element for Scrollbar { impl Element for Scrollbar {
@ -284,16 +353,18 @@ impl Element for Scrollbar {
.apply_along(axis.invert(), |width| width / 1.5), .apply_along(axis.invert(), |width| width / 1.5),
); );
let corners = Corners::all(thumb_bounds.size.along(axis.invert()) / 2.0); if thumb_state.is_dragging() || !self.state.auto_hide.borrow().is_hidden() {
let corners = Corners::all(thumb_bounds.size.along(axis.invert()) / 2.0);
window.paint_quad(quad( window.paint_quad(quad(
thumb_bounds, thumb_bounds,
corners, corners,
thumb_background, thumb_background,
Edges::default(), Edges::default(),
Hsla::transparent_black(), Hsla::transparent_black(),
BorderStyle::default(), BorderStyle::default(),
)); ));
}
if thumb_state.is_dragging() { if thumb_state.is_dragging() {
window.set_window_cursor_style(CursorStyle::Arrow); window.set_window_cursor_style(CursorStyle::Arrow);
@ -361,13 +432,18 @@ impl Element for Scrollbar {
}); });
window.on_mouse_event({ window.on_mouse_event({
let state = self.state.clone();
let scroll_handle = self.state.scroll_handle().clone(); let scroll_handle = self.state.scroll_handle().clone();
move |event: &ScrollWheelEvent, phase, window, _| { move |event: &ScrollWheelEvent, phase, window, cx| {
if phase.bubble() && bounds.contains(&event.position) { if phase.bubble() {
let current_offset = scroll_handle.offset(); state.unhide(&event.position, cx);
scroll_handle.set_offset(
current_offset + event.delta.pixel_delta(window.line_height()), if bounds.contains(&event.position) {
); let current_offset = scroll_handle.offset();
scroll_handle.set_offset(
current_offset + event.delta.pixel_delta(window.line_height()),
);
}
} }
} }
}); });
@ -376,6 +452,8 @@ impl Element for Scrollbar {
let state = self.state.clone(); let state = self.state.clone();
move |event: &MouseMoveEvent, phase, window, cx| { move |event: &MouseMoveEvent, phase, window, cx| {
if phase.bubble() { if phase.bubble() {
state.unhide(&event.position, cx);
match state.thumb_state.get() { match state.thumb_state.get() {
ThumbState::Dragging(drag_state) if event.dragging() => { ThumbState::Dragging(drag_state) if event.dragging() => {
let scroll_handle = state.scroll_handle(); let scroll_handle = state.scroll_handle();