use gpui_shared_string::SharedString;
use gpui_types::{Bounds, Edges, Pixels, Point, Size, px, size};
use scheduler::Instant;
use std::{rc::Rc, sync::Arc, time::Duration};
use crate::{DisplayId, DynRendererFactory, RendererFactory, WindowId, popup::PopupOptions};
pub const DEFAULT_WINDOW_SIZE: Size<Pixels> = size(px(1536.), px(1095.));
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
pub enum WindowAppearance {
#[default]
Light,
VibrantLight,
Dark,
VibrantDark,
}
#[derive(Copy, Clone, Debug, Default, PartialEq)]
pub enum WindowBackgroundAppearance {
#[default]
Opaque,
Transparent,
Blurred,
MicaBackdrop,
MicaAltBackdrop,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ResizeEdge {
Top,
TopRight,
Right,
BottomRight,
Bottom,
BottomLeft,
Left,
TopLeft,
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Default)]
pub enum WindowDecorations {
#[default]
Server,
Client,
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Default)]
pub enum Decorations {
#[default]
Server,
Client {
tiling: Tiling,
},
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub struct WindowControls {
pub fullscreen: bool,
pub maximize: bool,
pub minimize: bool,
pub window_menu: bool,
}
impl Default for WindowControls {
fn default() -> Self {
Self {
fullscreen: true,
maximize: true,
minimize: true,
window_menu: true,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum WindowButton {
Minimize,
Maximize,
Close,
}
impl WindowButton {
pub fn id(&self) -> &'static str {
match self {
WindowButton::Minimize => "minimize",
WindowButton::Maximize => "maximize",
WindowButton::Close => "close",
}
}
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
fn index(&self) -> usize {
match self {
WindowButton::Minimize => 0,
WindowButton::Maximize => 1,
WindowButton::Close => 2,
}
}
}
pub const MAX_BUTTONS_PER_SIDE: usize = 3;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct WindowButtonLayout {
pub left: [Option<WindowButton>; MAX_BUTTONS_PER_SIDE],
pub right: [Option<WindowButton>; MAX_BUTTONS_PER_SIDE],
}
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
impl WindowButtonLayout {
pub fn linux_default() -> Self {
Self {
left: [None; MAX_BUTTONS_PER_SIDE],
right: [
Some(WindowButton::Minimize),
Some(WindowButton::Maximize),
Some(WindowButton::Close),
],
}
}
pub fn parse(layout_string: &str) -> anyhow::Result<Self> {
fn parse_side(
s: &str,
seen_buttons: &mut [bool; MAX_BUTTONS_PER_SIDE],
unrecognized: &mut Vec<String>,
) -> [Option<WindowButton>; MAX_BUTTONS_PER_SIDE] {
let mut result = [None; MAX_BUTTONS_PER_SIDE];
let mut i = 0;
for name in s.split(',') {
let trimmed = name.trim();
if trimmed.is_empty() {
continue;
}
let button = match trimmed {
"minimize" => Some(WindowButton::Minimize),
"maximize" => Some(WindowButton::Maximize),
"close" => Some(WindowButton::Close),
other => {
unrecognized.push(other.to_string());
None
}
};
if let Some(button) = button {
if seen_buttons[button.index()] {
continue;
}
if let Some(slot) = result.get_mut(i) {
*slot = Some(button);
seen_buttons[button.index()] = true;
i += 1;
}
}
}
result
}
let (left_str, right_str) = layout_string.split_once(':').unwrap_or(("", layout_string));
let mut unrecognized = Vec::new();
let mut seen_buttons = [false; MAX_BUTTONS_PER_SIDE];
let layout = Self {
left: parse_side(left_str, &mut seen_buttons, &mut unrecognized),
right: parse_side(right_str, &mut seen_buttons, &mut unrecognized),
};
if !unrecognized.is_empty()
&& layout.left.iter().all(Option::is_none)
&& layout.right.iter().all(Option::is_none)
{
anyhow::bail!(
"button layout string {:?} contains no valid buttons (unrecognized: {})",
layout_string,
unrecognized.join(", ")
);
}
Ok(layout)
}
pub fn format(&self) -> String {
fn format_side(buttons: &[Option<WindowButton>; MAX_BUTTONS_PER_SIDE]) -> String {
buttons
.iter()
.flatten()
.map(|button| match button {
WindowButton::Minimize => "minimize",
WindowButton::Maximize => "maximize",
WindowButton::Close => "close",
})
.collect::<Vec<_>>()
.join(",")
}
format!("{}:{}", format_side(&self.left), format_side(&self.right))
}
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Default)]
pub struct Tiling {
pub top: bool,
pub left: bool,
pub right: bool,
pub bottom: bool,
}
impl Tiling {
pub fn tiled() -> Self {
Self {
top: true,
left: true,
right: true,
bottom: true,
}
}
pub fn is_tiled(&self) -> bool {
self.top || self.left || self.right || self.bottom
}
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Default)]
#[expect(missing_docs)]
pub struct RequestFrameOptions {
pub require_presentation: bool,
pub force_render: bool,
pub signal_at: Option<Instant>,
pub signal_source: FrameRequestSource,
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Default)]
pub enum FrameRequestSource {
#[default]
NativeCallback,
LocalSchedule,
}
pub struct PlatformFrameSignal {
#[cfg(feature = "profiler")]
origin: Instant,
#[cfg(feature = "profiler")]
first_signal: std::sync::atomic::AtomicU64,
}
impl Default for PlatformFrameSignal {
fn default() -> Self {
Self::new()
}
}
impl PlatformFrameSignal {
pub fn new() -> Self {
Self {
#[cfg(feature = "profiler")]
origin: Instant::now(),
#[cfg(feature = "profiler")]
first_signal: std::sync::atomic::AtomicU64::new(u64::MAX),
}
}
#[inline]
pub fn capture(capture: impl FnOnce() -> Instant) -> Option<Instant> {
if cfg!(feature = "profiler") {
Some(capture())
} else {
None
}
}
#[inline]
#[cfg_attr(not(feature = "profiler"), expect(unused_variables))]
pub fn record(&self, at: Instant, source: FrameRequestSource) {
#[cfg(feature = "profiler")]
{
let nanoseconds = at
.saturating_duration_since(self.origin)
.as_nanos()
.min(u128::from((u64::MAX >> 1) - 1)) as u64;
let source_bit = match source {
FrameRequestSource::NativeCallback => 0,
FrameRequestSource::LocalSchedule => 1,
};
self.first_signal.fetch_min(
(nanoseconds << 1) | source_bit,
std::sync::atomic::Ordering::Relaxed,
);
}
}
#[inline]
pub fn take(&self) -> Option<(Instant, FrameRequestSource)> {
#[cfg(feature = "profiler")]
{
let signal = self
.first_signal
.swap(u64::MAX, std::sync::atomic::Ordering::Relaxed);
(signal != u64::MAX).then(|| {
let source = match signal & 1 {
0 => FrameRequestSource::NativeCallback,
_ => FrameRequestSource::LocalSchedule,
};
(self.origin + Duration::from_nanos(signal >> 1), source)
})
}
#[cfg(not(feature = "profiler"))]
{
None
}
}
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct WindowInsets {
pub safe_area: Edges<Pixels>,
pub ime: Edges<Pixels>,
}
impl WindowInsets {
pub fn effective(&self) -> Edges<Pixels> {
Edges {
top: self.safe_area.top.max(self.ime.top),
right: self.safe_area.right.max(self.ime.right),
bottom: self.safe_area.bottom.max(self.ime.bottom),
left: self.safe_area.left.max(self.ime.left),
}
}
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub enum TextInputStateChange {
FocusGained,
FocusLost,
SelectionChanged,
ContentChanged,
}
#[derive(Debug)]
pub struct WindowOptions {
pub window_bounds: Option<WindowBounds>,
pub titlebar: Option<TitlebarOptions>,
pub focus: bool,
pub show: bool,
pub kind: WindowKind,
pub is_movable: bool,
pub app_owns_titlebar_drag: bool,
pub inactive_frame_interval: Option<Duration>,
pub is_resizable: bool,
pub is_minimizable: bool,
pub display_id: Option<DisplayId>,
pub window_background: WindowBackgroundAppearance,
pub app_id: Option<String>,
pub window_min_size: Option<Size<Pixels>>,
pub window_decorations: Option<WindowDecorations>,
pub icon: Option<Arc<image::RgbaImage>>,
pub tabbing_identifier: Option<String>,
pub renderer_factory: Option<DynRendererFactory>,
}
#[derive(Debug)]
#[cfg_attr(
all(
any(target_os = "linux", target_os = "freebsd"),
not(any(feature = "x11", feature = "wayland"))
),
allow(dead_code)
)]
#[allow(missing_docs)]
pub struct WindowParams {
pub bounds: Bounds<Pixels>,
#[cfg_attr(feature = "wayland", allow(dead_code))]
pub titlebar: Option<TitlebarOptions>,
#[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
pub kind: WindowKind,
#[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
pub is_movable: bool,
#[cfg_attr(
any(target_os = "linux", target_os = "freebsd", target_os = "windows"),
allow(dead_code)
)]
pub app_owns_titlebar_drag: bool,
#[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
pub is_resizable: bool,
#[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
pub is_minimizable: bool,
#[cfg_attr(
any(target_os = "linux", target_os = "freebsd", target_os = "windows"),
allow(dead_code)
)]
pub focus: bool,
#[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
pub show: bool,
#[cfg_attr(feature = "wayland", allow(dead_code))]
pub icon: Option<Arc<image::RgbaImage>>,
#[cfg_attr(feature = "wayland", allow(dead_code))]
pub display_id: Option<DisplayId>,
#[cfg_attr(feature = "wayland", allow(dead_code))]
pub app_id: Option<String>,
pub window_min_size: Option<Size<Pixels>>,
#[cfg(target_os = "macos")]
pub tabbing_identifier: Option<String>,
pub renderer_factory: Option<DynRendererFactory>,
}
#[derive(Debug, Copy, Clone, PartialEq)]
pub enum WindowBounds {
Windowed(Bounds<Pixels>),
Maximized(Bounds<Pixels>),
Fullscreen(Bounds<Pixels>),
}
impl Default for WindowBounds {
fn default() -> Self {
WindowBounds::Windowed(Bounds::default())
}
}
impl WindowBounds {
pub fn get_bounds(&self) -> Bounds<Pixels> {
match self {
WindowBounds::Windowed(bounds) => *bounds,
WindowBounds::Maximized(bounds) => *bounds,
WindowBounds::Fullscreen(bounds) => *bounds,
}
}
}
impl Default for WindowOptions {
fn default() -> Self {
Self {
window_bounds: None,
titlebar: Some(TitlebarOptions {
title: Default::default(),
appears_transparent: Default::default(),
traffic_light_position: Default::default(),
}),
focus: true,
show: true,
kind: WindowKind::Normal,
is_movable: true,
app_owns_titlebar_drag: false,
inactive_frame_interval: Some(Duration::from_micros(33_333)),
is_resizable: true,
is_minimizable: true,
display_id: None,
window_background: WindowBackgroundAppearance::default(),
icon: None,
app_id: None,
window_min_size: None,
window_decorations: None,
tabbing_identifier: None,
renderer_factory: None,
}
}
}
impl WindowOptions {
pub fn with_renderer_factory(mut self, factory: impl RendererFactory) -> Self {
self.renderer_factory = Some(DynRendererFactory(Rc::new(factory)));
self
}
}
#[derive(Debug, Default)]
pub struct TitlebarOptions {
pub title: Option<SharedString>,
pub appears_transparent: bool,
pub traffic_light_position: Option<Point<Pixels>>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum WindowKind {
Normal,
PopUp,
AnchoredPopup(PopupOptions),
Floating,
#[cfg(all(target_os = "linux", feature = "wayland"))]
LayerShell(crate::layer_shell::LayerShellOptions),
Dialog,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum WindowControlArea {
Drag,
Close,
Max,
Min,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
#[expect(missing_docs)]
pub struct DispatchEventResult {
pub propagate: bool,
pub default_prevented: bool,
}
#[doc(hidden)]
#[derive(Clone, PartialEq, Eq)]
pub struct SystemWindowTab {
pub id: WindowId,
pub title: SharedString,
pub handle: WindowId,
pub last_active_at: scheduler::Instant,
}
impl SystemWindowTab {
pub fn new(title: SharedString, handle: WindowId) -> Self {
Self {
id: handle,
title,
handle,
last_active_at: scheduler::Instant::now(),
}
}
}
#[cfg(test)]
mod frame_signal_tests {
use super::*;
#[cfg(feature = "profiler")]
#[test]
fn coalesced_signals_retain_the_earliest_until_drained() {
for (first_source, later_source) in [
(
FrameRequestSource::NativeCallback,
FrameRequestSource::LocalSchedule,
),
(
FrameRequestSource::LocalSchedule,
FrameRequestSource::NativeCallback,
),
] {
let signal = PlatformFrameSignal::new();
let first =
PlatformFrameSignal::capture(Instant::now).expect("profiling captures requests");
assert_eq!(signal.take(), None);
signal.record(first + Duration::from_millis(16), later_source);
signal.record(first, first_source);
signal.record(first + Duration::from_millis(32), later_source);
assert_eq!(signal.take(), Some((first, first_source)));
assert_eq!(signal.take(), None);
let next = first + Duration::from_millis(48);
signal.record(next, later_source);
assert_eq!(signal.take(), Some((next, later_source)));
}
}
#[cfg(not(feature = "profiler"))]
#[test]
fn disabled_profiling_does_not_capture_or_store_timestamps() {
let captured = PlatformFrameSignal::capture(|| {
panic!("profiler-disabled builds must not evaluate the capture closure");
});
assert_eq!(captured, None);
let signal = PlatformFrameSignal::new();
signal.record(Instant::now(), FrameRequestSource::LocalSchedule);
assert_eq!(signal.take(), None);
}
}