diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index 51bf65207f..0a147aa1a4 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -2951,7 +2951,7 @@ impl AgentPanel { if let BaseView::AgentThread { conversation_view } = &self.base_view { if conversation_view.entity_id() == draft.entity_id() { if focus { - self.focus_handle(cx).focus(window, cx); + self.activation_focus_handle(cx).focus(window, cx); } return; } @@ -3545,7 +3545,7 @@ impl AgentPanel { active_thread.update(cx, |active_thread, cx| { active_thread.expand_message_editor(&ExpandMessageEditor, window, cx); - active_thread.focus_handle(cx).focus(window, cx); + active_thread.activation_focus_handle(cx).focus(window, cx); }) } @@ -3659,7 +3659,7 @@ impl AgentPanel { cx.emit(PanelEvent::ZoomOut); } else { if !self.focus_handle(cx).contains_focused(window, cx) { - cx.focus_self(window); + self.activation_focus_handle(cx).focus(window, cx); } cx.emit(PanelEvent::ZoomIn); } @@ -4233,7 +4233,7 @@ impl AgentPanel { self.refresh_base_view_subscriptions(window, cx); if focus { - self.focus_handle(cx).focus(window, cx); + self.activation_focus_handle(cx).focus(window, cx); } cx.emit(AgentPanelEvent::ActiveViewChanged); } @@ -4923,12 +4923,8 @@ impl agent::SiblingThreadHost for AgentPanelSiblingHost { } impl Focusable for AgentPanel { - fn focus_handle(&self, cx: &App) -> FocusHandle { - match self.visible_surface() { - VisibleSurface::Uninitialized => self.focus_handle.clone(), - VisibleSurface::AgentThread(conversation_view) => conversation_view.focus_handle(cx), - VisibleSurface::Terminal(terminal_view) => terminal_view.focus_handle(cx), - } + fn focus_handle(&self, _cx: &App) -> FocusHandle { + self.focus_handle.clone() } } @@ -4956,6 +4952,16 @@ impl Panel for AgentPanel { AGENT_PANEL_KEY } + fn activation_focus_handle(&self, cx: &App) -> FocusHandle { + match self.visible_surface() { + VisibleSurface::Uninitialized => self.focus_handle.clone(), + VisibleSurface::AgentThread(conversation_view) => { + conversation_view.read(cx).activation_focus_handle(cx) + } + VisibleSurface::Terminal(terminal_view) => terminal_view.focus_handle(cx), + } + } + fn position(&self, _window: &Window, cx: &App) -> DockPosition { agent_panel_dock_position(cx) } @@ -5355,7 +5361,10 @@ impl AgentPanel { let conversation_view = conversation_view.downgrade(); move |_: &menu::Confirm, window, cx| { if let Some(conversation_view) = conversation_view.upgrade() { - conversation_view.focus_handle(cx).focus(window, cx); + conversation_view + .read(cx) + .activation_focus_handle(cx) + .focus(window, cx); } } }) @@ -5363,7 +5372,10 @@ impl AgentPanel { let conversation_view = conversation_view.downgrade(); move |_: &editor::actions::Cancel, window, cx| { if let Some(conversation_view) = conversation_view.upgrade() { - conversation_view.focus_handle(cx).focus(window, cx); + conversation_view + .read(cx) + .activation_focus_handle(cx) + .focus(window, cx); } } }) @@ -6440,6 +6452,7 @@ impl Render for AgentPanel { .relative() .size_full() .justify_between() + .track_focus(&self.focus_handle) .bg(cx.theme().colors().panel_background) .on_action(cx.listener(|this, action: &NewThread, window, cx| { this.new_thread(action, window, cx); @@ -6820,7 +6833,7 @@ mod tests { use anyhow::{Result, anyhow}; use feature_flags::FeatureFlagAppExt; use fs::FakeFs; - use gpui::{App, TestAppContext, UpdateGlobal, VisualTestContext}; + use gpui::{App, Modifiers, TestAppContext, UpdateGlobal, VisualTestContext, px, size}; use parking_lot::Mutex; use project::{Project, WorktreePaths}; use settings::{SettingsStore, WorkingDirectory}; @@ -7021,6 +7034,121 @@ mod tests { } } + #[gpui::test] + async fn test_clicking_tool_call_output_keeps_agent_panel_focused_and_zoomed( + cx: &mut TestAppContext, + ) { + init_test(cx); + cx.update(|cx| { + agent::ThreadStore::init_global(cx); + language_model::LanguageModelRegistry::test(cx); + }); + + let fs = FakeFs::new(cx.executor()); + let project = Project::test(fs, [], cx).await; + let multi_workspace = + cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = multi_workspace + .read_with(cx, |multi_workspace, _cx| { + multi_workspace.workspace().clone() + }) + .unwrap(); + let cx = &mut VisualTestContext::from_window(multi_workspace.into(), cx); + cx.simulate_resize(size(px(900.), px(700.))); + + let connection = StubAgentConnection::new(); + let panel = workspace.update_in(cx, |workspace, window, cx| { + let panel = cx.new(|cx| AgentPanel::new(workspace, window, cx)); + workspace.add_panel(panel.clone(), window, cx); + workspace.focus_panel::(window, cx); + panel + }); + open_thread_with_connection(&panel, connection.clone(), cx); + + let session_id = active_session_id(&panel, cx); + let tool_call_id = acp::ToolCallId::new("tool-call-output-focus-regression"); + cx.update(|_window, cx| { + connection.send_update( + session_id.clone(), + acp::SessionUpdate::ToolCall( + acp::ToolCall::new(tool_call_id.clone(), "Read file") + .kind(acp::ToolKind::Fetch) + .status(acp::ToolCallStatus::InProgress), + ), + cx, + ); + connection.send_update( + session_id, + acp::SessionUpdate::ToolCallUpdate(acp::ToolCallUpdate::new( + tool_call_id.clone(), + acp::ToolCallUpdateFields::new() + .status(acp::ToolCallStatus::Completed) + .content(vec![acp::ToolCallContent::Content(acp::Content::new( + acp::ContentBlock::Text(acp::TextContent::new( + "tool output text".to_string(), + )), + ))]), + )), + cx, + ); + }); + cx.run_until_parked(); + + let thread_view = panel.read_with(cx, |panel, cx| panel.active_thread_view(cx).unwrap()); + thread_view.update(cx, |thread_view, cx| { + thread_view.entry_view_state.update(cx, |state, _cx| { + state.expand_tool_call(tool_call_id); + }); + cx.notify(); + }); + + panel.update_in(cx, |panel, window, cx| { + panel.toggle_zoom(&ToggleZoom, window, cx); + }); + + // The thread receives only tool-call updates, so entry index 0 should remain stable. + let output_bounds = cx + .debug_bounds("tool-call-output-0-0") + .expect("tool call output should be rendered"); + cx.simulate_click(output_bounds.center(), Modifiers::default()); + cx.run_until_parked(); + + panel.update_in(cx, |panel, window, cx| { + assert!( + panel.focus_handle(cx).contains_focused(window, cx), + "clicking tool call output should keep focus within the agent panel" + ); + assert!( + panel.is_zoomed(window, cx), + "clicking tool call output should not close Zen Mode" + ); + }); + + let title_editor_focus_handle = panel.read_with(cx, |panel, cx| { + panel + .active_thread_view(cx) + .expect("active thread view should be present") + .read(cx) + .title_editor + .focus_handle(cx) + }); + cx.update(|window, cx| { + title_editor_focus_handle.focus(window, cx); + }); + cx.run_until_parked(); + + panel.update_in(cx, |panel, window, cx| { + assert!( + panel.focus_handle(cx).contains_focused(window, cx), + "focusing the thread title editor should keep focus within the agent panel" + ); + assert!( + panel.is_zoomed(window, cx), + "focusing the thread title editor should not close Zen Mode" + ); + }); + } + #[gpui::test] async fn test_active_thread_serialize_and_load_round_trip(cx: &mut TestAppContext) { init_test(cx); diff --git a/crates/agent_ui/src/conversation_view.rs b/crates/agent_ui/src/conversation_view.rs index 35f0e70b09..1d0276c4d9 100644 --- a/crates/agent_ui/src/conversation_view.rs +++ b/crates/agent_ui/src/conversation_view.rs @@ -707,7 +707,7 @@ impl ConversationView { connected.navigate_to_thread(session_id); if let Some(view) = self.active_thread() { - view.focus_handle(cx).focus(window, cx); + view.read(cx).activation_focus_handle(cx).focus(window, cx); } cx.emit(AcpServerViewEvent::ActiveThreadChanged); cx.notify(); @@ -3333,6 +3333,14 @@ impl Focusable for ConversationView { } } +impl ConversationView { + pub(crate) fn activation_focus_handle(&self, cx: &App) -> FocusHandle { + self.active_thread() + .map(|thread| thread.read(cx).activation_focus_handle(cx)) + .unwrap_or_else(|| self.focus_handle.clone()) + } +} + #[cfg(any(test, feature = "test-support"))] impl ConversationView { /// Expands a tool call so its content is visible. diff --git a/crates/agent_ui/src/conversation_view/thread_view.rs b/crates/agent_ui/src/conversation_view/thread_view.rs index 469e88a530..004084c8e5 100644 --- a/crates/agent_ui/src/conversation_view/thread_view.rs +++ b/crates/agent_ui/src/conversation_view/thread_view.rs @@ -647,7 +647,13 @@ pub struct ThreadView { pub(crate) thread_search_visible: bool, } impl Focusable for ThreadView { - fn focus_handle(&self, cx: &App) -> FocusHandle { + fn focus_handle(&self, _cx: &App) -> FocusHandle { + self.focus_handle.clone() + } +} + +impl ThreadView { + pub(crate) fn activation_focus_handle(&self, cx: &App) -> FocusHandle { if self.parent_session_id.is_some() { self.focus_handle.clone() } else { @@ -2057,7 +2063,7 @@ impl ThreadView { this.update_in(cx, |thread, window, cx| { cx.emit(AcpThreadViewEvent::Interacted); thread.send_impl(message_editor, window, cx); - thread.focus_handle(cx).focus(window, cx); + thread.activation_focus_handle(cx).focus(window, cx); })?; anyhow::Ok(()) }) @@ -8356,8 +8362,13 @@ impl ThreadView { .iter() .enumerate() .map(|(content_ix, content)| { - div().id(("tool-call-output", entry_ix)).child( - self.render_tool_call_content( + let output_id = SharedString::from(format!( + "tool-call-output-{entry_ix}-{content_ix}" + )); + div() + .id(output_id.clone()) + .debug_selector(move || output_id.to_string()) + .child(self.render_tool_call_content( active_session_id, entry_ix, content, @@ -8368,8 +8379,7 @@ impl ThreadView { focus_handle, window, cx, - ), - ) + )) }), ) .when(!use_card_layout, |this| { diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 444030f996..83f9e46f71 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -6,7 +6,7 @@ use crate::{CollaborationPanelSettings, channel_view::ChannelView}; use anyhow::Context as _; use call::ActiveCall; use channel::{Channel, ChannelEvent, ChannelStore}; -use client::{ChannelId, Client, Contact, Notification, User, UserStore}; +use client::{ChannelId, Client, Contact, Notification, Status, User, UserStore}; use collections::{HashMap, HashSet}; use contact_finder::ContactFinder; use db::kvp::KeyValueStore; @@ -350,6 +350,17 @@ enum ListEntry { } impl CollabPanel { + fn is_collaboration_disabled_by_organization(&self, cx: &App) -> bool { + self.user_store + .read(cx) + .current_organization_configuration() + .is_some_and(|config| !config.is_collaboration_enabled) + } + + fn is_signed_in_view_visible(status: Status, is_collaboration_disabled: bool) -> bool { + !is_collaboration_disabled && status.is_or_was_connected() && !status.is_signing_in() + } + pub fn new( workspace: &mut Workspace, window: &mut Window, @@ -1486,7 +1497,7 @@ impl CollabPanel { if this.context_menu.as_ref().is_some_and(|context_menu| { context_menu.0.focus_handle(cx).contains_focused(window, cx) }) { - cx.focus_self(window); + this.activation_focus_handle(cx).focus(window, cx); } this.context_menu.take(); cx.notify(); @@ -1670,7 +1681,7 @@ impl CollabPanel { if this.context_menu.as_ref().is_some_and(|context_menu| { context_menu.0.focus_handle(cx).contains_focused(window, cx) }) { - cx.focus_self(window); + this.activation_focus_handle(cx).focus(window, cx); } this.context_menu.take(); cx.notify(); @@ -1738,7 +1749,7 @@ impl CollabPanel { if this.context_menu.as_ref().is_some_and(|context_menu| { context_menu.0.focus_handle(cx).contains_focused(window, cx) }) { - cx.focus_self(window); + this.activation_focus_handle(cx).focus(window, cx); } this.context_menu.take(); this.contact_context_menu.take(); @@ -1972,7 +1983,7 @@ impl CollabPanel { cx.notify(); } } - cx.focus_self(window); + self.activation_focus_handle(cx).focus(window, cx); true } else { false @@ -2039,7 +2050,7 @@ impl CollabPanel { self.serialize(cx); self.update_entries(true, cx); cx.notify(); - cx.focus_self(window); + self.activation_focus_handle(cx).focus(window, cx); } fn is_channel_collapsed(&self, channel_id: ChannelId) -> bool { @@ -2549,8 +2560,10 @@ impl CollabPanel { .update(cx, |channels, _| channels.remove_channel(channel_id)) .await .notify_workspace_async_err(workspace, &mut cx); - this.update_in(cx, |_, window, cx| cx.focus_self(window)) - .ok(); + this.update_in(cx, |this, window, cx| { + this.activation_focus_handle(cx).focus(window, cx); + }) + .ok(); } anyhow::Ok(()) }) @@ -3853,11 +3866,9 @@ impl Render for CollabPanel { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { let status = *self.client.status().borrow(); - let is_collaboration_disabled = self - .user_store - .read(cx) - .current_organization_configuration() - .is_some_and(|config| !config.is_collaboration_enabled); + let is_collaboration_disabled = self.is_collaboration_disabled_by_organization(cx); + let is_signed_in_view_visible = + Self::is_signed_in_view_visible(status, is_collaboration_disabled); v_flex() .key_context(self.dispatch_context(window, cx)) @@ -3880,10 +3891,10 @@ impl Render for CollabPanel { .size_full() .child(if is_collaboration_disabled { self.render_disabled_by_organization(cx) - } else if !status.is_or_was_connected() || status.is_signing_in() { - self.render_signed_out(cx) - } else { + } else if is_signed_in_view_visible { self.render_signed_in(window, cx) + } else { + self.render_signed_out(cx) }) .children(self.context_menu.as_ref().map(|(menu, position, _)| { deferred( @@ -3900,6 +3911,17 @@ impl Render for CollabPanel { impl EventEmitter for CollabPanel {} impl Panel for CollabPanel { + fn activation_focus_handle(&self, cx: &App) -> FocusHandle { + let status = *self.client.status().borrow(); + let is_collaboration_disabled = self.is_collaboration_disabled_by_organization(cx); + + if Self::is_signed_in_view_visible(status, is_collaboration_disabled) { + self.filter_editor.focus_handle(cx) + } else { + self.focus_handle.clone() + } + } + fn position(&self, _window: &Window, cx: &App) -> DockPosition { CollaborationPanelSettings::get_global(cx).dock } @@ -3972,8 +3994,8 @@ impl Panel for CollabPanel { } impl Focusable for CollabPanel { - fn focus_handle(&self, cx: &App) -> gpui::FocusHandle { - self.filter_editor.focus_handle(cx) + fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle { + self.focus_handle.clone() } } diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs index 8b9a790a70..46fd0949df 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/crates/git_ui/src/git_panel.rs @@ -7321,7 +7321,7 @@ impl GitPanel { .focus_handle(cx) .contains_focused(window, cx) }) { - cx.focus_self(window); + this.activation_focus_handle(cx).focus(window, cx); } this.context_menu.take(); cx.notify(); @@ -8166,12 +8166,8 @@ impl Render for GitPanel { } impl Focusable for GitPanel { - fn focus_handle(&self, cx: &App) -> gpui::FocusHandle { - if self.entries.is_empty() || self.commit_editor_expanded { - self.commit_editor.focus_handle(cx) - } else { - self.focus_handle.clone() - } + fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle { + self.focus_handle.clone() } } @@ -8205,6 +8201,14 @@ impl editor::Addon for GitPanelAddon { } impl Panel for GitPanel { + fn activation_focus_handle(&self, cx: &App) -> FocusHandle { + if self.entries.is_empty() || self.commit_editor_expanded { + self.commit_editor.focus_handle(cx) + } else { + self.focus_handle.clone() + } + } + fn persistent_name() -> &'static str { "GitPanel" } diff --git a/crates/git_ui/src/worktree_service.rs b/crates/git_ui/src/worktree_service.rs index 4c840c466c..2425cbdf38 100644 --- a/crates/git_ui/src/worktree_service.rs +++ b/crates/git_ui/src/worktree_service.rs @@ -1367,7 +1367,7 @@ async fn open_worktree_workspace( if activate && let Some(dock_position) = focused_dock { let dock = workspace.dock_at_position(dock_position); if let Some(panel) = dock.read(cx).active_panel() { - panel.panel_focus_handle(cx).focus(window, cx); + panel.activation_focus_handle(cx).focus(window, cx); } } }); diff --git a/crates/outline_panel/src/outline_panel.rs b/crates/outline_panel/src/outline_panel.rs index c880891502..2afbdf1f4c 100644 --- a/crates/outline_panel/src/outline_panel.rs +++ b/crates/outline_panel/src/outline_panel.rs @@ -4952,6 +4952,10 @@ fn file_name(path: &Path) -> String { } impl Panel for OutlinePanel { + fn activation_focus_handle(&self, cx: &App) -> FocusHandle { + self.filter_editor.focus_handle(cx) + } + fn persistent_name() -> &'static str { "Outline Panel" } @@ -5052,8 +5056,8 @@ impl Panel for OutlinePanel { } impl Focusable for OutlinePanel { - fn focus_handle(&self, cx: &App) -> FocusHandle { - self.filter_editor.focus_handle(cx) + fn focus_handle(&self, _cx: &App) -> FocusHandle { + self.focus_handle.clone() } } diff --git a/crates/terminal_view/src/terminal_panel.rs b/crates/terminal_view/src/terminal_panel.rs index c40f8f8d9a..be9bda6f79 100644 --- a/crates/terminal_view/src/terminal_panel.rs +++ b/crates/terminal_view/src/terminal_panel.rs @@ -77,6 +77,7 @@ pub fn init(cx: &mut App) { pub struct TerminalPanel { pub(crate) active_pane: Entity, pub(crate) center: PaneGroup, + focus_handle: FocusHandle, fs: Arc, workspace: WeakEntity, pending_serialization: Task>, @@ -94,6 +95,7 @@ impl TerminalPanel { let terminal_panel = Self { center, active_pane: pane, + focus_handle: cx.focus_handle(), fs: workspace.app_state().fs.clone(), workspace: workspace.weak_handle(), pending_serialization: Task::ready(None), @@ -1372,20 +1374,23 @@ impl Render for TerminalPanel { .unwrap_or_else(div); self.workspace .update(cx, |workspace, cx| { - registrar.size_full().child(self.center.render( - workspace.zoomed_item(), - None, - &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, - )) + registrar + .track_focus(&self.focus_handle) + .size_full() + .child(self.center.render( + workspace.zoomed_item(), + None, + &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, + )) }) .ok() .map(|div| { @@ -1531,12 +1536,16 @@ impl Render for TerminalPanel { } impl Focusable for TerminalPanel { - fn focus_handle(&self, cx: &App) -> FocusHandle { - self.active_pane.focus_handle(cx) + fn focus_handle(&self, _cx: &App) -> FocusHandle { + self.focus_handle.clone() } } impl Panel for TerminalPanel { + fn activation_focus_handle(&self, cx: &App) -> FocusHandle { + self.active_pane.focus_handle(cx) + } + fn position(&self, _window: &Window, cx: &App) -> DockPosition { TerminalSettings::get_global(cx).dock.into() } diff --git a/crates/workspace/src/dock.rs b/crates/workspace/src/dock.rs index b61830d67e..cf5dcc2faa 100644 --- a/crates/workspace/src/dock.rs +++ b/crates/workspace/src/dock.rs @@ -36,6 +36,14 @@ pub use proto::PanelId; pub trait Panel: Focusable + EventEmitter + Render + Sized { fn persistent_name() -> &'static str; fn panel_key() -> &'static str; + /// The `Focusable::focus_handle` root identifies the panel's subtree for containment checks + /// and must be tracked by the panel's root element. This method returns the handle that should + /// receive focus when the panel is activated, such as a filter, commit, or message editor; it + /// must be a focus-tree descendant of the root or containment checks such as Zen-mode auto-close + /// and toggle-focus will misbehave. + fn activation_focus_handle(&self, cx: &App) -> FocusHandle { + self.focus_handle(cx) + } fn position(&self, window: &Window, cx: &App) -> DockPosition; fn position_is_valid(&self, position: DockPosition) -> bool; fn set_position(&mut self, position: DockPosition, window: &mut Window, cx: &mut Context); @@ -119,6 +127,8 @@ pub trait PanelHandle: Send + Sync { fn toggle_action(&self, window: &Window, cx: &App) -> Box; fn icon_label(&self, window: &Window, cx: &App) -> Option; fn panel_focus_handle(&self, cx: &App) -> FocusHandle; + /// See `Panel::activation_focus_handle`. + fn activation_focus_handle(&self, cx: &App) -> FocusHandle; fn to_any(&self) -> AnyView; fn activation_priority(&self, cx: &App) -> u32; fn enabled(&self, cx: &App) -> bool; @@ -241,6 +251,10 @@ where self.read(cx).focus_handle(cx) } + fn activation_focus_handle(&self, cx: &App) -> FocusHandle { + self.read(cx).activation_focus_handle(cx) + } + fn activation_priority(&self, cx: &App) -> u32 { self.read(cx).activation_priority() } @@ -401,7 +415,10 @@ impl Dock { let focus_subscription = cx.on_focus(&focus_handle, window, |dock: &mut Dock, window, cx| { if let Some(active_entry) = dock.active_panel_entry() { - active_entry.panel.panel_focus_handle(cx).focus(window, cx) + active_entry + .panel + .activation_focus_handle(cx) + .focus(window, cx) } }); let zoom_subscription = cx.subscribe(&workspace, |dock, workspace, e: &Event, cx| { @@ -662,7 +679,7 @@ impl Dock { this.set_panel_zoomed(&panel.to_any(), true, window, cx); if !PanelHandle::panel_focus_handle(panel, cx).contains_focused(window, cx) { - window.focus(&panel.focus_handle(cx), cx); + window.focus(&panel.read(cx).activation_focus_handle(cx), cx); } workspace .update(cx, |workspace, cx| { @@ -694,7 +711,7 @@ impl Dock { { this.set_open(true, window, cx); this.activate_panel(ix, window, cx); - window.focus(&panel.read(cx).focus_handle(cx), cx); + window.focus(&panel.read(cx).activation_focus_handle(cx), cx); } } PanelEvent::Close => { @@ -1433,6 +1450,7 @@ pub mod test { pub zoomed: bool, pub active: bool, pub focus_handle: FocusHandle, + pub activation_focus_handle: Option, pub default_size: Pixels, pub flexible: bool, pub activation_priority: u32, @@ -1448,6 +1466,7 @@ pub mod test { zoomed: false, active: false, focus_handle: cx.focus_handle(), + activation_focus_handle: None, default_size: px(300.), flexible: false, activation_priority, @@ -1464,15 +1483,37 @@ pub mod test { ..Self::new(position, activation_priority, cx) } } + + pub fn new_with_activation_child( + position: DockPosition, + activation_priority: u32, + cx: &mut App, + ) -> Self { + Self { + activation_focus_handle: Some(cx.focus_handle()), + ..Self::new(position, activation_priority, cx) + } + } } impl Render for TestPanel { fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - div().id("test").track_focus(&self.focus_handle(cx)) + div() + .id("test") + .track_focus(&self.focus_handle(cx)) + .children(self.activation_focus_handle.iter().map(|focus_handle| { + div().id("test-activation-child").track_focus(focus_handle) + })) } } impl Panel for TestPanel { + fn activation_focus_handle(&self, cx: &App) -> FocusHandle { + self.activation_focus_handle + .clone() + .unwrap_or_else(|| self.focus_handle(cx)) + } + fn persistent_name() -> &'static str { "TestPanel" } diff --git a/crates/workspace/src/multi_workspace.rs b/crates/workspace/src/multi_workspace.rs index 49b19bd134..5710577fbc 100644 --- a/crates/workspace/src/multi_workspace.rs +++ b/crates/workspace/src/multi_workspace.rs @@ -1696,7 +1696,7 @@ impl MultiWorkspace { if dock.is_open() { if let Some(panel) = dock.active_panel() { if panel.is_zoomed(window, cx) { - target = Some(panel.panel_focus_handle(cx)); + target = Some(panel.activation_focus_handle(cx)); break; } } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index d4c1d3c4e2..573e01dd09 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -4205,8 +4205,8 @@ impl Workspace { focus_center = true; } } else { - let focus_handle = &active_panel.panel_focus_handle(cx); - window.focus(focus_handle, cx); + let focus_handle = active_panel.activation_focus_handle(cx); + window.focus(&focus_handle, cx); reveal_dock = true; } } @@ -4407,7 +4407,7 @@ impl Workspace { if let Some(panel) = panel.as_ref() { if should_focus(&**panel, window, cx) { dock.set_open(true, window, cx); - panel.panel_focus_handle(cx).focus(window, cx); + panel.activation_focus_handle(cx).focus(window, cx); } else { focus_center = true; } @@ -5395,7 +5395,7 @@ impl Workspace { window.defer(cx, move |window, cx| { let dock = dock.read(cx); if let Some(panel) = dock.active_panel() { - panel.panel_focus_handle(cx).focus(window, cx); + panel.activation_focus_handle(cx).focus(window, cx); } else { log::error!("Could not find a focus target when in switching focus in {direction} direction for a {:?} dock", dock.position()); } @@ -8184,7 +8184,7 @@ impl Workspace { fn dock_content_handle(dock: &Entity, cx: &App) -> FocusHandle { let dock = dock.read(cx); dock.active_panel() - .map(|panel| panel.panel_focus_handle(cx)) + .map(|panel| panel.activation_focus_handle(cx)) .unwrap_or_else(|| dock.focus_handle(cx)) } @@ -13427,6 +13427,49 @@ mod tests { }); } + #[gpui::test] + async fn test_panel_activation_and_region_navigation_focus_activation_handle( + cx: &mut gpui::TestAppContext, + ) { + init_test(cx); + let fs = FakeFs::new(cx.executor()); + + let project = Project::test(fs, [], cx).await; + let (workspace, cx) = + cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx)); + + let panel = workspace.update_in(cx, |workspace, window, cx| { + let panel = + cx.new(|cx| TestPanel::new_with_activation_child(DockPosition::Right, 100, cx)); + workspace.add_panel(panel.clone(), window, cx); + panel + }); + cx.run_until_parked(); + + workspace.update_in(cx, |workspace, window, cx| { + workspace.toggle_panel_focus::(window, cx); + }); + + let activation_focus_handle = workspace.update_in(cx, |_workspace, window, cx| { + let activation_focus_handle = panel + .read(cx) + .activation_focus_handle + .clone() + .expect("test panel should have an activation child"); + assert!(activation_focus_handle.is_focused(window)); + assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx)); + activation_focus_handle + }); + + workspace.update_in(cx, |workspace, window, cx| { + workspace.active_pane().focus_handle(cx).focus(window, cx); + workspace.move_part_focus(true, window, cx); + }); + workspace.update_in(cx, |_workspace, window, _cx| { + assert!(activation_focus_handle.is_focused(window)); + }); + } + #[gpui::test] async fn test_close_panel_on_toggle(cx: &mut gpui::TestAppContext) { init_test(cx);