diff options
| author | srdusr <[email protected]> | 2025-02-15 14:56:00 +0200 |
|---|---|---|
| committer | srdusr <[email protected]> | 2025-02-15 14:56:00 +0200 |
| commit | 0a4c4b5941fe982ccb3d3175e26d9d83f0025ffd (patch) | |
| tree | 7672d0af277664f457c6c9462925c0005fe35dcf /crates/wayland/src/input/focus.rs | |
| parent | 413daa7ba2ea0ebd1424c024fd0566423aaea3f8 (diff) | |
| download | srdwm-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.rs | 138 |
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); +} |