teksilo_platform/window_system.rs
1// SPDX-License-Identifier: MPL-2.0
2// SPDX-FileCopyrightText: 2026 FernTech
3
4//! Which windowing system a Linux/BSD session is running on, and what that
5//! implies for window chrome and modal windows.
6//!
7//! Two detectors live here and they are **not** interchangeable:
8//!
9//! - [`active_window_system`] predicts, from the environment, which backend
10//! winit will pick. It is only valid *before* a window exists — its one job
11//! is deciding `WindowAttributes::with_decorations(..)`, which has to be
12//! chosen at window-construction time. It mirrors winit's own precedence
13//! exactly; any divergence means Teksilo and winit
14//! disagree about the session and the chrome comes out wrong.
15//! - [`window_system_for_display_handle`] reads the *live* handle of an
16//! already-created window. It is authoritative and should be preferred
17//! everywhere a window (or a `ParentHandle`) is in reach — the title-bar host
18//! factory and the external-DnD backend both dispatch on it, so they can
19//! never disagree with each other or with winit.
20
21/// The windowing system backing a window (or the session, when predicted).
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub enum WindowSystem {
24 Wayland,
25 X11,
26 Unknown,
27}
28
29/// Predict winit's backend choice from the environment.
30///
31/// This mirrors winit 0.30's `platform_impl::linux::EventLoop::new` precedence
32/// verbatim: a non-empty `WAYLAND_DISPLAY` **or** `WAYLAND_SOCKET` selects
33/// Wayland; otherwise a non-empty `DISPLAY` selects X11.
34///
35/// `XDG_SESSION_TYPE` is deliberately **not** consulted — winit ignores it, and
36/// honouring it here would make Teksilo disagree with the backend that actually
37/// gets created. Concretely: a Wayland session where the user clears
38/// `WAYLAND_DISPLAY` to force an X11 client still reports
39/// `XDG_SESSION_TYPE=wayland`, so trusting it would claim Wayland for a window
40/// winit created on X11.
41#[cfg(all(unix, not(target_os = "macos")))]
42fn detect_from_env(
43 wayland_display: Option<&str>,
44 wayland_socket: Option<&str>,
45 display: Option<&str>,
46) -> WindowSystem {
47 let non_empty = |value: Option<&str>| value.is_some_and(|value| !value.is_empty());
48
49 if non_empty(wayland_display) || non_empty(wayland_socket) {
50 WindowSystem::Wayland
51 } else if non_empty(display) {
52 WindowSystem::X11
53 } else {
54 WindowSystem::Unknown
55 }
56}
57
58/// The window system winit is expected to use for windows created in this
59/// process. See the module docs: this is a *prediction* for the pre-window-
60/// creation decisions only. Once a window exists, prefer
61/// [`window_system_for_display_handle`].
62pub fn active_window_system() -> WindowSystem {
63 #[cfg(all(unix, not(target_os = "macos")))]
64 {
65 detect_from_env(
66 std::env::var("WAYLAND_DISPLAY").ok().as_deref(),
67 std::env::var("WAYLAND_SOCKET").ok().as_deref(),
68 std::env::var("DISPLAY").ok().as_deref(),
69 )
70 }
71
72 #[cfg(not(all(unix, not(target_os = "macos"))))]
73 {
74 WindowSystem::Unknown
75 }
76}
77
78/// The window system a live window actually runs on, read from its raw display
79/// handle. Authoritative — unlike [`active_window_system`] it cannot disagree
80/// with winit, because the handle *is* winit's answer.
81///
82/// Every consumer that has a window (or a
83/// [`ParentHandle`](teksilo_core::raw_handle::ParentHandle)) in hand should
84/// dispatch on this so the title-bar host and the DnD backend never diverge.
85pub fn window_system_for_display_handle(
86 handle: &raw_window_handle::RawDisplayHandle,
87) -> WindowSystem {
88 use raw_window_handle::RawDisplayHandle;
89
90 match handle {
91 RawDisplayHandle::Wayland(_) => WindowSystem::Wayland,
92 RawDisplayHandle::Xlib(_) | RawDisplayHandle::Xcb(_) => WindowSystem::X11,
93 _ => WindowSystem::Unknown,
94 }
95}
96
97/// Whether a modal dialog should be presented as a real OS window rather than
98/// an in-tree overlay.
99///
100/// `false` for the whole Linux/BSD family. On Wayland there is no protocol for
101/// a client to make another surface input-blocking at all; on X11
102/// `_NET_WM_STATE_MODAL` is a *hint* — window managers use it for stacking and
103/// focus policy, but none of them enforce input blocking on the parent, so a
104/// "native" modal there would look modal without behaving modally. An in-tree
105/// modal is genuinely modal on both, so both use it.
106pub fn supports_native_modal_windows() -> bool {
107 #[cfg(all(unix, not(target_os = "macos")))]
108 {
109 false
110 }
111
112 #[cfg(not(all(unix, not(target_os = "macos"))))]
113 {
114 true
115 }
116}
117
118/// Attach `child` as a child window of `parent` using
119/// `NSWindowOrderingMode::Above` on macOS. Call this *after* both
120/// windows' AccessKit adapters have been created (and thus after the
121/// child becomes visible). Works for modal and non-modal parented
122/// windows alike — popover-as-window, inspector palettes, floating
123/// tool panels are all expected to route through this path as the
124/// multi-window system matures.
125///
126/// On non-macOS targets the parent relationship is already set by
127/// winit's `WindowAttributes::with_parent_window` at creation time,
128/// so this is a no-op. macOS needs the deferred call because AppKit's
129/// `-[NSWindow addChildWindow:ordered:]` orders the child window
130/// front (making it visible), which conflicts with AccessKit's
131/// requirement that its adapter be created while the window is still
132/// hidden.
133pub fn attach_child_window(parent: &winit::window::Window, child: &winit::window::Window) {
134 #[cfg(target_os = "macos")]
135 {
136 use objc2::rc::Retained;
137 use objc2_app_kit::{NSView, NSWindowOrderingMode};
138 use winit::raw_window_handle::{HasWindowHandle, RawWindowHandle};
139
140 let Ok(parent_handle) = parent.window_handle() else {
141 return;
142 };
143 let Ok(child_handle) = child.window_handle() else {
144 return;
145 };
146
147 let (RawWindowHandle::AppKit(p), RawWindowHandle::AppKit(c)) =
148 (parent_handle.as_raw(), child_handle.as_raw())
149 else {
150 return;
151 };
152
153 // SAFETY: the ns_view pointers are valid while `parent` /
154 // `child` are alive; both are held by the caller (typically
155 // the WindowManager) for the lifetime of this call.
156 unsafe {
157 let Some(parent_view): Option<Retained<NSView>> =
158 Retained::retain(p.ns_view.as_ptr().cast())
159 else {
160 return;
161 };
162 let Some(child_view): Option<Retained<NSView>> =
163 Retained::retain(c.ns_view.as_ptr().cast())
164 else {
165 return;
166 };
167 if let (Some(pw), Some(cw)) = (parent_view.window(), child_view.window()) {
168 pw.addChildWindow_ordered(&cw, NSWindowOrderingMode::Above);
169 }
170 }
171 }
172
173 #[cfg(not(target_os = "macos"))]
174 {
175 let _ = (parent, child);
176 }
177}
178
179#[cfg(test)]
180mod tests {
181 use super::*;
182
183 // The env-detection table below is winit 0.30's own precedence
184 // (`platform_impl/linux/mod.rs`): Wayland vars win over `DISPLAY`, empty
185 // strings count as unset. If a winit upgrade changes that order, these
186 // tests are the tripwire — Teksilo predicting a different backend than
187 // winit creates means custom chrome is requested for the wrong protocol.
188 #[cfg(all(unix, not(target_os = "macos")))]
189 #[test]
190 fn prefers_wayland_when_both_displays_exist() {
191 assert_eq!(
192 detect_from_env(Some("wayland-0"), None, Some(":0")),
193 WindowSystem::Wayland
194 );
195 }
196
197 #[cfg(all(unix, not(target_os = "macos")))]
198 #[test]
199 fn wayland_socket_also_selects_wayland() {
200 assert_eq!(
201 detect_from_env(None, Some("4"), Some(":0")),
202 WindowSystem::Wayland
203 );
204 }
205
206 #[cfg(all(unix, not(target_os = "macos")))]
207 #[test]
208 fn detects_x11_from_display() {
209 assert_eq!(detect_from_env(None, None, Some(":0")), WindowSystem::X11);
210 }
211
212 /// The XWayland escape hatch: clearing `WAYLAND_DISPLAY` in a Wayland
213 /// session makes winit create a genuine X11 client. Teksilo must follow,
214 /// which is exactly why `XDG_SESSION_TYPE` (still "wayland" here) is not
215 /// consulted.
216 #[cfg(all(unix, not(target_os = "macos")))]
217 #[test]
218 fn empty_wayland_display_falls_back_to_x11() {
219 assert_eq!(
220 detect_from_env(Some(""), Some(""), Some(":0")),
221 WindowSystem::X11
222 );
223 }
224
225 #[cfg(all(unix, not(target_os = "macos")))]
226 #[test]
227 fn returns_unknown_without_display_hints() {
228 assert_eq!(detect_from_env(None, None, None), WindowSystem::Unknown);
229 }
230
231 #[cfg(all(unix, not(target_os = "macos")))]
232 #[test]
233 fn returns_unknown_when_all_hints_are_empty() {
234 assert_eq!(
235 detect_from_env(Some(""), Some(""), Some("")),
236 WindowSystem::Unknown
237 );
238 }
239
240 #[test]
241 fn display_handle_discriminates_the_live_backend() {
242 use raw_window_handle::{RawDisplayHandle, XcbDisplayHandle, XlibDisplayHandle};
243
244 assert_eq!(
245 window_system_for_display_handle(&RawDisplayHandle::Xlib(XlibDisplayHandle::new(
246 None, 0
247 ))),
248 WindowSystem::X11
249 );
250 assert_eq!(
251 window_system_for_display_handle(&RawDisplayHandle::Xcb(XcbDisplayHandle::new(
252 None, 0
253 ))),
254 WindowSystem::X11
255 );
256 }
257
258 /// Linux/BSD never gets a native modal window: Wayland has no protocol for
259 /// it and X11's `_NET_WM_STATE_MODAL` is advisory. See the fn docs.
260 #[cfg(all(unix, not(target_os = "macos")))]
261 #[test]
262 fn linux_family_never_uses_native_modals() {
263 assert!(!supports_native_modal_windows());
264 }
265}