diff --git a/assets/settings/default.json b/assets/settings/default.json index ab8aa001af875e2668bad4bbb33e83317a0b70c6..f08366365d4c9f868d3da6d8cad0145e48195990 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -226,7 +226,10 @@ // "on_typing_and_movement" "hide_mouse": "on_typing_and_movement", // Determines whether the focused panel follows the mouse location. - "focus_follows_mouse": false, + "focus_follows_mouse": { + "enabled": false, + "debounce_ms": 250, + }, // Determines how snippets are sorted relative to other completion items. // // 1. Place snippets at the top of the completion list: diff --git a/crates/settings_content/src/workspace.rs b/crates/settings_content/src/workspace.rs index 61eb68b5b80461e34045455f4a28e14fd5d92a25..9b2ec8180abcea08e76117986607b56431907567 100644 --- a/crates/settings_content/src/workspace.rs +++ b/crates/settings_content/src/workspace.rs @@ -124,7 +124,7 @@ pub struct WorkspaceSettingsContent { pub window_decorations: Option, /// Whether the focused panel follows the mouse location /// Default: false - pub focus_follows_mouse: Option, + pub focus_follows_mouse: Option, } #[with_fallible_options] @@ -923,3 +923,10 @@ impl DocumentSymbols { self == &Self::On } } + +#[with_fallible_options] +#[derive(Copy, Clone, PartialEq, Default, Serialize, Deserialize, JsonSchema, MergeFrom, Debug)] +pub struct FocusFollowsMouse { + pub enabled: Option, + pub debounce_ms: Option, +} diff --git a/crates/settings_ui/src/page_data.rs b/crates/settings_ui/src/page_data.rs index f3ec197c0da068909fb2dad95a8621365528224c..672e641b9ca7c9914470576e64dea847212a969c 100644 --- a/crates/settings_ui/src/page_data.rs +++ b/crates/settings_ui/src/page_data.rs @@ -4121,7 +4121,7 @@ fn window_and_layout_page() -> SettingsPage { ] } - fn layout_section() -> [SettingsPageItem; 5] { + fn layout_section() -> [SettingsPageItem; 6] { [ SettingsPageItem::SectionHeader("Layout"), SettingsPageItem::SettingItem(SettingItem { @@ -4189,12 +4189,43 @@ fn window_and_layout_page() -> SettingsPage { title: "Focus Follows Mouse", description: "Whether to change focus to a pane when the mouse hovers over it.", field: Box::new(SettingField { - json_path: Some("focus_follows_mouse"), + json_path: Some("focus_follows_mouse.enabled"), pick: |settings_content| { - settings_content.workspace.focus_follows_mouse.as_ref() + settings_content + .workspace + .focus_follows_mouse + .as_ref() + .and_then(|s| s.enabled.as_ref()) + }, + write: |settings_content, value| { + settings_content + .workspace + .focus_follows_mouse + .get_or_insert_default() + .enabled = value; + }, + }), + metadata: None, + files: USER, + }), + SettingsPageItem::SettingItem(SettingItem { + title: "Focus Follows Mouse Debounce ms", + description: "Amount of time to wait before changing focus.", + field: Box::new(SettingField { + json_path: Some("focus_follows_mouse.debounce_ms"), + pick: |settings_content| { + settings_content + .workspace + .focus_follows_mouse + .as_ref() + .and_then(|s| s.debounce_ms.as_ref()) }, write: |settings_content, value| { - settings_content.workspace.focus_follows_mouse = value; + settings_content + .workspace + .focus_follows_mouse + .get_or_insert_default() + .debounce_ms = value; }, }), metadata: None, diff --git a/crates/workspace/src/focus_follows_mouse.rs b/crates/workspace/src/focus_follows_mouse.rs index 23d18b8c4155532885fccf6d982e39f1fc2a13a0..49a87e5d3779bd7de7a76b95abd17f3d6dadad0b 100644 --- a/crates/workspace/src/focus_follows_mouse.rs +++ b/crates/workspace/src/focus_follows_mouse.rs @@ -1,11 +1,55 @@ -use gpui::{Context, Focusable, StatefulInteractiveElement}; +use std::sync::LazyLock; + +use gpui::{ + AnyWindowHandle, AppContext, Context, FocusHandle, Focusable, StatefulInteractiveElement, Task, +}; +use parking_lot::Mutex; + +use crate::workspace_settings; + +#[derive(Default)] +struct FfmState { + // The window and element to be focused + handles: Option<(AnyWindowHandle, FocusHandle)>, + // The debounced task which will do the focusing + debounce_task: Option>, +} + +// Global focus-follows-mouse state. +static FFM_STATE: LazyLock> = LazyLock::new(Default::default); pub trait FocusFollowsMouse: StatefulInteractiveElement { - fn focus_follows_mouse(self, enabled: bool, cx: &Context) -> Self { - if enabled { + fn focus_follows_mouse( + self, + settings: workspace_settings::FocusFollowsMouse, + cx: &Context, + ) -> Self { + if settings.enabled { self.on_hover(cx.listener(move |this, enter, window, cx| { if *enter { - window.focus(&this.focus_handle(cx), cx); + let window_handle = window.window_handle(); + let focus_handle = this.focus_handle(cx); + + let mut state = FFM_STATE.lock(); + + // Set the window/element to be focused to the most recent hovered element. + state.handles.replace((window_handle, focus_handle)); + + // Start a task to focus the most recent target after the debounce period + state + .debounce_task + .replace(cx.spawn(async move |_this, cx| { + cx.background_executor().timer(settings.debounce).await; + + let mut state = FFM_STATE.lock(); + let Some((window, focus)) = state.handles.take() else { + return; + }; + + let _ = cx.update_window(window, move |_view, window, cx| { + window.focus(&focus, cx); + }); + })); } })) } else { diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 6c6ed23520d46cfa20b8f7c3bf251f998c02a100..4826adafd3e15313cd2979ac8d9aa85e81bc0fab 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -2,7 +2,7 @@ use crate::{ CloseWindow, NewFile, NewTerminal, OpenInTerminal, OpenOptions, OpenTerminal, OpenVisible, SplitDirection, ToggleFileFinder, ToggleProjectSymbols, ToggleZoom, Workspace, WorkspaceItemBuilder, ZoomIn, ZoomOut, - focus_follows_mouse::FocusFollowsMouse, + focus_follows_mouse::FocusFollowsMouse as _, invalid_item_view::InvalidItemView, item::{ ActivateOnClose, ClosePosition, Item, ItemBufferKind, ItemHandle, ItemSettings, @@ -12,7 +12,7 @@ use crate::{ move_item, notifications::NotifyResultExt, toolbar::Toolbar, - workspace_settings::{AutosaveSetting, TabBarSettings, WorkspaceSettings}, + workspace_settings::{AutosaveSetting, FocusFollowsMouse, TabBarSettings, WorkspaceSettings}, }; use anyhow::Result; use collections::{BTreeSet, HashMap, HashSet, VecDeque}; @@ -414,7 +414,7 @@ pub struct Pane { pinned_tab_count: usize, diagnostics: HashMap, zoom_out_on_close: bool, - focus_follows_mouse: bool, + focus_follows_mouse: FocusFollowsMouse, 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>, diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 3dd6c8401b0d3df29fa500262e1817b6fdf1cb2d..b51364dd0846a67aeddd37a2bdbb110d266967d1 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -144,8 +144,8 @@ use util::{ }; use uuid::Uuid; pub use workspace_settings::{ - AutosaveSetting, BottomDockLayout, RestoreOnStartupBehavior, StatusBarSettings, TabBarSettings, - WorkspaceSettings, + AutosaveSetting, BottomDockLayout, FocusFollowsMouse, RestoreOnStartupBehavior, + StatusBarSettings, TabBarSettings, WorkspaceSettings, }; use zed_actions::{Spawn, feedback::FileBugReport, theme::ToggleMode}; diff --git a/crates/workspace/src/workspace_settings.rs b/crates/workspace/src/workspace_settings.rs index b0f2b7d703b78fa7a942a5bdffbce35cc0ecf1fb..ab080cb923769f9553cebb0c39ba886bc3092c7f 100644 --- a/crates/workspace/src/workspace_settings.rs +++ b/crates/workspace/src/workspace_settings.rs @@ -1,4 +1,4 @@ -use std::num::NonZeroUsize; +use std::{num::NonZeroUsize, time::Duration}; use crate::DockPosition; use collections::HashMap; @@ -35,7 +35,13 @@ pub struct WorkspaceSettings { pub use_system_window_tabs: bool, pub zoomed_padding: bool, pub window_decorations: settings::WindowDecorations, - pub focus_follows_mouse: bool, + pub focus_follows_mouse: FocusFollowsMouse, +} + +#[derive(Copy, Clone, Deserialize)] +pub struct FocusFollowsMouse { + pub enabled: bool, + pub debounce: Duration, } #[derive(Copy, Clone, PartialEq, Debug, Default)] @@ -114,7 +120,20 @@ 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(), + focus_follows_mouse: FocusFollowsMouse { + enabled: workspace + .focus_follows_mouse + .unwrap() + .enabled + .unwrap_or(false), + debounce: Duration::from_millis( + workspace + .focus_follows_mouse + .unwrap() + .debounce_ms + .unwrap_or(250), + ), + }, } } }