From 44e5a962e66e669d84edc7f51433c000460d2fb9 Mon Sep 17 00:00:00 2001 From: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> Date: Thu, 30 Oct 2025 15:43:32 -0400 Subject: [PATCH] debugger: Add horizontal scroll bars to variable list, memory view, and breakpoint list (#41594) 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 --- .../src/session/running/breakpoint_list.rs | 36 ++++++++++++++- .../src/session/running/memory_view.rs | 18 ++++++-- .../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(-) diff --git a/crates/debugger_ui/src/session/running/breakpoint_list.rs b/crates/debugger_ui/src/session/running/breakpoint_list.rs index c9f2a58dae28c2e41e49aecc847857ca6191c0eb..36e627a3ebac677e0420bf4f5dd93f3d1cd62a5b 100644 --- a/crates/debugger_ui/src/session/running/breakpoint_list.rs +++ b/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, + max_width_index: Option, input: Entity, strip_mode: Option, serialize_exception_breakpoints_task: Option>>, @@ -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( diff --git a/crates/debugger_ui/src/session/running/memory_view.rs b/crates/debugger_ui/src/session/running/memory_view.rs index bc6e90ed09a9c6ac519cca8345a0ffbb6459f249..8670beb0f5f93f68a6052b868a866e22b82c92fd 100644 --- a/crates/debugger_ui/src/session/running/memory_view.rs +++ b/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, + ), ) } } diff --git a/crates/debugger_ui/src/session/running/variable_list.rs b/crates/debugger_ui/src/session/running/variable_list.rs index c69bdfbe7ca8712284dd971d2e86f31f99cd696d..3da1bd33c4a6de3d161a78b5ff5188f655d019c7 100644 --- a/crates/debugger_ui/src/session/running/variable_list.rs +++ b/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, + max_width_index: Option, entry_states: HashMap, selected_stack_frame_id: Option, 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, + ) } } diff --git a/crates/gpui/src/elements/uniform_list.rs b/crates/gpui/src/elements/uniform_list.rs index 93082563c02f4168b1d73e2929a6bf9dbd153237..739fa1c5e25eb62378fbe57eea1b62c833780d9d 100644 --- a/crates/gpui/src/elements/uniform_list.rs +++ b/crates/gpui/src/elements/uniform_list.rs @@ -251,6 +251,8 @@ impl Element for UniformList { None } + // self.max_found_width = 0.0 + // fn request_layout( &mut self, global_id: Option<&GlobalElementId>, diff --git a/crates/ui/src/components/scrollbar.rs b/crates/ui/src/components/scrollbar.rs index d3d33a296bbd65edb24371d8f5f1e6462e77e3fe..b7548218371d0772b422adb04f1e326de040241f 100644 --- a/crates/ui/src/components/scrollbar.rs +++ b/crates/ui/src/components/scrollbar.rs @@ -392,7 +392,7 @@ pub struct Scrollbars { 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() -> Scrollbars { diff --git a/crates/ui/src/styles/typography.rs b/crates/ui/src/styles/typography.rs index 0d7d5af9e74f11f7d77c9d03362f6be41dc9b2ec..2bb0b35720be715251bc7c11a139a1fccfaf6035 100644 --- a/crates/ui/src/styles/typography.rs +++ b/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