srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/crates/x11/src/platform/mod.rs
blob: 0f097df7f293183068fc75d0749bec84f6671b5b (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
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
//! X11 backend for srdwm: a classic reparenting window manager that draws
//! its own title bar (close/maximize/minimize buttons, drag-to-move,
//! edge/corner resize) rather than relying on any toolkit's decorations -
//! this is what gives "full title bar support" parity with Windows/macOS on
//! X11.
//!
//! Compared to the legacy C++ `x11_platform.cc` (see docs/PRIOR_ART.md),
//! this fixes several real bugs rather than porting them:
//! - the titlebar was drawn at a hardcoded 800px width; here it's sized to
//!   the actual frame width every time (see `redraw_decoration`).
//! - there was no drag/resize/button hit-testing at all; here it's the
//!   shared `srdwm_core::window::ResizeEdge::hit_test` used by every backend.
//! - `check_for_other_wm` always returned `true` because its error handler
//!   discarded errors; here we do a *checked* `change_window_attributes`
//!   with `SUBSTRUCTURE_REDIRECT` and propagate a real `BadAccess` as
//!   [`PlatformError::AnotherWmRunning`].
//! - RandR monitor geometry used the output's physical size in
//!   millimeters instead of the CRTC's pixel mode; here it reads the CRTC.
//!
//! Not implemented (documented rather than faked): XKB-level keymaps (only
//! a hand-maintained keysym table covering common keys, shared with the
//! Wayland backend via `srdwm_core::keysyms`), ICCCM `WM_HINTS`/urgency, and
//! EWMH pager/taskbar hints beyond
//! `_NET_SUPPORTED`/`_NET_CLIENT_LIST`/`_NET_WM_STATE` maximize.

use srdwm_core::keysyms;
use srdwm_core::{Event, Modifiers, MouseButton, TitlebarHit, Window as CoreWindow, WindowId, TITLEBAR_HEIGHT};
use srdwm_core::{Monitor, Rect, WindowManager};
use srdwm_platform::{Platform, PlatformError, PlatformKind, Result as PlatformResult};
use std::cell::RefCell;
use std::collections::HashMap;
use std::os::unix::io::AsRawFd;
use std::rc::Rc;
use x11rb::connection::Connection;
use x11rb::protocol::randr::ConnectionExt as _;
use x11rb::protocol::xproto::{
    ButtonIndex, ChangeWindowAttributesAux, ConfigureWindowAux, ConnectionExt as _, CreateGCAux, CreateWindowAux,
    EventMask, GrabMode, ModMask, Rectangle, StackMode, Window as XWindow, WindowClass,
};
use x11rb::protocol::Event as XEvent;
use x11rb::rust_connection::RustConnection;
use x11rb::wrapper::ConnectionExt as _;
use x11rb::COPY_DEPTH_FROM_PARENT;

x11rb::atom_manager! {
    pub Atoms: AtomsCookie {
        WM_PROTOCOLS,
        WM_DELETE_WINDOW,
        WM_STATE,
        _NET_SUPPORTED,
        _NET_WM_NAME,
        _NET_WM_STATE,
        _NET_WM_STATE_MAXIMIZED_VERT,
        _NET_WM_STATE_MAXIMIZED_HORZ,
        _NET_CLIENT_LIST,
        _NET_ACTIVE_WINDOW,
        _NET_WM_STRUT,
        _NET_WM_STRUT_PARTIAL,
        UTF8_STRING,
        // Global-menu properties - see `read_global_menu`'s doc comment.
        // A native X11 client is exactly the same GTK/Qt app the Wayland
        // backend's `xwayland.rs::read_global_menu` already reads these
        // from (XWayland is just another X server as far as a toolkit is
        // concerned), so this is the identical atom set for the identical
        // reason.
        _GTK_UNIQUE_BUS_NAME,
        _GTK_APPLICATION_OBJECT_PATH,
        _GTK_WINDOW_OBJECT_PATH,
        _GTK_MENUBAR_OBJECT_PATH,
        _GTK_APP_MENU_OBJECT_PATH,
        _UNITY_OBJECT_PATH,
        // KWin's own global-menu property pair - what `libdbusmenu-qt`'s
        // KDE integration sets. Already a complete, unambiguous
        // `com.canonical.dbusmenu` address on its own (no classification
        // needed the way the GTK/Unity atoms above need), and checked
        // first in `read_global_menu` for exactly that reason - see that
        // method's doc comment.
        _KDE_NET_WM_APPMENU_SERVICE_NAME,
        _KDE_NET_WM_APPMENU_OBJECT_PATH,
    }
}

struct Frame {
    frame: XWindow,
    client: XWindow,
    supports_delete: bool,
}

/// One window's own `_NET_WM_STRUT_PARTIAL` (or the older, span-free
/// `_NET_WM_STRUT`) reservation - the X11 equivalent of a Wayland
/// layer-shell surface's exclusive zone (`zwlr_layer_surface_v1::set_
/// exclusive_zone`, read on the Wayland backends via `layer_map_for_
/// output(...).non_exclusive_zone()`). At most one edge is ever nonzero
/// for a real panel/dock (a bar reserves *one* strip, not several), but
/// the property itself allows all four at once, so all four are kept.
///
/// Values are in root-window (screen-global) pixels, matching every other
/// X11 geometry value in this backend - there is no separate logical/
/// physical scale to convert between the way the Wayland backends' own
/// `zone_physical` conversion needs (`udev/platform.rs`'s `monitors()`),
/// since this compositor's X11 backend has no independent output-scale
/// concept at all.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
struct Strut {
    left: u32,
    right: u32,
    top: u32,
    bottom: u32,
    left_start_y: i32,
    left_end_y: i32,
    right_start_y: i32,
    right_end_y: i32,
    top_start_x: i32,
    top_end_x: i32,
    bottom_start_x: i32,
    bottom_end_x: i32,
}

fn err(e: impl std::fmt::Display) -> PlatformError {
    PlatformError::Other(e.to_string())
}

/// Finds which of `ModMask::M1`..`M5` a keycode is bound to, given a
/// `GetModifierMappingReply`'s flattened `keycodes` list (8 fixed slots --
/// Shift, Lock, Control, Mod1..Mod5 - each `keycodes_per_modifier` long,
/// zero-padded). Only scans the Mod1..Mod5 slots (indices 3..8): Shift/
/// Lock/Control are never where Num Lock lands in practice, and this is
/// only ever called looking for it. Returns an empty mask if the keycode
/// isn't bound to any modifier at all (a keyboard with no Num Lock key, or
/// a keycode of `0` from a lookup that found nothing).
fn modmask_for_keycode_in_mod_slots(keycode: u8, keycodes_per_modifier: usize, keycodes: &[u8]) -> ModMask {
    if keycode == 0 || keycodes_per_modifier == 0 {
        return ModMask::from(0u16);
    }
    (3..8usize)
        .find(|&slot| {
            let start = slot * keycodes_per_modifier;
            keycodes.get(start..start + keycodes_per_modifier).is_some_and(|ks| ks.contains(&keycode))
        })
        .map(|slot| ModMask::from(1u16 << slot))
        .unwrap_or(ModMask::from(0u16))
}

/// Packs an RGB triple into the `0x00RRGGBB` pixel value X11's
/// `border_pixel`/GC `foreground` etc. expect on a TrueColor visual --
/// matching the format the hardcoded titlebar colour constants in
/// `redraw_decoration` already use.
fn rgb_to_pixel((r, g, b): (u8, u8, u8)) -> u32 {
    ((r as u32) << 16) | ((g as u32) << 8) | (b as u32)
}

/// The actual arithmetic behind `Platform::apply_geometry`'s frame
/// placement - pulled out so it's testable without a real X11
/// connection, the same reasoning `modmask_for_keycode_in_mod_slots`
/// above already gets.
///
/// X11's native `border_width` window attribute is drawn OUTSIDE a
/// window's own declared width/height, on all four sides, by the X
/// server itself - unlike every other backend's own border (rendered as
/// ordinary pixels *inside* the allocated geometry rect, Wayland's
/// `decoration.rs`, concretely). `geometry` is the true, already-decided
/// on-screen rect (`Window::maximize_geometry`'s own doc comment); this
/// shifts the *configured* origin inward and the *configured* size down
/// by `border_width` on both axes so the window's real, visible footprint
/// (native border included) still lands exactly on `geometry`, instead of
/// spilling `border_width` pixels past every edge of it. Returns
/// `(frame_x, frame_y, frame_width, frame_height)` - the caller applies
/// `band` (the titlebar reservation) on top of `frame_height` separately,
/// same as before this existed.
fn frame_geometry_for(geometry: Rect, border_width: u32) -> (i32, i32, u32, u32) {
    let bw = border_width as i32;
    (geometry.x + bw, geometry.y + bw, geometry.width.saturating_sub(2 * border_width), geometry.height.saturating_sub(2 * border_width))
}

pub struct X11Platform {
    conn: RustConnection,
    root: XWindow,
    atoms: Atoms,
    gc: x11rb::protocol::xproto::Gcontext,
    font: x11rb::protocol::xproto::Font,
    wm: Rc<RefCell<WindowManager>>,
    xid_to_core: HashMap<XWindow, WindowId>,
    frames: HashMap<WindowId, Frame>,
    min_keycode: u8,
    max_keycode: u8,
    keysyms_per_keycode: u8,
    keyboard_mapping: Vec<u32>,
    /// Whichever of `ModMask::M1`..`M5` the server has Num Lock bound to --
    /// see `grab_keybindings`'s doc comment for why this needs grabbing
    /// alongside every binding, not just the modifiers a config actually
    /// asked for.
    numlock_mask: ModMask,
    /// `srd`'s control socket - see `srdwm_platform::IpcServer`'s module
    /// doc comment. `None` if binding it failed (a stale socket from a
    /// still-running instance, an unwritable runtime dir): the compositor
    /// itself still starts either way, matching how the Wayland backends
    /// already treat this as non-fatal.
    ipc: Option<srdwm_platform::IpcServer>,
    /// `com.canonical.AppMenu.Registrar` - the classic Qt/`appmenu-qt5`
    /// global-menu source, see `srdwm_platform::appmenu_registrar`'s module
    /// doc comment. Unlike the Wayland backend (where this is `None` until
    /// XWayland finishes starting up), a native X11 session always has a
    /// real X server the moment this struct exists, so it's started
    /// unconditionally in `connect` - still `Option` because starting the
    /// D-Bus service itself can independently fail (see that module's own
    /// `None` handling).
    appmenu_registrar: Option<srdwm_platform::AppmenuRegistrarState>,
    /// Every currently-mapped window's own `_NET_WM_STRUT_PARTIAL`/`_NET_
    /// WM_STRUT` reservation, keyed by its X window id - populated on
    /// `MapNotify` (see `events.rs`) and kept fresh via `PropertyNotify`
    /// on the same two atoms, removed on `UnmapNotify`/`DestroyNotify`.
    /// Read by `monitors()` to shrink each monitor's own usable rect the
    /// same way the Wayland backends' layer-shell exclusive zones already
    /// do - see that method's own doc comment. Deliberately keyed on
    /// *any* mapped window that sets the property, not gated on `_NET_WM_
    /// WINDOW_TYPE_DOCK` specifically: the EWMH spec's actual reservation
    /// mechanism is the strut property itself, and real struts also come
    /// from override-redirect panels that never go through `manage_new_
    /// window`/`xid_to_core` at all (confirmed live by a peer session's
    /// own aegis bar, an override-redirect `_NET_WM_WINDOW_TYPE_DOCK`
    /// window) - so this can't be folded into the existing managed-
    /// window bookkeeping.
    struts: HashMap<XWindow, Strut>,
    /// The currently-open right-click titlebar window menu, if any --
    /// the built row set/geometry alongside the override-redirect popup
    /// window it's drawn into. See `context_menu.rs`'s module doc comment
    /// for why X11 needs its own popup window and pointer grab where the
    /// Wayland backend just reads a compositor-space struct.
    context_menu: Option<(srdwm_core::context_menu::ContextMenu, XWindow)>,
}



/// Small helper so we can grab an owned snapshot of a `&Window` out of a
/// `Ref<WindowManager>` borrow without holding the borrow across the redraw call.
trait ClonedForRender {
    fn cloned_for_render(self) -> Option<CoreWindow>;
}
impl ClonedForRender for Option<&CoreWindow> {
    fn cloned_for_render(self) -> Option<CoreWindow> {
        self.cloned()
    }
}

mod actions;
mod connect;
mod context_menu;
mod events;
mod global_menu;
mod struts;
mod trait_impl;
mod window;

#[cfg(test)]
use struts::usable_rect;

#[cfg(test)]
mod tests;