From dce6e96c16c75acc4b404def923f02624b343700 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Wed, 14 May 2025 13:32:51 +0200 Subject: [PATCH] debugger: Tidy up dropdown menus (#30679) Before ![CleanShot 2025-05-14 at 13 22 44@2x](https://github.com/user-attachments/assets/c6c06c5c-571d-4913-a691-161f44bba27c) After ![CleanShot 2025-05-14 at 13 22 17@2x](https://github.com/user-attachments/assets/0a25a053-81a3-4b96-8963-4b770b1e5b45) Release Notes: - N/A --- crates/debugger_ui/src/debugger_panel.rs | 190 ++++++++-------------- crates/debugger_ui/src/debugger_ui.rs | 1 + crates/debugger_ui/src/dropdown_menus.rs | 186 +++++++++++++++++++++ crates/debugger_ui/src/session.rs | 12 +- crates/debugger_ui/src/session/running.rs | 56 ++----- crates/ui/src/components/dropdown_menu.rs | 53 +++++- 6 files changed, 329 insertions(+), 169 deletions(-) create mode 100644 crates/debugger_ui/src/dropdown_menus.rs diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index dea9402321..9106f6e1e8 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -1,5 +1,6 @@ use crate::persistence::DebuggerPaneItem; use crate::session::DebugSession; +use crate::session::running::RunningState; use crate::{ ClearAllBreakpoints, Continue, Detach, FocusBreakpointList, FocusConsole, FocusFrames, FocusLoadedSources, FocusModules, FocusTerminal, FocusVariables, Pause, Restart, @@ -30,7 +31,7 @@ use settings::Settings; use std::any::TypeId; use std::sync::Arc; use task::{DebugScenario, TaskContext}; -use ui::{ContextMenu, Divider, DropdownMenu, Tooltip, prelude::*}; +use ui::{ContextMenu, Divider, Tooltip, prelude::*}; use workspace::SplitDirection; use workspace::{ Pane, Workspace, @@ -87,7 +88,20 @@ impl DebugPanel { }) } - fn filter_action_types(&self, cx: &mut App) { + pub(crate) fn sessions(&self) -> Vec> { + self.sessions.clone() + } + + pub fn active_session(&self) -> Option> { + self.active_session.clone() + } + + pub(crate) fn running_state(&self, cx: &mut App) -> Option> { + self.active_session() + .map(|session| session.read(cx).running_state().clone()) + } + + pub(crate) fn filter_action_types(&self, cx: &mut App) { let (has_active_session, supports_restart, support_step_back, status) = self .active_session() .map(|item| { @@ -273,7 +287,7 @@ impl DebugPanel { .detach_and_log_err(cx); } - async fn register_session( + pub(crate) async fn register_session( this: WeakEntity, session: Entity, cx: &mut AsyncWindowContext, @@ -342,7 +356,7 @@ impl DebugPanel { Ok(debug_session) } - fn handle_restart_request( + pub(crate) fn handle_restart_request( &mut self, mut curr_session: Entity, window: &mut Window, @@ -416,11 +430,12 @@ impl DebugPanel { .detach_and_log_err(cx); } - pub fn active_session(&self) -> Option> { - self.active_session.clone() - } - - fn close_session(&mut self, entity_id: EntityId, window: &mut Window, cx: &mut Context) { + pub(crate) fn close_session( + &mut self, + entity_id: EntityId, + window: &mut Window, + cx: &mut Context, + ) { let Some(session) = self .sessions .iter() @@ -474,93 +489,8 @@ impl DebugPanel { }) .detach(); } - fn sessions_drop_down_menu( - &self, - active_session: &Entity, - window: &mut Window, - cx: &mut Context, - ) -> DropdownMenu { - let sessions = self.sessions.clone(); - let weak = cx.weak_entity(); - let label = active_session.read(cx).label_element(cx); - DropdownMenu::new_with_element( - "debugger-session-list", - label, - ContextMenu::build(window, cx, move |mut this, _, cx| { - let context_menu = cx.weak_entity(); - for session in sessions.into_iter() { - let weak_session = session.downgrade(); - let weak_session_id = weak_session.entity_id(); - - this = this.custom_entry( - { - let weak = weak.clone(); - let context_menu = context_menu.clone(); - move |_, cx| { - weak_session - .read_with(cx, |session, cx| { - let context_menu = context_menu.clone(); - let id: SharedString = - format!("debug-session-{}", session.session_id(cx).0) - .into(); - h_flex() - .w_full() - .group(id.clone()) - .justify_between() - .child(session.label_element(cx)) - .child( - IconButton::new( - "close-debug-session", - IconName::Close, - ) - .visible_on_hover(id.clone()) - .icon_size(IconSize::Small) - .on_click({ - let weak = weak.clone(); - move |_, window, cx| { - weak.update(cx, |panel, cx| { - panel.close_session( - weak_session_id, - window, - cx, - ); - }) - .ok(); - context_menu - .update(cx, |this, cx| { - this.cancel( - &Default::default(), - window, - cx, - ); - }) - .ok(); - } - }), - ) - .into_any_element() - }) - .unwrap_or_else(|_| div().into_any_element()) - } - }, - { - let weak = weak.clone(); - move |window, cx| { - weak.update(cx, |panel, cx| { - panel.activate_session(session.clone(), window, cx); - }) - .ok(); - } - }, - ); - } - this - }), - ) - } - - fn deploy_context_menu( + pub(crate) fn deploy_context_menu( &mut self, position: Point, window: &mut Window, @@ -611,7 +541,11 @@ impl DebugPanel { } } - fn top_controls_strip(&self, window: &mut Window, cx: &mut Context) -> Option
{ + pub(crate) fn top_controls_strip( + &mut self, + window: &mut Window, + cx: &mut Context, + ) -> Option
{ let active_session = self.active_session.clone(); let focus_handle = self.focus_handle.clone(); let is_side = self.position(window, cx).axis() == gpui::Axis::Horizontal; @@ -651,12 +585,12 @@ impl DebugPanel { active_session .as_ref() .map(|session| session.read(cx).running_state()), - |this, running_session| { + |this, running_state| { let thread_status = - running_session.read(cx).thread_status(cx).unwrap_or( + running_state.read(cx).thread_status(cx).unwrap_or( project::debugger::session::ThreadStatus::Exited, ); - let capabilities = running_session.read(cx).capabilities(cx); + let capabilities = running_state.read(cx).capabilities(cx); this.map(|this| { if thread_status == ThreadStatus::Running { this.child( @@ -667,7 +601,7 @@ impl DebugPanel { .icon_size(IconSize::XSmall) .shape(ui::IconButtonShape::Square) .on_click(window.listener_for( - &running_session, + &running_state, |this, _, _window, cx| { this.pause_thread(cx); }, @@ -694,7 +628,7 @@ impl DebugPanel { .icon_size(IconSize::XSmall) .shape(ui::IconButtonShape::Square) .on_click(window.listener_for( - &running_session, + &running_state, |this, _, _window, cx| this.continue_thread(cx), )) .disabled(thread_status != ThreadStatus::Stopped) @@ -718,7 +652,7 @@ impl DebugPanel { .icon_size(IconSize::XSmall) .shape(ui::IconButtonShape::Square) .on_click(window.listener_for( - &running_session, + &running_state, |this, _, _window, cx| { this.step_over(cx); }, @@ -742,7 +676,7 @@ impl DebugPanel { .icon_size(IconSize::XSmall) .shape(ui::IconButtonShape::Square) .on_click(window.listener_for( - &running_session, + &running_state, |this, _, _window, cx| { this.step_out(cx); }, @@ -769,7 +703,7 @@ impl DebugPanel { .icon_size(IconSize::XSmall) .shape(ui::IconButtonShape::Square) .on_click(window.listener_for( - &running_session, + &running_state, |this, _, _window, cx| { this.step_in(cx); }, @@ -819,7 +753,7 @@ impl DebugPanel { || thread_status == ThreadStatus::Ended, ) .on_click(window.listener_for( - &running_session, + &running_state, |this, _, _window, cx| { this.toggle_ignore_breakpoints(cx); }, @@ -842,7 +776,7 @@ impl DebugPanel { IconButton::new("debug-restart", IconName::DebugRestart) .icon_size(IconSize::XSmall) .on_click(window.listener_for( - &running_session, + &running_state, |this, _, _window, cx| { this.restart_session(cx); }, @@ -864,7 +798,7 @@ impl DebugPanel { IconButton::new("debug-stop", IconName::Power) .icon_size(IconSize::XSmall) .on_click(window.listener_for( - &running_session, + &running_state, |this, _, _window, cx| { this.stop_thread(cx); }, @@ -898,7 +832,7 @@ impl DebugPanel { IconButton::new("debug-disconnect", IconName::DebugDetach) .icon_size(IconSize::XSmall) .on_click(window.listener_for( - &running_session, + &running_state, |this, _, _, cx| { this.detach_client(cx); }, @@ -932,30 +866,42 @@ impl DebugPanel { .as_ref() .map(|session| session.read(cx).running_state()) .cloned(), - |this, session| { - this.child( - session.update(cx, |this, cx| { - this.thread_dropdown(window, cx) - }), - ) + |this, running_state| { + this.children({ + let running_state = running_state.clone(); + let threads = + running_state.update(cx, |running_state, cx| { + let session = running_state.session(); + session + .update(cx, |session, cx| session.threads(cx)) + }); + + self.render_thread_dropdown( + &running_state, + threads, + window, + cx, + ) + }) .when(!is_side, |this| this.gap_2().child(Divider::vertical())) }, ), ) .child( h_flex() - .when_some(active_session.as_ref(), |this, session| { - let context_menu = - self.sessions_drop_down_menu(session, window, cx); - this.child(context_menu).gap_2().child(Divider::vertical()) - }) + .children(self.render_session_menu( + self.active_session(), + self.running_state(cx), + window, + cx, + )) .when(!is_side, |this| this.child(new_session_button())), ), ), ) } - fn activate_pane_in_direction( + pub(crate) fn activate_pane_in_direction( &mut self, direction: SplitDirection, window: &mut Window, @@ -970,7 +916,7 @@ impl DebugPanel { } } - fn activate_item( + pub(crate) fn activate_item( &mut self, item: DebuggerPaneItem, window: &mut Window, @@ -985,7 +931,7 @@ impl DebugPanel { } } - fn activate_session( + pub(crate) fn activate_session( &mut self, session_item: Entity, window: &mut Window, diff --git a/crates/debugger_ui/src/debugger_ui.rs b/crates/debugger_ui/src/debugger_ui.rs index 62778ade91..c8bdcb53dc 100644 --- a/crates/debugger_ui/src/debugger_ui.rs +++ b/crates/debugger_ui/src/debugger_ui.rs @@ -13,6 +13,7 @@ use workspace::{ItemHandle, ShutdownDebugAdapters, Workspace}; pub mod attach_modal; pub mod debugger_panel; +mod dropdown_menus; mod new_session_modal; mod persistence; pub(crate) mod session; diff --git a/crates/debugger_ui/src/dropdown_menus.rs b/crates/debugger_ui/src/dropdown_menus.rs new file mode 100644 index 0000000000..7a6da979f4 --- /dev/null +++ b/crates/debugger_ui/src/dropdown_menus.rs @@ -0,0 +1,186 @@ +use gpui::Entity; +use project::debugger::session::{ThreadId, ThreadStatus}; +use ui::{ContextMenu, DropdownMenu, DropdownStyle, Indicator, prelude::*}; + +use crate::{ + debugger_panel::DebugPanel, + session::{DebugSession, running::RunningState}, +}; + +impl DebugPanel { + fn dropdown_label(label: impl Into) -> Label { + Label::new(label).size(LabelSize::Small) + } + + pub fn render_session_menu( + &mut self, + active_session: Option>, + running_state: Option>, + window: &mut Window, + cx: &mut Context, + ) -> Option { + if let Some(running_state) = running_state { + let sessions = self.sessions().clone(); + let weak = cx.weak_entity(); + let running_state = running_state.read(cx); + let label = if let Some(active_session) = active_session { + active_session.read(cx).session(cx).read(cx).label() + } else { + SharedString::new_static("Unknown Session") + }; + + let is_terminated = running_state.session().read(cx).is_terminated(); + let session_state_indicator = { + if is_terminated { + Some(Indicator::dot().color(Color::Error)) + } else { + match running_state.thread_status(cx).unwrap_or_default() { + project::debugger::session::ThreadStatus::Stopped => { + Some(Indicator::dot().color(Color::Conflict)) + } + _ => Some(Indicator::dot().color(Color::Success)), + } + } + }; + + let trigger = h_flex() + .gap_2() + .when_some(session_state_indicator, |this, indicator| { + this.child(indicator) + }) + .justify_between() + .child( + DebugPanel::dropdown_label(label) + .when(is_terminated, |this| this.strikethrough()), + ) + .into_any_element(); + + Some( + DropdownMenu::new_with_element( + "debugger-session-list", + trigger, + ContextMenu::build(window, cx, move |mut this, _, cx| { + let context_menu = cx.weak_entity(); + for session in sessions.into_iter() { + let weak_session = session.downgrade(); + let weak_session_id = weak_session.entity_id(); + + this = this.custom_entry( + { + let weak = weak.clone(); + let context_menu = context_menu.clone(); + move |_, cx| { + weak_session + .read_with(cx, |session, cx| { + let context_menu = context_menu.clone(); + let id: SharedString = format!( + "debug-session-{}", + session.session_id(cx).0 + ) + .into(); + h_flex() + .w_full() + .group(id.clone()) + .justify_between() + .child(session.label_element(cx)) + .child( + IconButton::new( + "close-debug-session", + IconName::Close, + ) + .visible_on_hover(id.clone()) + .icon_size(IconSize::Small) + .on_click({ + let weak = weak.clone(); + move |_, window, cx| { + weak.update(cx, |panel, cx| { + panel.close_session( + weak_session_id, + window, + cx, + ); + }) + .ok(); + context_menu + .update(cx, |this, cx| { + this.cancel( + &Default::default(), + window, + cx, + ); + }) + .ok(); + } + }), + ) + .into_any_element() + }) + .unwrap_or_else(|_| div().into_any_element()) + } + }, + { + let weak = weak.clone(); + move |window, cx| { + weak.update(cx, |panel, cx| { + panel.activate_session(session.clone(), window, cx); + }) + .ok(); + } + }, + ); + } + this + }), + ) + .style(DropdownStyle::Ghost), + ) + } else { + None + } + } + + pub(crate) fn render_thread_dropdown( + &self, + running_state: &Entity, + threads: Vec<(dap::Thread, ThreadStatus)>, + window: &mut Window, + cx: &mut Context, + ) -> Option { + let running_state = running_state.clone(); + let running_state_read = running_state.read(cx); + let thread_id = running_state_read.thread_id(); + let session = running_state_read.session(); + let session_id = session.read(cx).session_id(); + let session_terminated = session.read(cx).is_terminated(); + let selected_thread_name = threads + .iter() + .find(|(thread, _)| thread_id.map(|id| id.0) == Some(thread.id)) + .map(|(thread, _)| thread.name.clone()); + + if let Some(selected_thread_name) = selected_thread_name { + let trigger = DebugPanel::dropdown_label(selected_thread_name).into_any_element(); + Some( + DropdownMenu::new_with_element( + ("thread-list", session_id.0), + trigger, + ContextMenu::build_eager(window, cx, move |mut this, _, _| { + for (thread, _) in threads { + let running_state = running_state.clone(); + let thread_id = thread.id; + this = this.entry(thread.name, None, move |window, cx| { + running_state.update(cx, |running_state, cx| { + running_state.select_thread(ThreadId(thread_id), window, cx); + }); + }); + } + this + }), + ) + .disabled(session_terminated) + .style(DropdownStyle::Ghost), + ) + } else { + None + } + } +} diff --git a/crates/debugger_ui/src/session.rs b/crates/debugger_ui/src/session.rs index ec341f2a40..b6581f6ca1 100644 --- a/crates/debugger_ui/src/session.rs +++ b/crates/debugger_ui/src/session.rs @@ -1,7 +1,6 @@ pub mod running; -use std::{cell::OnceCell, sync::OnceLock}; - +use crate::{StackTraceView, debugger_panel::DebugPanel, persistence::SerializedLayout}; use dap::client::SessionId; use gpui::{ App, Axis, Entity, EventEmitter, FocusHandle, Focusable, Subscription, Task, WeakEntity, @@ -11,14 +10,13 @@ use project::debugger::session::Session; use project::worktree_store::WorktreeStore; use rpc::proto; use running::RunningState; +use std::{cell::OnceCell, sync::OnceLock}; use ui::{Indicator, prelude::*}; use workspace::{ CollaboratorId, FollowableItem, ViewId, Workspace, item::{self, Item}, }; -use crate::{StackTraceView, debugger_panel::DebugPanel, persistence::SerializedLayout}; - pub struct DebugSession { remote_id: Option, running_state: Entity, @@ -159,7 +157,11 @@ impl DebugSession { .gap_2() .when_some(icon, |this, indicator| this.child(indicator)) .justify_between() - .child(Label::new(label).when(is_terminated, |this| this.strikethrough())) + .child( + Label::new(label) + .size(LabelSize::Small) + .when(is_terminated, |this| this.strikethrough()), + ) .into_any_element() } } diff --git a/crates/debugger_ui/src/session/running.rs b/crates/debugger_ui/src/session/running.rs index 69b49fba98..71c51bd0a1 100644 --- a/crates/debugger_ui/src/session/running.rs +++ b/crates/debugger_ui/src/session/running.rs @@ -43,11 +43,10 @@ use task::{ }; use terminal_view::TerminalView; use ui::{ - ActiveTheme, AnyElement, App, ButtonCommon as _, Clickable as _, Context, ContextMenu, - Disableable, DropdownMenu, FluentBuilder, IconButton, IconName, IconSize, InteractiveElement, - IntoElement, Label, LabelCommon as _, ParentElement, Render, SharedString, - StatefulInteractiveElement, Styled, Tab, Tooltip, VisibleOnHover, VisualContext, Window, div, - h_flex, v_flex, + ActiveTheme, AnyElement, App, ButtonCommon as _, Clickable as _, Context, FluentBuilder, + IconButton, IconName, IconSize, InteractiveElement, IntoElement, Label, LabelCommon as _, + ParentElement, Render, SharedString, StatefulInteractiveElement, Styled, Tab, Tooltip, + VisibleOnHover, VisualContext, Window, div, h_flex, v_flex, }; use util::ResultExt; use variable_list::VariableList; @@ -78,6 +77,12 @@ pub struct RunningState { _schedule_serialize: Option>, } +impl RunningState { + pub(crate) fn thread_id(&self) -> Option { + self.thread_id + } +} + impl Render for RunningState { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { let zoomed_pane = self @@ -515,7 +520,7 @@ impl Focusable for DebugTerminal { } impl RunningState { - pub fn new( + pub(crate) fn new( session: Entity, project: Entity, workspace: WeakEntity, @@ -1311,7 +1316,12 @@ impl RunningState { .map(|id| self.session().read(cx).thread_status(id)) } - fn select_thread(&mut self, thread_id: ThreadId, window: &mut Window, cx: &mut Context) { + pub(crate) fn select_thread( + &mut self, + thread_id: ThreadId, + window: &mut Window, + cx: &mut Context, + ) { if self.thread_id.is_some_and(|id| id == thread_id) { return; } @@ -1448,38 +1458,6 @@ impl RunningState { }); } - pub(crate) fn thread_dropdown( - &self, - window: &mut Window, - cx: &mut Context<'_, RunningState>, - ) -> DropdownMenu { - let state = cx.entity(); - let session_terminated = self.session.read(cx).is_terminated(); - let threads = self.session.update(cx, |this, cx| this.threads(cx)); - let selected_thread_name = threads - .iter() - .find(|(thread, _)| self.thread_id.map(|id| id.0) == Some(thread.id)) - .map(|(thread, _)| thread.name.clone()) - .unwrap_or("Threads".to_owned()); - DropdownMenu::new( - ("thread-list", self.session_id.0), - selected_thread_name, - ContextMenu::build_eager(window, cx, move |mut this, _, _| { - for (thread, _) in threads { - let state = state.clone(); - let thread_id = thread.id; - this = this.entry(thread.name, None, move |window, cx| { - state.update(cx, |state, cx| { - state.select_thread(ThreadId(thread_id), window, cx); - }); - }); - } - this - }), - ) - .disabled(session_terminated) - } - fn default_pane_layout( project: Entity, workspace: &WeakEntity, diff --git a/crates/ui/src/components/dropdown_menu.rs b/crates/ui/src/components/dropdown_menu.rs index 8f191c5431..174f893b5b 100644 --- a/crates/ui/src/components/dropdown_menu.rs +++ b/crates/ui/src/components/dropdown_menu.rs @@ -1,7 +1,14 @@ -use gpui::{ClickEvent, Corner, CursorStyle, Entity, MouseButton}; +use gpui::{ClickEvent, Corner, CursorStyle, Entity, Hsla, MouseButton}; use crate::{ContextMenu, PopoverMenu, prelude::*}; +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)] +pub enum DropdownStyle { + #[default] + Solid, + Ghost, +} + enum LabelKind { Text(SharedString), Element(AnyElement), @@ -11,6 +18,7 @@ enum LabelKind { pub struct DropdownMenu { id: ElementId, label: LabelKind, + style: DropdownStyle, menu: Entity, full_width: bool, disabled: bool, @@ -25,6 +33,7 @@ impl DropdownMenu { Self { id: id.into(), label: LabelKind::Text(label.into()), + style: DropdownStyle::default(), menu, full_width: false, disabled: false, @@ -39,12 +48,18 @@ impl DropdownMenu { Self { id: id.into(), label: LabelKind::Element(label), + style: DropdownStyle::default(), menu, full_width: false, disabled: false, } } + pub fn style(mut self, style: DropdownStyle) -> Self { + self.style = style; + self + } + pub fn full_width(mut self, full_width: bool) -> Self { self.full_width = full_width; self @@ -66,7 +81,8 @@ impl RenderOnce for DropdownMenu { .trigger( DropdownMenuTrigger::new(self.label) .full_width(self.full_width) - .disabled(self.disabled), + .disabled(self.disabled) + .style(self.style), ) .attach(Corner::BottomLeft) } @@ -135,12 +151,35 @@ impl Component for DropdownMenu { } } +#[derive(Debug, Clone, Copy)] +pub struct DropdownTriggerStyle { + pub bg: Hsla, +} + +impl DropdownTriggerStyle { + pub fn for_style(style: DropdownStyle, cx: &App) -> Self { + let colors = cx.theme().colors(); + + if style == DropdownStyle::Solid { + Self { + // why is this editor_background? + bg: colors.editor_background, + } + } else { + Self { + bg: colors.ghost_element_background, + } + } + } +} + #[derive(IntoElement)] struct DropdownMenuTrigger { label: LabelKind, full_width: bool, selected: bool, disabled: bool, + style: DropdownStyle, cursor_style: CursorStyle, on_click: Option>, } @@ -152,6 +191,7 @@ impl DropdownMenuTrigger { full_width: false, selected: false, disabled: false, + style: DropdownStyle::default(), cursor_style: CursorStyle::default(), on_click: None, } @@ -161,6 +201,11 @@ impl DropdownMenuTrigger { self.full_width = full_width; self } + + pub fn style(mut self, style: DropdownStyle) -> Self { + self.style = style; + self + } } impl Disableable for DropdownMenuTrigger { @@ -193,11 +238,13 @@ impl RenderOnce for DropdownMenuTrigger { fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { let disabled = self.disabled; + let style = DropdownTriggerStyle::for_style(self.style, cx); + h_flex() .id("dropdown-menu-trigger") .justify_between() .rounded_sm() - .bg(cx.theme().colors().editor_background) + .bg(style.bg) .pl_2() .pr_1p5() .py_0p5()