Fix zoomed panels closing when focus moves within the panel (#59439)
Closes FR-139 Most focus-lost issues that close the agent panel in Zen mode stem from panels returning a transient child editor's focus handle from `Focusable::focus_handle`. The Zen-mode check closes the zoomed panel when the focused element is not a descendant of the panel's focus handle, so any focus movement inside the panel that landed outside that child editor looked like the panel losing focus. This PR separates the two roles that handle was serving: - `Focusable::focus_handle` now always returns a stable handle tracked at the panel's root element, used for containment checks (Zen-mode auto-close, `toggle_panel_focus`, dock focus subscriptions). - The new `Panel::activation_focus_handle` returns what should receive focus when the panel is activated (e.g. a filter/commit/message editor) and must be a focus-tree descendant of the root handle. All "focus the panel" callsites in workspace/dock now use it. Migrated panels: `AgentPanel` (including thread views and the toolbar title editor), `CollabPanel`, `GitPanel`, `OutlinePanel`, and `TerminalPanel` (previously delegated to the active pane, so focus in a non-active pane dropped containment). `CollabPanel` activation falls back to the panel root when signed out or collaboration is disabled, since the filter editor isn't rendered in those states. Also fixes duplicate element ids for tool calls with multiple content blocks. Tests: a regression test that clicks tool-call output and focuses the thread title editor while zoomed (both previously closed Zen mode), and a dock-level test asserting the activation handle receives focus on panel activation while the panel root reports containment. Release Notes: - Fixed zoomed panels (e.g. the agent panel in Zen mode) closing unexpectedly when focus moved to elements inside the panel, such as tool call output or the thread title editor.
This commit is contained in:
1 parent
1c2a643cde
commit
ecac6c00a6
11 files changed
+343
-74
No files matched your search
@@ -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::<AgentPanel>(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);
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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| {
|
||||
|
||||
@@ -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<Self>) -> 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<PanelEvent> 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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -77,6 +77,7 @@ pub fn init(cx: &mut App) {
|
||||
pub struct TerminalPanel {
|
||||
pub(crate) active_pane: Entity<Pane>,
|
||||
pub(crate) center: PaneGroup,
|
||||
focus_handle: FocusHandle,
|
||||
fs: Arc<dyn Fs>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
pending_serialization: Task<Option<()>>,
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -36,6 +36,14 @@ pub use proto::PanelId;
|
||||
pub trait Panel: Focusable + EventEmitter<PanelEvent> + 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<Self>);
|
||||
@@ -119,6 +127,8 @@ pub trait PanelHandle: Send + Sync {
|
||||
fn toggle_action(&self, window: &Window, cx: &App) -> Box<dyn Action>;
|
||||
fn icon_label(&self, window: &Window, cx: &App) -> Option<String>;
|
||||
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<FocusHandle>,
|
||||
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<Self>) -> 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"
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Dock>, 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::<TestPanel>(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);
|
||||
|
||||
Reference in new issue
Block a user