editor: Add scrollbar to info popovers (#20184)

Piotr Osiewicz created

Related to https://github.com/zed-industries/zed/issues/5034

Release Notes:

- Added scrollbar to info popovers in editor.

Change summary

crates/editor/src/hover_popover.rs    | 78 ++++++++++++++++++++++------
crates/ui/src/components/scrollbar.rs |  5 -
2 files changed, 62 insertions(+), 21 deletions(-)

Detailed changes

crates/editor/src/hover_popover.rs 🔗

@@ -7,7 +7,7 @@ use crate::{
 };
 use gpui::{
     div, px, AnyElement, AsyncWindowContext, FontWeight, Hsla, InteractiveElement, IntoElement,
-    MouseButton, ParentElement, Pixels, ScrollHandle, Size, StatefulInteractiveElement,
+    MouseButton, ParentElement, Pixels, ScrollHandle, Size, Stateful, StatefulInteractiveElement,
     StyleRefinement, Styled, Task, TextStyleRefinement, View, ViewContext,
 };
 use itertools::Itertools;
@@ -21,7 +21,7 @@ use std::rc::Rc;
 use std::{borrow::Cow, cell::RefCell};
 use std::{ops::Range, sync::Arc, time::Duration};
 use theme::ThemeSettings;
-use ui::{prelude::*, window_is_transparent};
+use ui::{prelude::*, window_is_transparent, Scrollbar, ScrollbarState};
 use util::TryFutureExt;
 pub const HOVER_DELAY_MILLIS: u64 = 350;
 pub const HOVER_REQUEST_DELAY_MILLIS: u64 = 200;
@@ -144,10 +144,12 @@ pub fn hover_at_inlay(editor: &mut Editor, inlay_hover: InlayHover, cx: &mut Vie
                 let blocks = vec![inlay_hover.tooltip];
                 let parsed_content = parse_blocks(&blocks, &language_registry, None, &mut cx).await;
 
+                let scroll_handle = ScrollHandle::new();
                 let hover_popover = InfoPopover {
                     symbol_range: RangeInEditor::Inlay(inlay_hover.range.clone()),
                     parsed_content,
-                    scroll_handle: ScrollHandle::new(),
+                    scrollbar_state: ScrollbarState::new(scroll_handle.clone()),
+                    scroll_handle,
                     keyboard_grace: Rc::new(RefCell::new(false)),
                     anchor: None,
                 };
@@ -435,12 +437,14 @@ fn show_hover(
                 let language = hover_result.language;
                 let parsed_content =
                     parse_blocks(&blocks, &language_registry, language, &mut cx).await;
+                let scroll_handle = ScrollHandle::new();
                 info_popover_tasks.push((
                     range.clone(),
                     InfoPopover {
                         symbol_range: RangeInEditor::Text(range),
                         parsed_content,
-                        scroll_handle: ScrollHandle::new(),
+                        scrollbar_state: ScrollbarState::new(scroll_handle.clone()),
+                        scroll_handle,
                         keyboard_grace: Rc::new(RefCell::new(ignore_timeout)),
                         anchor: Some(anchor),
                     },
@@ -680,13 +684,13 @@ impl HoverState {
 }
 
 #[derive(Debug, Clone)]
-
-pub struct InfoPopover {
-    pub symbol_range: RangeInEditor,
-    pub parsed_content: Option<View<Markdown>>,
-    pub scroll_handle: ScrollHandle,
-    pub keyboard_grace: Rc<RefCell<bool>>,
-    pub anchor: Option<Anchor>,
+pub(crate) struct InfoPopover {
+    pub(crate) symbol_range: RangeInEditor,
+    pub(crate) parsed_content: Option<View<Markdown>>,
+    pub(crate) scroll_handle: ScrollHandle,
+    pub(crate) scrollbar_state: ScrollbarState,
+    pub(crate) keyboard_grace: Rc<RefCell<bool>>,
+    pub(crate) anchor: Option<Anchor>,
 }
 
 impl InfoPopover {
@@ -695,10 +699,6 @@ impl InfoPopover {
         let mut d = div()
             .id("info_popover")
             .elevation_2(cx)
-            .overflow_y_scroll()
-            .track_scroll(&self.scroll_handle)
-            .max_w(max_size.width)
-            .max_h(max_size.height)
             // Prevent a mouse down/move on the popover from being propagated to the editor,
             // because that would dismiss the popover.
             .on_mouse_move(|_, cx| cx.stop_propagation())
@@ -706,11 +706,21 @@ impl InfoPopover {
                 let mut keyboard_grace = keyboard_grace.borrow_mut();
                 *keyboard_grace = false;
                 cx.stop_propagation();
-            })
-            .p_2();
+            });
 
         if let Some(markdown) = &self.parsed_content {
-            d = d.child(markdown.clone());
+            d = d
+                .child(
+                    div()
+                        .id("info-md-container")
+                        .overflow_y_scroll()
+                        .max_w(max_size.width)
+                        .max_h(max_size.height)
+                        .p_2()
+                        .track_scroll(&self.scroll_handle)
+                        .child(markdown.clone()),
+                )
+                .child(self.render_vertical_scrollbar(cx));
         }
         d.into_any_element()
     }
@@ -724,6 +734,38 @@ impl InfoPopover {
         cx.notify();
         self.scroll_handle.set_offset(current);
     }
+    fn render_vertical_scrollbar(&self, cx: &mut ViewContext<Editor>) -> Stateful<Div> {
+        div()
+            .occlude()
+            .id("info-popover-vertical-scroll")
+            .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()))
+    }
 }
 
 #[derive(Debug, Clone)]

crates/ui/src/components/scrollbar.rs 🔗

@@ -16,7 +16,7 @@ pub struct Scrollbar {
 }
 
 /// Wrapper around scroll handles.
-#[derive(Clone)]
+#[derive(Clone, Debug)]
 pub enum ScrollableHandle {
     Uniform(UniformListScrollHandle),
     NonUniform(ScrollHandle),
@@ -91,7 +91,7 @@ impl From<ScrollHandle> for ScrollableHandle {
 }
 
 /// A scrollbar state that should be persisted across frames.
-#[derive(Clone)]
+#[derive(Clone, Debug)]
 pub struct ScrollbarState {
     // If Some(), there's an active drag, offset by percentage from the origin of a thumb.
     drag: Rc<Cell<Option<f32>>>,
@@ -142,7 +142,6 @@ impl ScrollbarState {
         }
         let mut percentage = current_offset / main_dimension_size;
         let viewport_size = self.scroll_handle.viewport().size;
-
         let end_offset = (current_offset + viewport_size.along(axis).0) / main_dimension_size;
         // Scroll handle might briefly report an offset greater than the length of a list;
         // in such case we'll adjust the starting offset as well to keep the scrollbar thumb length stable.