Skip to content

Commit af7de9a

Browse files
Fix project panel follow focus issues (#60974)
Closes #60939 Closes #60940 Release Notes: - Fixed focus follows mouse in project panel's blank space
1 parent d0a977e commit af7de9a

3 files changed

Lines changed: 149 additions & 3 deletions

File tree

crates/gpui/src/window.rs

Lines changed: 75 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2723,6 +2723,7 @@ impl Window {
27232723
self.next_frame.clear();
27242724
let current_focus_path = self.rendered_frame.focus_path();
27252725
let current_window_active = self.rendered_frame.window_active;
2726+
let mut focus_before_listeners = self.focus;
27262727

27272728
if previous_focus_path != current_focus_path
27282729
|| previous_window_active != current_window_active
@@ -2731,6 +2732,11 @@ impl Window {
27312732
self.focus_lost_listeners
27322733
.clone()
27332734
.retain(&(), |listener| listener(self, cx));
2735+
// The focus-lost fallback (e.g. a workspace refocusing itself) may target
2736+
// an element that isn't part of the element tree, in which case scheduling
2737+
// a redraw below would dispatch focus-lost again, looping forever. Only
2738+
// track focus movement caused by the focus listeners.
2739+
focus_before_listeners = self.focus;
27342740
}
27352741

27362742
let event = WindowFocusEvent {
@@ -2755,6 +2761,13 @@ impl Window {
27552761
self.reset_cursor_style(cx);
27562762
self.refreshing = false;
27572763
self.invalidator.set_phase(DrawPhase::None);
2764+
// Focus listeners may move focus (e.g. a dock forwarding focus to its active
2765+
// panel). `Window::focus` suppresses `refresh` while a draw is in progress, so
2766+
// schedule another frame here to render the new focus state and dispatch the
2767+
// resulting focus events.
2768+
if self.focus != focus_before_listeners {
2769+
self.refresh();
2770+
}
27582771
self.needs_present.set(true);
27592772

27602773
if let Some(draw_start) = draw_started_at {
@@ -6380,8 +6393,9 @@ pub fn outline(
63806393
#[cfg(test)]
63816394
mod tests {
63826395
use crate::{
6383-
AppContext as _, Bounds, Context, IntoElement, ParentElement as _, Pixels, Render,
6384-
Styled as _, TestAppContext, Window, canvas, div, px, size,
6396+
AppContext as _, Bounds, Context, FocusHandle, InteractiveElement as _, IntoElement,
6397+
ParentElement as _, Pixels, Render, Styled as _, TestAppContext, Window, canvas, div, px,
6398+
size,
63856399
};
63866400
use std::{cell::Cell, rc::Rc};
63876401

@@ -6449,4 +6463,63 @@ mod tests {
64496463

64506464
assert_eq!(child_bounds.get().size, size(px(300.), px(200.)));
64516465
}
6466+
6467+
struct FocusForwarder {
6468+
a: FocusHandle,
6469+
b: FocusHandle,
6470+
}
6471+
6472+
impl Render for FocusForwarder {
6473+
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
6474+
div()
6475+
.size_full()
6476+
.child(div().w(px(50.)).h(px(50.)).track_focus(&self.a))
6477+
.child(div().w(px(50.)).h(px(50.)).track_focus(&self.b))
6478+
}
6479+
}
6480+
6481+
/// When a focus listener moves focus again (e.g. a dock forwarding focus to its
6482+
/// active panel), the resulting focus events must be dispatched without waiting
6483+
/// for an unrelated redraw of the window.
6484+
#[gpui::test]
6485+
fn test_focus_moved_by_focus_listener_is_dispatched(cx: &mut TestAppContext) {
6486+
let b_focus_count = Rc::new(Cell::new(0));
6487+
let window = cx.add_window({
6488+
let b_focus_count = b_focus_count.clone();
6489+
move |window, cx| {
6490+
let a = cx.focus_handle();
6491+
let b = cx.focus_handle();
6492+
cx.on_focus(&a, window, |this: &mut FocusForwarder, window, cx| {
6493+
let b = this.b.clone();
6494+
window.focus(&b, cx);
6495+
})
6496+
.detach();
6497+
cx.on_focus(&b, window, move |_, _, _| {
6498+
b_focus_count.set(b_focus_count.get() + 1);
6499+
})
6500+
.detach();
6501+
FocusForwarder { a, b }
6502+
}
6503+
});
6504+
6505+
window
6506+
.update(cx, |_, window, _| window.activate_window())
6507+
.unwrap();
6508+
cx.executor().run_until_parked();
6509+
6510+
window
6511+
.update(cx, |this, window, cx| {
6512+
let a = this.a.clone();
6513+
window.focus(&a, cx);
6514+
})
6515+
.unwrap();
6516+
cx.executor().run_until_parked();
6517+
6518+
window
6519+
.update(cx, |this, window, _| {
6520+
assert!(this.b.is_focused(window));
6521+
})
6522+
.unwrap();
6523+
assert_eq!(b_focus_count.get(), 1);
6524+
}
64526525
}

crates/project_panel/src/project_panel.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,8 +74,9 @@ use util::{
7474
};
7575
use workspace::{
7676
DraggedSelection, OpenInTerminal, OpenMode, OpenOptions, OpenVisible, PreviewTabsSettings,
77-
SelectedEntry, SplitDirection, Workspace,
77+
SelectedEntry, SplitDirection, Workspace, WorkspaceSettings,
7878
dock::{DockPosition, Panel, PanelEvent},
79+
focus_follows_mouse::FocusFollowsMouse as _,
7980
notifications::{DetachAndPromptErr, NotifyResultExt, NotifyTaskExt},
8081
};
8182
use worktree::CreatedEntry;
@@ -7244,6 +7245,13 @@ impl Render for ProjectPanel {
72447245
div()
72457246
.id("project-panel-blank-area")
72467247
.block_mouse_except_scroll()
7248+
// `block_mouse_except_scroll` prevents the dock's own
7249+
// focus-follows-mouse hover handler from seeing this area,
7250+
// so handle it here directly.
7251+
.focus_follows_mouse(
7252+
WorkspaceSettings::get_global(cx).focus_follows_mouse,
7253+
cx,
7254+
)
72477255
.flex_grow_1()
72487256
.on_scroll_wheel({
72497257
let scroll_handle = self.scroll_handle.clone();

crates/project_panel/src/project_panel_tests.rs

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10979,6 +10979,71 @@ async fn run_create_file_in_folded_path_case(
1097910979
}
1098010980
}
1098110981

10982+
#[gpui::test]
10983+
async fn test_focus_follows_mouse_into_blank_area(cx: &mut gpui::TestAppContext) {
10984+
init_test_with_editor(cx);
10985+
cx.update(|cx| {
10986+
cx.update_global::<SettingsStore, _>(|store, cx| {
10987+
store.update_user_settings(cx, |settings| {
10988+
settings.workspace.focus_follows_mouse = Some(settings::FocusFollowsMouse {
10989+
enabled: Some(true),
10990+
debounce_ms: Some(100),
10991+
});
10992+
});
10993+
});
10994+
});
10995+
10996+
let fs = FakeFs::new(cx.executor());
10997+
fs.insert_tree(path!("/root"), json!({ "a.txt": "" })).await;
10998+
10999+
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
11000+
let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
11001+
let workspace = window
11002+
.read_with(cx, |mw, _| mw.workspace().clone())
11003+
.unwrap();
11004+
let cx = &mut VisualTestContext::from_window(window.into(), cx);
11005+
let panel = workspace.update_in(cx, |workspace, window, cx| {
11006+
let panel = ProjectPanel::new(workspace, window, cx);
11007+
workspace.add_panel(panel.clone(), window, cx);
11008+
workspace.open_panel::<ProjectPanel>(window, cx);
11009+
panel
11010+
});
11011+
cx.run_until_parked();
11012+
11013+
workspace
11014+
.update_in(cx, |workspace, window, cx| {
11015+
let worktree_id = workspace.worktrees(cx).next().unwrap().read(cx).id();
11016+
let project_path = ProjectPath {
11017+
worktree_id,
11018+
path: rel_path("a.txt").into(),
11019+
};
11020+
workspace.open_path(project_path, None, true, window, cx)
11021+
})
11022+
.await
11023+
.unwrap();
11024+
cx.run_until_parked();
11025+
11026+
panel.update_in(cx, |panel, window, cx| {
11027+
assert!(
11028+
!panel.focus_handle(cx).is_focused(window),
11029+
"Editor should be focused after opening a file"
11030+
);
11031+
});
11032+
11033+
// Hover over the blank space below the last entry in the project panel,
11034+
// which lives in the right dock by default.
11035+
cx.simulate_mouse_move(point(px(1800.), px(600.)), None, Modifiers::none());
11036+
cx.executor().advance_clock(Duration::from_millis(200));
11037+
cx.run_until_parked();
11038+
11039+
panel.update_in(cx, |panel, window, cx| {
11040+
assert!(
11041+
panel.focus_handle(cx).is_focused(window),
11042+
"Project panel should be focused after hovering the blank area below the entries"
11043+
);
11044+
});
11045+
}
11046+
1098211047
pub(crate) fn init_test(cx: &mut TestAppContext) {
1098311048
cx.update(|cx| {
1098411049
let settings_store = SettingsStore::test(cx);

0 commit comments

Comments
 (0)