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