debugger: Add horizontal scroll bars to variable list, memory view, and breakpoint list (#41594)

Anthony Eid and MrSubidubi created

Closes #40360

This PR added heuristics to determine what variable/breakpoint list
entry has the longest width when rendered. I added this in so the
uniform list would correctly determine which item has the longest width
and use that to calculate the scrollbar size.

The heuristic can be off if a non-mono space font is used in the UI; in
most cases, it's more than accurate enough though.

Release Notes:

- debugger: Add horizontal scroll bars to variable list, memory view,
and breakpoint list

---------

Co-authored-by: MrSubidubi <dev@bahn.sh>

Change summary

crates/debugger_ui/src/session/running/breakpoint_list.rs | 36 ++++++
crates/debugger_ui/src/session/running/memory_view.rs     | 18 +++
crates/debugger_ui/src/session/running/variable_list.rs   | 45 ++++++++-
crates/gpui/src/elements/uniform_list.rs                  |  2 
crates/ui/src/components/scrollbar.rs                     |  2 
crates/ui/src/styles/typography.rs                        | 13 ++
6 files changed, 105 insertions(+), 11 deletions(-)

Detailed changes

crates/debugger_ui/src/session/running/breakpoint_list.rs 🔗

@@ -12,6 +12,7 @@ use gpui::{
     Action, AppContext, ClickEvent, Entity, FocusHandle, Focusable, MouseButton, ScrollStrategy,
     Task, UniformListScrollHandle, WeakEntity, actions, uniform_list,
 };
+use itertools::Itertools;
 use language::Point;
 use project::{
     Project,
@@ -24,7 +25,7 @@ use project::{
 };
 use ui::{
     Divider, DividerColor, FluentBuilder as _, Indicator, IntoElement, ListItem, Render,
-    StatefulInteractiveElement, Tooltip, WithScrollbar, prelude::*,
+    ScrollAxes, StatefulInteractiveElement, Tooltip, WithScrollbar, prelude::*,
 };
 use util::rel_path::RelPath;
 use workspace::Workspace;
@@ -55,6 +56,7 @@ pub(crate) struct BreakpointList {
     focus_handle: FocusHandle,
     scroll_handle: UniformListScrollHandle,
     selected_ix: Option<usize>,
+    max_width_index: Option<usize>,
     input: Entity<Editor>,
     strip_mode: Option<ActiveBreakpointStripMode>,
     serialize_exception_breakpoints_task: Option<Task<anyhow::Result<()>>>,
@@ -95,6 +97,7 @@ impl BreakpointList {
                 dap_store,
                 worktree_store,
                 breakpoints: Default::default(),
+                max_width_index: None,
                 workspace,
                 session,
                 focus_handle,
@@ -570,6 +573,8 @@ impl BreakpointList {
                     .collect()
             }),
         )
+        .with_horizontal_sizing_behavior(gpui::ListHorizontalSizingBehavior::Unconstrained)
+        .with_width_from_item(self.max_width_index)
         .track_scroll(self.scroll_handle.clone())
         .flex_1()
     }
@@ -732,6 +737,26 @@ impl Render for BreakpointList {
                 .chain(exception_breakpoints),
         );
 
+        let text_pixels = ui::TextSize::Default.pixels(cx).to_f64() as f32;
+
+        self.max_width_index = self
+            .breakpoints
+            .iter()
+            .map(|entry| match &entry.kind {
+                BreakpointEntryKind::LineBreakpoint(line_bp) => {
+                    let name_and_line = format!("{}:{}", line_bp.name, line_bp.line);
+                    let dir_len = line_bp.dir.as_ref().map(|d| d.len()).unwrap_or(0);
+                    (name_and_line.len() + dir_len) as f32 * text_pixels
+                }
+                BreakpointEntryKind::ExceptionBreakpoint(exc_bp) => {
+                    exc_bp.data.label.len() as f32 * text_pixels
+                }
+                BreakpointEntryKind::DataBreakpoint(data_bp) => {
+                    data_bp.0.context.human_readable_label().len() as f32 * text_pixels
+                }
+            })
+            .position_max_by(|left, right| left.total_cmp(right));
+
         v_flex()
             .id("breakpoint-list")
             .key_context("BreakpointList")
@@ -749,7 +774,14 @@ impl Render for BreakpointList {
             .size_full()
             .pt_1()
             .child(self.render_list(cx))
-            .vertical_scrollbar_for(self.scroll_handle.clone(), window, cx)
+            .custom_scrollbars(
+                ui::Scrollbars::new(ScrollAxes::Both)
+                    .tracked_scroll_handle(self.scroll_handle.clone())
+                    .with_track_along(ScrollAxes::Both, cx.theme().colors().panel_background)
+                    .tracked_entity(cx.entity_id()),
+                window,
+                cx,
+            )
             .when_some(self.strip_mode, |this, _| {
                 this.child(Divider::horizontal().color(DividerColor::Border))
                     .child(

crates/debugger_ui/src/session/running/memory_view.rs 🔗

@@ -10,8 +10,9 @@ use std::{
 use editor::{Editor, EditorElement, EditorStyle};
 use gpui::{
     Action, Along, AppContext, Axis, DismissEvent, DragMoveEvent, Empty, Entity, FocusHandle,
-    Focusable, MouseButton, Point, ScrollStrategy, ScrollWheelEvent, Subscription, Task, TextStyle,
-    UniformList, UniformListScrollHandle, WeakEntity, actions, anchored, deferred, uniform_list,
+    Focusable, ListHorizontalSizingBehavior, MouseButton, Point, ScrollStrategy, ScrollWheelEvent,
+    Subscription, Task, TextStyle, UniformList, UniformListScrollHandle, WeakEntity, actions,
+    anchored, deferred, uniform_list,
 };
 use notifications::status_toast::{StatusToast, ToastIcon};
 use project::debugger::{MemoryCell, dap_command::DataBreakpointContext, session::Session};
@@ -229,6 +230,7 @@ impl MemoryView {
             },
         )
         .track_scroll(view_state.scroll_handle)
+        .with_horizontal_sizing_behavior(ListHorizontalSizingBehavior::Unconstrained)
         .on_scroll_wheel(cx.listener(|this, evt: &ScrollWheelEvent, window, _| {
             let mut view_state = this.view_state();
             let delta = evt.delta.pixel_delta(window.line_height());
@@ -917,7 +919,17 @@ impl Render for MemoryView {
                         )
                         .with_priority(1)
                     }))
-                    .vertical_scrollbar_for(self.view_state_handle.clone(), window, cx),
+                    .custom_scrollbars(
+                        ui::Scrollbars::new(ui::ScrollAxes::Both)
+                            .tracked_scroll_handle(self.view_state_handle.clone())
+                            .with_track_along(
+                                ui::ScrollAxes::Both,
+                                cx.theme().colors().panel_background,
+                            )
+                            .tracked_entity(cx.entity_id()),
+                        window,
+                        cx,
+                    ),
             )
     }
 }

crates/debugger_ui/src/session/running/variable_list.rs 🔗

@@ -11,15 +11,18 @@ use gpui::{
     FocusHandle, Focusable, Hsla, MouseDownEvent, Point, Subscription, TextStyleRefinement,
     UniformListScrollHandle, WeakEntity, actions, anchored, deferred, uniform_list,
 };
+use itertools::Itertools;
 use menu::{SelectFirst, SelectLast, SelectNext, SelectPrevious};
 use project::debugger::{
     dap_command::DataBreakpointContext,
     session::{Session, SessionEvent, Watcher},
 };
 use std::{collections::HashMap, ops::Range, sync::Arc};
-use ui::{ContextMenu, ListItem, ScrollableHandle, Tooltip, WithScrollbar, prelude::*};
+use ui::{ContextMenu, ListItem, ScrollAxes, ScrollableHandle, Tooltip, WithScrollbar, prelude::*};
 use util::{debug_panic, maybe};
 
+static INDENT_STEP_SIZE: Pixels = px(10.0);
+
 actions!(
     variable_list,
     [
@@ -185,6 +188,7 @@ struct VariableColor {
 
 pub struct VariableList {
     entries: Vec<ListEntry>,
+    max_width_index: Option<usize>,
     entry_states: HashMap<EntryPath, EntryState>,
     selected_stack_frame_id: Option<StackFrameId>,
     list_handle: UniformListScrollHandle,
@@ -243,6 +247,7 @@ impl VariableList {
             disabled: false,
             edited_path: None,
             entries: Default::default(),
+            max_width_index: None,
             entry_states: Default::default(),
             weak_running,
             memory_view,
@@ -368,6 +373,26 @@ impl VariableList {
         }
 
         self.entries = entries;
+
+        let text_pixels = ui::TextSize::Default.pixels(cx).to_f64() as f32;
+        let indent_size = INDENT_STEP_SIZE.to_f64() as f32;
+
+        self.max_width_index = self
+            .entries
+            .iter()
+            .map(|entry| match &entry.entry {
+                DapEntry::Scope(scope) => scope.name.len() as f32 * text_pixels,
+                DapEntry::Variable(variable) => {
+                    (variable.value.len() + variable.name.len()) as f32 * text_pixels
+                        + (entry.path.indices.len() as f32 * indent_size)
+                }
+                DapEntry::Watcher(watcher) => {
+                    (watcher.value.len() + watcher.expression.len()) as f32 * text_pixels
+                        + (entry.path.indices.len() as f32 * indent_size)
+                }
+            })
+            .position_max_by(|left, right| left.total_cmp(right));
+
         cx.notify();
     }
 
@@ -1244,7 +1269,7 @@ impl VariableList {
                 .disabled(self.disabled)
                 .selectable(false)
                 .indent_level(state.depth)
-                .indent_step_size(px(10.))
+                .indent_step_size(INDENT_STEP_SIZE)
                 .always_show_disclosure_icon(true)
                 .when(var_ref > 0, |list_item| {
                     list_item.toggle(state.is_expanded).on_toggle(cx.listener({
@@ -1445,7 +1470,7 @@ impl VariableList {
                 .disabled(self.disabled)
                 .selectable(false)
                 .indent_level(state.depth)
-                .indent_step_size(px(10.))
+                .indent_step_size(INDENT_STEP_SIZE)
                 .always_show_disclosure_icon(true)
                 .when(var_ref > 0, |list_item| {
                     list_item.toggle(state.is_expanded).on_toggle(cx.listener({
@@ -1507,7 +1532,6 @@ impl Render for VariableList {
             .key_context("VariableList")
             .id("variable-list")
             .group("variable-list")
-            .overflow_y_scroll()
             .size_full()
             .on_action(cx.listener(Self::select_first))
             .on_action(cx.listener(Self::select_last))
@@ -1533,6 +1557,9 @@ impl Render for VariableList {
                     }),
                 )
                 .track_scroll(self.list_handle.clone())
+                .with_width_from_item(self.max_width_index)
+                .with_sizing_behavior(gpui::ListSizingBehavior::Auto)
+                .with_horizontal_sizing_behavior(gpui::ListHorizontalSizingBehavior::Unconstrained)
                 .gap_1_5()
                 .size_full()
                 .flex_grow(),
@@ -1546,7 +1573,15 @@ impl Render for VariableList {
                 )
                 .with_priority(1)
             }))
-            .vertical_scrollbar_for(self.list_handle.clone(), window, cx)
+            // .vertical_scrollbar_for(self.list_handle.clone(), window, cx)
+            .custom_scrollbars(
+                ui::Scrollbars::new(ScrollAxes::Both)
+                    .tracked_scroll_handle(self.list_handle.clone())
+                    .with_track_along(ScrollAxes::Both, cx.theme().colors().panel_background)
+                    .tracked_entity(cx.entity_id()),
+                window,
+                cx,
+            )
     }
 }
 

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

@@ -392,7 +392,7 @@ pub struct Scrollbars<T: ScrollableHandle = ScrollHandle> {
 
 impl Scrollbars {
     pub fn new(show_along: ScrollAxes) -> Self {
-        Self::new_with_setting(show_along, |_| ShowScrollbar::default())
+        Self::new_with_setting(show_along, |_| ShowScrollbar::Always)
     }
 
     pub fn for_settings<S: ScrollbarVisibility>() -> Scrollbars {

crates/ui/src/styles/typography.rs 🔗

@@ -144,6 +144,19 @@ impl TextSize {
             Self::Editor => rems_from_px(theme_settings.buffer_font_size(cx)),
         }
     }
+
+    pub fn pixels(self, cx: &App) -> Pixels {
+        let theme_settings = ThemeSettings::get_global(cx);
+
+        match self {
+            Self::Large => px(16.),
+            Self::Default => px(14.),
+            Self::Small => px(12.),
+            Self::XSmall => px(10.),
+            Self::Ui => theme_settings.ui_font_size(cx),
+            Self::Editor => theme_settings.buffer_font_size(cx),
+        }
+    }
 }
 
 /// The size of a [`Headline`] element