Skip to main content

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}