diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index a71841b8a2..a671b6ebee 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -30,9 +30,8 @@ use task::DebugTaskDefinition; use terminal_view::terminal_panel::TerminalPanel; use ui::{ContextMenu, Divider, DropdownMenu, Tooltip, prelude::*}; use workspace::{ - Pane, Workspace, + Workspace, dock::{DockPosition, Panel, PanelEvent}, - pane, }; pub enum DebugPanelEvent { @@ -55,11 +54,13 @@ pub enum DebugPanelEvent { actions!(debug_panel, [ToggleFocus]); pub struct DebugPanel { size: Pixels, - pane: Entity, + sessions: Vec>, + active_session: Option>, /// This represents the last debug definition that was created in the new session modal pub(crate) past_debug_definition: Option, project: WeakEntity, workspace: WeakEntity, + focus_handle: FocusHandle, _subscriptions: Vec, } @@ -72,36 +73,17 @@ impl DebugPanel { cx.new(|cx| { let project = workspace.project().clone(); let dap_store = project.read(cx).dap_store(); - let pane = cx.new(|cx| { - let mut pane = Pane::new( - workspace.weak_handle(), - project.clone(), - Default::default(), - None, - gpui::NoAction.boxed_clone(), - window, - cx, - ); - pane.set_can_split(None); - pane.set_can_navigate(true, cx); - pane.display_nav_history_buttons(None); - pane.set_should_display_tab_bar(|_window, _cx| false); - pane.set_close_pane_if_empty(true, cx); - pane - }); - - let _subscriptions = vec![ - cx.observe(&pane, |_, _, cx| cx.notify()), - cx.subscribe_in(&pane, window, Self::handle_pane_event), - cx.subscribe_in(&dap_store, window, Self::handle_dap_store_event), - ]; + let _subscriptions = + vec![cx.subscribe_in(&dap_store, window, Self::handle_dap_store_event)]; let debug_panel = Self { - pane, size: px(300.), + sessions: vec![], + active_session: None, _subscriptions, past_debug_definition: None, + focus_handle: cx.focus_handle(), project: project.downgrade(), workspace: workspace.weak_handle(), }; @@ -130,7 +112,7 @@ impl DebugPanel { cx.observe(&debug_panel, |_, debug_panel, cx| { let (has_active_session, supports_restart, support_step_back) = debug_panel .update(cx, |this, cx| { - this.active_session(cx) + this.active_session() .map(|item| { let running = item.read(cx).mode().as_running().cloned(); @@ -192,11 +174,8 @@ impl DebugPanel { }) } - pub fn active_session(&self, cx: &App) -> Option> { - self.pane - .read(cx) - .active_item() - .and_then(|panel| panel.downcast::()) + pub fn active_session(&self) -> Option> { + self.active_session.clone() } pub fn debug_panel_items_by_client( @@ -204,10 +183,8 @@ impl DebugPanel { client_id: &SessionId, cx: &Context, ) -> Vec> { - self.pane - .read(cx) - .items() - .filter_map(|item| item.downcast::()) + self.sessions + .iter() .filter(|item| item.read(cx).session_id(cx) == Some(*client_id)) .map(|item| item.clone()) .collect() @@ -218,15 +195,14 @@ impl DebugPanel { client_id: SessionId, cx: &mut Context, ) -> Option> { - self.pane - .read(cx) - .items() - .filter_map(|item| item.downcast::()) + self.sessions + .iter() .find(|item| { let item = item.read(cx); item.session_id(cx) == Some(client_id) }) + .cloned() } fn handle_dap_store_event( @@ -248,10 +224,11 @@ impl DebugPanel { return log::error!("Debug Panel out lived it's weak reference to Project"); }; - if self.pane.read_with(cx, |pane, cx| { - pane.items_of_type::() - .any(|item| item.read(cx).session_id(cx) == Some(*session_id)) - }) { + if self + .sessions + .iter() + .any(|item| item.read(cx).session_id(cx) == Some(*session_id)) + { // We already have an item for this session. return; } @@ -264,11 +241,8 @@ impl DebugPanel { cx, ); - self.pane.update(cx, |pane, cx| { - pane.add_item(Box::new(session_item), true, true, None, window, cx); - window.focus(&pane.focus_handle(cx)); - cx.notify(); - }); + self.sessions.push(session_item.clone()); + self.activate_session(session_item, window, cx); } dap_store::DapStoreEvent::RunInTerminal { title, @@ -362,63 +336,9 @@ impl DebugPanel { }) } - fn handle_pane_event( - &mut self, - _: &Entity, - event: &pane::Event, - window: &mut Window, - cx: &mut Context, - ) { - match event { - pane::Event::Remove { .. } => cx.emit(PanelEvent::Close), - pane::Event::ZoomIn => cx.emit(PanelEvent::ZoomIn), - pane::Event::ZoomOut => cx.emit(PanelEvent::ZoomOut), - pane::Event::AddItem { item } => { - self.workspace - .update(cx, |workspace, cx| { - item.added_to_pane(workspace, self.pane.clone(), window, cx) - }) - .ok(); - } - pane::Event::RemovedItem { item } => { - if let Some(debug_session) = item.downcast::() { - debug_session.update(cx, |session, cx| { - session.shutdown(cx); - }) - } - } - pane::Event::ActivateItem { - local: _, - focus_changed, - } => { - if *focus_changed { - if let Some(debug_session) = self - .pane - .read(cx) - .active_item() - .and_then(|item| item.downcast::()) - { - if let Some(running) = debug_session - .read_with(cx, |session, _| session.mode().as_running().cloned()) - { - running.update(cx, |running, cx| { - running.go_to_selected_stack_frame(window, cx); - }); - } - } - } - } - - _ => {} - } - } - fn top_controls_strip(&self, window: &mut Window, cx: &mut Context) -> Option
{ - let active_session = self - .pane - .read(cx) - .active_item() - .and_then(|item| item.downcast::()); + let active_session = self.active_session.clone(); + Some( h_flex() .border_b_1() @@ -609,34 +529,29 @@ impl DebugPanel { }, ) .when_some(active_session.as_ref(), |this, session| { - let pane = self.pane.downgrade(); + let sessions = self.sessions.clone(); + let weak = cx.weak_entity(); let label = session.read(cx).label(cx); this.child(DropdownMenu::new( "debugger-session-list", label, ContextMenu::build(window, cx, move |mut this, _, cx| { - let sessions = pane - .read_with(cx, |pane, _| { - pane.items().map(|item| item.boxed_clone()).collect() - }) - .ok() - .unwrap_or_else(Vec::new); - for (index, item) in sessions.into_iter().enumerate() { - if let Some(session) = item.downcast::() { - let pane = pane.clone(); - this = this.entry( - session.read(cx).label(cx), - None, - move |window, cx| { - pane.update(cx, |pane, cx| { - pane.activate_item( - index, true, true, window, cx, - ); - }) - .ok(); - }, - ); - } + for item in sessions { + let weak = weak.clone(); + this = this.entry( + session.read(cx).label(cx), + None, + move |window, cx| { + weak.update(cx, |panel, cx| { + panel.activate_session( + item.clone(), + window, + cx, + ); + }) + .ok(); + }, + ); } this }), @@ -680,6 +595,25 @@ impl DebugPanel { ), ) } + + fn activate_session( + &mut self, + session_item: Entity, + window: &mut Window, + cx: &mut Context, + ) { + debug_assert!(self.sessions.contains(&session_item)); + session_item.focus_handle(cx).focus(window); + session_item.update(cx, |this, cx| { + if let Some(running) = this.mode().as_running() { + running.update(cx, |this, cx| { + this.go_to_selected_stack_frame(window, cx); + }); + } + }); + self.active_session = Some(session_item); + cx.notify(); + } } impl EventEmitter for DebugPanel {} @@ -687,16 +621,12 @@ impl EventEmitter for DebugPanel {} impl EventEmitter for DebugPanel {} impl Focusable for DebugPanel { - fn focus_handle(&self, cx: &App) -> FocusHandle { - self.pane.focus_handle(cx) + fn focus_handle(&self, _: &App) -> FocusHandle { + self.focus_handle.clone() } } impl Panel for DebugPanel { - fn pane(&self) -> Option> { - Some(self.pane.clone()) - } - fn persistent_name() -> &'static str { "DebugPanel" } @@ -753,7 +683,9 @@ impl Panel for DebugPanel { impl Render for DebugPanel { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let has_sessions = self.pane.read(cx).items_len() > 0; + let has_sessions = self.sessions.len() > 0; + debug_assert_eq!(has_sessions, self.active_session.is_some()); + v_flex() .size_full() .key_context("DebugPanel") @@ -761,7 +693,7 @@ impl Render for DebugPanel { .track_focus(&self.focus_handle(cx)) .map(|this| { if has_sessions { - this.child(self.pane.clone()) + this.children(self.active_session.clone()) } else { this.child( v_flex() diff --git a/crates/debugger_ui/src/debugger_ui.rs b/crates/debugger_ui/src/debugger_ui.rs index bd38fee7cd..1e1db067b7 100644 --- a/crates/debugger_ui/src/debugger_ui.rs +++ b/crates/debugger_ui/src/debugger_ui.rs @@ -52,7 +52,7 @@ pub fn init(cx: &mut App) { if let Some(debug_panel) = workspace.panel::(cx) { if let Some(active_item) = debug_panel.read_with(cx, |panel, cx| { panel - .active_session(cx) + .active_session() .and_then(|session| session.read(cx).mode().as_running().cloned()) }) { active_item.update(cx, |item, cx| item.pause_thread(cx)) @@ -63,7 +63,7 @@ pub fn init(cx: &mut App) { if let Some(debug_panel) = workspace.panel::(cx) { if let Some(active_item) = debug_panel.read_with(cx, |panel, cx| { panel - .active_session(cx) + .active_session() .and_then(|session| session.read(cx).mode().as_running().cloned()) }) { active_item.update(cx, |item, cx| item.restart_session(cx)) @@ -74,7 +74,7 @@ pub fn init(cx: &mut App) { if let Some(debug_panel) = workspace.panel::(cx) { if let Some(active_item) = debug_panel.read_with(cx, |panel, cx| { panel - .active_session(cx) + .active_session() .and_then(|session| session.read(cx).mode().as_running().cloned()) }) { active_item.update(cx, |item, cx| item.step_in(cx)) @@ -85,7 +85,7 @@ pub fn init(cx: &mut App) { if let Some(debug_panel) = workspace.panel::(cx) { if let Some(active_item) = debug_panel.read_with(cx, |panel, cx| { panel - .active_session(cx) + .active_session() .and_then(|session| session.read(cx).mode().as_running().cloned()) }) { active_item.update(cx, |item, cx| item.step_over(cx)) @@ -96,7 +96,7 @@ pub fn init(cx: &mut App) { if let Some(debug_panel) = workspace.panel::(cx) { if let Some(active_item) = debug_panel.read_with(cx, |panel, cx| { panel - .active_session(cx) + .active_session() .and_then(|session| session.read(cx).mode().as_running().cloned()) }) { active_item.update(cx, |item, cx| item.step_back(cx)) @@ -107,7 +107,7 @@ pub fn init(cx: &mut App) { if let Some(debug_panel) = workspace.panel::(cx) { if let Some(active_item) = debug_panel.read_with(cx, |panel, cx| { panel - .active_session(cx) + .active_session() .and_then(|session| session.read(cx).mode().as_running().cloned()) }) { active_item.update(cx, |item, cx| item.stop_thread(cx)) @@ -118,7 +118,7 @@ pub fn init(cx: &mut App) { if let Some(debug_panel) = workspace.panel::(cx) { if let Some(active_item) = debug_panel.read_with(cx, |panel, cx| { panel - .active_session(cx) + .active_session() .and_then(|session| session.read(cx).mode().as_running().cloned()) }) { active_item.update(cx, |item, cx| item.toggle_ignore_breakpoints(cx)) diff --git a/crates/debugger_ui/src/session.rs b/crates/debugger_ui/src/session.rs index a08fb8f584..22768538a9 100644 --- a/crates/debugger_ui/src/session.rs +++ b/crates/debugger_ui/src/session.rs @@ -43,14 +43,6 @@ pub enum DebugPanelItemEvent { Stopped { go_to_stack_frame: bool }, } -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub enum ThreadItem { - Console, - LoadedSource, - Modules, - Variables, -} - impl DebugSession { pub(crate) fn running( project: Entity, @@ -60,7 +52,15 @@ impl DebugSession { window: &mut Window, cx: &mut App, ) -> Entity { - let mode = cx.new(|cx| RunningState::new(session.clone(), workspace.clone(), window, cx)); + let mode = cx.new(|cx| { + RunningState::new( + session.clone(), + project.clone(), + workspace.clone(), + window, + cx, + ) + }); cx.new(|cx| Self { _subscriptions: [cx.subscribe(&mode, |_, _, _, cx| { @@ -81,6 +81,7 @@ impl DebugSession { } } + #[expect(unused)] pub(crate) fn shutdown(&mut self, cx: &mut Context) { match &self.mode { DebugSessionState::Running(state) => state.update(cx, |state, cx| state.shutdown(cx)), diff --git a/crates/debugger_ui/src/session/running.rs b/crates/debugger_ui/src/session/running.rs index 9c444796d3..6d07e01203 100644 --- a/crates/debugger_ui/src/session/running.rs +++ b/crates/debugger_ui/src/session/running.rs @@ -4,47 +4,66 @@ mod module_list; pub mod stack_frame_list; pub mod variable_list; -use super::{DebugPanelItemEvent, ThreadItem}; +use std::{any::Any, ops::ControlFlow, sync::Arc}; + +use super::DebugPanelItemEvent; +use collections::HashMap; use console::Console; use dap::{Capabilities, Thread, client::SessionId, debugger_settings::DebuggerSettings}; -use gpui::{AppContext, Entity, EventEmitter, FocusHandle, Focusable, Subscription, WeakEntity}; +use gpui::{ + Action as _, AnyView, AppContext, Entity, EntityId, EventEmitter, FocusHandle, Focusable, + NoAction, Subscription, WeakEntity, +}; use loaded_source_list::LoadedSourceList; use module_list::ModuleList; -use project::debugger::session::{Session, SessionEvent, ThreadId, ThreadStatus}; +use project::{ + Project, + debugger::session::{Session, SessionEvent, ThreadId, ThreadStatus}, +}; use rpc::proto::ViewId; use settings::Settings; use stack_frame_list::StackFrameList; use ui::{ - ActiveTheme, AnyElement, App, Button, Context, ContextMenu, DropdownMenu, FluentBuilder, - Indicator, InteractiveElement, IntoElement, ParentElement, Render, SharedString, - StatefulInteractiveElement, Styled, Window, div, h_flex, v_flex, + App, Context, ContextMenu, DropdownMenu, InteractiveElement, IntoElement, ParentElement, + Render, SharedString, Styled, Window, div, h_flex, v_flex, }; use util::ResultExt; use variable_list::VariableList; -use workspace::Workspace; +use workspace::{ + ActivePaneDecorator, DraggedTab, Item, Pane, PaneGroup, Workspace, move_item, pane::Event, +}; pub struct RunningState { session: Entity, thread_id: Option, - console: Entity, focus_handle: FocusHandle, _remote_id: Option, - show_console_indicator: bool, - module_list: Entity, - active_thread_item: ThreadItem, workspace: WeakEntity, session_id: SessionId, variable_list: Entity, _subscriptions: Vec, stack_frame_list: Entity, - loaded_source_list: Entity, + _module_list: Entity, + _console: Entity, + panes: PaneGroup, + pane_close_subscriptions: HashMap, } impl Render for RunningState { - fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { - let threads = self.session.update(cx, |this, cx| this.threads(cx)); - self.select_current_thread(&threads, cx); - + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + let active = self.panes.panes().into_iter().next(); + let x = if let Some(active) = active { + self.panes + .render( + None, + &ActivePaneDecorator::new(active, &self.workspace), + window, + cx, + ) + .into_any_element() + } else { + div().into_any_element() + }; let thread_status = self .thread_id .map(|thread_id| self.session.read(cx).thread_status(thread_id)) @@ -53,88 +72,185 @@ impl Render for RunningState { self.variable_list.update(cx, |this, cx| { this.disabled(thread_status != ThreadStatus::Stopped, cx); }); - - let active_thread_item = &self.active_thread_item; - - let capabilities = self.capabilities(cx); - h_flex() - .key_context("DebugPanelItem") - .track_focus(&self.focus_handle(cx)) + v_flex() .size_full() - .items_start() - .child( - v_flex().size_full().items_start().child( - h_flex() - .size_full() - .items_start() - .p_1() - .gap_4() - .child(self.stack_frame_list.clone()), - ), - ) - .child( - v_flex() - .border_l_1() - .border_color(cx.theme().colors().border_variant) - .size_full() - .items_start() - .child( - h_flex() - .border_b_1() - .w_full() - .border_color(cx.theme().colors().border_variant) - .child(self.render_entry_button( - &SharedString::from("Variables"), - ThreadItem::Variables, - cx, - )) - .when( - capabilities.supports_modules_request.unwrap_or_default(), - |this| { - this.child(self.render_entry_button( - &SharedString::from("Modules"), - ThreadItem::Modules, - cx, - )) - }, - ) - .when( - capabilities - .supports_loaded_sources_request - .unwrap_or_default(), - |this| { - this.child(self.render_entry_button( - &SharedString::from("Loaded Sources"), - ThreadItem::LoadedSource, - cx, - )) - }, - ) - .child(self.render_entry_button( - &SharedString::from("Console"), - ThreadItem::Console, - cx, - )), - ) - .when(*active_thread_item == ThreadItem::Variables, |this| { - this.child(self.variable_list.clone()) - }) - .when(*active_thread_item == ThreadItem::Modules, |this| { - this.size_full().child(self.module_list.clone()) - }) - .when(*active_thread_item == ThreadItem::LoadedSource, |this| { - this.size_full().child(self.loaded_source_list.clone()) - }) - .when(*active_thread_item == ThreadItem::Console, |this| { - this.child(self.console.clone()) - }), - ) + .key_context("DebugSessionItem") + .track_focus(&self.focus_handle(cx)) + .child(h_flex().flex_1().child(x)) } } +struct SubView { + inner: AnyView, + pane_focus_handle: FocusHandle, + tab_name: SharedString, +} + +impl SubView { + fn new( + pane_focus_handle: FocusHandle, + view: AnyView, + tab_name: SharedString, + cx: &mut App, + ) -> Entity { + cx.new(|_| Self { + tab_name, + inner: view, + pane_focus_handle, + }) + } +} +impl Focusable for SubView { + fn focus_handle(&self, _: &App) -> FocusHandle { + self.pane_focus_handle.clone() + } +} +impl EventEmitter<()> for SubView {} +impl Item for SubView { + type Event = (); + fn tab_content_text(&self, _window: &Window, _cx: &App) -> Option { + Some(self.tab_name.clone()) + } +} + +impl Render for SubView { + fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { + v_flex().size_full().child(self.inner.clone()) + } +} + +fn new_debugger_pane( + workspace: WeakEntity, + project: Entity, + window: &mut Window, + cx: &mut Context, +) -> Entity { + let weak_running = cx.weak_entity(); + let custom_drop_handle = { + let workspace = workspace.clone(); + let project = project.downgrade(); + let weak_running = weak_running.clone(); + move |pane: &mut Pane, any: &dyn Any, window: &mut Window, cx: &mut Context| { + let Some(tab) = any.downcast_ref::() else { + return ControlFlow::Break(()); + }; + let Some(project) = project.upgrade() else { + return ControlFlow::Break(()); + }; + let this_pane = cx.entity().clone(); + let item = if tab.pane == this_pane { + pane.item_for_index(tab.ix) + } else { + tab.pane.read(cx).item_for_index(tab.ix) + }; + let Some(item) = item.filter(|item| item.downcast::().is_some()) else { + return ControlFlow::Break(()); + }; + + let source = tab.pane.clone(); + let item_id_to_move = item.item_id(); + + let Ok(new_split_pane) = pane + .drag_split_direction() + .map(|split_direction| { + weak_running.update(cx, |running, cx| { + let new_pane = + new_debugger_pane(workspace.clone(), project.clone(), window, cx); + let _previous_subscription = running.pane_close_subscriptions.insert( + new_pane.entity_id(), + cx.subscribe(&new_pane, RunningState::handle_pane_event), + ); + debug_assert!(_previous_subscription.is_none()); + running + .panes + .split(&this_pane, &new_pane, split_direction)?; + anyhow::Ok(new_pane) + }) + }) + .transpose() + else { + return ControlFlow::Break(()); + }; + + match new_split_pane.transpose() { + // Source pane may be the one currently updated, so defer the move. + Ok(Some(new_pane)) => cx + .spawn_in(window, async move |_, cx| { + cx.update(|window, cx| { + move_item( + &source, + &new_pane, + item_id_to_move, + new_pane.read(cx).active_item_index(), + window, + cx, + ); + }) + .ok(); + }) + .detach(), + // If we drop into existing pane or current pane, + // regular pane drop handler will take care of it, + // using the right tab index for the operation. + Ok(None) => return ControlFlow::Continue(()), + err @ Err(_) => { + err.log_err(); + return ControlFlow::Break(()); + } + }; + + ControlFlow::Break(()) + } + }; + + let ret = cx.new(move |cx| { + let mut pane = Pane::new( + workspace.clone(), + project.clone(), + Default::default(), + None, + NoAction.boxed_clone(), + window, + cx, + ); + pane.set_can_split(Some(Arc::new(move |pane, dragged_item, _window, cx| { + if let Some(tab) = dragged_item.downcast_ref::() { + let is_current_pane = tab.pane == cx.entity(); + let Some(can_drag_away) = weak_running + .update(cx, |running_state, _| { + let current_panes = running_state.panes.panes(); + !current_panes.contains(&&tab.pane) + || current_panes.len() > 1 + || (!is_current_pane || pane.items_len() > 1) + }) + .ok() + else { + return false; + }; + if can_drag_away { + let item = if is_current_pane { + pane.item_for_index(tab.ix) + } else { + tab.pane.read(cx).item_for_index(tab.ix) + }; + if let Some(item) = item { + return item.downcast::().is_some(); + } + } + } + false + }))); + pane.display_nav_history_buttons(None); + pane.set_custom_drop_handle(cx, custom_drop_handle); + pane + }); + + ret +} impl RunningState { pub fn new( session: Entity, + project: Entity, workspace: WeakEntity, window: &mut Window, cx: &mut Context, @@ -151,6 +267,7 @@ impl RunningState { let module_list = cx.new(|cx| ModuleList::new(session.clone(), workspace.clone(), cx)); + #[expect(unused)] let loaded_source_list = cx.new(|cx| LoadedSourceList::new(session.clone(), cx)); let console = cx.new(|cx| { @@ -188,24 +305,116 @@ impl RunningState { }), ]; + let leftmost_pane = new_debugger_pane(workspace.clone(), project.clone(), window, cx); + leftmost_pane.update(cx, |this, cx| { + this.add_item( + Box::new(SubView::new( + this.focus_handle(cx), + stack_frame_list.clone().into(), + SharedString::new_static("Frames"), + cx, + )), + true, + false, + None, + window, + cx, + ); + }); + let center_pane = new_debugger_pane(workspace.clone(), project.clone(), window, cx); + center_pane.update(cx, |this, cx| { + this.add_item( + Box::new(SubView::new( + variable_list.focus_handle(cx), + variable_list.clone().into(), + SharedString::new_static("Variables"), + cx, + )), + true, + false, + None, + window, + cx, + ); + this.add_item( + Box::new(SubView::new( + this.focus_handle(cx), + module_list.clone().into(), + SharedString::new_static("Modules"), + cx, + )), + true, + false, + None, + window, + cx, + ); + }); + let rightmost_pane = new_debugger_pane(workspace.clone(), project.clone(), window, cx); + rightmost_pane.update(cx, |this, cx| { + this.add_item( + Box::new(SubView::new( + this.focus_handle(cx), + console.clone().into(), + SharedString::new_static("Console"), + cx, + )), + true, + false, + None, + window, + cx, + ); + }); + let pane_close_subscriptions = HashMap::from_iter( + [&leftmost_pane, ¢er_pane, &rightmost_pane] + .into_iter() + .map(|entity| { + ( + entity.entity_id(), + cx.subscribe(entity, Self::handle_pane_event), + ) + }), + ); + let group_root = workspace::PaneAxis::new( + gpui::Axis::Horizontal, + [leftmost_pane, center_pane, rightmost_pane] + .into_iter() + .map(workspace::Member::Pane) + .collect(), + ); + + let panes = PaneGroup::with_root(workspace::Member::Axis(group_root)); + Self { session, - console, workspace, - module_list, focus_handle, variable_list, _subscriptions, thread_id: None, _remote_id: None, stack_frame_list, - loaded_source_list, session_id, - show_console_indicator: false, - active_thread_item: ThreadItem::Variables, + panes, + _module_list: module_list, + _console: console, + pane_close_subscriptions, } } + fn handle_pane_event( + this: &mut RunningState, + source_pane: Entity, + event: &Event, + cx: &mut Context, + ) { + if let Event::Remove { .. } = event { + let _did_find_pane = this.panes.remove(&source_pane).is_ok(); + debug_assert!(_did_find_pane); + cx.notify(); + } + } pub(crate) fn go_to_selected_stack_frame(&self, window: &Window, cx: &mut Context) { if self.thread_id.is_some() { self.stack_frame_list @@ -221,12 +430,6 @@ impl RunningState { self.session_id } - #[cfg(test)] - pub fn set_thread_item(&mut self, thread_item: ThreadItem, cx: &mut Context) { - self.active_thread_item = thread_item; - cx.notify() - } - #[cfg(test)] pub fn stack_frame_list(&self) -> &Entity { &self.stack_frame_list @@ -234,14 +437,31 @@ impl RunningState { #[cfg(test)] pub fn console(&self) -> &Entity { - &self.console + &self._console } #[cfg(test)] pub(crate) fn module_list(&self) -> &Entity { - &self.module_list + &self._module_list } + #[cfg(test)] + pub(crate) fn activate_variable_list(&self, window: &mut Window, cx: &mut App) { + let (variable_list_position, pane) = self + .panes + .panes() + .into_iter() + .find_map(|pane| { + pane.read(cx) + .items_of_type::() + .position(|view| view.read(cx).tab_name == *"Variables") + .map(|view| (view, pane)) + }) + .unwrap(); + pane.update(cx, |this, cx| { + this.activate_item(variable_list_position, true, true, window, cx); + }) + } #[cfg(test)] pub(crate) fn variable_list(&self) -> &Entity { &self.variable_list @@ -292,41 +512,6 @@ impl RunningState { cx.notify(); } - fn render_entry_button( - &self, - label: &SharedString, - thread_item: ThreadItem, - cx: &mut Context, - ) -> AnyElement { - let has_indicator = - matches!(thread_item, ThreadItem::Console) && self.show_console_indicator; - - div() - .id(label.clone()) - .px_2() - .py_1() - .cursor_pointer() - .border_b_2() - .when(self.active_thread_item == thread_item, |this| { - this.border_color(cx.theme().colors().border) - }) - .child( - h_flex() - .child(Button::new(label.clone(), label.clone())) - .when(has_indicator, |this| this.child(Indicator::dot())), - ) - .on_click(cx.listener(move |this, _, _window, cx| { - this.active_thread_item = thread_item; - - if matches!(this.active_thread_item, ThreadItem::Console) { - this.show_console_indicator = false; - } - - cx.notify(); - })) - .into_any_element() - } - pub fn continue_thread(&mut self, cx: &mut Context) { let Some(thread_id) = self.thread_id else { return; diff --git a/crates/debugger_ui/src/session/running/variable_list.rs b/crates/debugger_ui/src/session/running/variable_list.rs index 2310dcb74f..05352b3b17 100644 --- a/crates/debugger_ui/src/session/running/variable_list.rs +++ b/crates/debugger_ui/src/session/running/variable_list.rs @@ -687,6 +687,7 @@ impl VariableList { .child( ListItem::new(SharedString::from(format!("scope-{}", var_ref))) .selectable(false) + .disabled(self.disabled) .indent_level(state.depth + 1) .indent_step_size(px(20.)) .always_show_disclosure_icon(true) @@ -695,7 +696,15 @@ impl VariableList { let var_path = entry.path.clone(); cx.listener(move |this, _, _, cx| this.toggle_entry(&var_path, cx)) }) - .child(div().text_ui(cx).w_full().child(scope.name.clone())), + .child( + div() + .text_ui(cx) + .w_full() + .when(self.disabled, |this| { + this.text_color(Color::Disabled.color(cx)) + }) + .child(scope.name.clone()), + ), ) .into_any() } @@ -716,20 +725,27 @@ impl VariableList { }; let syntax_color_for = |name| cx.theme().syntax().get(name).color; - let variable_name_color = match &dap - .presentation_hint - .as_ref() - .and_then(|hint| hint.kind.as_ref()) - .unwrap_or(&VariablePresentationHintKind::Unknown) - { - VariablePresentationHintKind::Class - | VariablePresentationHintKind::BaseClass - | VariablePresentationHintKind::InnerClass - | VariablePresentationHintKind::MostDerivedClass => syntax_color_for("type"), - VariablePresentationHintKind::Data => syntax_color_for("variable"), - VariablePresentationHintKind::Unknown | _ => syntax_color_for("variable"), + let variable_name_color = if self.disabled { + Some(Color::Disabled.color(cx)) + } else { + match &dap + .presentation_hint + .as_ref() + .and_then(|hint| hint.kind.as_ref()) + .unwrap_or(&VariablePresentationHintKind::Unknown) + { + VariablePresentationHintKind::Class + | VariablePresentationHintKind::BaseClass + | VariablePresentationHintKind::InnerClass + | VariablePresentationHintKind::MostDerivedClass => syntax_color_for("type"), + VariablePresentationHintKind::Data => syntax_color_for("variable"), + VariablePresentationHintKind::Unknown | _ => syntax_color_for("variable"), + } }; - let variable_color = syntax_color_for("variable.special"); + let variable_color = self + .disabled + .then(|| Color::Disabled.color(cx)) + .or_else(|| syntax_color_for("variable.special")); let var_ref = dap.variables_reference; let colors = get_entry_color(cx); diff --git a/crates/debugger_ui/src/tests.rs b/crates/debugger_ui/src/tests.rs index 441f5c3da8..0db43bb379 100644 --- a/crates/debugger_ui/src/tests.rs +++ b/crates/debugger_ui/src/tests.rs @@ -76,7 +76,7 @@ pub fn active_debug_session_panel( .update(cx, |workspace, _window, cx| { let debug_panel = workspace.panel::(cx).unwrap(); debug_panel - .update(cx, |this, cx| this.active_session(cx)) + .update(cx, |this, _| this.active_session()) .unwrap() }) .unwrap() diff --git a/crates/debugger_ui/src/tests/console.rs b/crates/debugger_ui/src/tests/console.rs index 43d5de0c60..b665473e5a 100644 --- a/crates/debugger_ui/src/tests/console.rs +++ b/crates/debugger_ui/src/tests/console.rs @@ -101,10 +101,6 @@ async fn test_handle_output_event(executor: BackgroundExecutor, cx: &mut TestApp .clone() }); - running_state.update(cx, |state, cx| { - state.set_thread_item(session::ThreadItem::Console, cx); - cx.refresh_windows(); - }); cx.run_until_parked(); // assert we have output from before the thread stopped @@ -112,7 +108,7 @@ async fn test_handle_output_event(executor: BackgroundExecutor, cx: &mut TestApp .update(cx, |workspace, _window, cx| { let debug_panel = workspace.panel::(cx).unwrap(); let active_debug_session_panel = debug_panel - .update(cx, |this, cx| this.active_session(cx)) + .update(cx, |this, _| this.active_session()) .unwrap(); assert_eq!( @@ -151,8 +147,7 @@ async fn test_handle_output_event(executor: BackgroundExecutor, cx: &mut TestApp .await; cx.run_until_parked(); - running_state.update(cx, |state, cx| { - state.set_thread_item(session::ThreadItem::Console, cx); + running_state.update(cx, |_, cx| { cx.refresh_windows(); }); cx.run_until_parked(); @@ -162,7 +157,7 @@ async fn test_handle_output_event(executor: BackgroundExecutor, cx: &mut TestApp .update(cx, |workspace, _window, cx| { let debug_panel = workspace.panel::(cx).unwrap(); let active_session_panel = debug_panel - .update(cx, |this, cx| this.active_session(cx)) + .update(cx, |this, _| this.active_session()) .unwrap(); assert_eq!( diff --git a/crates/debugger_ui/src/tests/debugger_panel.rs b/crates/debugger_ui/src/tests/debugger_panel.rs index 48722676f5..e685de7a14 100644 --- a/crates/debugger_ui/src/tests/debugger_panel.rs +++ b/crates/debugger_ui/src/tests/debugger_panel.rs @@ -85,9 +85,8 @@ async fn test_basic_show_debug_panel(executor: BackgroundExecutor, cx: &mut Test workspace .update(cx, |workspace, _window, cx| { let debug_panel = workspace.panel::(cx).unwrap(); - let active_session = debug_panel.update(cx, |debug_panel, cx| { - debug_panel.active_session(cx).unwrap() - }); + let active_session = + debug_panel.update(cx, |debug_panel, _| debug_panel.active_session().unwrap()); let running_state = active_session.update(cx, |active_session, _| { active_session @@ -98,9 +97,7 @@ async fn test_basic_show_debug_panel(executor: BackgroundExecutor, cx: &mut Test }); debug_panel.update(cx, |this, cx| { - assert!(this.active_session(cx).is_some()); - // we have one active session - assert_eq!(1, this.pane().unwrap().read(cx).items_len()); + assert!(this.active_session().is_some()); assert!(running_state.read(cx).selected_thread_id().is_none()); }); }) @@ -124,7 +121,7 @@ async fn test_basic_show_debug_panel(executor: BackgroundExecutor, cx: &mut Test .update(cx, |workspace, _window, cx| { let debug_panel = workspace.panel::(cx).unwrap(); let active_session = debug_panel - .update(cx, |this, cx| this.active_session(cx)) + .update(cx, |this, _| this.active_session()) .unwrap(); let running_state = active_session.update(cx, |active_session, _| { @@ -135,11 +132,6 @@ async fn test_basic_show_debug_panel(executor: BackgroundExecutor, cx: &mut Test .clone() }); - // we have one active session - assert_eq!( - 1, - debug_panel.update(cx, |this, cx| this.pane().unwrap().read(cx).items_len()) - ); assert_eq!(client.id(), running_state.read(cx).session_id()); assert_eq!( ThreadId(1), @@ -162,7 +154,7 @@ async fn test_basic_show_debug_panel(executor: BackgroundExecutor, cx: &mut Test let debug_panel = workspace.panel::(cx).unwrap(); let active_session = debug_panel - .update(cx, |this, cx| this.active_session(cx)) + .update(cx, |this, _| this.active_session()) .unwrap(); let running_state = active_session.update(cx, |active_session, _| { @@ -174,8 +166,7 @@ async fn test_basic_show_debug_panel(executor: BackgroundExecutor, cx: &mut Test }); debug_panel.update(cx, |this, cx| { - assert!(this.active_session(cx).is_some()); - assert_eq!(1, this.pane().unwrap().read(cx).items_len()); + assert!(this.active_session().is_some()); assert_eq!( ThreadId(1), running_state.read(cx).selected_thread_id().unwrap() @@ -243,10 +234,8 @@ async fn test_we_can_only_have_one_panel_per_debug_session( .update(cx, |workspace, _window, cx| { let debug_panel = workspace.panel::(cx).unwrap(); - debug_panel.update(cx, |this, cx| { - assert!(this.active_session(cx).is_some()); - // we have one active session - assert_eq!(1, this.pane().unwrap().read(cx).items_len()); + debug_panel.update(cx, |this, _| { + assert!(this.active_session().is_some()); }); }) .unwrap(); @@ -270,7 +259,7 @@ async fn test_we_can_only_have_one_panel_per_debug_session( .update(cx, |workspace, _window, cx| { let debug_panel = workspace.panel::(cx).unwrap(); let active_session = debug_panel - .update(cx, |this, cx| this.active_session(cx)) + .update(cx, |this, _| this.active_session()) .unwrap(); let running_state = active_session.update(cx, |active_session, _| { @@ -281,11 +270,6 @@ async fn test_we_can_only_have_one_panel_per_debug_session( .clone() }); - // we have one active session - assert_eq!( - 1, - debug_panel.update(cx, |this, cx| this.pane().unwrap().read(cx).items_len()) - ); assert_eq!(client.id(), active_session.read(cx).session_id(cx).unwrap()); assert_eq!( ThreadId(1), @@ -312,7 +296,7 @@ async fn test_we_can_only_have_one_panel_per_debug_session( .update(cx, |workspace, _window, cx| { let debug_panel = workspace.panel::(cx).unwrap(); let active_session = debug_panel - .update(cx, |this, cx| this.active_session(cx)) + .update(cx, |this, _| this.active_session()) .unwrap(); let running_state = active_session.update(cx, |active_session, _| { @@ -323,11 +307,6 @@ async fn test_we_can_only_have_one_panel_per_debug_session( .clone() }); - // we have one active session - assert_eq!( - 1, - debug_panel.update(cx, |this, cx| this.pane().unwrap().read(cx).items_len()) - ); assert_eq!(client.id(), active_session.read(cx).session_id(cx).unwrap()); assert_eq!( ThreadId(1), @@ -349,7 +328,7 @@ async fn test_we_can_only_have_one_panel_per_debug_session( .update(cx, |workspace, _window, cx| { let debug_panel = workspace.panel::(cx).unwrap(); let active_session = debug_panel - .update(cx, |this, cx| this.active_session(cx)) + .update(cx, |this, _| this.active_session()) .unwrap(); let running_state = active_session.update(cx, |active_session, _| { @@ -361,8 +340,7 @@ async fn test_we_can_only_have_one_panel_per_debug_session( }); debug_panel.update(cx, |this, cx| { - assert!(this.active_session(cx).is_some()); - assert_eq!(1, this.pane().unwrap().read(cx).items_len()); + assert!(this.active_session().is_some()); assert_eq!( ThreadId(1), running_state.read(cx).selected_thread_id().unwrap() diff --git a/crates/debugger_ui/src/tests/module_list.rs b/crates/debugger_ui/src/tests/module_list.rs index 3aa2000829..8f5848864e 100644 --- a/crates/debugger_ui/src/tests/module_list.rs +++ b/crates/debugger_ui/src/tests/module_list.rs @@ -1,6 +1,5 @@ use crate::{ debugger_panel::DebugPanel, - session::ThreadItem, tests::{active_debug_session_panel, init_test, init_test_workspace}, }; use dap::{ @@ -139,13 +138,7 @@ async fn test_module_list(executor: BackgroundExecutor, cx: &mut TestAppContext) .clone() }); - assert!( - !called_modules.load(std::sync::atomic::Ordering::SeqCst), - "Request Modules shouldn't be called before it's needed" - ); - - running_state.update(cx, |state, cx| { - state.set_thread_item(ThreadItem::Modules, cx); + running_state.update(cx, |_, cx| { cx.refresh_windows(); }); @@ -157,9 +150,6 @@ async fn test_module_list(executor: BackgroundExecutor, cx: &mut TestAppContext) ); active_debug_session_panel(workspace, cx).update(cx, |_, cx| { - running_state.update(cx, |state, cx| { - state.set_thread_item(ThreadItem::Modules, cx) - }); let actual_modules = running_state.update(cx, |state, cx| { state.module_list().update(cx, |list, cx| list.modules(cx)) }); diff --git a/crates/debugger_ui/src/tests/stack_frame_list.rs b/crates/debugger_ui/src/tests/stack_frame_list.rs index 7fd67e9600..0aa806e3e9 100644 --- a/crates/debugger_ui/src/tests/stack_frame_list.rs +++ b/crates/debugger_ui/src/tests/stack_frame_list.rs @@ -410,7 +410,7 @@ async fn test_select_stack_frame(executor: BackgroundExecutor, cx: &mut TestAppC .update(cx, |workspace, _window, cx| { let debug_panel = workspace.panel::(cx).unwrap(); let active_debug_panel_item = debug_panel - .update(cx, |this, cx| this.active_session(cx)) + .update(cx, |this, _| this.active_session()) .unwrap(); active_debug_panel_item diff --git a/crates/debugger_ui/src/tests/variable_list.rs b/crates/debugger_ui/src/tests/variable_list.rs index ffaaff2003..d74fe17955 100644 --- a/crates/debugger_ui/src/tests/variable_list.rs +++ b/crates/debugger_ui/src/tests/variable_list.rs @@ -207,7 +207,9 @@ async fn test_basic_fetch_initial_scope_and_variables( .expect("Session should be running by this point") .clone() }); - + running_state.update_in(cx, |this, window, cx| { + this.activate_variable_list(window, cx); + }); cx.run_until_parked(); running_state.update(cx, |running_state, cx| { @@ -222,7 +224,6 @@ async fn test_basic_fetch_initial_scope_and_variables( running_state .variable_list() .update(cx, |variable_list, _| { - assert_eq!(1, variable_list.scopes().len()); assert_eq!(scopes, variable_list.scopes()); assert_eq!( vec![variables[0].clone(), variables[1].clone(),], @@ -480,7 +481,9 @@ async fn test_fetch_variables_for_multiple_scopes( .expect("Session should be running by this point") .clone() }); - + running_state.update_in(cx, |this, window, cx| { + this.activate_variable_list(window, cx); + }); cx.run_until_parked(); running_state.update(cx, |running_state, cx| { @@ -797,7 +800,11 @@ async fn test_keyboard_navigation(executor: BackgroundExecutor, cx: &mut TestApp variable_list.update(cx, |_, cx| cx.focus_self(window)); running }); - + running_state.update_in(cx, |this, window, cx| { + this.activate_variable_list(window, cx); + }); + cx.run_until_parked(); + cx.dispatch_action(SelectFirst); cx.dispatch_action(SelectFirst); cx.run_until_parked(); @@ -1541,16 +1548,13 @@ async fn test_variable_list_only_sends_requests_when_rendering( }) .await; - let running_state = active_debug_session_panel(workspace, cx).update_in(cx, |item, _, cx| { + let running_state = active_debug_session_panel(workspace, cx).update_in(cx, |item, _, _| { let state = item .mode() .as_running() .expect("Session should be running by this point") .clone(); - state.update(cx, |state, cx| { - state.set_thread_item(crate::session::ThreadItem::Modules, cx) - }); state }); @@ -1577,9 +1581,10 @@ async fn test_variable_list_only_sends_requests_when_rendering( assert!(!made_scopes_request.load(Ordering::SeqCst)); cx.focus_self(window); - running_state.set_thread_item(crate::session::ThreadItem::Variables, cx); }); - + running_state.update_in(cx, |this, window, cx| { + this.activate_variable_list(window, cx); + }); cx.run_until_parked(); running_state.update(cx, |running_state, cx| { @@ -1893,7 +1898,9 @@ async fn test_it_fetches_scopes_variables_when_you_select_a_stack_frame( .expect("Session should be running by this point") .clone() }); - + running_state.update_in(cx, |this, window, cx| { + this.activate_variable_list(window, cx); + }); cx.run_until_parked(); running_state.update(cx, |running_state, cx| { diff --git a/crates/terminal_view/src/terminal_panel.rs b/crates/terminal_view/src/terminal_panel.rs index 75f7b35e17..2f558eab0b 100644 --- a/crates/terminal_view/src/terminal_panel.rs +++ b/crates/terminal_view/src/terminal_panel.rs @@ -1202,12 +1202,15 @@ impl Render for TerminalPanel { self.workspace .update(cx, |workspace, cx| { registrar.size_full().child(self.center.render( - workspace.project(), - &HashMap::default(), - None, - &self.active_pane, workspace.zoomed_item(), - workspace.app_state(), + &workspace::PaneRenderContext { + follower_states: &&HashMap::default(), + active_call: workspace.active_call(), + active_pane: &self.active_pane, + app_state: &workspace.app_state(), + project: workspace.project(), + workspace: &workspace.weak_handle(), + }, window, cx, )) diff --git a/crates/workspace/src/pane_group.rs b/crates/workspace/src/pane_group.rs index e094d93641..2b9938923e 100644 --- a/crates/workspace/src/pane_group.rs +++ b/crates/workspace/src/pane_group.rs @@ -8,8 +8,8 @@ use call::{ActiveCall, ParticipantLocation}; use client::proto::PeerId; use collections::HashMap; use gpui::{ - Along, AnyView, AnyWeakView, Axis, Bounds, Context, Entity, IntoElement, MouseButton, Pixels, - Point, StyleRefinement, Window, point, size, + Along, AnyView, AnyWeakView, Axis, Bounds, Entity, Hsla, IntoElement, MouseButton, Pixels, + Point, StyleRefinement, WeakEntity, Window, point, size, }; use parking_lot::Mutex; use project::Project; @@ -124,26 +124,12 @@ impl PaneGroup { pub fn render( &self, - project: &Entity, - follower_states: &HashMap, - active_call: Option<&Entity>, - active_pane: &Entity, zoomed: Option<&AnyWeakView>, - app_state: &Arc, + render_cx: &dyn PaneLeaderDecorator, window: &mut Window, - cx: &mut Context, + cx: &mut App, ) -> impl IntoElement { - self.root.render( - project, - 0, - follower_states, - active_call, - active_pane, - zoomed, - app_state, - window, - cx, - ) + self.root.render(0, zoomed, render_cx, window, cx) } pub fn panes(&self) -> Vec<&Entity> { @@ -195,6 +181,160 @@ pub enum Member { Pane(Entity), } +#[derive(Clone, Copy)] +pub struct PaneRenderContext<'a> { + pub project: &'a Entity, + pub follower_states: &'a HashMap, + pub active_call: Option<&'a Entity>, + pub active_pane: &'a Entity, + pub app_state: &'a Arc, + pub workspace: &'a WeakEntity, +} + +#[derive(Default)] +pub struct LeaderDecoration { + border: Option, + status_box: Option, +} + +pub trait PaneLeaderDecorator { + fn decorate(&self, pane: &Entity, cx: &App) -> LeaderDecoration; + fn active_pane(&self) -> &Entity; + fn workspace(&self) -> &WeakEntity; +} + +pub struct ActivePaneDecorator<'a> { + active_pane: &'a Entity, + workspace: &'a WeakEntity, +} + +impl<'a> ActivePaneDecorator<'a> { + pub fn new(active_pane: &'a Entity, workspace: &'a WeakEntity) -> Self { + Self { + active_pane, + workspace, + } + } +} + +impl PaneLeaderDecorator for ActivePaneDecorator<'_> { + fn decorate(&self, _: &Entity, _: &App) -> LeaderDecoration { + LeaderDecoration::default() + } + fn active_pane(&self) -> &Entity { + self.active_pane + } + + fn workspace(&self) -> &WeakEntity { + self.workspace + } +} + +impl PaneLeaderDecorator for PaneRenderContext<'_> { + fn decorate(&self, pane: &Entity, cx: &App) -> LeaderDecoration { + let follower_state = self.follower_states.iter().find_map(|(leader_id, state)| { + if state.center_pane == *pane { + Some((*leader_id, state)) + } else { + None + } + }); + let leader = follower_state.as_ref().and_then(|(leader_id, _)| { + let room = self.active_call?.read(cx).room()?.read(cx); + room.remote_participant_for_peer_id(*leader_id) + }); + let Some(leader) = leader else { + return LeaderDecoration::default(); + }; + let is_in_unshared_view = follower_state.as_ref().map_or(false, |(_, state)| { + state + .active_view_id + .is_some_and(|view_id| !state.items_by_leader_view_id.contains_key(&view_id)) + }); + let is_in_panel = follower_state + .as_ref() + .map_or(false, |(_, state)| state.dock_pane.is_some()); + + let mut leader_join_data = None; + let leader_status_box = match leader.location { + ParticipantLocation::SharedProject { + project_id: leader_project_id, + } => { + if Some(leader_project_id) == self.project.read(cx).remote_id() { + is_in_unshared_view.then(|| { + Label::new(format!( + "{} is in an unshared pane", + leader.user.github_login + )) + }) + } else { + leader_join_data = Some((leader_project_id, leader.user.id)); + Some(Label::new(format!( + "Follow {} to their active project", + leader.user.github_login, + ))) + } + } + ParticipantLocation::UnsharedProject => Some(Label::new(format!( + "{} is viewing an unshared Zed project", + leader.user.github_login + ))), + ParticipantLocation::External => Some(Label::new(format!( + "{} is viewing a window outside of Zed", + leader.user.github_login + ))), + }; + let mut leader_color = cx + .theme() + .players() + .color_for_participant(leader.participant_index.0) + .cursor; + if is_in_panel { + leader_color.fade_out(0.75); + } else { + leader_color.fade_out(0.3); + } + let status_box = leader_status_box.map(|status| { + div() + .absolute() + .w_96() + .bottom_3() + .right_3() + .elevation_2(cx) + .p_1() + .child(status) + .when_some( + leader_join_data, + |this, (leader_project_id, leader_user_id)| { + let app_state = self.app_state.clone(); + this.cursor_pointer() + .on_mouse_down(MouseButton::Left, move |_, _, cx| { + crate::join_in_room_project( + leader_project_id, + leader_user_id, + app_state.clone(), + cx, + ) + .detach_and_log_err(cx); + }) + }, + ) + .into_any_element() + }); + LeaderDecoration { + status_box, + border: Some(leader_color), + } + } + + fn active_pane(&self) -> &Entity { + self.active_pane + } + + fn workspace(&self) -> &WeakEntity { + self.workspace + } +} impl Member { fn new_axis(old_pane: Entity, new_pane: Entity, direction: SplitDirection) -> Self { use Axis::*; @@ -222,15 +362,11 @@ impl Member { pub fn render( &self, - project: &Entity, basis: usize, - follower_states: &HashMap, - active_call: Option<&Entity>, - active_pane: &Entity, zoomed: Option<&AnyWeakView>, - app_state: &Arc, + render_cx: &dyn PaneLeaderDecorator, window: &mut Window, - cx: &mut Context, + cx: &mut App, ) -> impl IntoElement { match self { Member::Pane(pane) => { @@ -238,76 +374,7 @@ impl Member { return div().into_any(); } - let follower_state = follower_states.iter().find_map(|(leader_id, state)| { - if state.center_pane == *pane { - Some((*leader_id, state)) - } else { - None - } - }); - - let leader = follower_state.as_ref().and_then(|(leader_id, _)| { - let room = active_call?.read(cx).room()?.read(cx); - room.remote_participant_for_peer_id(*leader_id) - }); - - let is_in_unshared_view = follower_state.as_ref().map_or(false, |(_, state)| { - state.active_view_id.is_some_and(|view_id| { - !state.items_by_leader_view_id.contains_key(&view_id) - }) - }); - - let is_in_panel = follower_state - .as_ref() - .map_or(false, |(_, state)| state.dock_pane.is_some()); - - let mut leader_border = None; - let mut leader_status_box = None; - let mut leader_join_data = None; - if let Some(leader) = &leader { - let mut leader_color = cx - .theme() - .players() - .color_for_participant(leader.participant_index.0) - .cursor; - if is_in_panel { - leader_color.fade_out(0.75); - } else { - leader_color.fade_out(0.3); - } - leader_border = Some(leader_color); - - leader_status_box = match leader.location { - ParticipantLocation::SharedProject { - project_id: leader_project_id, - } => { - if Some(leader_project_id) == project.read(cx).remote_id() { - if is_in_unshared_view { - Some(Label::new(format!( - "{} is in an unshared pane", - leader.user.github_login - ))) - } else { - None - } - } else { - leader_join_data = Some((leader_project_id, leader.user.id)); - Some(Label::new(format!( - "Follow {} to their active project", - leader.user.github_login, - ))) - } - } - ParticipantLocation::UnsharedProject => Some(Label::new(format!( - "{} is viewing an unshared Zed project", - leader.user.github_login - ))), - ParticipantLocation::External => Some(Label::new(format!( - "{} is viewing a window outside of Zed", - leader.user.github_login - ))), - }; - } + let decoration = render_cx.decorate(pane, cx); div() .relative() @@ -317,7 +384,7 @@ impl Member { AnyView::from(pane.clone()) .cached(StyleRefinement::default().v_flex().size_full()), ) - .when_some(leader_border, |this, color| { + .when_some(decoration.border, |this, color| { this.child( div() .absolute() @@ -328,49 +395,11 @@ impl Member { .border_color(color), ) }) - .when_some(leader_status_box, |this, status_box| { - this.child( - div() - .absolute() - .w_96() - .bottom_3() - .right_3() - .elevation_2(cx) - .p_1() - .child(status_box) - .when_some( - leader_join_data, - |this, (leader_project_id, leader_user_id)| { - this.cursor_pointer().on_mouse_down( - MouseButton::Left, - cx.listener(move |this, _, _, cx| { - crate::join_in_room_project( - leader_project_id, - leader_user_id, - this.app_state().clone(), - cx, - ) - .detach_and_log_err(cx); - }), - ) - }, - ), - ) - }) + .children(decoration.status_box) .into_any() } Member::Axis(axis) => axis - .render( - project, - basis + 1, - follower_states, - active_call, - active_pane, - zoomed, - app_state, - window, - cx, - ) + .render(basis + 1, zoomed, render_cx, window, cx) .into_any(), } } @@ -671,15 +700,11 @@ impl PaneAxis { fn render( &self, - project: &Entity, basis: usize, - follower_states: &HashMap, - active_call: Option<&Entity>, - active_pane: &Entity, zoomed: Option<&AnyWeakView>, - app_state: &Arc, + render_cx: &dyn PaneLeaderDecorator, window: &mut Window, - cx: &mut Context, + cx: &mut App, ) -> gpui::AnyElement { debug_assert!(self.members.len() == self.flexes.lock().len()); let mut active_pane_ix = None; @@ -689,24 +714,14 @@ impl PaneAxis { basis, self.flexes.clone(), self.bounding_boxes.clone(), - cx.entity().downgrade(), + render_cx.workspace().clone(), ) .children(self.members.iter().enumerate().map(|(ix, member)| { - if matches!(member, Member::Pane(pane) if pane == active_pane) { + if matches!(member, Member::Pane(pane) if pane == render_cx.active_pane()) { active_pane_ix = Some(ix); } member - .render( - project, - (basis + ix) * 10, - follower_states, - active_call, - active_pane, - zoomed, - app_state, - window, - cx, - ) + .render((basis + ix) * 10, zoomed, render_cx, window, cx) .into_any_element() })) .with_active_pane(active_pane_ix) diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 78e232d260..5a9dce7c01 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -5561,12 +5561,16 @@ impl Render for Workspace { this.child(p.border_r_1()) }) .child(self.center.render( - &self.project, - &self.follower_states, - self.active_call(), - &self.active_pane, self.zoomed.as_ref(), - &self.app_state, + &PaneRenderContext { + follower_states: + &self.follower_states, + active_call: self.active_call(), + active_pane: &self.active_pane, + app_state: &self.app_state, + project: &self.project, + workspace: &self.weak_self, + }, window, cx, ))