Detailed changes
@@ -21,10 +21,10 @@ use editor::{
use file_icons::FileIcons;
use gpui::{
Action, Animation, AnimationExt, App, BorderStyle, EdgesRefinement, Empty, Entity, EntityId,
- FocusHandle, Focusable, Hsla, Length, ListOffset, ListState, PlatformDisplay, SharedString,
- StyleRefinement, Subscription, Task, TextStyle, TextStyleRefinement, Transformation,
- UnderlineStyle, WeakEntity, Window, WindowHandle, div, linear_color_stop, linear_gradient,
- list, percentage, point, prelude::*, pulsating_between,
+ FocusHandle, Focusable, Hsla, Length, ListOffset, ListState, MouseButton, PlatformDisplay,
+ SharedString, Stateful, StyleRefinement, Subscription, Task, TextStyle, TextStyleRefinement,
+ Transformation, UnderlineStyle, WeakEntity, Window, WindowHandle, div, linear_color_stop,
+ linear_gradient, list, percentage, point, prelude::*, pulsating_between,
};
use language::language_settings::SoftWrap;
use language::{Buffer, Language};
@@ -34,7 +34,9 @@ use project::Project;
use settings::Settings as _;
use text::{Anchor, BufferSnapshot};
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 workspace::{CollaboratorId, Workspace};
use zed_actions::agent::{Chat, NextHistoryMessage, PreviousHistoryMessage};
@@ -69,6 +71,7 @@ pub struct AcpThreadView {
notification_subscriptions: HashMap<WindowHandle<AgentNotification>, Vec<Subscription>>,
last_error: Option<Entity<Markdown>>,
list_state: ListState,
+ scrollbar_state: ScrollbarState,
auth_task: Option<Task<()>>,
expanded_tool_calls: HashSet<acp::ToolCallId>,
expanded_thinking_blocks: HashSet<(usize, usize)>,
@@ -187,7 +190,8 @@ impl AcpThreadView {
notifications: Vec::new(),
notification_subscriptions: HashMap::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,
auth_task: None,
expanded_tool_calls: HashSet::default(),
@@ -2479,6 +2483,39 @@ impl AcpThreadView {
.child(open_as_markdown)
.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 {
@@ -2553,6 +2590,7 @@ impl Render for AcpThreadView {
.flex_grow()
.into_any(),
)
+ .child(self.render_vertical_scrollbar(cx))
.children(match thread_clone.read(cx).status() {
ThreadStatus::Idle | ThreadStatus::WaitingForToolConfirmation => {
None
@@ -69,8 +69,6 @@ pub struct ActiveThread {
messages: Vec<MessageId>,
list_state: ListState,
scrollbar_state: ScrollbarState,
- show_scrollbar: bool,
- hide_scrollbar_task: Option<Task<()>>,
rendered_messages_by_id: HashMap<MessageId, RenderedMessage>,
rendered_tool_uses: HashMap<LanguageModelToolUseId, RenderedToolUse>,
editing_message: Option<(MessageId, EditingMessageState)>,
@@ -805,9 +803,7 @@ impl ActiveThread {
expanded_thinking_segments: HashMap::default(),
expanded_code_blocks: HashMap::default(),
list_state: list_state.clone(),
- scrollbar_state: ScrollbarState::new(list_state),
- show_scrollbar: false,
- hide_scrollbar_task: None,
+ scrollbar_state: ScrollbarState::new(list_state).parent_entity(&cx.entity()),
editing_message: None,
last_error: None,
copied_code_block_ids: HashSet::default(),
@@ -3502,60 +3498,37 @@ impl ActiveThread {
}
}
- fn render_vertical_scrollbar(&self, cx: &mut Context<Self>) -> Option<Stateful<Div>> {
- if !self.show_scrollbar && !self.scrollbar_state.is_dragging() {
- return None;
- }
-
- Some(
- div()
- .occlude()
- .id("active-thread-scrollbar")
- .on_mouse_move(cx.listener(|_, _, _, cx| {
- cx.notify();
- cx.stop_propagation()
- }))
- .on_hover(|_, _, cx| {
- cx.stop_propagation();
- })
- .on_any_mouse_down(|_, _, cx| {
+ fn render_vertical_scrollbar(&self, cx: &mut Context<Self>) -> Stateful<Div> {
+ div()
+ .occlude()
+ .id("active-thread-scrollbar")
+ .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_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())),
- )
- }
-
- 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();
- }))
+ }),
+ )
+ .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)))
}
pub fn is_codeblock_expanded(&self, message_id: MessageId, ix: usize) -> bool {
@@ -3596,26 +3569,8 @@ impl Render for ActiveThread {
.size_full()
.relative()
.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())
- .when_some(self.render_vertical_scrollbar(cx), |this, scrollbar| {
- this.child(scrollbar)
- })
+ .child(self.render_vertical_scrollbar(cx))
}
}
@@ -29,7 +29,6 @@ use ui::{
Scrollbar, ScrollbarState, SharedString, StatefulInteractiveElement, Styled, Toggleable,
Tooltip, Window, div, h_flex, px, v_flex,
};
-use util::ResultExt;
use workspace::Workspace;
use zed_actions::{ToggleEnableBreakpoint, UnsetBreakpoint};
@@ -56,8 +55,6 @@ pub(crate) struct BreakpointList {
scrollbar_state: ScrollbarState,
breakpoints: Vec<BreakpointEntry>,
session: Option<Entity<Session>>,
- hide_scrollbar_task: Option<Task<()>>,
- show_scrollbar: bool,
focus_handle: FocusHandle,
scroll_handle: UniformListScrollHandle,
selected_ix: Option<usize>,
@@ -103,8 +100,6 @@ impl BreakpointList {
worktree_store,
scrollbar_state,
breakpoints: Default::default(),
- hide_scrollbar_task: None,
- show_scrollbar: false,
workspace,
session,
focus_handle,
@@ -565,21 +560,6 @@ impl BreakpointList {
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 {
let selected_ix = self.selected_ix;
let focus_handle = self.focus_handle.clone();
@@ -614,43 +594,39 @@ impl BreakpointList {
.flex_grow()
}
- fn render_vertical_scrollbar(&self, cx: &mut Context<Self>) -> Option<Stateful<Div>> {
- if !(self.show_scrollbar || self.scrollbar_state.is_dragging()) {
- return None;
- }
- Some(
- div()
- .occlude()
- .id("breakpoint-list-vertical-scrollbar")
- .on_mouse_move(cx.listener(|_, _, _, cx| {
- cx.notify();
- cx.stop_propagation()
- }))
- .on_hover(|_, _, cx| {
- cx.stop_propagation();
- })
- .on_any_mouse_down(|_, _, cx| {
+ fn render_vertical_scrollbar(&self, cx: &mut Context<Self>) -> Stateful<Div> {
+ div()
+ .occlude()
+ .id("breakpoint-list-vertical-scrollbar")
+ .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_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())),
- )
+ }),
+ )
+ .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)))
}
+
pub(crate) fn render_control_strip(&self) -> AnyElement {
let selection_kind = self.selection_kind();
let focus_handle = self.focus_handle.clone();
@@ -819,15 +795,6 @@ impl Render for BreakpointList {
.id("breakpoint-list")
.key_context("BreakpointList")
.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_previous))
.on_action(cx.listener(Self::select_first))
@@ -844,7 +811,7 @@ impl Render for BreakpointList {
v_flex()
.size_full()
.child(self.render_list(cx))
- .children(self.render_vertical_scrollbar(cx)),
+ .child(self.render_vertical_scrollbar(cx)),
)
.when_some(self.strip_mode, |this, _| {
this.child(Divider::horizontal()).child(
@@ -23,7 +23,6 @@ use ui::{
ParentElement, Pixels, PopoverMenuHandle, Render, Scrollbar, ScrollbarState, SharedString,
StatefulInteractiveElement, Styled, TextSize, Tooltip, Window, div, h_flex, px, v_flex,
};
-use util::ResultExt;
use workspace::Workspace;
use crate::{ToggleDataBreakpoint, session::running::stack_frame_list::StackFrameList};
@@ -34,9 +33,7 @@ pub(crate) struct MemoryView {
workspace: WeakEntity<Workspace>,
scroll_handle: UniformListScrollHandle,
scroll_state: ScrollbarState,
- show_scrollbar: bool,
stack_frame_list: WeakEntity<StackFrameList>,
- hide_scrollbar_task: Option<Task<()>>,
focus_handle: FocusHandle,
view_state: ViewState,
query_editor: Entity<Editor>,
@@ -150,8 +147,6 @@ impl MemoryView {
scroll_state,
scroll_handle,
stack_frame_list,
- show_scrollbar: false,
- hide_scrollbar_task: None,
focus_handle: cx.focus_handle(),
view_state,
query_editor,
@@ -168,61 +163,42 @@ impl MemoryView {
.detach();
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>> {
- if !(self.show_scrollbar || self.scroll_state.is_dragging()) {
- return None;
- }
- Some(
- div()
- .occlude()
- .id("memory-view-vertical-scrollbar")
- .on_drag_move(cx.listener(|this, evt, _, cx| {
- let did_handle = this.handle_scroll_drag(evt);
- cx.notify();
- if did_handle {
- cx.stop_propagation()
- }
- }))
- .on_drag(ScrollbarDragging, |_, _, _, cx| cx.new(|_| Empty))
- .on_hover(|_, _, cx| {
- cx.stop_propagation();
- })
- .on_any_mouse_down(|_, _, cx| {
+ fn render_vertical_scrollbar(&self, cx: &mut Context<Self>) -> Stateful<Div> {
+ div()
+ .occlude()
+ .id("memory-view-vertical-scrollbar")
+ .on_drag_move(cx.listener(|this, evt, _, cx| {
+ let did_handle = this.handle_scroll_drag(evt);
+ cx.notify();
+ if did_handle {
+ cx.stop_propagation()
+ }
+ }))
+ .on_drag(ScrollbarDragging, |_, _, _, cx| cx.new(|_| Empty))
+ .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_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.scroll_state.clone())),
- )
+ }),
+ )
+ .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.scroll_state.clone()).map(|s| s.auto_hide(cx)))
}
fn render_memory(&self, cx: &mut Context<Self>) -> UniformList {
@@ -920,15 +896,6 @@ impl Render for MemoryView {
.on_action(cx.listener(Self::page_up))
.size_full()
.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(
h_flex()
.w_full()
@@ -978,7 +945,7 @@ impl Render for MemoryView {
)
.with_priority(1)
}))
- .children(self.render_vertical_scrollbar(cx)),
+ .child(self.render_vertical_scrollbar(cx)),
)
}
}
@@ -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 gpui::{
Along, App, Axis as ScrollbarAxis, BorderStyle, Bounds, ContentMask, Corners, CursorStyle,
Edges, Element, ElementId, Entity, EntityId, GlobalElementId, Hitbox, HitboxBehavior, Hsla,
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 {
@@ -108,6 +117,25 @@ pub struct ScrollbarState {
thumb_state: Rc<Cell<ThumbState>>,
parent_id: Option<EntityId>,
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 {
@@ -116,6 +144,7 @@ impl ScrollbarState {
thumb_state: Default::default(),
parent_id: None,
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;
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 {
@@ -189,6 +250,14 @@ impl Scrollbar {
let thumb = state.thumb_range(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 {
@@ -284,16 +353,18 @@ impl Element for Scrollbar {
.apply_along(axis.invert(), |width| width / 1.5),
);
- let corners = Corners::all(thumb_bounds.size.along(axis.invert()) / 2.0);
-
- window.paint_quad(quad(
- thumb_bounds,
- corners,
- thumb_background,
- Edges::default(),
- Hsla::transparent_black(),
- BorderStyle::default(),
- ));
+ 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(
+ thumb_bounds,
+ corners,
+ thumb_background,
+ Edges::default(),
+ Hsla::transparent_black(),
+ BorderStyle::default(),
+ ));
+ }
if thumb_state.is_dragging() {
window.set_window_cursor_style(CursorStyle::Arrow);
@@ -361,13 +432,18 @@ impl Element for Scrollbar {
});
window.on_mouse_event({
+ let state = self.state.clone();
let scroll_handle = self.state.scroll_handle().clone();
- move |event: &ScrollWheelEvent, phase, window, _| {
- if phase.bubble() && bounds.contains(&event.position) {
- let current_offset = scroll_handle.offset();
- scroll_handle.set_offset(
- current_offset + event.delta.pixel_delta(window.line_height()),
- );
+ move |event: &ScrollWheelEvent, phase, window, cx| {
+ if phase.bubble() {
+ state.unhide(&event.position, cx);
+
+ 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();
move |event: &MouseMoveEvent, phase, window, cx| {
if phase.bubble() {
+ state.unhide(&event.position, cx);
+
match state.thumb_state.get() {
ThumbState::Dragging(drag_state) if event.dragging() => {
let scroll_handle = state.scroll_handle();