Skip to main content

teksilo_platform/title_bar_host/
x11.rs

1// SPDX-License-Identifier: MPL-2.0
2// SPDX-FileCopyrightText: 2026 FernTech
3
4//! X11 title bar host.
5//!
6//! Like the Wayland backend this is pure delegation to winit — no raw X11 code
7//! is needed for the move/resize operations themselves. winit's X11
8//! `drag_window` / `drag_resize_window` already implement the EWMH
9//! `_NET_WM_MOVERESIZE` handshake correctly, including the mandatory
10//! `XUngrabPointer` before the client message (skipping that is what makes a
11//! window jump when the drag starts, because the WM cannot take the pointer
12//! while the client still holds the implicit grab from the button press).
13//!
14//! Two things differ from Wayland:
15//!
16//! - **The host refuses to exist without a capable window manager.** Server-side
17//!   decorations are suppressed via `_MOTIF_WM_HINTS`, and `_NET_WM_MOVERESIZE`
18//!   is then the *only* way the window can be moved or resized. If the WM does
19//!   not advertise it, [`X11Host::new`] returns [`PlatformError::Unsupported`],
20//!   the factory hands back `None`, and the app keeps native decorations —
21//!   rather than shipping a borderless window the user cannot move.
22//!   [`crate::x11::ewmh`] runs the probe once per process, before window
23//!   creation, because the decoration flag has to be decided at
24//!   `WindowAttributes` time.
25//! - **There is no system window menu.** winit's X11 `show_window_menu` is an
26//!   empty stub, and `_GTK_SHOW_WINDOW_MENU` is unimplemented by KWin
27//!   (KDE bug 454756), so `has_window_menu` reports `false` and the `TitleBar`
28//!   widget builds its own.
29//!
30//! Keyboard move/resize is *not* lost by going borderless: Alt+F7 / Alt+F8 (and
31//! the WM's own window-menu shortcut) are global window-manager bindings that
32//! work regardless of who draws the frame.
33
34use std::sync::Arc;
35
36use teksilo_canvas::{Point, Size};
37use teksilo_core::{
38    HitRegions, PlatformError, PlatformTitleBarHost, ResizeEdge, TitleBarHostCallbacks,
39};
40use winit::window::Window;
41
42use super::edge_to_direction;
43
44pub struct X11Host {
45    window: Arc<Window>,
46}
47
48impl X11Host {
49    pub fn new(
50        window: Arc<Window>,
51        _callbacks: TitleBarHostCallbacks,
52    ) -> Result<Self, PlatformError> {
53        // `callbacks` is unused here for the same reason as on Wayland: close
54        // flows through `WindowState::close` on the widget-tree side. The
55        // parameter is kept so the factory keeps one construction shape.
56        if !crate::x11::capabilities().supports_custom_chrome() {
57            return Err(PlatformError::Unsupported);
58        }
59        Ok(Self { window })
60    }
61}
62
63impl PlatformTitleBarHost for X11Host {
64    fn reserved_leading_inset(&self) -> Size {
65        Size::ZERO
66    }
67
68    fn reserved_trailing_inset(&self) -> Size {
69        Size::ZERO
70    }
71
72    fn renders_custom_controls(&self) -> bool {
73        true
74    }
75
76    fn needs_custom_resize_handles(&self) -> bool {
77        // With `_MOTIF_WM_HINTS` decorations off the WM draws no frame and
78        // provides no resize border, so the client owns the whole edge hit
79        // area and hands each press back via `begin_resize`.
80        true
81    }
82
83    fn begin_drag(&self) -> Result<(), PlatformError> {
84        self.window
85            .drag_window()
86            .map_err(|e| PlatformError::Os(e.to_string()))
87    }
88
89    fn begin_resize(&self, edge: ResizeEdge) -> Result<(), PlatformError> {
90        self.window
91            .drag_resize_window(edge_to_direction(edge))
92            .map_err(|e| PlatformError::Os(e.to_string()))
93    }
94
95    fn show_window_menu(&self, _at: Point) -> Result<(), PlatformError> {
96        // Unreachable in practice: `has_window_menu` is false, so `TitleBar`
97        // opens its own menu and never calls this. Reported honestly rather
98        // than returning `Ok(())` for a menu that would never appear.
99        Err(PlatformError::Unsupported)
100    }
101
102    fn has_window_menu(&self) -> bool {
103        false
104    }
105
106    fn update_hit_regions(&self, _regions: &HitRegions) {
107        // Nothing to publish: with no server-side frame every pointer event
108        // reaches the widget tree, which initiates drag / resize explicitly.
109        // (Windows needs this because the OS owns the non-client area.)
110    }
111}