agent: Hide the scrollbar if there's no mouse movement (#28129)

Danilo Leal , Agus Zubiaga , and Agus Zubiaga created

Release Notes:

- agent: The scrollbar now automatically hides if there's no mouse
movement on the thread list.

---------

Co-authored-by: Agus Zubiaga <agus@zed.dev>
Co-authored-by: Agus Zubiaga <hi@aguz.me>

Change summary

crates/agent/src/active_thread.rs | 107 +++++++++++++++++++++++---------
1 file changed, 76 insertions(+), 31 deletions(-)

Detailed changes

crates/agent/src/active_thread.rs 🔗

@@ -46,6 +46,8 @@ 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_use_labels: HashMap<LanguageModelToolUseId, Entity<Markdown>>,
     editing_message: Option<(MessageId, EditMessageState)>,
@@ -377,6 +379,8 @@ impl ActiveThread {
             expanded_thinking_segments: HashMap::default(),
             list_state: list_state.clone(),
             scrollbar_state: ScrollbarState::new(list_state),
+            show_scrollbar: false,
+            hide_scrollbar_task: None,
             editing_message: None,
             last_error: None,
             notifications: Vec::new(),
@@ -2197,37 +2201,60 @@ impl ActiveThread {
         }
     }
 
-    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| {
+    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_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_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())),
+        )
+    }
+
+    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();
+        }))
     }
 }
 
@@ -2236,8 +2263,26 @@ impl Render for ActiveThread {
         v_flex()
             .size_full()
             .relative()
+            .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()).flex_grow())
-            .child(self.render_vertical_scrollbar(cx))
+            .when_some(self.render_vertical_scrollbar(cx), |this, scrollbar| {
+                this.child(scrollbar)
+            })
     }
 }