Detailed changes
@@ -225,6 +225,11 @@
// 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": {
+ "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:
@@ -999,6 +999,7 @@ impl VsCodeSettings {
}
}),
zoomed_padding: None,
+ focus_follows_mouse: None,
}
}
@@ -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<FocusFollowsMouse>,
}
#[with_fallible_options]
@@ -928,3 +931,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>,
+}
@@ -4159,7 +4159,7 @@ fn window_and_layout_page() -> SettingsPage {
]
}
- fn layout_section() -> [SettingsPageItem; 4] {
+ fn layout_section() -> [SettingsPageItem; 6] {
[
SettingsPageItem::SectionHeader("Layout"),
SettingsPageItem::SettingItem(SettingItem {
@@ -4223,6 +4223,52 @@ fn window_and_layout_page() -> SettingsPage {
}),
metadata: None,
}),
+ SettingsPageItem::SettingItem(SettingItem {
+ 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.enabled"),
+ pick: |settings_content| {
+ 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
+ .get_or_insert_default()
+ .debounce_ms = value;
+ },
+ }),
+ metadata: None,
+ files: USER,
+ }),
]
}
@@ -1,5 +1,6 @@
+use crate::focus_follows_mouse::FocusFollowsMouse as _;
use crate::persistence::model::DockData;
-use crate::{DraggedDock, Event, ModalLayer, Pane};
+use crate::{DraggedDock, Event, FocusFollowsMouse, ModalLayer, Pane, WorkspaceSettings};
use crate::{Workspace, status_bar::StatusItemView};
use anyhow::Context as _;
use client::proto;
@@ -12,7 +13,7 @@ use gpui::{
px,
};
use serde::{Deserialize, Serialize};
-use settings::SettingsStore;
+use settings::{Settings, SettingsStore};
use std::sync::Arc;
use ui::{
ContextMenu, CountBadge, Divider, DividerColor, IconButton, Tooltip, prelude::*,
@@ -252,6 +253,7 @@ pub struct Dock {
is_open: bool,
active_panel_index: Option<usize>,
focus_handle: FocusHandle,
+ focus_follows_mouse: FocusFollowsMouse,
pub(crate) serialized_dock: Option<DockData>,
zoom_layer_open: bool,
modal_layer: Entity<ModalLayer>,
@@ -376,6 +378,7 @@ impl Dock {
active_panel_index: None,
is_open: false,
focus_handle: focus_handle.clone(),
+ focus_follows_mouse: WorkspaceSettings::get_global(cx).focus_follows_mouse,
_subscriptions: [focus_subscription, zoom_subscription],
serialized_dock: None,
zoom_layer_open: false,
@@ -1086,8 +1089,10 @@ impl Render for Dock {
};
div()
+ .id("dock-panel")
.key_context(dispatch_context)
.track_focus(&self.focus_handle(cx))
+ .focus_follows_mouse(self.focus_follows_mouse, cx)
.flex()
.bg(cx.theme().colors().panel_background)
.border_color(cx.theme().colors().border)
@@ -1121,6 +1126,7 @@ impl Render for Dock {
})
} else {
div()
+ .id("dock-panel")
.key_context(dispatch_context)
.track_focus(&self.focus_handle(cx))
}
@@ -0,0 +1,71 @@
+use gpui::{
+ AnyWindowHandle, AppContext as _, Context, FocusHandle, Focusable, Global,
+ StatefulInteractiveElement, Task,
+};
+
+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<()>>,
+}
+
+impl Global for FfmState {}
+
+pub trait FocusFollowsMouse<E: Focusable>: StatefulInteractiveElement {
+ 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 {
+ let window_handle = window.window_handle();
+ let focus_handle = this.focus_handle(cx);
+
+ let state = cx.try_global::<FfmState>();
+
+ // Only replace the target if the new handle doesn't contain the existing one.
+ // This ensures that hovering over a parent (e.g., Dock) doesn't override
+ // a more specific child target (e.g., a Pane inside the Dock).
+ let should_replace = state
+ .and_then(|s| s.handles.as_ref())
+ .map(|(_, existing)| !focus_handle.contains(existing, window))
+ .unwrap_or(true);
+
+ if !should_replace {
+ return;
+ }
+
+ let debounce_task = cx.spawn(async move |_this, cx| {
+ cx.background_executor().timer(settings.debounce).await;
+
+ cx.update(|cx| {
+ let state = cx.default_global::<FfmState>();
+ let Some((window, focus)) = state.handles.take() else {
+ return;
+ };
+
+ let _ = cx.update_window(window, move |_view, window, cx| {
+ window.focus(&focus, cx);
+ });
+ });
+ });
+
+ cx.set_global(FfmState {
+ handles: Some((window_handle, focus_handle)),
+ _debounce_task: Some(debounce_task),
+ });
+ }
+ }))
+ } else {
+ self
+ }
+ }
+}
+
+impl<E: Focusable, T: StatefulInteractiveElement> FocusFollowsMouse<E> for T {}
@@ -2,6 +2,7 @@ use crate::{
CloseWindow, NewFile, NewTerminal, OpenInTerminal, OpenOptions, OpenTerminal, OpenVisible,
SplitDirection, ToggleFileFinder, ToggleProjectSymbols, ToggleZoom, Workspace,
WorkspaceItemBuilder, ZoomIn, ZoomOut,
+ focus_follows_mouse::FocusFollowsMouse as _,
invalid_item_view::InvalidItemView,
item::{
ActivateOnClose, ClosePosition, Item, ItemBufferKind, ItemHandle, ItemSettings,
@@ -11,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};
@@ -443,6 +444,7 @@ pub struct Pane {
pinned_tab_count: usize,
diagnostics: HashMap<ProjectPath, DiagnosticSeverity>,
zoom_out_on_close: 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>>,
@@ -615,6 +617,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,
@@ -782,7 +785,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;
@@ -795,6 +797,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);
@@ -4460,6 +4468,7 @@ impl Render for Pane {
placeholder.child(self.welcome_page.clone().unwrap())
}
}
+ .focus_follows_mouse(self.focus_follows_mouse, cx)
})
.child(
// drag target
@@ -19,6 +19,7 @@ mod security_modal;
pub mod shared_screen;
use db::smol::future::yield_now;
pub use shared_screen::SharedScreen;
+pub mod focus_follows_mouse;
mod status_bar;
pub mod tasks;
mod theme_preview;
@@ -147,8 +148,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,6 +35,13 @@ pub struct WorkspaceSettings {
pub use_system_window_tabs: bool,
pub zoomed_padding: bool,
pub window_decorations: settings::WindowDecorations,
+ pub focus_follows_mouse: FocusFollowsMouse,
+}
+
+#[derive(Copy, Clone, Deserialize)]
+pub struct FocusFollowsMouse {
+ pub enabled: bool,
+ pub debounce: Duration,
}
#[derive(Copy, Clone, PartialEq, Debug, Default)]
@@ -113,6 +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: 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),
+ ),
+ },
}
}
}