Detailed changes
@@ -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:
@@ -124,7 +124,7 @@ pub struct WorkspaceSettingsContent {
pub window_decorations: Option<WindowDecorations>,
/// Whether the focused panel follows the mouse location
/// Default: false
- pub focus_follows_mouse: Option<bool>,
+ pub focus_follows_mouse: Option<FocusFollowsMouse>,
}
#[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<bool>,
+ pub debounce_ms: Option<u64>,
+}
@@ -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,
@@ -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<Task<()>>,
+}
+
+// Global focus-follows-mouse state.
+static FFM_STATE: LazyLock<Mutex<FfmState>> = LazyLock::new(Default::default);
pub trait FocusFollowsMouse<E: Focusable>: StatefulInteractiveElement {
- fn focus_follows_mouse(self, enabled: bool, cx: &Context<E>) -> Self {
- if enabled {
+ fn focus_follows_mouse(
+ self,
+ settings: workspace_settings::FocusFollowsMouse,
+ cx: &Context<E>,
+ ) -> 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 {
@@ -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<ProjectPath, DiagnosticSeverity>,
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<ProjectItemKind, Box<dyn Any + Send>>,
@@ -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};
@@ -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),
+ ),
+ },
}
}
}