Skip to main content

gpui_platform/
window.rs

1//! Window vocabulary shared by `gpui` and its platform backends.
2
3use gpui_shared_string::SharedString;
4use gpui_types::{Bounds, Edges, Pixels, Point, Size, px, size};
5use std::{rc::Rc, sync::Arc, time::Duration};
6
7use crate::{DisplayId, DynRendererFactory, RendererFactory, WindowId, popup::PopupOptions};
8
9/// Default window size used when no explicit size is provided.
10pub const DEFAULT_WINDOW_SIZE: Size<Pixels> = size(px(1536.), px(1095.));
11
12/// The appearance of the window, as defined by the operating system.
13///
14/// On macOS, this corresponds to named [`NSAppearance`](https://developer.apple.com/documentation/appkit/nsappearance)
15/// values.
16#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
17pub enum WindowAppearance {
18    /// A light appearance.
19    ///
20    /// On macOS, this corresponds to the `aqua` appearance.
21    #[default]
22    Light,
23
24    /// A light appearance with vibrant colors.
25    ///
26    /// On macOS, this corresponds to the `NSAppearanceNameVibrantLight` appearance.
27    VibrantLight,
28
29    /// A dark appearance.
30    ///
31    /// On macOS, this corresponds to the `darkAqua` appearance.
32    Dark,
33
34    /// A dark appearance with vibrant colors.
35    ///
36    /// On macOS, this corresponds to the `NSAppearanceNameVibrantDark` appearance.
37    VibrantDark,
38}
39
40/// The appearance of the background of the window itself, when there is
41/// no content or the content is transparent.
42#[derive(Copy, Clone, Debug, Default, PartialEq)]
43pub enum WindowBackgroundAppearance {
44    /// Opaque.
45    ///
46    /// This lets the window manager know that content behind this
47    /// window does not need to be drawn.
48    ///
49    /// Actual color depends on the system and themes should define a fully
50    /// opaque background color instead.
51    #[default]
52    Opaque,
53    /// Plain alpha transparency.
54    Transparent,
55    /// Transparency, but the contents behind the window are blurred.
56    ///
57    /// Not always supported.
58    Blurred,
59    /// The Mica backdrop material, supported on Windows 11.
60    MicaBackdrop,
61    /// The Mica Alt backdrop material, supported on Windows 11.
62    MicaAltBackdrop,
63}
64
65/// Which part of the window to resize
66#[derive(Debug, Clone, Copy, PartialEq, Eq)]
67pub enum ResizeEdge {
68    /// The top edge
69    Top,
70    /// The top right corner
71    TopRight,
72    /// The right edge
73    Right,
74    /// The bottom right corner
75    BottomRight,
76    /// The bottom edge
77    Bottom,
78    /// The bottom left corner
79    BottomLeft,
80    /// The left edge
81    Left,
82    /// The top left corner
83    TopLeft,
84}
85
86/// A type to describe the appearance of a window
87#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Default)]
88pub enum WindowDecorations {
89    #[default]
90    /// Server side decorations
91    Server,
92    /// Client side decorations
93    Client,
94}
95
96/// A type to describe how this window is currently configured
97#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Default)]
98pub enum Decorations {
99    /// The window is configured to use server side decorations
100    #[default]
101    Server,
102    /// The window is configured to use client side decorations
103    Client {
104        /// The edge tiling state
105        tiling: Tiling,
106    },
107}
108
109/// What window controls this platform supports
110#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
111pub struct WindowControls {
112    /// Whether this platform supports fullscreen
113    pub fullscreen: bool,
114    /// Whether this platform supports maximize
115    pub maximize: bool,
116    /// Whether this platform supports minimize
117    pub minimize: bool,
118    /// Whether this platform supports a window menu
119    pub window_menu: bool,
120}
121
122impl Default for WindowControls {
123    fn default() -> Self {
124        // Assume that we can do anything, unless told otherwise
125        Self {
126            fullscreen: true,
127            maximize: true,
128            minimize: true,
129            window_menu: true,
130        }
131    }
132}
133
134/// A window control button type used in [`WindowButtonLayout`].
135#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
136pub enum WindowButton {
137    /// The minimize button
138    Minimize,
139    /// The maximize button
140    Maximize,
141    /// The close button
142    Close,
143}
144
145impl WindowButton {
146    /// Returns a stable element ID for rendering this button.
147    pub fn id(&self) -> &'static str {
148        match self {
149            WindowButton::Minimize => "minimize",
150            WindowButton::Maximize => "maximize",
151            WindowButton::Close => "close",
152        }
153    }
154
155    #[cfg(any(target_os = "linux", target_os = "freebsd"))]
156    fn index(&self) -> usize {
157        match self {
158            WindowButton::Minimize => 0,
159            WindowButton::Maximize => 1,
160            WindowButton::Close => 2,
161        }
162    }
163}
164
165/// Maximum number of [`WindowButton`]s per side in the titlebar.
166pub const MAX_BUTTONS_PER_SIDE: usize = 3;
167
168/// Describes which [`WindowButton`]s appear on each side of the titlebar.
169///
170/// On Linux, this is read from the desktop environment's configuration
171/// (e.g. GNOME's `gtk-decoration-layout` gsetting) via [`WindowButtonLayout::parse`].
172#[derive(Debug, Clone, Copy, PartialEq, Eq)]
173pub struct WindowButtonLayout {
174    /// Buttons on the left side of the titlebar.
175    pub left: [Option<WindowButton>; MAX_BUTTONS_PER_SIDE],
176    /// Buttons on the right side of the titlebar.
177    pub right: [Option<WindowButton>; MAX_BUTTONS_PER_SIDE],
178}
179
180#[cfg(any(target_os = "linux", target_os = "freebsd"))]
181impl WindowButtonLayout {
182    /// Returns Zed's built-in fallback button layout for Linux titlebars.
183    pub fn linux_default() -> Self {
184        Self {
185            left: [None; MAX_BUTTONS_PER_SIDE],
186            right: [
187                Some(WindowButton::Minimize),
188                Some(WindowButton::Maximize),
189                Some(WindowButton::Close),
190            ],
191        }
192    }
193
194    /// Parses a GNOME-style `button-layout` string (e.g. `"close,minimize:maximize"`).
195    pub fn parse(layout_string: &str) -> anyhow::Result<Self> {
196        fn parse_side(
197            s: &str,
198            seen_buttons: &mut [bool; MAX_BUTTONS_PER_SIDE],
199            unrecognized: &mut Vec<String>,
200        ) -> [Option<WindowButton>; MAX_BUTTONS_PER_SIDE] {
201            let mut result = [None; MAX_BUTTONS_PER_SIDE];
202            let mut i = 0;
203            for name in s.split(',') {
204                let trimmed = name.trim();
205                if trimmed.is_empty() {
206                    continue;
207                }
208                let button = match trimmed {
209                    "minimize" => Some(WindowButton::Minimize),
210                    "maximize" => Some(WindowButton::Maximize),
211                    "close" => Some(WindowButton::Close),
212                    other => {
213                        unrecognized.push(other.to_string());
214                        None
215                    }
216                };
217                if let Some(button) = button {
218                    if seen_buttons[button.index()] {
219                        continue;
220                    }
221                    if let Some(slot) = result.get_mut(i) {
222                        *slot = Some(button);
223                        seen_buttons[button.index()] = true;
224                        i += 1;
225                    }
226                }
227            }
228            result
229        }
230
231        let (left_str, right_str) = layout_string.split_once(':').unwrap_or(("", layout_string));
232        let mut unrecognized = Vec::new();
233        let mut seen_buttons = [false; MAX_BUTTONS_PER_SIDE];
234        let layout = Self {
235            left: parse_side(left_str, &mut seen_buttons, &mut unrecognized),
236            right: parse_side(right_str, &mut seen_buttons, &mut unrecognized),
237        };
238
239        if !unrecognized.is_empty()
240            && layout.left.iter().all(Option::is_none)
241            && layout.right.iter().all(Option::is_none)
242        {
243            anyhow::bail!(
244                "button layout string {:?} contains no valid buttons (unrecognized: {})",
245                layout_string,
246                unrecognized.join(", ")
247            );
248        }
249
250        Ok(layout)
251    }
252
253    /// Formats the layout back into a GNOME-style `button-layout` string.
254    pub fn format(&self) -> String {
255        fn format_side(buttons: &[Option<WindowButton>; MAX_BUTTONS_PER_SIDE]) -> String {
256            buttons
257                .iter()
258                .flatten()
259                .map(|button| match button {
260                    WindowButton::Minimize => "minimize",
261                    WindowButton::Maximize => "maximize",
262                    WindowButton::Close => "close",
263                })
264                .collect::<Vec<_>>()
265                .join(",")
266        }
267
268        format!("{}:{}", format_side(&self.left), format_side(&self.right))
269    }
270}
271
272/// A type to describe which sides of the window are currently tiled in some way
273#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Default)]
274pub struct Tiling {
275    /// Whether the top edge is tiled
276    pub top: bool,
277    /// Whether the left edge is tiled
278    pub left: bool,
279    /// Whether the right edge is tiled
280    pub right: bool,
281    /// Whether the bottom edge is tiled
282    pub bottom: bool,
283}
284
285impl Tiling {
286    /// Initializes a [`Tiling`] type with all sides tiled
287    pub fn tiled() -> Self {
288        Self {
289            top: true,
290            left: true,
291            right: true,
292            bottom: true,
293        }
294    }
295
296    /// Whether any edge is tiled
297    pub fn is_tiled(&self) -> bool {
298        self.top || self.left || self.right || self.bottom
299    }
300}
301
302#[derive(Debug, Copy, Clone, Eq, PartialEq, Default)]
303#[expect(missing_docs)]
304pub struct RequestFrameOptions {
305    /// Whether a presentation is required.
306    pub require_presentation: bool,
307    /// Force refresh of all rendering states when true.
308    pub force_render: bool,
309}
310
311/// Regions of a window that are obscured or reserved by the system.
312///
313/// Mobile applications often share space in their window with system-specific
314/// geometry, from keyboards to camera notches. In GPUI, all this is abstracted
315/// into a single "inset" which should be overlaid on the window's bounds.
316/// It is up to the application develop to determine how to handle these cases.
317#[derive(Debug, Clone, Default, PartialEq)]
318pub struct WindowInsets {
319    /// Regions covered by system UI or hardware: status bar, display
320    /// cutouts/notch, home indicator, navigation bars.
321    /// (iOS: `safeAreaInsets`. Android: `WindowInsets` of types
322    /// `systemBars() | displayCutout()`.)
323    pub safe_area: Edges<Pixels>,
324    /// The region covered by the keyboard, when present.
325    /// (iOS: derived from `keyboardWillShow`/frame-change notifications.
326    /// Android: `WindowInsets.Type.ime()`.)
327    pub ime: Edges<Pixels>,
328}
329
330impl WindowInsets {
331    /// The combined inset content should avoid.
332    pub fn effective(&self) -> Edges<Pixels> {
333        Edges {
334            top: self.safe_area.top.max(self.ime.top),
335            right: self.safe_area.right.max(self.ime.right),
336            bottom: self.safe_area.bottom.max(self.ime.bottom),
337            left: self.safe_area.left.max(self.ime.left),
338        }
339    }
340}
341
342/// A change in the state of the focused text input.
343#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
344pub enum TextInputStateChange {
345    /// The window changed from having no active text input to having one.
346    FocusGained,
347    /// The window no longer has an active text input.
348    FocusLost,
349    /// The selection or caret moved
350    SelectionChanged,
351    /// The document content changed outside of platform-initiated edits.
352    ContentChanged,
353}
354
355/// The variables that can be configured when creating a new window
356#[derive(Debug)]
357pub struct WindowOptions {
358    /// Specifies the state and bounds of the window in screen coordinates.
359    /// - `None`: Inherit the bounds.
360    /// - `Some(WindowBounds)`: Open a window with corresponding state and its restore size.
361    pub window_bounds: Option<WindowBounds>,
362
363    /// The titlebar configuration of the window
364    pub titlebar: Option<TitlebarOptions>,
365
366    /// Whether the window should be focused when created
367    pub focus: bool,
368
369    /// Whether the window should be shown when created
370    pub show: bool,
371
372    /// The kind of window to create
373    pub kind: WindowKind,
374
375    /// Whether the window can be moved by the user. When `false`, the user cannot drag
376    /// the window (on macOS this sets `NSWindow.isMovable`, which also disables the
377    /// Window-menu tiling items); programmatic moves are still allowed.
378    pub is_movable: bool,
379
380    /// Whether the application owns dragging of the (custom) titlebar, rather than
381    /// AppKit. Only has an effect on macOS.
382    ///
383    /// Set this to `true` for windows that draw their own titlebar and move the window
384    /// themselves via `Window::start_window_move`. It marks the whole content view as
385    /// app-owned titlebar content, so AppKit neither drags the window from the titlebar
386    /// nor delays titlebar clicks while disambiguating double-clicks (a delay first
387    /// observed on macOS 27). It is independent of `is_movable`, so such windows stay
388    /// user-movable (via their own drag) and keep the Window-menu tiling items enabled.
389    ///
390    /// Leave this `false` for windows that rely on AppKit's native titlebar dragging.
391    pub app_owns_titlebar_drag: bool,
392
393    /// The minimum interval between animation frames while the window is inactive.
394    ///
395    /// Set to `None` to disable inactive-window animation frame throttling.
396    pub inactive_frame_interval: Option<Duration>,
397
398    /// Whether the window should be resizable by the user
399    pub is_resizable: bool,
400
401    /// Whether the window should be minimized by the user
402    pub is_minimizable: bool,
403
404    /// The display to create the window on, if this is None,
405    /// the window will be created on the main display
406    pub display_id: Option<DisplayId>,
407
408    /// The appearance of the window background.
409    pub window_background: WindowBackgroundAppearance,
410
411    /// Application identifier of the window. Can by used by desktop environments to group applications together.
412    pub app_id: Option<String>,
413
414    /// Window minimum size
415    pub window_min_size: Option<Size<Pixels>>,
416
417    /// Whether to use client or server-side decorations on X11 and Wayland.
418    /// The platform may ignore requests it cannot satisfy.
419    pub window_decorations: Option<WindowDecorations>,
420
421    /// Icon image (X11 only)
422    pub icon: Option<Arc<image::RgbaImage>>,
423
424    /// Tab group name, allows opening the window as a native tab on macOS 10.12+. Windows with the same tabbing identifier will be grouped together.
425    pub tabbing_identifier: Option<String>,
426
427    /// The renderer this window should draw through, if the application wants its own.
428    ///
429    /// The factory runs once for the window, after the native window exists and before the
430    /// first frame, because a renderer binds to a surface and the surface comes from the
431    /// window. With `None` the backend builds the renderer it builds today, by the same
432    /// construction call, so a window that installs nothing behaves exactly as before.
433    pub renderer_factory: Option<DynRendererFactory>,
434}
435
436/// The variables that can be configured when creating a new window
437#[derive(Debug)]
438#[cfg_attr(
439    all(
440        any(target_os = "linux", target_os = "freebsd"),
441        not(any(feature = "x11", feature = "wayland"))
442    ),
443    allow(dead_code)
444)]
445#[allow(missing_docs)]
446pub struct WindowParams {
447    pub bounds: Bounds<Pixels>,
448
449    /// The titlebar configuration of the window
450    #[cfg_attr(feature = "wayland", allow(dead_code))]
451    pub titlebar: Option<TitlebarOptions>,
452
453    /// The kind of window to create
454    #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
455    pub kind: WindowKind,
456
457    /// Whether the window should be movable by the user
458    #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
459    pub is_movable: bool,
460
461    /// Whether the application owns dragging of the (custom) titlebar (macOS only)
462    #[cfg_attr(
463        any(target_os = "linux", target_os = "freebsd", target_os = "windows"),
464        allow(dead_code)
465    )]
466    pub app_owns_titlebar_drag: bool,
467
468    /// Whether the window should be resizable by the user
469    #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
470    pub is_resizable: bool,
471
472    /// Whether the window should be minimized by the user
473    #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
474    pub is_minimizable: bool,
475
476    #[cfg_attr(
477        any(target_os = "linux", target_os = "freebsd", target_os = "windows"),
478        allow(dead_code)
479    )]
480    pub focus: bool,
481
482    #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
483    pub show: bool,
484
485    /// An image to set as the window icon (x11 only)
486    #[cfg_attr(feature = "wayland", allow(dead_code))]
487    pub icon: Option<Arc<image::RgbaImage>>,
488
489    #[cfg_attr(feature = "wayland", allow(dead_code))]
490    pub display_id: Option<DisplayId>,
491
492    #[cfg_attr(feature = "wayland", allow(dead_code))]
493    pub app_id: Option<String>,
494
495    pub window_min_size: Option<Size<Pixels>>,
496
497    #[cfg(target_os = "macos")]
498    pub tabbing_identifier: Option<String>,
499
500    pub renderer_factory: Option<DynRendererFactory>,
501}
502
503/// Represents the status of how a window should be opened.
504#[derive(Debug, Copy, Clone, PartialEq)]
505pub enum WindowBounds {
506    /// Indicates that the window should open in a windowed state with the given bounds.
507    Windowed(Bounds<Pixels>),
508    /// Indicates that the window should open in a maximized state.
509    /// The bounds provided here represent the restore size of the window.
510    Maximized(Bounds<Pixels>),
511    /// Indicates that the window should open in fullscreen mode.
512    /// The bounds provided here represent the restore size of the window.
513    Fullscreen(Bounds<Pixels>),
514}
515
516impl Default for WindowBounds {
517    fn default() -> Self {
518        WindowBounds::Windowed(Bounds::default())
519    }
520}
521
522impl WindowBounds {
523    /// Retrieve the inner bounds
524    pub fn get_bounds(&self) -> Bounds<Pixels> {
525        match self {
526            WindowBounds::Windowed(bounds) => *bounds,
527            WindowBounds::Maximized(bounds) => *bounds,
528            WindowBounds::Fullscreen(bounds) => *bounds,
529        }
530    }
531}
532
533impl Default for WindowOptions {
534    fn default() -> Self {
535        Self {
536            window_bounds: None,
537            titlebar: Some(TitlebarOptions {
538                title: Default::default(),
539                appears_transparent: Default::default(),
540                traffic_light_position: Default::default(),
541            }),
542            focus: true,
543            show: true,
544            kind: WindowKind::Normal,
545            is_movable: true,
546            app_owns_titlebar_drag: false,
547            inactive_frame_interval: Some(Duration::from_micros(33_333)),
548            is_resizable: true,
549            is_minimizable: true,
550            display_id: None,
551            window_background: WindowBackgroundAppearance::default(),
552            icon: None,
553            app_id: None,
554            window_min_size: None,
555            window_decorations: None,
556            tabbing_identifier: None,
557            renderer_factory: None,
558        }
559    }
560}
561
562impl WindowOptions {
563    /// Draw this window through a renderer of the application's own rather than the
564    /// backend's default.
565    ///
566    /// The factory is invoked once, when the window is created: see
567    /// [`renderer_factory`](Self::renderer_factory).
568    pub fn with_renderer_factory(mut self, factory: impl RendererFactory) -> Self {
569        self.renderer_factory = Some(DynRendererFactory(Rc::new(factory)));
570        self
571    }
572}
573
574/// The options that can be configured for a window's titlebar
575#[derive(Debug, Default)]
576pub struct TitlebarOptions {
577    /// The initial title of the window
578    pub title: Option<SharedString>,
579
580    /// Should the default system titlebar be hidden to allow for a custom-drawn titlebar? (macOS and Windows only)
581    /// Refer to [`WindowOptions::window_decorations`] on Linux
582    pub appears_transparent: bool,
583
584    /// The position of the macOS traffic light buttons
585    pub traffic_light_position: Option<Point<Pixels>>,
586}
587
588/// The kind of window to create
589#[derive(Clone, Debug, PartialEq, Eq)]
590pub enum WindowKind {
591    /// A normal application window
592    Normal,
593    /// A window that appears above all other windows, usually used for alerts or popups
594    /// use sparingly!
595    PopUp,
596
597    /// A parent-anchored, platform-native popup window for menus, comboboxes, context menus and
598    /// tooltips. Unlike [`WindowKind::PopUp`], it is positioned relative to a parent window.
599    ///
600    /// The popup's size comes from [`WindowOptions::window_bounds`], whose origin is ignored.
601    /// See [`PopupOptions`] for the placement options. Platforms without a native
602    /// implementation reject it with [`PopupNotSupportedError`].
603    AnchoredPopup(PopupOptions),
604
605    /// A floating window that appears on top of its parent window
606    Floating,
607
608    /// A Wayland LayerShell window, used to draw overlays or backgrounds for applications such as
609    /// docks, notifications or wallpapers.
610    #[cfg(all(target_os = "linux", feature = "wayland"))]
611    LayerShell(crate::layer_shell::LayerShellOptions),
612
613    /// A window that appears on top of its parent window and blocks interaction with it
614    /// until the modal window is closed
615    Dialog,
616}
617
618/// A type of window control area that corresponds to the platform window.
619#[derive(Clone, Copy, Debug, Eq, PartialEq)]
620pub enum WindowControlArea {
621    /// An area that allows dragging of the platform window.
622    Drag,
623    /// An area that allows closing of the platform window.
624    Close,
625    /// An area that allows maximizing of the platform window.
626    Max,
627    /// An area that allows minimizing of the platform window.
628    Min,
629}
630
631#[derive(Clone, Debug, Default, PartialEq, Eq)]
632#[expect(missing_docs)]
633pub struct DispatchEventResult {
634    pub propagate: bool,
635    pub default_prevented: bool,
636}
637
638/// A tab in the platform's native window tab bar.
639#[doc(hidden)]
640#[derive(Clone, PartialEq, Eq)]
641pub struct SystemWindowTab {
642    /// The window id of the tab.
643    pub id: WindowId,
644    /// The title shown on the tab.
645    pub title: SharedString,
646    /// The window id this tab activates.
647    pub handle: WindowId,
648    /// When this tab was last the active tab.
649    pub last_active_at: scheduler::Instant,
650}
651
652impl SystemWindowTab {
653    /// Create a new instance of the window tab.
654    pub fn new(title: SharedString, handle: WindowId) -> Self {
655        Self {
656            id: handle,
657            title,
658            handle,
659            last_active_at: scheduler::Instant::now(),
660        }
661    }
662}