workspace: implement focus-follows-mouse for panes

Josh Robson Chase created

Change summary

Cargo.lock                                            |  8 ++++++
Cargo.toml                                            |  1 
assets/settings/default.json                          |  2 +
crates/focus_follows_mouse/Cargo.toml                 | 14 ++++++++++
crates/focus_follows_mouse/LICENSE-GPL                |  1 
crates/focus_follows_mouse/src/focus_follows_mouse.rs | 17 +++++++++++++
crates/settings/src/vscode_import.rs                  |  1 
crates/settings_content/src/workspace.rs              |  3 ++
crates/workspace/Cargo.toml                           |  1 
crates/workspace/src/pane.rs                          | 11 +++++++
crates/workspace/src/workspace_settings.rs            |  2 +
11 files changed, 60 insertions(+), 1 deletion(-)

Detailed changes

Cargo.lock 🔗

@@ -6451,6 +6451,13 @@ version = "1.0.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
 
+[[package]]
+name = "focus_follows_mouse"
+version = "0.1.0"
+dependencies = [
+ "gpui",
+]
+
 [[package]]
 name = "foldhash"
 version = "0.1.5"
@@ -21521,6 +21528,7 @@ dependencies = [
  "component",
  "db",
  "feature_flags",
+ "focus_follows_mouse",
  "fs",
  "futures 0.3.31",
  "git",

Cargo.toml 🔗

@@ -78,6 +78,7 @@ members = [
     "crates/feedback",
     "crates/file_finder",
     "crates/file_icons",
+    "crates/focus_follows_mouse",
     "crates/fs",
     "crates/fs_benchmarks",
     "crates/fuzzy",

assets/settings/default.json 🔗

@@ -225,6 +225,8 @@
   // 3. Hide on both typing and cursor movement:
   //    "on_typing_and_movement"
   "hide_mouse": "on_typing_and_movement",
+  // Determines whether the focused panel follows the mouse location.
+  "focus_follows_mouse": false,
   // Determines how snippets are sorted relative to other completion items.
   //
   // 1. Place snippets at the top of the completion list:

crates/focus_follows_mouse/Cargo.toml 🔗

@@ -0,0 +1,14 @@
+[package]
+name = "focus_follows_mouse"
+version = "0.1.0"
+publish.workspace = true
+edition.workspace = true
+
+[lib]
+path = "src/focus_follows_mouse.rs"
+
+[dependencies]
+gpui.workspace = true
+
+[lints]
+workspace = true

crates/focus_follows_mouse/src/focus_follows_mouse.rs 🔗

@@ -0,0 +1,17 @@
+use gpui::{Context, Focusable, StatefulInteractiveElement};
+
+pub trait FocusFollowsMouse<E: Focusable>: StatefulInteractiveElement {
+    fn focus_follows_mouse(self, enabled: bool, cx: &Context<E>) -> Self {
+        if enabled {
+            self.on_hover(cx.listener(move |this, enter, window, cx| {
+                if *enter {
+                    window.focus(&this.focus_handle(cx), cx);
+                }
+            }))
+        } else {
+            self
+        }
+    }
+}
+
+impl<E: Focusable, T: StatefulInteractiveElement> FocusFollowsMouse<E> for T {}

crates/settings_content/src/workspace.rs 🔗

@@ -122,6 +122,9 @@ pub struct WorkspaceSettingsContent {
     /// What draws window decorations/titlebar, the client application (Zed) or display server
     /// Default: client
     pub window_decorations: Option<WindowDecorations>,
+    /// Whether the focused panel follows the mouse location
+    /// Default: false
+    pub focus_follows_mouse: Option<bool>,
 }
 
 #[with_fallible_options]

crates/workspace/Cargo.toml 🔗

@@ -67,6 +67,7 @@ util.workspace = true
 uuid.workspace = true
 vim_mode_setting.workspace = true
 zed_actions.workspace = true
+focus_follows_mouse = { version = "0.1.0", path = "../focus_follows_mouse" }
 
 [target.'cfg(target_os = "windows")'.dependencies]
 windows.workspace = true

crates/workspace/src/pane.rs 🔗

@@ -15,6 +15,7 @@ use crate::{
 };
 use anyhow::Result;
 use collections::{BTreeSet, HashMap, HashSet, VecDeque};
+use focus_follows_mouse::FocusFollowsMouse;
 use futures::{StreamExt, stream::FuturesUnordered};
 use gpui::{
     Action, AnyElement, App, AsyncWindowContext, ClickEvent, ClipboardItem, Context, Corner, Div,
@@ -413,6 +414,7 @@ pub struct Pane {
     pinned_tab_count: usize,
     diagnostics: HashMap<ProjectPath, DiagnosticSeverity>,
     zoom_out_on_close: bool,
+    focus_follows_mouse: bool,
     diagnostic_summary_update: Task<()>,
     /// If a certain project item wants to get recreated with specific data, it can persist its data before the recreation here.
     pub project_item_restoration_data: HashMap<ProjectItemKind, Box<dyn Any + Send>>,
@@ -585,6 +587,7 @@ impl Pane {
             pinned_tab_count: 0,
             diagnostics: Default::default(),
             zoom_out_on_close: true,
+            focus_follows_mouse: WorkspaceSettings::get_global(cx).focus_follows_mouse,
             diagnostic_summary_update: Task::ready(()),
             project_item_restoration_data: HashMap::default(),
             welcome_page: None,
@@ -752,7 +755,6 @@ impl Pane {
 
     fn settings_changed(&mut self, window: &mut Window, cx: &mut Context<Self>) {
         let tab_bar_settings = TabBarSettings::get_global(cx);
-        let new_max_tabs = WorkspaceSettings::get_global(cx).max_tabs;
 
         if let Some(display_nav_history_buttons) = self.display_nav_history_buttons.as_mut() {
             *display_nav_history_buttons = tab_bar_settings.show_nav_history_buttons;
@@ -765,6 +767,12 @@ impl Pane {
             self.nav_history.0.lock().preview_item_id = None;
         }
 
+        let workspace_settings = WorkspaceSettings::get_global(cx);
+
+        self.focus_follows_mouse = workspace_settings.focus_follows_mouse;
+
+        let new_max_tabs = workspace_settings.max_tabs;
+
         if self.use_max_tabs && new_max_tabs != self.max_tabs {
             self.max_tabs = new_max_tabs;
             self.close_items_on_settings_change(window, cx);
@@ -4429,6 +4437,7 @@ impl Render for Pane {
                                 placeholder.child(self.welcome_page.clone().unwrap())
                             }
                         }
+                        .focus_follows_mouse(self.focus_follows_mouse, cx)
                     })
                     .child(
                         // drag target

crates/workspace/src/workspace_settings.rs 🔗

@@ -35,6 +35,7 @@ pub struct WorkspaceSettings {
     pub use_system_window_tabs: bool,
     pub zoomed_padding: bool,
     pub window_decorations: settings::WindowDecorations,
+    pub focus_follows_mouse: bool,
 }
 
 #[derive(Copy, Clone, PartialEq, Debug, Default)]
@@ -113,6 +114,7 @@ impl Settings for WorkspaceSettings {
             use_system_window_tabs: workspace.use_system_window_tabs.unwrap(),
             zoomed_padding: workspace.zoomed_padding.unwrap(),
             window_decorations: workspace.window_decorations.unwrap(),
+            focus_follows_mouse: workspace.focus_follows_mouse.unwrap(),
         }
     }
 }