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}