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/// Show `window`, built hidden, without making it the key or focused window:
119/// on macOS `orderFront:`, where winit's `set_visible(true)` is
120/// `makeKeyAndOrderFront:`. Elsewhere winit's `set_visible(true)`, which on
121/// Windows shows a window built with `with_active(false)` with
122/// `SW_SHOWNOACTIVATE` the first time, and on X11 and Wayland leaves the
123/// focus to the window manager or compositor.
124pub(crate) fn show_without_activating(window: &winit::window::Window) {
125    #[cfg(target_os = "macos")]
126    {
127        use objc2::rc::Retained;
128        use objc2_app_kit::NSView;
129        use winit::raw_window_handle::{HasWindowHandle, RawWindowHandle};
130
131        if let Ok(handle) = window.window_handle()
132            && let RawWindowHandle::AppKit(appkit) = handle.as_raw()
133        {
134            // SAFETY: the ns_view pointer is valid while `window` is alive,
135            // which the caller holds for this call; winit runs on the main
136            // thread, as AppKit requires.
137            unsafe {
138                if let Some(view) = Retained::<NSView>::retain(appkit.ns_view.as_ptr().cast())
139                    && let Some(ns_window) = view.window()
140                {
141                    ns_window.orderFront(None);
142                    return;
143                }
144            }
145        }
146        window.set_visible(true);
147    }
148
149    #[cfg(not(target_os = "macos"))]
150    window.set_visible(true);
151}
152
153/// Attach `child` as a child window of `parent` using
154/// `NSWindowOrderingMode::Above` on macOS. Call this *after* both
155/// windows' AccessKit adapters have been created (and thus after the
156/// child becomes visible). Works for modal and non-modal parented
157/// windows alike — popover-as-window, inspector palettes, floating
158/// tool panels are all expected to route through this path as the
159/// multi-window system matures.
160///
161/// On Windows the parent relationship is the owner window winit sets at
162/// creation, and on X11 it is [`set_transient_for_before_show`], so this is a
163/// no-op there. macOS needs the deferred call because AppKit's
164/// `-[NSWindow addChildWindow:ordered:]` orders the child window
165/// front (making it visible), which conflicts with AccessKit's
166/// requirement that its adapter be created while the window is still
167/// hidden.
168pub fn attach_child_window(parent: &winit::window::Window, child: &winit::window::Window) {
169    #[cfg(target_os = "macos")]
170    {
171        use objc2::rc::Retained;
172        use objc2_app_kit::{NSView, NSWindowOrderingMode};
173        use winit::raw_window_handle::{HasWindowHandle, RawWindowHandle};
174
175        let Ok(parent_handle) = parent.window_handle() else {
176            return;
177        };
178        let Ok(child_handle) = child.window_handle() else {
179            return;
180        };
181
182        let (RawWindowHandle::AppKit(p), RawWindowHandle::AppKit(c)) =
183            (parent_handle.as_raw(), child_handle.as_raw())
184        else {
185            return;
186        };
187
188        // SAFETY: the ns_view pointers are valid while `parent` /
189        // `child` are alive; both are held by the caller (typically
190        // the WindowManager) for the lifetime of this call.
191        unsafe {
192            let Some(parent_view): Option<Retained<NSView>> =
193                Retained::retain(p.ns_view.as_ptr().cast())
194            else {
195                return;
196            };
197            let Some(child_view): Option<Retained<NSView>> =
198                Retained::retain(c.ns_view.as_ptr().cast())
199            else {
200                return;
201            };
202            if let (Some(pw), Some(cw)) = (parent_view.window(), child_view.window()) {
203                // Just above the parent first: a dialog shown without
204                // activating went in front of every window but the active
205                // application's front one, and its parent came up with it,
206                // above the active application's other windows.
207                cw.orderWindow_relativeTo(NSWindowOrderingMode::Above, pw.windowNumber());
208                pw.addChildWindow_ordered(&cw, NSWindowOrderingMode::Above);
209            }
210        }
211    }
212
213    #[cfg(not(target_os = "macos"))]
214    {
215        let _ = (parent, child);
216    }
217}
218
219/// Keep `window` out of the Window menu's window list and the Dock icon's
220/// menu on macOS, or list it there again; nothing on other platforms.
221///
222/// AppKit lists every titled window it orders in, and `teksilo-app` keeps
223/// a modal dialog and a window kept out of the taskbar (a palette) out, as
224/// AppKit's own sheets and panels are: the list is for the windows a user
225/// switches between, and a dialog comes forward with the window it blocks. Call it before the window is first shown, so its
226/// row never appears.
227#[doc(hidden)]
228pub fn set_listed_in_window_menu(window: &winit::window::Window, listed: bool) {
229    #[cfg(target_os = "macos")]
230    {
231        use objc2::rc::Retained;
232        use objc2_app_kit::NSView;
233        use winit::raw_window_handle::{HasWindowHandle, RawWindowHandle};
234
235        let Ok(handle) = window.window_handle() else {
236            return;
237        };
238        let RawWindowHandle::AppKit(appkit) = handle.as_raw() else {
239            return;
240        };
241        // SAFETY: the ns_view pointer is valid while `window` is alive, and
242        // the caller holds it for the duration of this call.
243        let view: Option<Retained<NSView>> =
244            unsafe { Retained::retain(appkit.ns_view.as_ptr().cast()) };
245        if let Some(ns_window) = view.and_then(|view| view.window()) {
246            ns_window.setExcludedFromWindowsMenu(!listed);
247        }
248    }
249
250    #[cfg(not(target_os = "macos"))]
251    {
252        let _ = (window, listed);
253    }
254}
255
256/// Make `child` a transient window of `parent` on X11 (ICCCM
257/// `WM_TRANSIENT_FOR`), before `child` is shown for the first time; nothing
258/// on other window systems.
259///
260/// winit 0.30 has no call for it: its `with_parent_window` creates an X11
261/// window *inside* the parent, confined to and clipped by its client area,
262/// and is ignored on Wayland, where no protocol open to every client sets a
263/// parent. The window manager keeps a transient window above its parent and
264/// treats it as a dialog. The property is written before the first map, as
265/// [`skip_taskbar_before_show`](crate::taskbar::skip_taskbar_before_show)
266/// writes its own. A failure is reported on stderr and leaves `child` a window
267/// of its own.
268pub fn set_transient_for_before_show(
269    child: &winit::window::Window,
270    parent: &winit::window::Window,
271) {
272    #[cfg(all(unix, not(target_os = "macos")))]
273    {
274        let (Some(child), Some(parent)) = (x11_window_id(child), x11_window_id(parent)) else {
275            return;
276        };
277        if let Err(err) = crate::x11::ewmh::set_transient_for(child, parent) {
278            eprintln!(
279                "teksilo-platform: could not make the window transient for its parent ({err}); \
280                 the window manager will treat it as a window of its own"
281            );
282        }
283    }
284    #[cfg(not(all(unix, not(target_os = "macos"))))]
285    let _ = (child, parent);
286}
287
288/// Make `window`, a modal dialog, modal to the window manager on X11
289/// (`_NET_WM_STATE_MODAL`), before it is shown for the first time; nothing on
290/// other window systems. With `WM_TRANSIENT_FOR`
291/// ([`set_transient_for_before_show`]) and the dialog window type, it is how
292/// GTK marks a modal dialog. A failure is reported on stderr and leaves the
293/// dialog a plain transient window.
294#[doc(hidden)]
295pub fn mark_modal_before_show(window: &winit::window::Window) {
296    #[cfg(all(unix, not(target_os = "macos")))]
297    {
298        let Some(xid) = x11_window_id(window) else {
299            return;
300        };
301        if let Err(err) = crate::x11::ewmh::mark_modal(xid) {
302            eprintln!(
303                "teksilo-platform: could not mark the dialog modal ({err}); the window manager \
304                 will treat it as a plain transient window"
305            );
306        }
307    }
308    #[cfg(not(all(unix, not(target_os = "macos"))))]
309    let _ = window;
310}
311
312/// The X11 window behind `window`, `None` on any other window system.
313#[cfg(all(unix, not(target_os = "macos")))]
314pub(crate) fn x11_window_id(window: &winit::window::Window) -> Option<u32> {
315    use winit::raw_window_handle::{HasDisplayHandle, HasWindowHandle, RawWindowHandle};
316
317    let display = window.display_handle().ok()?;
318    if window_system_for_display_handle(&display.as_raw()) != WindowSystem::X11 {
319        return None;
320    }
321    match window.window_handle().ok()?.as_raw() {
322        RawWindowHandle::Xlib(handle) => Some(handle.window as u32),
323        RawWindowHandle::Xcb(handle) => Some(handle.window.get()),
324        _ => None,
325    }
326}
327
328#[cfg(test)]
329mod tests {
330    use super::*;
331
332    // The env-detection table below is winit 0.30's own precedence
333    // (`platform_impl/linux/mod.rs`): Wayland vars win over `DISPLAY`, empty
334    // strings count as unset. If a winit upgrade changes that order, these
335    // tests are the tripwire — Teksilo predicting a different backend than
336    // winit creates means custom chrome is requested for the wrong protocol.
337    #[cfg(all(unix, not(target_os = "macos")))]
338    #[test]
339    fn prefers_wayland_when_both_displays_exist() {
340        assert_eq!(
341            detect_from_env(Some("wayland-0"), None, Some(":0")),
342            WindowSystem::Wayland
343        );
344    }
345
346    #[cfg(all(unix, not(target_os = "macos")))]
347    #[test]
348    fn wayland_socket_also_selects_wayland() {
349        assert_eq!(
350            detect_from_env(None, Some("4"), Some(":0")),
351            WindowSystem::Wayland
352        );
353    }
354
355    #[cfg(all(unix, not(target_os = "macos")))]
356    #[test]
357    fn detects_x11_from_display() {
358        assert_eq!(detect_from_env(None, None, Some(":0")), WindowSystem::X11);
359    }
360
361    /// The XWayland escape hatch: clearing `WAYLAND_DISPLAY` in a Wayland
362    /// session makes winit create a genuine X11 client. Teksilo must follow,
363    /// which is exactly why `XDG_SESSION_TYPE` (still "wayland" here) is not
364    /// consulted.
365    #[cfg(all(unix, not(target_os = "macos")))]
366    #[test]
367    fn empty_wayland_display_falls_back_to_x11() {
368        assert_eq!(
369            detect_from_env(Some(""), Some(""), Some(":0")),
370            WindowSystem::X11
371        );
372    }
373
374    #[cfg(all(unix, not(target_os = "macos")))]
375    #[test]
376    fn returns_unknown_without_display_hints() {
377        assert_eq!(detect_from_env(None, None, None), WindowSystem::Unknown);
378    }
379
380    #[cfg(all(unix, not(target_os = "macos")))]
381    #[test]
382    fn returns_unknown_when_all_hints_are_empty() {
383        assert_eq!(
384            detect_from_env(Some(""), Some(""), Some("")),
385            WindowSystem::Unknown
386        );
387    }
388
389    #[test]
390    fn display_handle_discriminates_the_live_backend() {
391        use raw_window_handle::{RawDisplayHandle, XcbDisplayHandle, XlibDisplayHandle};
392
393        assert_eq!(
394            window_system_for_display_handle(&RawDisplayHandle::Xlib(XlibDisplayHandle::new(
395                None, 0
396            ))),
397            WindowSystem::X11
398        );
399        assert_eq!(
400            window_system_for_display_handle(&RawDisplayHandle::Xcb(XcbDisplayHandle::new(
401                None, 0
402            ))),
403            WindowSystem::X11
404        );
405    }
406
407    /// Linux/BSD never gets a native modal window: Wayland has no protocol for
408    /// it and X11's `_NET_WM_STATE_MODAL` is advisory. See the fn docs.
409    #[cfg(all(unix, not(target_os = "macos")))]
410    #[test]
411    fn linux_family_never_uses_native_modals() {
412        assert!(!supports_native_modal_windows());
413    }
414}