srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/crates/wayland/src/input/focus.rs
diff options
context:
space:
mode:
authorsrdusr <[email protected]>2025-02-15 14:56:00 +0200
committersrdusr <[email protected]>2025-02-15 14:56:00 +0200
commit0a4c4b5941fe982ccb3d3175e26d9d83f0025ffd (patch)
tree7672d0af277664f457c6c9462925c0005fe35dcf /crates/wayland/src/input/focus.rs
parent413daa7ba2ea0ebd1424c024fd0566423aaea3f8 (diff)
downloadsrdwm-0a4c4b5941fe982ccb3d3175e26d9d83f0025ffd.tar.gz
srdwm-0a4c4b5941fe982ccb3d3175e26d9d83f0025ffd.zip
Checkpoint: preserve all uncommitted rust-rewrite worktree work
Safety commit before reconciling this worktree with main, which has diverged with its own separate fixes today. Nothing here is reviewed or curated yet - this exists purely so none of this work can be lost to a git operation, disk issue, or worktree cleanup while that reconciliation happens.
Diffstat (limited to 'crates/wayland/src/input/focus.rs')
-rw-r--r--crates/wayland/src/input/focus.rs138
1 files changed, 138 insertions, 0 deletions
diff --git a/crates/wayland/src/input/focus.rs b/crates/wayland/src/input/focus.rs
new file mode 100644
index 0000000..ade0759
--- /dev/null
+++ b/crates/wayland/src/input/focus.rs
@@ -0,0 +1,138 @@
+//! 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) {
+ 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);
+}