1use gpui::{
2 AnyWindowHandle, AppContext as _, Context, FocusHandle, Focusable, Global,
3 StatefulInteractiveElement, Task,
4};
5
6use crate::workspace_settings;
7
8#[derive(Default)]
9struct FfmState {
10 // The window and element to be focused
11 handles: Option<(AnyWindowHandle, FocusHandle)>,
12 // The debounced task which will do the focusing
13 _debounce_task: Option<Task<()>>,
14}
15
16impl Global for FfmState {}
17
18pub trait FocusFollowsMouse<E: Focusable>: StatefulInteractiveElement {
19 fn focus_follows_mouse(
20 self,
21 settings: workspace_settings::FocusFollowsMouse,
22 cx: &Context<E>,
23 ) -> Self {
24 if settings.enabled {
25 self.on_hover(cx.listener(move |this, enter, window, cx| {
26 if *enter {
27 let window_handle = window.window_handle();
28 let focus_handle = this.focus_handle(cx);
29
30 let state = cx.try_global::<FfmState>();
31
32 // Only replace the target if the new handle doesn't contain the existing one.
33 // This ensures that hovering over a parent (e.g., Dock) doesn't override
34 // a more specific child target (e.g., a Pane inside the Dock).
35 let should_replace = state
36 .and_then(|s| s.handles.as_ref())
37 .map(|(_, existing)| !focus_handle.contains(existing, window))
38 .unwrap_or(true);
39
40 if !should_replace {
41 return;
42 }
43
44 let debounce_task = cx.spawn(async move |_this, cx| {
45 cx.background_executor().timer(settings.debounce).await;
46
47 cx.update(|cx| {
48 let state = cx.default_global::<FfmState>();
49 let Some((window, focus)) = state.handles.take() else {
50 return;
51 };
52
53 let _ = cx.update_window(window, move |_view, window, cx| {
54 window.focus(&focus, cx);
55 });
56 });
57 });
58
59 cx.set_global(FfmState {
60 handles: Some((window_handle, focus_handle)),
61 _debounce_task: Some(debounce_task),
62 });
63 }
64 }))
65 } else {
66 self
67 }
68 }
69}
70
71impl<E: Focusable, T: StatefulInteractiveElement> FocusFollowsMouse<E> for T {}