srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/crates/wayland/src/input/focus.rs
blob: 3fcff5855323a32fd8c5d9517e263ab02807bf2a (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
//! Focusing, raising, and closing a window - the small set of helpers
//! every other input-handling module needs regardless of what triggered
//! the focus change (a click, a keybinding, an IPC call, a closed window's
//! fallback).

use smithay::desktop::Window as DWindow;
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;

use srdwm_core::{Event as CoreEvent, WindowId};

use crate::state::CompState;

/// The underlying `wl_surface` for a mapped window, regardless of whether
/// it's a native `xdg-shell` toplevel or an XWayland `X11Surface` --
/// `desktop::Window` exposes these as two separate accessors with no
/// shared one.
pub(crate) fn dwindow_wl_surface(w: &DWindow) -> Option<WlSurface> {
    if let Some(top) = w.toplevel() {
        return Some(top.wl_surface().clone());
    }
    w.x11_surface().and_then(|x| x.wl_surface())
}

/// Whether `w` is actually visible right now - on the current workspace and
/// not minimized - matching `WindowManager::visible_windows`'s own filter.
///
/// `state.space` (smithay's `Space`) is not workspace-aware: a window stays
/// mapped in it, and so stays hit-testable by `Space::element_under`, from
/// the moment it's created until it's explicitly minimized or destroyed --
/// switching workspace never unmaps anything (see `minimize` in
/// `udev::platform`, the only other place that calls `unmap_elem`, and the
/// absence of any workspace-switch handler that touches `self.space` at
/// all). Without this check, `element_under` freely returns a window sitting
/// on a workspace that isn't even shown, and a click "through" empty desktop
/// on the current workspace silently focuses/raises/moves motion onto that
/// invisible window instead of whatever (if anything) is really there.
pub(super) fn dwindow_is_visible(state: &CompState, w: &DWindow) -> bool {
    let Some(id) = dwindow_wl_surface(w).and_then(|s| state.surface_to_id.get(&s).copied()) else { return false };
    let wm = state.wm.borrow();
    wm.window(id).is_some_and(|win| !win.minimized && win.workspace == wm.current_workspace())
}

/// Requests a client close its window, whichever kind it is.
pub(crate) fn close_dwindow(w: &DWindow) {
    if let Some(top) = w.toplevel() {
        top.send_close();
    } else if let Some(x11) = w.x11_surface() {
        let _ = x11.close();
    }
}

/// Focuses `id` in our own `WindowManager` *and* gives its surface real
/// Wayland/X11 keyboard focus - without this, a window can be raised and
/// tiled correctly yet never receive a single keystroke.
pub(crate) fn focus_window(state: &mut CompState, id: WindowId) {
    // Real desktop convention (Windows/GNOME/macOS all do this): a
    // selected desktop icon stays highlighted only until something else
    // takes focus. Reported live as "highlighted desktop icons don't
    // become not highlighted anymore" when clicking a window - `select_
    // desktop_icon(None)` (clearing selection) was only ever called from
    // `start_desktop_marquee` (a new marquee-select on bare desktop), never
    // from the one place every focus path already funnels through
    // regardless of how it got triggered (a click, Alt-Tab, a dock's IPC
    // focus dispatch, scratchpad show, ...) - see this function's own
    // doc comment on why that funnel already exists for raising. A no-op,
    // cheap, when nothing was selected to begin with.
    state.select_desktop_icon(None);
    state.wm.borrow_mut().focus_window(id);
    // Raises the window in smithay's own `Space` too, not just core's
    // `order` - `Space` keeps a completely independent stacking order of
    // its own, which is what actually renders on top *and* what
    // `space.element_under` hit-tests against; `WindowManager::order`
    // (which `focus_window` above already updates) has no effect on
    // either. Without this, any focus path that doesn't also happen to
    // raise `Space` manually (Alt-Tab, a dock's IPC "focus" dispatch,
    // scratchpad show, the Snap-Layouts flyout, ...) left a window
    // genuinely focused - keyboard input, core's own idea of "topmost"
    // both correct - while it kept rendering *underneath* whatever was
    // already on top, and a click on the visible (stale-topmost) window
    // silently reached that one instead. "Focus doesn't bring a window to
    // the front" and "clicking through a window that's fully covering
    // another" are the same root cause, not two bugs. Previously only the
    // plain-content-click branch in `handle_pointer_button` did this,
    // manually, immediately before calling this function - every other
    // caller went through unraised. Cheap even when the window is already
    // topmost (`raise_element` on an already-last element is a no-op
    // reinsertion), so unconditional here rather than gated on whether
    // focus is actually changing.
    raise_in_space(state, id);
    state.pending.borrow_mut().push(CoreEvent::WindowFocused(id));
    let surface = state.id_to_window.get(&id).and_then(dwindow_wl_surface);
    // Routed through `set_keyboard_focus` (rather than calling
    // `KeyboardHandle::set_focus` directly) so clipboard/primary-selection
    // focus follows window focus too - see that method's doc comment.
    state.set_keyboard_focus(surface);
}

/// Raises `id` to the top of smithay's own `Space` stacking order (see
/// `focus_window`'s doc comment for why `Space`'s own order, separate from
/// core's, has to be kept in sync) without touching core's focus or
/// workspace state at all.
///
/// Split out of `focus_window` specifically so the udev/winit backends'
/// post-IPC-mutation re-sync (`crate::input::focus_window`'s doc comment
/// on *that* call site explains why it exists at all: an IPC-only focus
/// change needs `Space` to catch up too) can re-raise the already-focused
/// window without going through `WindowManager::focus_window` a second
/// time. That core method has its own side effect of switching to the
/// target's workspace if it differs from the current one - correct for a
/// real focus change, but wrong here: calling it on a window that is
/// already focused (just re-raising it for `Space`'s benefit) compared the
/// still-current, still-on-its-old-workspace window against whatever
/// `current_workspace` had just been set to by the same IPC mutation this
/// re-sync is reacting to, and silently switched it right back --
/// confirmed live as `srd dispatch activate workspace <id>` (and, by
/// extension, any AGS workspace-switcher click going through the same IPC
/// path) visibly changing `current_workspace` for a moment and then
/// reverting within milliseconds, every time, unless the same IPC call
/// also happened to change which window was focused. Exactly the same bug
/// `main.rs`'s `sync()` was already fixed for (see its own doc comment) --
/// this is a second call site with the identical unconditional-`focus_
/// window`-reassertion shape, never fixed at the same time.
pub(crate) fn raise_in_space(state: &mut CompState, id: WindowId) {
    if let Some(w) = state.id_to_window.get(&id).cloned() {
        state.space.raise_element(&w, true);
        state.raise_pinned();
    }
}

/// Re-syncs real Wayland/X11 keyboard focus to whatever `WindowManager`
/// already considers focused, without changing what that is.
///
/// For callers where core's own focus already moved on its own --
/// specifically `WindowManager::remove_window`'s fallback to
/// `self.order.last()` when the just-closed window was the focused one --
/// and only the Wayland/X11 side needs to catch up to it. Without this, the
/// window core now considers focused (and renders as such) never actually
/// receives a keystroke until it's clicked, since nothing told
/// `set_keyboard_focus` focus had moved.
///
/// `focus_window` above is for the opposite direction: driving core's
/// focus deliberately (a click, a keybinding) and syncing outward from
/// that. This is "core already decided, catch the rest of the compositor
/// up" - `wm.focus_window` must not be called again here, since the id
/// core picked (or `None`, if nothing is left) is exactly what should win.
pub(crate) fn sync_keyboard_focus(state: &mut CompState) {
    let focused = state.wm.borrow().focused_id();
    let surface = focused.and_then(|id| state.id_to_window.get(&id)).and_then(dwindow_wl_surface);
    state.set_keyboard_focus(surface);
}