Linux window decorations (#13611)

This PR adds support for full client side decorations on X11 and Wayland

TODO:
- [x] Adjust GPUI APIs to expose CSD related information
- [x] Implement remaining CSD features (Resizing, window border, window
shadow)
- [x] Integrate with existing background appearance and window
transparency
- [x] Figure out how to check if the window is tiled on X11
- [x] Implement in Zed
- [x] Repeatedly maximizing and unmaximizing can panic
- [x] Resizing is strangely slow
- [x] X11 resizing and movement doesn't work for this:
https://discord.com/channels/869392257814519848/1204679850208657418/1256816908519604305
- [x] The top corner can clip with current styling
- [x] Pressing titlebar buttons doesn't work
- [x] Not showing maximize / unmaximize buttons
- [x] Noisy transparency logs / surface transparency problem
https://github.com/zed-industries/zed/pull/13611#issuecomment-2201685030
- [x] Strange offsets when dragging the project panel
https://github.com/zed-industries/zed/pull/13611#pullrequestreview-2154606261
- [x] Shadow inset with `_GTK_FRAME_EXTENTS` doesn't respect tiling on
X11 (observe by snapping an X11 window in any direction)

Release Notes:

- N/A

---------

Co-authored-by: conrad <conrad@zed.dev>
Co-authored-by: Owen Law <81528246+someone13574@users.noreply.github.com>
Co-authored-by: apricotbucket28 <71973804+apricotbucket28@users.noreply.github.com>
Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
This commit is contained in:
Mikayla Maki 2024-07-03 11:28:09 -07:00 committed by GitHub
parent 98699a65c1
commit 47aa761ca9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
28 changed files with 1633 additions and 540 deletions

View file

@ -10,7 +10,7 @@ use std::{rc::Rc, sync::Arc};
pub use collab_panel::CollabPanel; pub use collab_panel::CollabPanel;
use gpui::{ use gpui::{
point, AppContext, Pixels, PlatformDisplay, Size, WindowBackgroundAppearance, WindowBounds, point, AppContext, Pixels, PlatformDisplay, Size, WindowBackgroundAppearance, WindowBounds,
WindowKind, WindowOptions, WindowDecorations, WindowKind, WindowOptions,
}; };
use panel_settings::MessageEditorSettings; use panel_settings::MessageEditorSettings;
pub use panel_settings::{ pub use panel_settings::{
@ -63,8 +63,9 @@ fn notification_window_options(
kind: WindowKind::PopUp, kind: WindowKind::PopUp,
is_movable: false, is_movable: false,
display_id: Some(screen.id()), display_id: Some(screen.id()),
window_background: WindowBackgroundAppearance::default(), window_background: WindowBackgroundAppearance::Transparent,
app_id: Some(app_id.to_owned()), app_id: Some(app_id.to_owned()),
window_min_size: None, window_min_size: None,
window_decorations: Some(WindowDecorations::Client),
} }
} }

View file

@ -133,6 +133,7 @@ x11rb = { version = "0.13.0", features = [
"xinput", "xinput",
"cursor", "cursor",
"resource_manager", "resource_manager",
"sync",
] } ] }
xkbcommon = { version = "0.7", features = ["wayland", "x11"] } xkbcommon = { version = "0.7", features = ["wayland", "x11"] }
xim = { git = "https://github.com/npmania/xim-rs", rev = "27132caffc5b9bc9c432ca4afad184ab6e7c16af", features = [ xim = { git = "https://github.com/npmania/xim-rs", rev = "27132caffc5b9bc9c432ca4afad184ab6e7c16af", features = [
@ -160,6 +161,10 @@ path = "examples/image/image.rs"
name = "set_menus" name = "set_menus"
path = "examples/set_menus.rs" path = "examples/set_menus.rs"
[[example]]
name = "window_shadow"
path = "examples/window_shadow.rs"
[[example]] [[example]]
name = "input" name = "input"
path = "examples/input.rs" path = "examples/input.rs"

View file

@ -23,7 +23,7 @@ impl Render for HelloWorld {
fn main() { fn main() {
App::new().run(|cx: &mut AppContext| { App::new().run(|cx: &mut AppContext| {
let bounds = Bounds::centered(None, size(px(600.0), px(600.0)), cx); let bounds = Bounds::centered(None, size(px(300.0), px(300.0)), cx);
cx.open_window( cx.open_window(
WindowOptions { WindowOptions {
window_bounds: Some(WindowBounds::Windowed(bounds)), window_bounds: Some(WindowBounds::Windowed(bounds)),

View file

@ -52,6 +52,7 @@ fn main() {
is_movable: false, is_movable: false,
app_id: None, app_id: None,
window_min_size: None, window_min_size: None,
window_decorations: None,
} }
}; };

View file

@ -0,0 +1,222 @@
use gpui::*;
use prelude::FluentBuilder;
struct WindowShadow {}
/*
Things to do:
1. We need a way of calculating which edge or corner the mouse is on,
and then dispatch on that
2. We need to improve the shadow rendering significantly
3. We need to implement the techniques in here in Zed
*/
impl Render for WindowShadow {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let decorations = cx.window_decorations();
let rounding = px(10.0);
let shadow_size = px(10.0);
let border_size = px(1.0);
let grey = rgb(0x808080);
cx.set_client_inset(shadow_size);
div()
.id("window-backdrop")
.bg(transparent_black())
.map(|div| match decorations {
Decorations::Server => div,
Decorations::Client { tiling, .. } => div
.bg(gpui::transparent_black())
.child(
canvas(
|_bounds, cx| {
cx.insert_hitbox(
Bounds::new(
point(px(0.0), px(0.0)),
cx.window_bounds().get_bounds().size,
),
false,
)
},
move |_bounds, hitbox, cx| {
let mouse = cx.mouse_position();
let size = cx.window_bounds().get_bounds().size;
let Some(edge) = resize_edge(mouse, shadow_size, size) else {
return;
};
cx.set_cursor_style(
match edge {
ResizeEdge::Top | ResizeEdge::Bottom => {
CursorStyle::ResizeUpDown
}
ResizeEdge::Left | ResizeEdge::Right => {
CursorStyle::ResizeLeftRight
}
ResizeEdge::TopLeft | ResizeEdge::BottomRight => {
CursorStyle::ResizeUpLeftDownRight
}
ResizeEdge::TopRight | ResizeEdge::BottomLeft => {
CursorStyle::ResizeUpRightDownLeft
}
},
&hitbox,
);
},
)
.size_full()
.absolute(),
)
.when(!(tiling.top || tiling.right), |div| {
div.rounded_tr(rounding)
})
.when(!(tiling.top || tiling.left), |div| div.rounded_tl(rounding))
.when(!tiling.top, |div| div.pt(shadow_size))
.when(!tiling.bottom, |div| div.pb(shadow_size))
.when(!tiling.left, |div| div.pl(shadow_size))
.when(!tiling.right, |div| div.pr(shadow_size))
.on_mouse_move(|_e, cx| cx.refresh())
.on_mouse_down(MouseButton::Left, move |e, cx| {
let size = cx.window_bounds().get_bounds().size;
let pos = e.position;
match resize_edge(pos, shadow_size, size) {
Some(edge) => cx.start_window_resize(edge),
None => cx.start_window_move(),
};
}),
})
.size_full()
.child(
div()
.cursor(CursorStyle::Arrow)
.map(|div| match decorations {
Decorations::Server => div,
Decorations::Client { tiling } => div
.border_color(grey)
.when(!(tiling.top || tiling.right), |div| {
div.rounded_tr(rounding)
})
.when(!(tiling.top || tiling.left), |div| div.rounded_tl(rounding))
.when(!tiling.top, |div| div.border_t(border_size))
.when(!tiling.bottom, |div| div.border_b(border_size))
.when(!tiling.left, |div| div.border_l(border_size))
.when(!tiling.right, |div| div.border_r(border_size))
.when(!tiling.is_tiled(), |div| {
div.shadow(smallvec::smallvec![gpui::BoxShadow {
color: Hsla {
h: 0.,
s: 0.,
l: 0.,
a: 0.4,
},
blur_radius: shadow_size / 2.,
spread_radius: px(0.),
offset: point(px(0.0), px(0.0)),
}])
}),
})
.on_mouse_move(|_e, cx| {
cx.stop_propagation();
})
.bg(gpui::rgb(0xCCCCFF))
.size_full()
.flex()
.flex_col()
.justify_around()
.child(
div().w_full().flex().flex_row().justify_around().child(
div()
.flex()
.bg(white())
.size(Length::Definite(Pixels(300.0).into()))
.justify_center()
.items_center()
.shadow_lg()
.border_1()
.border_color(rgb(0x0000ff))
.text_xl()
.text_color(rgb(0xffffff))
.child(
div()
.id("hello")
.w(px(200.0))
.h(px(100.0))
.bg(green())
.shadow(smallvec::smallvec![gpui::BoxShadow {
color: Hsla {
h: 0.,
s: 0.,
l: 0.,
a: 1.0,
},
blur_radius: px(20.0),
spread_radius: px(0.0),
offset: point(px(0.0), px(0.0)),
}])
.map(|div| match decorations {
Decorations::Server => div,
Decorations::Client { .. } => div
.on_mouse_down(MouseButton::Left, |_e, cx| {
cx.start_window_move();
})
.on_click(|e, cx| {
if e.down.button == MouseButton::Right {
cx.show_window_menu(e.up.position);
}
})
.text_color(black())
.child("this is the custom titlebar"),
}),
),
),
),
)
}
}
fn resize_edge(pos: Point<Pixels>, shadow_size: Pixels, size: Size<Pixels>) -> Option<ResizeEdge> {
let edge = if pos.y < shadow_size && pos.x < shadow_size {
ResizeEdge::TopLeft
} else if pos.y < shadow_size && pos.x > size.width - shadow_size {
ResizeEdge::TopRight
} else if pos.y < shadow_size {
ResizeEdge::Top
} else if pos.y > size.height - shadow_size && pos.x < shadow_size {
ResizeEdge::BottomLeft
} else if pos.y > size.height - shadow_size && pos.x > size.width - shadow_size {
ResizeEdge::BottomRight
} else if pos.y > size.height - shadow_size {
ResizeEdge::Bottom
} else if pos.x < shadow_size {
ResizeEdge::Left
} else if pos.x > size.width - shadow_size {
ResizeEdge::Right
} else {
return None;
};
Some(edge)
}
fn main() {
App::new().run(|cx: &mut AppContext| {
let bounds = Bounds::centered(None, size(px(600.0), px(600.0)), cx);
cx.open_window(
WindowOptions {
window_bounds: Some(WindowBounds::Windowed(bounds)),
window_background: WindowBackgroundAppearance::Opaque,
window_decorations: Some(WindowDecorations::Client),
..Default::default()
},
|cx| {
cx.new_view(|cx| {
cx.observe_window_appearance(|_, cx| {
cx.refresh();
})
.detach();
WindowShadow {}
})
},
)
.unwrap();
});
}

View file

@ -309,6 +309,16 @@ pub fn transparent_black() -> Hsla {
} }
} }
/// Transparent black in [`Hsla`]
pub fn transparent_white() -> Hsla {
Hsla {
h: 0.,
s: 0.,
l: 1.,
a: 0.,
}
}
/// Opaque grey in [`Hsla`], values will be clamped to the range [0, 1] /// Opaque grey in [`Hsla`], values will be clamped to the range [0, 1]
pub fn opaque_grey(lightness: f32, opacity: f32) -> Hsla { pub fn opaque_grey(lightness: f32, opacity: f32) -> Hsla {
Hsla { Hsla {

View file

@ -883,6 +883,14 @@ where
self.size.height = self.size.height.clone() + double_amount; self.size.height = self.size.height.clone() + double_amount;
} }
/// inset the bounds by a specified amount
/// Note that this may panic if T does not support negative values
pub fn inset(&self, amount: T) -> Self {
let mut result = self.clone();
result.dilate(T::default() - amount);
result
}
/// Returns the center point of the bounds. /// Returns the center point of the bounds.
/// ///
/// Calculates the center by taking the origin's x and y coordinates and adding half the width and height /// Calculates the center by taking the origin's x and y coordinates and adding half the width and height
@ -1266,12 +1274,36 @@ where
/// size: Size { width: 10.0, height: 20.0 }, /// size: Size { width: 10.0, height: 20.0 },
/// }); /// });
/// ``` /// ```
pub fn map_origin(self, f: impl Fn(Point<T>) -> Point<T>) -> Bounds<T> { pub fn map_origin(self, f: impl Fn(T) -> T) -> Bounds<T> {
Bounds { Bounds {
origin: f(self.origin), origin: self.origin.map(f),
size: self.size, size: self.size,
} }
} }
/// Applies a function to the origin of the bounds, producing a new `Bounds` with the new origin
///
/// # Examples
///
/// ```
/// # use zed::{Bounds, Point, Size};
/// let bounds = Bounds {
/// origin: Point { x: 10.0, y: 10.0 },
/// size: Size { width: 10.0, height: 20.0 },
/// };
/// let new_bounds = bounds.map_size(|value| value * 1.5);
///
/// assert_eq!(new_bounds, Bounds {
/// origin: Point { x: 10.0, y: 10.0 },
/// size: Size { width: 15.0, height: 30.0 },
/// });
/// ```
pub fn map_size(self, f: impl Fn(T) -> T) -> Bounds<T> {
Bounds {
origin: self.origin,
size: self.size.map(f),
}
}
} }
/// Checks if the bounds represent an empty area. /// Checks if the bounds represent an empty area.

View file

@ -210,6 +210,83 @@ impl Debug for DisplayId {
unsafe impl Send for DisplayId {} unsafe impl Send for DisplayId {}
/// Which part of the window to resize
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ResizeEdge {
/// The top edge
Top,
/// The top right corner
TopRight,
/// The right edge
Right,
/// The bottom right corner
BottomRight,
/// The bottom edge
Bottom,
/// The bottom left corner
BottomLeft,
/// The left edge
Left,
/// The top left corner
TopLeft,
}
/// A type to describe the appearance of a window
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Default)]
pub enum WindowDecorations {
#[default]
/// Server side decorations
Server,
/// Client side decorations
Client,
}
/// A type to describe how this window is currently configured
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Default)]
pub enum Decorations {
/// The window is configured to use server side decorations
#[default]
Server,
/// The window is configured to use client side decorations
Client {
/// The edge tiling state
tiling: Tiling,
},
}
/// What window controls this platform supports
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Default)]
pub struct WindowControls {
/// Whether this platform supports fullscreen
pub fullscreen: bool,
/// Whether this platform supports maximize
pub maximize: bool,
/// Whether this platform supports minimize
pub minimize: bool,
/// Whether this platform supports a window menu
pub window_menu: bool,
}
/// A type to describe which sides of the window are currently tiled in some way
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Default)]
pub struct Tiling {
/// Whether the top edge is tiled
pub top: bool,
/// Whether the left edge is tiled
pub left: bool,
/// Whether the right edge is tiled
pub right: bool,
/// Whether the bottom edge is tiled
pub bottom: bool,
}
impl Tiling {
/// Whether any edge is tiled
pub fn is_tiled(&self) -> bool {
self.top || self.left || self.right || self.bottom
}
}
pub(crate) trait PlatformWindow: HasWindowHandle + HasDisplayHandle { pub(crate) trait PlatformWindow: HasWindowHandle + HasDisplayHandle {
fn bounds(&self) -> Bounds<Pixels>; fn bounds(&self) -> Bounds<Pixels>;
fn is_maximized(&self) -> bool; fn is_maximized(&self) -> bool;
@ -232,10 +309,7 @@ pub(crate) trait PlatformWindow: HasWindowHandle + HasDisplayHandle {
fn activate(&self); fn activate(&self);
fn is_active(&self) -> bool; fn is_active(&self) -> bool;
fn set_title(&mut self, title: &str); fn set_title(&mut self, title: &str);
fn set_app_id(&mut self, app_id: &str); fn set_background_appearance(&self, background_appearance: WindowBackgroundAppearance);
fn set_background_appearance(&mut self, background_appearance: WindowBackgroundAppearance);
fn set_edited(&mut self, edited: bool);
fn show_character_palette(&self);
fn minimize(&self); fn minimize(&self);
fn zoom(&self); fn zoom(&self);
fn toggle_fullscreen(&self); fn toggle_fullscreen(&self);
@ -252,12 +326,31 @@ pub(crate) trait PlatformWindow: HasWindowHandle + HasDisplayHandle {
fn completed_frame(&self) {} fn completed_frame(&self) {}
fn sprite_atlas(&self) -> Arc<dyn PlatformAtlas>; fn sprite_atlas(&self) -> Arc<dyn PlatformAtlas>;
// macOS specific methods
fn set_edited(&mut self, _edited: bool) {}
fn show_character_palette(&self) {}
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
fn get_raw_handle(&self) -> windows::HWND; fn get_raw_handle(&self) -> windows::HWND;
fn show_window_menu(&self, position: Point<Pixels>); // Linux specific methods
fn start_system_move(&self); fn request_decorations(&self, _decorations: WindowDecorations) {}
fn should_render_window_controls(&self) -> bool; fn show_window_menu(&self, _position: Point<Pixels>) {}
fn start_window_move(&self) {}
fn start_window_resize(&self, _edge: ResizeEdge) {}
fn window_decorations(&self) -> Decorations {
Decorations::Server
}
fn set_app_id(&mut self, _app_id: &str) {}
fn window_controls(&self) -> WindowControls {
WindowControls {
fullscreen: true,
maximize: true,
minimize: true,
window_menu: false,
}
}
fn set_client_inset(&self, _inset: Pixels) {}
#[cfg(any(test, feature = "test-support"))] #[cfg(any(test, feature = "test-support"))]
fn as_test(&mut self) -> Option<&mut TestWindow> { fn as_test(&mut self) -> Option<&mut TestWindow> {
@ -570,6 +663,10 @@ pub struct WindowOptions {
/// Window minimum size /// Window minimum size
pub window_min_size: Option<Size<Pixels>>, pub window_min_size: Option<Size<Pixels>>,
/// Whether to use client or server side decorations. Wayland only
/// Note that this may be ignored.
pub window_decorations: Option<WindowDecorations>,
} }
/// The variables that can be configured when creating a new window /// The variables that can be configured when creating a new window
@ -596,8 +693,6 @@ pub(crate) struct WindowParams {
pub display_id: Option<DisplayId>, pub display_id: Option<DisplayId>,
pub window_background: WindowBackgroundAppearance,
#[cfg_attr(target_os = "linux", allow(dead_code))] #[cfg_attr(target_os = "linux", allow(dead_code))]
pub window_min_size: Option<Size<Pixels>>, pub window_min_size: Option<Size<Pixels>>,
} }
@ -649,6 +744,7 @@ impl Default for WindowOptions {
window_background: WindowBackgroundAppearance::default(), window_background: WindowBackgroundAppearance::default(),
app_id: None, app_id: None,
window_min_size: None, window_min_size: None,
window_decorations: None,
} }
} }
} }
@ -659,7 +755,7 @@ pub struct TitlebarOptions {
/// The initial title of the window /// The initial title of the window
pub title: Option<SharedString>, pub title: Option<SharedString>,
/// Whether the titlebar should appear transparent /// Whether the titlebar should appear transparent (macOS only)
pub appears_transparent: bool, pub appears_transparent: bool,
/// The position of the macOS traffic light buttons /// The position of the macOS traffic light buttons
@ -805,6 +901,14 @@ pub enum CursorStyle {
/// corresponds to the CSS cursor value `ns-resize` /// corresponds to the CSS cursor value `ns-resize`
ResizeUpDown, ResizeUpDown,
/// A resize cursor directing up-left and down-right
/// corresponds to the CSS cursor value `nesw-resize`
ResizeUpLeftDownRight,
/// A resize cursor directing up-right and down-left
/// corresponds to the CSS cursor value `nwse-resize`
ResizeUpRightDownLeft,
/// A cursor indicating that the item/column can be resized horizontally. /// A cursor indicating that the item/column can be resized horizontally.
/// corresponds to the CSS curosr value `col-resize` /// corresponds to the CSS curosr value `col-resize`
ResizeColumn, ResizeColumn,

View file

@ -572,6 +572,8 @@ impl CursorStyle {
CursorStyle::ResizeUp => Shape::NResize, CursorStyle::ResizeUp => Shape::NResize,
CursorStyle::ResizeDown => Shape::SResize, CursorStyle::ResizeDown => Shape::SResize,
CursorStyle::ResizeUpDown => Shape::NsResize, CursorStyle::ResizeUpDown => Shape::NsResize,
CursorStyle::ResizeUpLeftDownRight => Shape::NwseResize,
CursorStyle::ResizeUpRightDownLeft => Shape::NeswResize,
CursorStyle::ResizeColumn => Shape::ColResize, CursorStyle::ResizeColumn => Shape::ColResize,
CursorStyle::ResizeRow => Shape::RowResize, CursorStyle::ResizeRow => Shape::RowResize,
CursorStyle::IBeamCursorForVerticalLayout => Shape::VerticalText, CursorStyle::IBeamCursorForVerticalLayout => Shape::VerticalText,
@ -599,6 +601,8 @@ impl CursorStyle {
CursorStyle::ResizeUp => "n-resize", CursorStyle::ResizeUp => "n-resize",
CursorStyle::ResizeDown => "s-resize", CursorStyle::ResizeDown => "s-resize",
CursorStyle::ResizeUpDown => "ns-resize", CursorStyle::ResizeUpDown => "ns-resize",
CursorStyle::ResizeUpLeftDownRight => "nwse-resize",
CursorStyle::ResizeUpRightDownLeft => "nesw-resize",
CursorStyle::ResizeColumn => "col-resize", CursorStyle::ResizeColumn => "col-resize",
CursorStyle::ResizeRow => "row-resize", CursorStyle::ResizeRow => "row-resize",
CursorStyle::IBeamCursorForVerticalLayout => "vertical-text", CursorStyle::IBeamCursorForVerticalLayout => "vertical-text",

View file

@ -138,7 +138,7 @@ impl Globals {
primary_selection_manager: globals.bind(&qh, 1..=1, ()).ok(), primary_selection_manager: globals.bind(&qh, 1..=1, ()).ok(),
shm: globals.bind(&qh, 1..=1, ()).unwrap(), shm: globals.bind(&qh, 1..=1, ()).unwrap(),
seat, seat,
wm_base: globals.bind(&qh, 1..=1, ()).unwrap(), wm_base: globals.bind(&qh, 2..=5, ()).unwrap(),
viewporter: globals.bind(&qh, 1..=1, ()).ok(), viewporter: globals.bind(&qh, 1..=1, ()).ok(),
fractional_scale_manager: globals.bind(&qh, 1..=1, ()).ok(), fractional_scale_manager: globals.bind(&qh, 1..=1, ()).ok(),
decoration_manager: globals.bind(&qh, 1..=1, ()).ok(), decoration_manager: globals.bind(&qh, 1..=1, ()).ok(),

View file

@ -25,9 +25,10 @@ use crate::platform::linux::wayland::serial::SerialKind;
use crate::platform::{PlatformAtlas, PlatformInputHandler, PlatformWindow}; use crate::platform::{PlatformAtlas, PlatformInputHandler, PlatformWindow};
use crate::scene::Scene; use crate::scene::Scene;
use crate::{ use crate::{
px, size, AnyWindowHandle, Bounds, Globals, Modifiers, Output, Pixels, PlatformDisplay, px, size, AnyWindowHandle, Bounds, Decorations, Globals, Modifiers, Output, Pixels,
PlatformInput, Point, PromptLevel, Size, WaylandClientStatePtr, WindowAppearance, PlatformDisplay, PlatformInput, Point, PromptLevel, ResizeEdge, Size, Tiling,
WindowBackgroundAppearance, WindowBounds, WindowParams, WaylandClientStatePtr, WindowAppearance, WindowBackgroundAppearance, WindowBounds,
WindowControls, WindowDecorations, WindowParams,
}; };
#[derive(Default)] #[derive(Default)]
@ -62,10 +63,12 @@ impl rwh::HasDisplayHandle for RawWindow {
} }
} }
#[derive(Debug)]
struct InProgressConfigure { struct InProgressConfigure {
size: Option<Size<Pixels>>, size: Option<Size<Pixels>>,
fullscreen: bool, fullscreen: bool,
maximized: bool, maximized: bool,
tiling: Tiling,
} }
pub struct WaylandWindowState { pub struct WaylandWindowState {
@ -84,14 +87,20 @@ pub struct WaylandWindowState {
bounds: Bounds<Pixels>, bounds: Bounds<Pixels>,
scale: f32, scale: f32,
input_handler: Option<PlatformInputHandler>, input_handler: Option<PlatformInputHandler>,
decoration_state: WaylandDecorationState, decorations: WindowDecorations,
background_appearance: WindowBackgroundAppearance,
fullscreen: bool, fullscreen: bool,
maximized: bool, maximized: bool,
windowed_bounds: Bounds<Pixels>, tiling: Tiling,
window_bounds: Bounds<Pixels>,
client: WaylandClientStatePtr, client: WaylandClientStatePtr,
handle: AnyWindowHandle, handle: AnyWindowHandle,
active: bool, active: bool,
in_progress_configure: Option<InProgressConfigure>, in_progress_configure: Option<InProgressConfigure>,
in_progress_window_controls: Option<WindowControls>,
window_controls: WindowControls,
inset: Option<Pixels>,
requested_inset: Option<Pixels>,
} }
#[derive(Clone)] #[derive(Clone)]
@ -142,7 +151,7 @@ impl WaylandWindowState {
height: options.bounds.size.height.0 as u32, height: options.bounds.size.height.0 as u32,
depth: 1, depth: 1,
}, },
transparent: options.window_background != WindowBackgroundAppearance::Opaque, transparent: true,
}; };
Ok(Self { Ok(Self {
@ -160,17 +169,34 @@ impl WaylandWindowState {
bounds: options.bounds, bounds: options.bounds,
scale: 1.0, scale: 1.0,
input_handler: None, input_handler: None,
decoration_state: WaylandDecorationState::Client, decorations: WindowDecorations::Client,
background_appearance: WindowBackgroundAppearance::Opaque,
fullscreen: false, fullscreen: false,
maximized: false, maximized: false,
windowed_bounds: options.bounds, tiling: Tiling::default(),
window_bounds: options.bounds,
in_progress_configure: None, in_progress_configure: None,
client, client,
appearance, appearance,
handle, handle,
active: false, active: false,
in_progress_window_controls: None,
// Assume that we can do anything, unless told otherwise
window_controls: WindowControls {
fullscreen: true,
maximize: true,
minimize: true,
window_menu: true,
},
inset: None,
requested_inset: None,
}) })
} }
pub fn is_transparent(&self) -> bool {
self.decorations == WindowDecorations::Client
|| self.background_appearance != WindowBackgroundAppearance::Opaque
}
} }
pub(crate) struct WaylandWindow(pub WaylandWindowStatePtr); pub(crate) struct WaylandWindow(pub WaylandWindowStatePtr);
@ -235,7 +261,7 @@ impl WaylandWindow {
.wm_base .wm_base
.get_xdg_surface(&surface, &globals.qh, surface.id()); .get_xdg_surface(&surface, &globals.qh, surface.id());
let toplevel = xdg_surface.get_toplevel(&globals.qh, surface.id()); let toplevel = xdg_surface.get_toplevel(&globals.qh, surface.id());
toplevel.set_min_size(200, 200); toplevel.set_min_size(50, 50);
if let Some(fractional_scale_manager) = globals.fractional_scale_manager.as_ref() { if let Some(fractional_scale_manager) = globals.fractional_scale_manager.as_ref() {
fractional_scale_manager.get_fractional_scale(&surface, &globals.qh, surface.id()); fractional_scale_manager.get_fractional_scale(&surface, &globals.qh, surface.id());
@ -246,13 +272,7 @@ impl WaylandWindow {
.decoration_manager .decoration_manager
.as_ref() .as_ref()
.map(|decoration_manager| { .map(|decoration_manager| {
let decoration = decoration_manager.get_toplevel_decoration( decoration_manager.get_toplevel_decoration(&toplevel, &globals.qh, surface.id())
&toplevel,
&globals.qh,
surface.id(),
);
decoration.set_mode(zxdg_toplevel_decoration_v1::Mode::ClientSide);
decoration
}); });
let viewport = globals let viewport = globals
@ -298,7 +318,7 @@ impl WaylandWindowStatePtr {
pub fn frame(&self, request_frame_callback: bool) { pub fn frame(&self, request_frame_callback: bool) {
if request_frame_callback { if request_frame_callback {
let state = self.state.borrow_mut(); let mut state = self.state.borrow_mut();
state.surface.frame(&state.globals.qh, state.surface.id()); state.surface.frame(&state.globals.qh, state.surface.id());
drop(state); drop(state);
} }
@ -311,6 +331,18 @@ impl WaylandWindowStatePtr {
pub fn handle_xdg_surface_event(&self, event: xdg_surface::Event) { pub fn handle_xdg_surface_event(&self, event: xdg_surface::Event) {
match event { match event {
xdg_surface::Event::Configure { serial } => { xdg_surface::Event::Configure { serial } => {
{
let mut state = self.state.borrow_mut();
if let Some(window_controls) = state.in_progress_window_controls.take() {
state.window_controls = window_controls;
drop(state);
let mut callbacks = self.callbacks.borrow_mut();
if let Some(appearance_changed) = callbacks.appearance_changed.as_mut() {
appearance_changed();
}
}
}
{ {
let mut state = self.state.borrow_mut(); let mut state = self.state.borrow_mut();
@ -318,18 +350,21 @@ impl WaylandWindowStatePtr {
let got_unmaximized = state.maximized && !configure.maximized; let got_unmaximized = state.maximized && !configure.maximized;
state.fullscreen = configure.fullscreen; state.fullscreen = configure.fullscreen;
state.maximized = configure.maximized; state.maximized = configure.maximized;
state.tiling = configure.tiling;
if got_unmaximized { if got_unmaximized {
configure.size = Some(state.windowed_bounds.size); configure.size = Some(state.window_bounds.size);
} else if !configure.fullscreen && !configure.maximized { } else if !configure.maximized {
configure.size =
compute_outer_size(state.inset, configure.size, state.tiling);
}
if !configure.fullscreen && !configure.maximized {
if let Some(size) = configure.size { if let Some(size) = configure.size {
state.windowed_bounds = Bounds { state.window_bounds = Bounds {
origin: Point::default(), origin: Point::default(),
size, size,
}; };
} }
} }
drop(state); drop(state);
if let Some(size) = configure.size { if let Some(size) = configure.size {
self.resize(size); self.resize(size);
@ -340,8 +375,11 @@ impl WaylandWindowStatePtr {
state.xdg_surface.ack_configure(serial); state.xdg_surface.ack_configure(serial);
let request_frame_callback = !state.acknowledged_first_configure; let request_frame_callback = !state.acknowledged_first_configure;
state.acknowledged_first_configure = true; state.acknowledged_first_configure = true;
drop(state);
self.frame(request_frame_callback); if request_frame_callback {
drop(state);
self.frame(true);
}
} }
_ => {} _ => {}
} }
@ -351,10 +389,21 @@ impl WaylandWindowStatePtr {
match event { match event {
zxdg_toplevel_decoration_v1::Event::Configure { mode } => match mode { zxdg_toplevel_decoration_v1::Event::Configure { mode } => match mode {
WEnum::Value(zxdg_toplevel_decoration_v1::Mode::ServerSide) => { WEnum::Value(zxdg_toplevel_decoration_v1::Mode::ServerSide) => {
self.set_decoration_state(WaylandDecorationState::Server) self.state.borrow_mut().decorations = WindowDecorations::Server;
if let Some(mut appearance_changed) =
self.callbacks.borrow_mut().appearance_changed.as_mut()
{
appearance_changed();
}
} }
WEnum::Value(zxdg_toplevel_decoration_v1::Mode::ClientSide) => { WEnum::Value(zxdg_toplevel_decoration_v1::Mode::ClientSide) => {
self.set_decoration_state(WaylandDecorationState::Client) self.state.borrow_mut().decorations = WindowDecorations::Client;
// Update background to be transparent
if let Some(mut appearance_changed) =
self.callbacks.borrow_mut().appearance_changed.as_mut()
{
appearance_changed();
}
} }
WEnum::Value(_) => { WEnum::Value(_) => {
log::warn!("Unknown decoration mode"); log::warn!("Unknown decoration mode");
@ -389,14 +438,44 @@ impl WaylandWindowStatePtr {
Some(size(px(width as f32), px(height as f32))) Some(size(px(width as f32), px(height as f32)))
}; };
let fullscreen = states.contains(&(xdg_toplevel::State::Fullscreen as u8)); let states = extract_states::<xdg_toplevel::State>(&states);
let maximized = states.contains(&(xdg_toplevel::State::Maximized as u8));
let mut tiling = Tiling::default();
let mut fullscreen = false;
let mut maximized = false;
for state in states {
match state {
xdg_toplevel::State::Maximized => {
maximized = true;
}
xdg_toplevel::State::Fullscreen => {
fullscreen = true;
}
xdg_toplevel::State::TiledTop => {
tiling.top = true;
}
xdg_toplevel::State::TiledLeft => {
tiling.left = true;
}
xdg_toplevel::State::TiledRight => {
tiling.right = true;
}
xdg_toplevel::State::TiledBottom => {
tiling.bottom = true;
}
_ => {
// noop
}
}
}
let mut state = self.state.borrow_mut(); let mut state = self.state.borrow_mut();
state.in_progress_configure = Some(InProgressConfigure { state.in_progress_configure = Some(InProgressConfigure {
size, size,
fullscreen, fullscreen,
maximized, maximized,
tiling,
}); });
false false
@ -415,6 +494,33 @@ impl WaylandWindowStatePtr {
true true
} }
} }
xdg_toplevel::Event::WmCapabilities { capabilities } => {
let mut window_controls = WindowControls::default();
let states = extract_states::<xdg_toplevel::WmCapabilities>(&capabilities);
for state in states {
match state {
xdg_toplevel::WmCapabilities::Maximize => {
window_controls.maximize = true;
}
xdg_toplevel::WmCapabilities::Minimize => {
window_controls.minimize = true;
}
xdg_toplevel::WmCapabilities::Fullscreen => {
window_controls.fullscreen = true;
}
xdg_toplevel::WmCapabilities::WindowMenu => {
window_controls.window_menu = true;
}
_ => {}
}
}
let mut state = self.state.borrow_mut();
state.in_progress_window_controls = Some(window_controls);
false
}
_ => false, _ => false,
} }
} }
@ -545,18 +651,6 @@ impl WaylandWindowStatePtr {
self.set_size_and_scale(None, Some(scale)); self.set_size_and_scale(None, Some(scale));
} }
/// Notifies the window of the state of the decorations.
///
/// # Note
///
/// This API is indirectly called by the wayland compositor and
/// not meant to be called by a user who wishes to change the state
/// of the decorations. This is because the state of the decorations
/// is managed by the compositor and not the client.
pub fn set_decoration_state(&self, state: WaylandDecorationState) {
self.state.borrow_mut().decoration_state = state;
}
pub fn close(&self) { pub fn close(&self) {
let mut callbacks = self.callbacks.borrow_mut(); let mut callbacks = self.callbacks.borrow_mut();
if let Some(fun) = callbacks.close.take() { if let Some(fun) = callbacks.close.take() {
@ -599,6 +693,17 @@ impl WaylandWindowStatePtr {
} }
} }
fn extract_states<'a, S: TryFrom<u32> + 'a>(states: &'a [u8]) -> impl Iterator<Item = S> + 'a
where
<S as TryFrom<u32>>::Error: 'a,
{
states
.chunks_exact(4)
.flat_map(TryInto::<[u8; 4]>::try_into)
.map(u32::from_ne_bytes)
.flat_map(S::try_from)
}
fn primary_output_scale(state: &mut RefMut<WaylandWindowState>) -> i32 { fn primary_output_scale(state: &mut RefMut<WaylandWindowState>) -> i32 {
let mut scale = 1; let mut scale = 1;
let mut current_output = state.display.take(); let mut current_output = state.display.take();
@ -639,9 +744,9 @@ impl PlatformWindow for WaylandWindow {
fn window_bounds(&self) -> WindowBounds { fn window_bounds(&self) -> WindowBounds {
let state = self.borrow(); let state = self.borrow();
if state.fullscreen { if state.fullscreen {
WindowBounds::Fullscreen(state.windowed_bounds) WindowBounds::Fullscreen(state.window_bounds)
} else if state.maximized { } else if state.maximized {
WindowBounds::Maximized(state.windowed_bounds) WindowBounds::Maximized(state.window_bounds)
} else { } else {
drop(state); drop(state);
WindowBounds::Windowed(self.bounds()) WindowBounds::Windowed(self.bounds())
@ -718,52 +823,10 @@ impl PlatformWindow for WaylandWindow {
self.borrow().toplevel.set_app_id(app_id.to_owned()); self.borrow().toplevel.set_app_id(app_id.to_owned());
} }
fn set_background_appearance(&mut self, background_appearance: WindowBackgroundAppearance) { fn set_background_appearance(&self, background_appearance: WindowBackgroundAppearance) {
let opaque = background_appearance == WindowBackgroundAppearance::Opaque;
let mut state = self.borrow_mut(); let mut state = self.borrow_mut();
state.renderer.update_transparency(!opaque); state.background_appearance = background_appearance;
update_window(state);
let region = state
.globals
.compositor
.create_region(&state.globals.qh, ());
region.add(0, 0, i32::MAX, i32::MAX);
if opaque {
// Promise the compositor that this region of the window surface
// contains no transparent pixels. This allows the compositor to
// do skip whatever is behind the surface for better performance.
state.surface.set_opaque_region(Some(&region));
} else {
state.surface.set_opaque_region(None);
}
if let Some(ref blur_manager) = state.globals.blur_manager {
if background_appearance == WindowBackgroundAppearance::Blurred {
if state.blur.is_none() {
let blur = blur_manager.create(&state.surface, &state.globals.qh, ());
blur.set_region(Some(&region));
state.blur = Some(blur);
}
state.blur.as_ref().unwrap().commit();
} else {
// It probably doesn't hurt to clear the blur for opaque windows
blur_manager.unset(&state.surface);
if let Some(b) = state.blur.take() {
b.release()
}
}
}
region.destroy();
}
fn set_edited(&mut self, _edited: bool) {
log::info!("ignoring macOS specific set_edited");
}
fn show_character_palette(&self) {
log::info!("ignoring macOS specific show_character_palette");
} }
fn minimize(&self) { fn minimize(&self) {
@ -831,6 +894,25 @@ impl PlatformWindow for WaylandWindow {
fn completed_frame(&self) { fn completed_frame(&self) {
let mut state = self.borrow_mut(); let mut state = self.borrow_mut();
if let Some(area) = state.requested_inset {
state.inset = Some(area);
}
let window_geometry = inset_by_tiling(
state.bounds.map_origin(|_| px(0.0)),
state.inset.unwrap_or(px(0.0)),
state.tiling,
)
.map(|v| v.0 as i32)
.map_size(|v| if v <= 0 { 1 } else { v });
state.xdg_surface.set_window_geometry(
window_geometry.origin.x,
window_geometry.origin.y,
window_geometry.size.width,
window_geometry.size.height,
);
state.surface.commit(); state.surface.commit();
} }
@ -850,22 +932,173 @@ impl PlatformWindow for WaylandWindow {
); );
} }
fn start_system_move(&self) { fn start_window_move(&self) {
let state = self.borrow(); let state = self.borrow();
let serial = state.client.get_serial(SerialKind::MousePress); let serial = state.client.get_serial(SerialKind::MousePress);
state.toplevel._move(&state.globals.seat, serial); state.toplevel._move(&state.globals.seat, serial);
} }
fn should_render_window_controls(&self) -> bool { fn start_window_resize(&self, edge: crate::ResizeEdge) {
self.borrow().decoration_state == WaylandDecorationState::Client let state = self.borrow();
state.toplevel.resize(
&state.globals.seat,
state.client.get_serial(SerialKind::MousePress),
edge.to_xdg(),
)
}
fn window_decorations(&self) -> Decorations {
let state = self.borrow();
match state.decorations {
WindowDecorations::Server => Decorations::Server,
WindowDecorations::Client => Decorations::Client {
tiling: state.tiling,
},
}
}
fn request_decorations(&self, decorations: WindowDecorations) {
let mut state = self.borrow_mut();
state.decorations = decorations;
if let Some(decoration) = state.decoration.as_ref() {
decoration.set_mode(decorations.to_xdg());
update_window(state);
}
}
fn window_controls(&self) -> WindowControls {
self.borrow().window_controls
}
fn set_client_inset(&self, inset: Pixels) {
let mut state = self.borrow_mut();
if Some(inset) != state.inset {
state.requested_inset = Some(inset);
update_window(state);
}
} }
} }
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] fn update_window(mut state: RefMut<WaylandWindowState>) {
pub enum WaylandDecorationState { let opaque = !state.is_transparent();
/// Decorations are to be provided by the client
Client,
/// Decorations are provided by the server state.renderer.update_transparency(!opaque);
Server, let mut opaque_area = state.window_bounds.map(|v| v.0 as i32);
if let Some(inset) = state.inset {
opaque_area.inset(inset.0 as i32);
}
let region = state
.globals
.compositor
.create_region(&state.globals.qh, ());
region.add(
opaque_area.origin.x,
opaque_area.origin.y,
opaque_area.size.width,
opaque_area.size.height,
);
// Note that rounded corners make this rectangle API hard to work with.
// As this is common when using CSD, let's just disable this API.
if state.background_appearance == WindowBackgroundAppearance::Opaque
&& state.decorations == WindowDecorations::Server
{
// Promise the compositor that this region of the window surface
// contains no transparent pixels. This allows the compositor to
// do skip whatever is behind the surface for better performance.
state.surface.set_opaque_region(Some(&region));
} else {
state.surface.set_opaque_region(None);
}
if let Some(ref blur_manager) = state.globals.blur_manager {
if state.background_appearance == WindowBackgroundAppearance::Blurred {
if state.blur.is_none() {
let blur = blur_manager.create(&state.surface, &state.globals.qh, ());
blur.set_region(Some(&region));
state.blur = Some(blur);
}
state.blur.as_ref().unwrap().commit();
} else {
// It probably doesn't hurt to clear the blur for opaque windows
blur_manager.unset(&state.surface);
if let Some(b) = state.blur.take() {
b.release()
}
}
}
region.destroy();
}
impl WindowDecorations {
fn to_xdg(&self) -> zxdg_toplevel_decoration_v1::Mode {
match self {
WindowDecorations::Client => zxdg_toplevel_decoration_v1::Mode::ClientSide,
WindowDecorations::Server => zxdg_toplevel_decoration_v1::Mode::ServerSide,
}
}
}
impl ResizeEdge {
fn to_xdg(&self) -> xdg_toplevel::ResizeEdge {
match self {
ResizeEdge::Top => xdg_toplevel::ResizeEdge::Top,
ResizeEdge::TopRight => xdg_toplevel::ResizeEdge::TopRight,
ResizeEdge::Right => xdg_toplevel::ResizeEdge::Right,
ResizeEdge::BottomRight => xdg_toplevel::ResizeEdge::BottomRight,
ResizeEdge::Bottom => xdg_toplevel::ResizeEdge::Bottom,
ResizeEdge::BottomLeft => xdg_toplevel::ResizeEdge::BottomLeft,
ResizeEdge::Left => xdg_toplevel::ResizeEdge::Left,
ResizeEdge::TopLeft => xdg_toplevel::ResizeEdge::TopLeft,
}
}
}
/// The configuration event is in terms of the window geometry, which we are constantly
/// updating to account for the client decorations. But that's not the area we want to render
/// to, due to our intrusize CSD. So, here we calculate the 'actual' size, by adding back in the insets
fn compute_outer_size(
inset: Option<Pixels>,
new_size: Option<Size<Pixels>>,
tiling: Tiling,
) -> Option<Size<Pixels>> {
let Some(inset) = inset else { return new_size };
new_size.map(|mut new_size| {
if !tiling.top {
new_size.height += inset;
}
if !tiling.bottom {
new_size.height += inset;
}
if !tiling.left {
new_size.width += inset;
}
if !tiling.right {
new_size.width += inset;
}
new_size
})
}
fn inset_by_tiling(mut bounds: Bounds<Pixels>, inset: Pixels, tiling: Tiling) -> Bounds<Pixels> {
if !tiling.top {
bounds.origin.y += inset;
bounds.size.height -= inset;
}
if !tiling.bottom {
bounds.size.height -= inset;
}
if !tiling.left {
bounds.origin.x += inset;
bounds.size.width -= inset;
}
if !tiling.right {
bounds.size.width -= inset;
}
bounds
} }

View file

@ -512,7 +512,7 @@ impl X11Client {
match event { match event {
Event::ClientMessage(event) => { Event::ClientMessage(event) => {
let window = self.get_window(event.window)?; let window = self.get_window(event.window)?;
let [atom, ..] = event.data.as_data32(); let [atom, _arg1, arg2, arg3, _arg4] = event.data.as_data32();
let mut state = self.0.borrow_mut(); let mut state = self.0.borrow_mut();
if atom == state.atoms.WM_DELETE_WINDOW { if atom == state.atoms.WM_DELETE_WINDOW {
@ -521,6 +521,12 @@ impl X11Client {
// Rest of the close logic is handled in drop_window() // Rest of the close logic is handled in drop_window()
window.close(); window.close();
} }
} else if atom == state.atoms._NET_WM_SYNC_REQUEST {
window.state.borrow_mut().last_sync_counter =
Some(x11rb::protocol::sync::Int64 {
lo: arg2,
hi: arg3 as i32,
})
} }
} }
Event::ConfigureNotify(event) => { Event::ConfigureNotify(event) => {
@ -537,6 +543,10 @@ impl X11Client {
let window = self.get_window(event.window)?; let window = self.get_window(event.window)?;
window.configure(bounds); window.configure(bounds);
} }
Event::PropertyNotify(event) => {
let window = self.get_window(event.window)?;
window.property_notify(event);
}
Event::Expose(event) => { Event::Expose(event) => {
let window = self.get_window(event.window)?; let window = self.get_window(event.window)?;
window.refresh(); window.refresh();

View file

@ -2,10 +2,11 @@ use anyhow::Context;
use crate::{ use crate::{
platform::blade::{BladeRenderer, BladeSurfaceConfig}, platform::blade::{BladeRenderer, BladeSurfaceConfig},
px, size, AnyWindowHandle, Bounds, DevicePixels, ForegroundExecutor, Modifiers, Pixels, px, size, AnyWindowHandle, Bounds, Decorations, DevicePixels, ForegroundExecutor, Modifiers,
PlatformAtlas, PlatformDisplay, PlatformInput, PlatformInputHandler, PlatformWindow, Point, Pixels, PlatformAtlas, PlatformDisplay, PlatformInput, PlatformInputHandler, PlatformWindow,
PromptLevel, Scene, Size, WindowAppearance, WindowBackgroundAppearance, WindowBounds, Point, PromptLevel, ResizeEdge, Scene, Size, Tiling, WindowAppearance,
WindowKind, WindowParams, X11ClientStatePtr, WindowBackgroundAppearance, WindowBounds, WindowDecorations, WindowKind, WindowParams,
X11ClientStatePtr,
}; };
use blade_graphics as gpu; use blade_graphics as gpu;
@ -15,24 +16,17 @@ use x11rb::{
connection::Connection, connection::Connection,
protocol::{ protocol::{
randr::{self, ConnectionExt as _}, randr::{self, ConnectionExt as _},
sync,
xinput::{self, ConnectionExt as _}, xinput::{self, ConnectionExt as _},
xproto::{ xproto::{self, ClientMessageEvent, ConnectionExt, EventMask, TranslateCoordinatesReply},
self, ClientMessageEvent, ConnectionExt as _, EventMask, TranslateCoordinatesReply,
},
}, },
wrapper::ConnectionExt as _, wrapper::ConnectionExt as _,
xcb_ffi::XCBConnection, xcb_ffi::XCBConnection,
}; };
use std::{ use std::{
cell::RefCell, cell::RefCell, ffi::c_void, mem::size_of, num::NonZeroU32, ops::Div, ptr::NonNull, rc::Rc,
ffi::c_void, sync::Arc, time::Duration,
num::NonZeroU32,
ops::Div,
ptr::NonNull,
rc::Rc,
sync::{self, Arc},
time::Duration,
}; };
use super::{X11Display, XINPUT_MASTER_DEVICE}; use super::{X11Display, XINPUT_MASTER_DEVICE};
@ -50,10 +44,16 @@ x11rb::atom_manager! {
_NET_WM_STATE_HIDDEN, _NET_WM_STATE_HIDDEN,
_NET_WM_STATE_FOCUSED, _NET_WM_STATE_FOCUSED,
_NET_ACTIVE_WINDOW, _NET_ACTIVE_WINDOW,
_NET_WM_SYNC_REQUEST,
_NET_WM_SYNC_REQUEST_COUNTER,
_NET_WM_BYPASS_COMPOSITOR,
_NET_WM_MOVERESIZE, _NET_WM_MOVERESIZE,
_NET_WM_WINDOW_TYPE, _NET_WM_WINDOW_TYPE,
_NET_WM_WINDOW_TYPE_NOTIFICATION, _NET_WM_WINDOW_TYPE_NOTIFICATION,
_NET_WM_SYNC,
_MOTIF_WM_HINTS,
_GTK_SHOW_WINDOW_MENU, _GTK_SHOW_WINDOW_MENU,
_GTK_FRAME_EXTENTS,
} }
} }
@ -70,6 +70,21 @@ fn query_render_extent(xcb_connection: &XCBConnection, x_window: xproto::Window)
} }
} }
impl ResizeEdge {
fn to_moveresize(&self) -> u32 {
match self {
ResizeEdge::TopLeft => 0,
ResizeEdge::Top => 1,
ResizeEdge::TopRight => 2,
ResizeEdge::Right => 3,
ResizeEdge::BottomRight => 4,
ResizeEdge::Bottom => 5,
ResizeEdge::BottomLeft => 6,
ResizeEdge::Left => 7,
}
}
}
#[derive(Debug)] #[derive(Debug)]
struct Visual { struct Visual {
id: xproto::Visualid, id: xproto::Visualid,
@ -166,6 +181,8 @@ pub struct X11WindowState {
executor: ForegroundExecutor, executor: ForegroundExecutor,
atoms: XcbAtoms, atoms: XcbAtoms,
x_root_window: xproto::Window, x_root_window: xproto::Window,
pub(crate) counter_id: sync::Counter,
pub(crate) last_sync_counter: Option<sync::Int64>,
_raw: RawWindow, _raw: RawWindow,
bounds: Bounds<Pixels>, bounds: Bounds<Pixels>,
scale_factor: f32, scale_factor: f32,
@ -173,7 +190,22 @@ pub struct X11WindowState {
display: Rc<dyn PlatformDisplay>, display: Rc<dyn PlatformDisplay>,
input_handler: Option<PlatformInputHandler>, input_handler: Option<PlatformInputHandler>,
appearance: WindowAppearance, appearance: WindowAppearance,
background_appearance: WindowBackgroundAppearance,
maximized_vertical: bool,
maximized_horizontal: bool,
hidden: bool,
active: bool,
fullscreen: bool,
decorations: WindowDecorations,
pub handle: AnyWindowHandle, pub handle: AnyWindowHandle,
last_insets: [u32; 4],
}
impl X11WindowState {
fn is_transparent(&self) -> bool {
self.decorations == WindowDecorations::Client
|| self.background_appearance != WindowBackgroundAppearance::Opaque
}
} }
#[derive(Clone)] #[derive(Clone)]
@ -230,19 +262,11 @@ impl X11WindowState {
.map_or(x_main_screen_index, |did| did.0 as usize); .map_or(x_main_screen_index, |did| did.0 as usize);
let visual_set = find_visuals(&xcb_connection, x_screen_index); let visual_set = find_visuals(&xcb_connection, x_screen_index);
let visual_maybe = match params.window_background {
WindowBackgroundAppearance::Opaque => visual_set.opaque, let visual = match visual_set.transparent {
WindowBackgroundAppearance::Transparent | WindowBackgroundAppearance::Blurred => {
visual_set.transparent
}
};
let visual = match visual_maybe {
Some(visual) => visual, Some(visual) => visual,
None => { None => {
log::warn!( log::warn!("Unable to find a transparent visual",);
"Unable to find a matching visual for {:?}",
params.window_background
);
visual_set.inherit visual_set.inherit
} }
}; };
@ -269,7 +293,8 @@ impl X11WindowState {
| xproto::EventMask::STRUCTURE_NOTIFY | xproto::EventMask::STRUCTURE_NOTIFY
| xproto::EventMask::FOCUS_CHANGE | xproto::EventMask::FOCUS_CHANGE
| xproto::EventMask::KEY_PRESS | xproto::EventMask::KEY_PRESS
| xproto::EventMask::KEY_RELEASE, | xproto::EventMask::KEY_RELEASE
| EventMask::PROPERTY_CHANGE,
); );
let mut bounds = params.bounds.to_device_pixels(scale_factor); let mut bounds = params.bounds.to_device_pixels(scale_factor);
@ -349,7 +374,26 @@ impl X11WindowState {
x_window, x_window,
atoms.WM_PROTOCOLS, atoms.WM_PROTOCOLS,
xproto::AtomEnum::ATOM, xproto::AtomEnum::ATOM,
&[atoms.WM_DELETE_WINDOW], &[atoms.WM_DELETE_WINDOW, atoms._NET_WM_SYNC_REQUEST],
)
.unwrap();
sync::initialize(xcb_connection, 3, 1).unwrap();
let sync_request_counter = xcb_connection.generate_id().unwrap();
sync::create_counter(
xcb_connection,
sync_request_counter,
sync::Int64 { lo: 0, hi: 0 },
)
.unwrap();
xcb_connection
.change_property32(
xproto::PropMode::REPLACE,
x_window,
atoms._NET_WM_SYNC_REQUEST_COUNTER,
xproto::AtomEnum::CARDINAL,
&[sync_request_counter],
) )
.unwrap(); .unwrap();
@ -396,7 +440,8 @@ impl X11WindowState {
// Note: this has to be done after the GPU init, or otherwise // Note: this has to be done after the GPU init, or otherwise
// the sizes are immediately invalidated. // the sizes are immediately invalidated.
size: query_render_extent(xcb_connection, x_window), size: query_render_extent(xcb_connection, x_window),
transparent: params.window_background != WindowBackgroundAppearance::Opaque, // In case we have window decorations to render
transparent: true,
}; };
xcb_connection.map_window(x_window).unwrap(); xcb_connection.map_window(x_window).unwrap();
@ -438,9 +483,19 @@ impl X11WindowState {
renderer: BladeRenderer::new(gpu, config), renderer: BladeRenderer::new(gpu, config),
atoms: *atoms, atoms: *atoms,
input_handler: None, input_handler: None,
active: false,
fullscreen: false,
maximized_vertical: false,
maximized_horizontal: false,
hidden: false,
appearance, appearance,
handle, handle,
background_appearance: WindowBackgroundAppearance::Opaque,
destroyed: false, destroyed: false,
decorations: WindowDecorations::Server,
last_insets: [0, 0, 0, 0],
counter_id: sync_request_counter,
last_sync_counter: None,
refresh_rate, refresh_rate,
}) })
} }
@ -511,7 +566,7 @@ impl X11Window {
scale_factor: f32, scale_factor: f32,
appearance: WindowAppearance, appearance: WindowAppearance,
) -> anyhow::Result<Self> { ) -> anyhow::Result<Self> {
Ok(Self(X11WindowStatePtr { let ptr = X11WindowStatePtr {
state: Rc::new(RefCell::new(X11WindowState::new( state: Rc::new(RefCell::new(X11WindowState::new(
handle, handle,
client, client,
@ -527,7 +582,12 @@ impl X11Window {
callbacks: Rc::new(RefCell::new(Callbacks::default())), callbacks: Rc::new(RefCell::new(Callbacks::default())),
xcb_connection: xcb_connection.clone(), xcb_connection: xcb_connection.clone(),
x_window, x_window,
})) };
let state = ptr.state.borrow_mut();
ptr.set_wm_properties(state);
Ok(Self(ptr))
} }
fn set_wm_hints(&self, wm_hint_property_state: WmHintPropertyState, prop1: u32, prop2: u32) { fn set_wm_hints(&self, wm_hint_property_state: WmHintPropertyState, prop1: u32, prop2: u32) {
@ -549,29 +609,6 @@ impl X11Window {
.unwrap(); .unwrap();
} }
fn get_wm_hints(&self) -> Vec<u32> {
let reply = self
.0
.xcb_connection
.get_property(
false,
self.0.x_window,
self.0.state.borrow().atoms._NET_WM_STATE,
xproto::AtomEnum::ATOM,
0,
u32::MAX,
)
.unwrap()
.reply()
.unwrap();
// Reply is in u8 but atoms are represented as u32
reply
.value
.chunks_exact(4)
.map(|chunk| u32::from_ne_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]))
.collect()
}
fn get_root_position(&self, position: Point<Pixels>) -> TranslateCoordinatesReply { fn get_root_position(&self, position: Point<Pixels>) -> TranslateCoordinatesReply {
let state = self.0.state.borrow(); let state = self.0.state.borrow();
self.0 self.0
@ -586,6 +623,48 @@ impl X11Window {
.reply() .reply()
.unwrap() .unwrap()
} }
fn send_moveresize(&self, flag: u32) {
let state = self.0.state.borrow();
self.0
.xcb_connection
.ungrab_pointer(x11rb::CURRENT_TIME)
.unwrap()
.check()
.unwrap();
let pointer = self
.0
.xcb_connection
.query_pointer(self.0.x_window)
.unwrap()
.reply()
.unwrap();
let message = ClientMessageEvent::new(
32,
self.0.x_window,
state.atoms._NET_WM_MOVERESIZE,
[
pointer.root_x as u32,
pointer.root_y as u32,
flag,
0, // Left mouse button
0,
],
);
self.0
.xcb_connection
.send_event(
false,
state.x_root_window,
EventMask::SUBSTRUCTURE_REDIRECT | EventMask::SUBSTRUCTURE_NOTIFY,
message,
)
.unwrap();
self.0.xcb_connection.flush().unwrap();
}
} }
impl X11WindowStatePtr { impl X11WindowStatePtr {
@ -600,6 +679,54 @@ impl X11WindowStatePtr {
} }
} }
pub fn property_notify(&self, event: xproto::PropertyNotifyEvent) {
let mut state = self.state.borrow_mut();
if event.atom == state.atoms._NET_WM_STATE {
self.set_wm_properties(state);
}
}
fn set_wm_properties(&self, mut state: std::cell::RefMut<X11WindowState>) {
let reply = self
.xcb_connection
.get_property(
false,
self.x_window,
state.atoms._NET_WM_STATE,
xproto::AtomEnum::ATOM,
0,
u32::MAX,
)
.unwrap()
.reply()
.unwrap();
let atoms = reply
.value
.chunks_exact(4)
.map(|chunk| u32::from_ne_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]));
state.active = false;
state.fullscreen = false;
state.maximized_vertical = false;
state.maximized_horizontal = false;
state.hidden = true;
for atom in atoms {
if atom == state.atoms._NET_WM_STATE_FOCUSED {
state.active = true;
} else if atom == state.atoms._NET_WM_STATE_FULLSCREEN {
state.fullscreen = true;
} else if atom == state.atoms._NET_WM_STATE_MAXIMIZED_VERT {
state.maximized_vertical = true;
} else if atom == state.atoms._NET_WM_STATE_MAXIMIZED_HORZ {
state.maximized_horizontal = true;
} else if atom == state.atoms._NET_WM_STATE_HIDDEN {
state.hidden = true;
}
}
}
pub fn close(&self) { pub fn close(&self) {
let mut callbacks = self.callbacks.borrow_mut(); let mut callbacks = self.callbacks.borrow_mut();
if let Some(fun) = callbacks.close.take() { if let Some(fun) = callbacks.close.take() {
@ -715,6 +842,9 @@ impl X11WindowStatePtr {
)); ));
resize_args = Some((state.content_size(), state.scale_factor)); resize_args = Some((state.content_size(), state.scale_factor));
} }
if let Some(value) = state.last_sync_counter.take() {
sync::set_counter(&self.xcb_connection, state.counter_id, value).unwrap();
}
} }
let mut callbacks = self.callbacks.borrow_mut(); let mut callbacks = self.callbacks.borrow_mut();
@ -737,8 +867,12 @@ impl X11WindowStatePtr {
} }
pub fn set_appearance(&mut self, appearance: WindowAppearance) { pub fn set_appearance(&mut self, appearance: WindowAppearance) {
self.state.borrow_mut().appearance = appearance; let mut state = self.state.borrow_mut();
state.appearance = appearance;
let is_transparent = state.is_transparent();
state.renderer.update_transparency(is_transparent);
state.appearance = appearance;
drop(state);
let mut callbacks = self.callbacks.borrow_mut(); let mut callbacks = self.callbacks.borrow_mut();
if let Some(ref mut fun) = callbacks.appearance_changed { if let Some(ref mut fun) = callbacks.appearance_changed {
(fun)() (fun)()
@ -757,11 +891,9 @@ impl PlatformWindow for X11Window {
fn is_maximized(&self) -> bool { fn is_maximized(&self) -> bool {
let state = self.0.state.borrow(); let state = self.0.state.borrow();
let wm_hints = self.get_wm_hints();
// A maximized window that gets minimized will still retain its maximized state. // A maximized window that gets minimized will still retain its maximized state.
!wm_hints.contains(&state.atoms._NET_WM_STATE_HIDDEN) !state.hidden && state.maximized_vertical && state.maximized_horizontal
&& wm_hints.contains(&state.atoms._NET_WM_STATE_MAXIMIZED_VERT)
&& wm_hints.contains(&state.atoms._NET_WM_STATE_MAXIMIZED_HORZ)
} }
fn window_bounds(&self) -> WindowBounds { fn window_bounds(&self) -> WindowBounds {
@ -862,9 +994,7 @@ impl PlatformWindow for X11Window {
} }
fn is_active(&self) -> bool { fn is_active(&self) -> bool {
let state = self.0.state.borrow(); self.0.state.borrow().active
self.get_wm_hints()
.contains(&state.atoms._NET_WM_STATE_FOCUSED)
} }
fn set_title(&mut self, title: &str) { fn set_title(&mut self, title: &str) {
@ -913,10 +1043,11 @@ impl PlatformWindow for X11Window {
log::info!("ignoring macOS specific set_edited"); log::info!("ignoring macOS specific set_edited");
} }
fn set_background_appearance(&mut self, background_appearance: WindowBackgroundAppearance) { fn set_background_appearance(&self, background_appearance: WindowBackgroundAppearance) {
let mut inner = self.0.state.borrow_mut(); let mut state = self.0.state.borrow_mut();
let transparent = background_appearance != WindowBackgroundAppearance::Opaque; state.background_appearance = background_appearance;
inner.renderer.update_transparency(transparent); let transparent = state.is_transparent();
state.renderer.update_transparency(transparent);
} }
fn show_character_palette(&self) { fn show_character_palette(&self) {
@ -962,9 +1093,7 @@ impl PlatformWindow for X11Window {
} }
fn is_fullscreen(&self) -> bool { fn is_fullscreen(&self) -> bool {
let state = self.0.state.borrow(); self.0.state.borrow().fullscreen
self.get_wm_hints()
.contains(&state.atoms._NET_WM_STATE_FULLSCREEN)
} }
fn on_request_frame(&self, callback: Box<dyn FnMut()>) { fn on_request_frame(&self, callback: Box<dyn FnMut()>) {
@ -1004,7 +1133,7 @@ impl PlatformWindow for X11Window {
inner.renderer.draw(scene); inner.renderer.draw(scene);
} }
fn sprite_atlas(&self) -> sync::Arc<dyn PlatformAtlas> { fn sprite_atlas(&self) -> Arc<dyn PlatformAtlas> {
let inner = self.0.state.borrow(); let inner = self.0.state.borrow();
inner.renderer.sprite_atlas().clone() inner.renderer.sprite_atlas().clone()
} }
@ -1035,41 +1164,109 @@ impl PlatformWindow for X11Window {
.unwrap(); .unwrap();
} }
fn start_system_move(&self) { fn start_window_move(&self) {
let state = self.0.state.borrow();
let pointer = self
.0
.xcb_connection
.query_pointer(self.0.x_window)
.unwrap()
.reply()
.unwrap();
const MOVERESIZE_MOVE: u32 = 8; const MOVERESIZE_MOVE: u32 = 8;
let message = ClientMessageEvent::new( self.send_moveresize(MOVERESIZE_MOVE);
32,
self.0.x_window,
state.atoms._NET_WM_MOVERESIZE,
[
pointer.root_x as u32,
pointer.root_y as u32,
MOVERESIZE_MOVE,
1, // Left mouse button
1,
],
);
self.0
.xcb_connection
.send_event(
false,
state.x_root_window,
EventMask::SUBSTRUCTURE_REDIRECT | EventMask::SUBSTRUCTURE_NOTIFY,
message,
)
.unwrap();
} }
fn should_render_window_controls(&self) -> bool { fn start_window_resize(&self, edge: ResizeEdge) {
false self.send_moveresize(edge.to_moveresize());
}
fn window_decorations(&self) -> crate::Decorations {
let state = self.0.state.borrow();
match state.decorations {
WindowDecorations::Server => Decorations::Server,
WindowDecorations::Client => {
// https://source.chromium.org/chromium/chromium/src/+/main:ui/ozone/platform/x11/x11_window.cc;l=2519;drc=1f14cc876cc5bf899d13284a12c451498219bb2d
Decorations::Client {
tiling: Tiling {
top: state.maximized_vertical,
bottom: state.maximized_vertical,
left: state.maximized_horizontal,
right: state.maximized_horizontal,
},
}
}
}
}
fn set_client_inset(&self, inset: Pixels) {
let mut state = self.0.state.borrow_mut();
let dp = (inset.0 * state.scale_factor) as u32;
let (left, right) = if state.maximized_horizontal {
(0, 0)
} else {
(dp, dp)
};
let (top, bottom) = if state.maximized_vertical {
(0, 0)
} else {
(dp, dp)
};
let insets = [left, right, top, bottom];
if state.last_insets != insets {
state.last_insets = insets;
self.0
.xcb_connection
.change_property(
xproto::PropMode::REPLACE,
self.0.x_window,
state.atoms._GTK_FRAME_EXTENTS,
xproto::AtomEnum::CARDINAL,
size_of::<u32>() as u8 * 8,
4,
bytemuck::cast_slice::<u32, u8>(&insets),
)
.unwrap();
}
}
fn request_decorations(&self, decorations: crate::WindowDecorations) {
// https://github.com/rust-windowing/winit/blob/master/src/platform_impl/linux/x11/util/hint.rs#L53-L87
let hints_data: [u32; 5] = match decorations {
WindowDecorations::Server => [1 << 1, 0, 1, 0, 0],
WindowDecorations::Client => [1 << 1, 0, 0, 0, 0],
};
let mut state = self.0.state.borrow_mut();
self.0
.xcb_connection
.change_property(
xproto::PropMode::REPLACE,
self.0.x_window,
state.atoms._MOTIF_WM_HINTS,
state.atoms._MOTIF_WM_HINTS,
std::mem::size_of::<u32>() as u8 * 8,
5,
bytemuck::cast_slice::<u32, u8>(&hints_data),
)
.unwrap();
match decorations {
WindowDecorations::Server => {
state.decorations = WindowDecorations::Server;
let is_transparent = state.is_transparent();
state.renderer.update_transparency(is_transparent);
}
WindowDecorations::Client => {
state.decorations = WindowDecorations::Client;
let is_transparent = state.is_transparent();
state.renderer.update_transparency(is_transparent);
}
}
drop(state);
let mut callbacks = self.0.callbacks.borrow_mut();
if let Some(appearance_changed) = callbacks.appearance_changed.as_mut() {
appearance_changed();
}
} }
} }

View file

@ -796,14 +796,24 @@ impl Platform for MacPlatform {
CursorStyle::ClosedHand => msg_send![class!(NSCursor), closedHandCursor], CursorStyle::ClosedHand => msg_send![class!(NSCursor), closedHandCursor],
CursorStyle::OpenHand => msg_send![class!(NSCursor), openHandCursor], CursorStyle::OpenHand => msg_send![class!(NSCursor), openHandCursor],
CursorStyle::PointingHand => msg_send![class!(NSCursor), pointingHandCursor], CursorStyle::PointingHand => msg_send![class!(NSCursor), pointingHandCursor],
CursorStyle::ResizeLeftRight => msg_send![class!(NSCursor), resizeLeftRightCursor],
CursorStyle::ResizeUpDown => msg_send![class!(NSCursor), verticalResizeCursor],
CursorStyle::ResizeLeft => msg_send![class!(NSCursor), resizeLeftCursor], CursorStyle::ResizeLeft => msg_send![class!(NSCursor), resizeLeftCursor],
CursorStyle::ResizeRight => msg_send![class!(NSCursor), resizeRightCursor], CursorStyle::ResizeRight => msg_send![class!(NSCursor), resizeRightCursor],
CursorStyle::ResizeLeftRight => msg_send![class!(NSCursor), resizeLeftRightCursor],
CursorStyle::ResizeColumn => msg_send![class!(NSCursor), resizeLeftRightCursor], CursorStyle::ResizeColumn => msg_send![class!(NSCursor), resizeLeftRightCursor],
CursorStyle::ResizeRow => msg_send![class!(NSCursor), resizeUpDownCursor],
CursorStyle::ResizeUp => msg_send![class!(NSCursor), resizeUpCursor], CursorStyle::ResizeUp => msg_send![class!(NSCursor), resizeUpCursor],
CursorStyle::ResizeDown => msg_send![class!(NSCursor), resizeDownCursor], CursorStyle::ResizeDown => msg_send![class!(NSCursor), resizeDownCursor],
CursorStyle::ResizeUpDown => msg_send![class!(NSCursor), resizeUpDownCursor],
CursorStyle::ResizeRow => msg_send![class!(NSCursor), resizeUpDownCursor], // Undocumented, private class methods:
// https://stackoverflow.com/questions/27242353/cocoa-predefined-resize-mouse-cursor
CursorStyle::ResizeUpLeftDownRight => {
msg_send![class!(NSCursor), _windowResizeNorthWestSouthEastCursor]
}
CursorStyle::ResizeUpRightDownLeft => {
msg_send![class!(NSCursor), _windowResizeNorthEastSouthWestCursor]
}
CursorStyle::IBeamCursorForVerticalLayout => { CursorStyle::IBeamCursorForVerticalLayout => {
msg_send![class!(NSCursor), IBeamCursorForVerticalLayout] msg_send![class!(NSCursor), IBeamCursorForVerticalLayout]
} }

View file

@ -497,7 +497,6 @@ impl MacWindow {
pub fn open( pub fn open(
handle: AnyWindowHandle, handle: AnyWindowHandle,
WindowParams { WindowParams {
window_background,
bounds, bounds,
titlebar, titlebar,
kind, kind,
@ -603,7 +602,7 @@ impl MacWindow {
native_window as *mut _, native_window as *mut _,
native_view as *mut _, native_view as *mut _,
bounds.size.map(|pixels| pixels.0), bounds.size.map(|pixels| pixels.0),
window_background != WindowBackgroundAppearance::Opaque, false,
), ),
request_frame_callback: None, request_frame_callback: None,
event_callback: None, event_callback: None,
@ -676,8 +675,6 @@ impl MacWindow {
native_window.setContentView_(native_view.autorelease()); native_window.setContentView_(native_view.autorelease());
native_window.makeFirstResponder_(native_view); native_window.makeFirstResponder_(native_view);
window.set_background_appearance(window_background);
match kind { match kind {
WindowKind::Normal => { WindowKind::Normal => {
native_window.setLevel_(NSNormalWindowLevel); native_window.setLevel_(NSNormalWindowLevel);
@ -956,7 +953,7 @@ impl PlatformWindow for MacWindow {
fn set_app_id(&mut self, _app_id: &str) {} fn set_app_id(&mut self, _app_id: &str) {}
fn set_background_appearance(&mut self, background_appearance: WindowBackgroundAppearance) { fn set_background_appearance(&self, background_appearance: WindowBackgroundAppearance) {
let mut this = self.0.as_ref().lock(); let mut this = self.0.as_ref().lock();
this.renderer this.renderer
.update_transparency(background_appearance != WindowBackgroundAppearance::Opaque); .update_transparency(background_appearance != WindowBackgroundAppearance::Opaque);
@ -1092,14 +1089,6 @@ impl PlatformWindow for MacWindow {
fn sprite_atlas(&self) -> Arc<dyn PlatformAtlas> { fn sprite_atlas(&self) -> Arc<dyn PlatformAtlas> {
self.0.lock().renderer.sprite_atlas().clone() self.0.lock().renderer.sprite_atlas().clone()
} }
fn show_window_menu(&self, _position: Point<Pixels>) {}
fn start_system_move(&self) {}
fn should_render_window_controls(&self) -> bool {
false
}
} }
impl rwh::HasWindowHandle for MacWindow { impl rwh::HasWindowHandle for MacWindow {

View file

@ -188,9 +188,7 @@ impl PlatformWindow for TestWindow {
fn set_app_id(&mut self, _app_id: &str) {} fn set_app_id(&mut self, _app_id: &str) {}
fn set_background_appearance(&mut self, _background: WindowBackgroundAppearance) { fn set_background_appearance(&self, _background: WindowBackgroundAppearance) {}
unimplemented!()
}
fn set_edited(&mut self, edited: bool) { fn set_edited(&mut self, edited: bool) {
self.0.lock().edited = edited; self.0.lock().edited = edited;
@ -262,13 +260,9 @@ impl PlatformWindow for TestWindow {
unimplemented!() unimplemented!()
} }
fn start_system_move(&self) { fn start_window_move(&self) {
unimplemented!() unimplemented!()
} }
fn should_render_window_controls(&self) -> bool {
false
}
} }
pub(crate) struct TestAtlasState { pub(crate) struct TestAtlasState {

View file

@ -274,7 +274,7 @@ impl WindowsWindow {
handle, handle,
hide_title_bar, hide_title_bar,
display, display,
transparent: params.window_background != WindowBackgroundAppearance::Opaque, transparent: true,
executor, executor,
current_cursor, current_cursor,
}; };
@ -511,9 +511,7 @@ impl PlatformWindow for WindowsWindow {
.ok(); .ok();
} }
fn set_app_id(&mut self, _app_id: &str) {} fn set_background_appearance(&self, background_appearance: WindowBackgroundAppearance) {
fn set_background_appearance(&mut self, background_appearance: WindowBackgroundAppearance) {
self.0 self.0
.state .state
.borrow_mut() .borrow_mut()
@ -521,12 +519,6 @@ impl PlatformWindow for WindowsWindow {
.update_transparency(background_appearance != WindowBackgroundAppearance::Opaque); .update_transparency(background_appearance != WindowBackgroundAppearance::Opaque);
} }
// todo(windows)
fn set_edited(&mut self, _edited: bool) {}
// todo(windows)
fn show_character_palette(&self) {}
fn minimize(&self) { fn minimize(&self) {
unsafe { ShowWindowAsync(self.0.hwnd, SW_MINIMIZE).ok().log_err() }; unsafe { ShowWindowAsync(self.0.hwnd, SW_MINIMIZE).ok().log_err() };
} }
@ -645,14 +637,6 @@ impl PlatformWindow for WindowsWindow {
fn get_raw_handle(&self) -> HWND { fn get_raw_handle(&self) -> HWND {
self.0.hwnd self.0.hwnd
} }
fn show_window_menu(&self, _position: Point<Pixels>) {}
fn start_system_move(&self) {}
fn should_render_window_controls(&self) -> bool {
false
}
} }
#[implement(IDropTarget)] #[implement(IDropTarget)]

View file

@ -1,19 +1,20 @@
use crate::{ use crate::{
hash, point, prelude::*, px, size, transparent_black, Action, AnyDrag, AnyElement, AnyTooltip, hash, point, prelude::*, px, size, transparent_black, Action, AnyDrag, AnyElement, AnyTooltip,
AnyView, AppContext, Arena, Asset, AsyncWindowContext, AvailableSpace, Bounds, BoxShadow, AnyView, AppContext, Arena, Asset, AsyncWindowContext, AvailableSpace, Bounds, BoxShadow,
Context, Corners, CursorStyle, DevicePixels, DispatchActionListener, DispatchNodeId, Context, Corners, CursorStyle, Decorations, DevicePixels, DispatchActionListener,
DispatchTree, DisplayId, Edges, Effect, Entity, EntityId, EventEmitter, FileDropEvent, Flatten, DispatchNodeId, DispatchTree, DisplayId, Edges, Effect, Entity, EntityId, EventEmitter,
FontId, Global, GlobalElementId, GlyphId, Hsla, ImageData, InputHandler, IsZero, KeyBinding, FileDropEvent, Flatten, FontId, Global, GlobalElementId, GlyphId, Hsla, ImageData,
KeyContext, KeyDownEvent, KeyEvent, KeyMatch, KeymatchResult, Keystroke, KeystrokeEvent, InputHandler, IsZero, KeyBinding, KeyContext, KeyDownEvent, KeyEvent, KeyMatch, KeymatchResult,
LayoutId, LineLayoutIndex, Model, ModelContext, Modifiers, ModifiersChangedEvent, Keystroke, KeystrokeEvent, LayoutId, LineLayoutIndex, Model, ModelContext, Modifiers,
MonochromeSprite, MouseButton, MouseEvent, MouseMoveEvent, MouseUpEvent, Path, Pixels, ModifiersChangedEvent, MonochromeSprite, MouseButton, MouseEvent, MouseMoveEvent, MouseUpEvent,
PlatformAtlas, PlatformDisplay, PlatformInput, PlatformInputHandler, PlatformWindow, Point, Path, Pixels, PlatformAtlas, PlatformDisplay, PlatformInput, PlatformInputHandler,
PolychromeSprite, PromptLevel, Quad, Render, RenderGlyphParams, RenderImageParams, PlatformWindow, Point, PolychromeSprite, PromptLevel, Quad, Render, RenderGlyphParams,
RenderSvgParams, ScaledPixels, Scene, Shadow, SharedString, Size, StrikethroughStyle, Style, RenderImageParams, RenderSvgParams, ResizeEdge, ScaledPixels, Scene, Shadow, SharedString,
SubscriberSet, Subscription, TaffyLayoutEngine, Task, TextStyle, TextStyleRefinement, Size, StrikethroughStyle, Style, SubscriberSet, Subscription, TaffyLayoutEngine, Task,
TransformationMatrix, Underline, UnderlineStyle, View, VisualContext, WeakView, TextStyle, TextStyleRefinement, TransformationMatrix, Underline, UnderlineStyle, View,
WindowAppearance, WindowBackgroundAppearance, WindowBounds, WindowOptions, WindowParams, VisualContext, WeakView, WindowAppearance, WindowBackgroundAppearance, WindowBounds,
WindowTextSystem, SUBPIXEL_VARIANTS, WindowControls, WindowDecorations, WindowOptions, WindowParams, WindowTextSystem,
SUBPIXEL_VARIANTS,
}; };
use anyhow::{anyhow, Context as _, Result}; use anyhow::{anyhow, Context as _, Result};
use collections::{FxHashMap, FxHashSet}; use collections::{FxHashMap, FxHashSet};
@ -610,7 +611,10 @@ fn default_bounds(display_id: Option<DisplayId>, cx: &mut AppContext) -> Bounds<
cx.active_window() cx.active_window()
.and_then(|w| w.update(cx, |_, cx| cx.bounds()).ok()) .and_then(|w| w.update(cx, |_, cx| cx.bounds()).ok())
.map(|bounds| bounds.map_origin(|origin| origin + DEFAULT_WINDOW_OFFSET)) .map(|mut bounds| {
bounds.origin += DEFAULT_WINDOW_OFFSET;
bounds
})
.unwrap_or_else(|| { .unwrap_or_else(|| {
let display = display_id let display = display_id
.map(|id| cx.find_display(id)) .map(|id| cx.find_display(id))
@ -639,6 +643,7 @@ impl Window {
window_background, window_background,
app_id, app_id,
window_min_size, window_min_size,
window_decorations,
} = options; } = options;
let bounds = window_bounds let bounds = window_bounds
@ -654,7 +659,6 @@ impl Window {
focus, focus,
show, show,
display_id, display_id,
window_background,
window_min_size, window_min_size,
}, },
)?; )?;
@ -672,6 +676,10 @@ impl Window {
let next_frame_callbacks: Rc<RefCell<Vec<FrameCallback>>> = Default::default(); let next_frame_callbacks: Rc<RefCell<Vec<FrameCallback>>> = Default::default();
let last_input_timestamp = Rc::new(Cell::new(Instant::now())); let last_input_timestamp = Rc::new(Cell::new(Instant::now()));
platform_window
.request_decorations(window_decorations.unwrap_or(WindowDecorations::Server));
platform_window.set_background_appearance(window_background);
if let Some(ref window_open_state) = window_bounds { if let Some(ref window_open_state) = window_bounds {
match window_open_state { match window_open_state {
WindowBounds::Fullscreen(_) => platform_window.toggle_fullscreen(), WindowBounds::Fullscreen(_) => platform_window.toggle_fullscreen(),
@ -990,6 +998,16 @@ impl<'a> WindowContext<'a> {
self.window.platform_window.is_maximized() self.window.platform_window.is_maximized()
} }
/// request a certain window decoration (Wayland)
pub fn request_decorations(&self, decorations: WindowDecorations) {
self.window.platform_window.request_decorations(decorations);
}
/// Start a window resize operation (Wayland)
pub fn start_window_resize(&self, edge: ResizeEdge) {
self.window.platform_window.start_window_resize(edge);
}
/// Return the `WindowBounds` to indicate that how a window should be opened /// Return the `WindowBounds` to indicate that how a window should be opened
/// after it has been closed /// after it has been closed
pub fn window_bounds(&self) -> WindowBounds { pub fn window_bounds(&self) -> WindowBounds {
@ -1217,13 +1235,23 @@ impl<'a> WindowContext<'a> {
/// Tells the compositor to take control of window movement (Wayland and X11) /// Tells the compositor to take control of window movement (Wayland and X11)
/// ///
/// Events may not be received during a move operation. /// Events may not be received during a move operation.
pub fn start_system_move(&self) { pub fn start_window_move(&self) {
self.window.platform_window.start_system_move() self.window.platform_window.start_window_move()
}
/// When using client side decorations, set this to the width of the invisible decorations (Wayland and X11)
pub fn set_client_inset(&self, inset: Pixels) {
self.window.platform_window.set_client_inset(inset);
} }
/// Returns whether the title bar window controls need to be rendered by the application (Wayland and X11) /// Returns whether the title bar window controls need to be rendered by the application (Wayland and X11)
pub fn should_render_window_controls(&self) -> bool { pub fn window_decorations(&self) -> Decorations {
self.window.platform_window.should_render_window_controls() self.window.platform_window.window_decorations()
}
/// Returns which window controls are currently visible (Wayland)
pub fn window_controls(&self) -> WindowControls {
self.window.platform_window.window_controls()
} }
/// Updates the window's title at the platform level. /// Updates the window's title at the platform level.
@ -1237,7 +1265,7 @@ impl<'a> WindowContext<'a> {
} }
/// Sets the window background appearance. /// Sets the window background appearance.
pub fn set_background_appearance(&mut self, background_appearance: WindowBackgroundAppearance) { pub fn set_background_appearance(&self, background_appearance: WindowBackgroundAppearance) {
self.window self.window
.platform_window .platform_window
.set_background_appearance(background_appearance); .set_background_appearance(background_appearance);

View file

@ -28,7 +28,8 @@ pub use settings::*;
pub use styles::*; pub use styles::*;
use gpui::{ use gpui::{
AppContext, AssetSource, Hsla, SharedString, WindowAppearance, WindowBackgroundAppearance, px, AppContext, AssetSource, Hsla, Pixels, SharedString, WindowAppearance,
WindowBackgroundAppearance,
}; };
use serde::Deserialize; use serde::Deserialize;
@ -38,6 +39,9 @@ pub enum Appearance {
Dark, Dark,
} }
pub const CLIENT_SIDE_DECORATION_ROUNDING: Pixels = px(10.0);
pub const CLIENT_SIDE_DECORATION_SHADOW: Pixels = px(10.0);
impl Appearance { impl Appearance {
pub fn is_light(&self) -> bool { pub fn is_light(&self) -> bool {
match self { match self {

View file

@ -1,4 +1,3 @@
pub mod platform_generic;
pub mod platform_linux; pub mod platform_linux;
pub mod platform_mac; pub mod platform_mac;
pub mod platform_windows; pub mod platform_windows;

View file

@ -1,47 +0,0 @@
use gpui::{prelude::*, Action};
use ui::prelude::*;
use crate::window_controls::{WindowControl, WindowControlType};
#[derive(IntoElement)]
pub struct GenericWindowControls {
close_window_action: Box<dyn Action>,
}
impl GenericWindowControls {
pub fn new(close_action: Box<dyn Action>) -> Self {
Self {
close_window_action: close_action,
}
}
}
impl RenderOnce for GenericWindowControls {
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
h_flex()
.id("generic-window-controls")
.px_3()
.gap_1p5()
.child(WindowControl::new(
"minimize",
WindowControlType::Minimize,
cx,
))
.child(WindowControl::new(
"maximize-or-restore",
if cx.is_maximized() {
WindowControlType::Restore
} else {
WindowControlType::Maximize
},
cx,
))
.child(WindowControl::new_close(
"close",
WindowControlType::Close,
self.close_window_action,
cx,
))
}
}

View file

@ -2,7 +2,7 @@ use gpui::{prelude::*, Action};
use ui::prelude::*; use ui::prelude::*;
use super::platform_generic::GenericWindowControls; use crate::window_controls::{WindowControl, WindowControlType};
#[derive(IntoElement)] #[derive(IntoElement)]
pub struct LinuxWindowControls { pub struct LinuxWindowControls {
@ -18,7 +18,30 @@ impl LinuxWindowControls {
} }
impl RenderOnce for LinuxWindowControls { impl RenderOnce for LinuxWindowControls {
fn render(self, _cx: &mut WindowContext) -> impl IntoElement { fn render(self, cx: &mut WindowContext) -> impl IntoElement {
GenericWindowControls::new(self.close_window_action.boxed_clone()).into_any_element() h_flex()
.id("generic-window-controls")
.px_3()
.gap_3()
.child(WindowControl::new(
"minimize",
WindowControlType::Minimize,
cx,
))
.child(WindowControl::new(
"maximize-or-restore",
if cx.is_maximized() {
WindowControlType::Restore
} else {
WindowControlType::Maximize
},
cx,
))
.child(WindowControl::new_close(
"close",
WindowControlType::Close,
self.close_window_action,
cx,
))
} }
} }

View file

@ -9,9 +9,9 @@ use call::{ActiveCall, ParticipantLocation};
use client::{Client, UserStore}; use client::{Client, UserStore};
use collab::render_color_ribbon; use collab::render_color_ribbon;
use gpui::{ use gpui::{
actions, div, px, Action, AnyElement, AppContext, Element, InteractiveElement, Interactivity, actions, div, px, Action, AnyElement, AppContext, Decorations, Element, InteractiveElement,
IntoElement, Model, ParentElement, Render, Stateful, StatefulInteractiveElement, Styled, Interactivity, IntoElement, Model, MouseButton, ParentElement, Render, Stateful,
Subscription, ViewContext, VisualContext, WeakView, StatefulInteractiveElement, Styled, Subscription, ViewContext, VisualContext, WeakView,
}; };
use project::{Project, RepositoryEntry}; use project::{Project, RepositoryEntry};
use recent_projects::RecentProjects; use recent_projects::RecentProjects;
@ -58,6 +58,7 @@ pub struct TitleBar {
user_store: Model<UserStore>, user_store: Model<UserStore>,
client: Arc<Client>, client: Arc<Client>,
workspace: WeakView<Workspace>, workspace: WeakView<Workspace>,
should_move: bool,
_subscriptions: Vec<Subscription>, _subscriptions: Vec<Subscription>,
} }
@ -73,8 +74,10 @@ impl Render for TitleBar {
let platform_supported = cfg!(target_os = "macos"); let platform_supported = cfg!(target_os = "macos");
let height = Self::height(cx); let height = Self::height(cx);
let supported_controls = cx.window_controls();
let decorations = cx.window_decorations();
let mut title_bar = h_flex() h_flex()
.id("titlebar") .id("titlebar")
.w_full() .w_full()
.pt(Self::top_padding(cx)) .pt(Self::top_padding(cx))
@ -88,6 +91,16 @@ impl Render for TitleBar {
this.pl_2() this.pl_2()
} }
}) })
.map(|el| {
match decorations {
Decorations::Server => el,
Decorations::Client { tiling, .. } => el
.when(!(tiling.top || tiling.right), |el| {
el.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING)
})
.when(!(tiling.top || tiling.left), |el| el.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING))
}
})
.bg(cx.theme().colors().title_bar_background) .bg(cx.theme().colors().title_bar_background)
.content_stretch() .content_stretch()
.child( .child(
@ -113,7 +126,7 @@ impl Render for TitleBar {
.children(self.render_project_host(cx)) .children(self.render_project_host(cx))
.child(self.render_project_name(cx)) .child(self.render_project_name(cx))
.children(self.render_project_branch(cx)) .children(self.render_project_branch(cx))
.on_mouse_move(|_, cx| cx.stop_propagation()), .on_mouse_down(MouseButton::Left, |_, cx| cx.stop_propagation())
) )
.child( .child(
h_flex() h_flex()
@ -145,7 +158,7 @@ impl Render for TitleBar {
this.children(current_user_face_pile.map(|face_pile| { this.children(current_user_face_pile.map(|face_pile| {
v_flex() v_flex()
.on_mouse_move(|_, cx| cx.stop_propagation()) .on_mouse_down(MouseButton::Left, |_, cx| cx.stop_propagation())
.child(face_pile) .child(face_pile)
.child(render_color_ribbon(player_colors.local().cursor)) .child(render_color_ribbon(player_colors.local().cursor))
})) }))
@ -208,7 +221,7 @@ impl Render for TitleBar {
h_flex() h_flex()
.gap_1() .gap_1()
.pr_1() .pr_1()
.on_mouse_move(|_, cx| cx.stop_propagation()) .on_mouse_down(MouseButton::Left, |_, cx| cx.stop_propagation())
.when_some(room, |this, room| { .when_some(room, |this, room| {
let room = room.read(cx); let room = room.read(cx);
let project = self.project.read(cx); let project = self.project.read(cx);
@ -373,34 +386,38 @@ impl Render for TitleBar {
} }
}), }),
) )
);
// Windows Window Controls ).when(
title_bar = title_bar.when(
self.platform_style == PlatformStyle::Windows && !cx.is_fullscreen(), self.platform_style == PlatformStyle::Windows && !cx.is_fullscreen(),
|title_bar| title_bar.child(platform_windows::WindowsWindowControls::new(height)), |title_bar| title_bar.child(platform_windows::WindowsWindowControls::new(height)),
); ).when(
// Linux Window Controls
title_bar = title_bar.when(
self.platform_style == PlatformStyle::Linux self.platform_style == PlatformStyle::Linux
&& !cx.is_fullscreen() && !cx.is_fullscreen()
&& cx.should_render_window_controls(), && matches!(decorations, Decorations::Client { .. }),
|title_bar| { |title_bar| {
title_bar title_bar
.child(platform_linux::LinuxWindowControls::new(close_action)) .child(platform_linux::LinuxWindowControls::new(close_action))
.on_mouse_down(gpui::MouseButton::Right, move |ev, cx| { .when(supported_controls.window_menu, |titlebar| {
cx.show_window_menu(ev.position) titlebar.on_mouse_down(gpui::MouseButton::Right, move |ev, cx| {
cx.show_window_menu(ev.position)
})
}) })
.on_mouse_move(move |ev, cx| {
if ev.dragging() {
cx.start_system_move();
}
})
},
);
title_bar .on_mouse_move(cx.listener(move |this, _ev, cx| {
if this.should_move {
this.should_move = false;
cx.start_window_move();
}
}))
.on_mouse_down_out(cx.listener(move |this, _ev, _cx| {
this.should_move = false;
}))
.on_mouse_down(gpui::MouseButton::Left, cx.listener(move |this, _ev, _cx| {
this.should_move = true;
}))
},
)
} }
} }
@ -430,6 +447,7 @@ impl TitleBar {
content: div().id(id.into()), content: div().id(id.into()),
children: SmallVec::new(), children: SmallVec::new(),
workspace: workspace.weak_handle(), workspace: workspace.weak_handle(),
should_move: false,
project, project,
user_store, user_store,
client, client,

View file

@ -38,7 +38,7 @@ impl WindowControlStyle {
Self { Self {
background: colors.ghost_element_background, background: colors.ghost_element_background,
background_hover: colors.ghost_element_background, background_hover: colors.ghost_element_hover,
icon: colors.icon, icon: colors.icon,
icon_hover: colors.icon_muted, icon_hover: colors.icon_muted,
} }
@ -127,7 +127,7 @@ impl WindowControl {
impl RenderOnce for WindowControl { impl RenderOnce for WindowControl {
fn render(self, _cx: &mut WindowContext) -> impl IntoElement { fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
let icon = svg() let icon = svg()
.size_5() .size_4()
.flex_none() .flex_none()
.path(self.icon.icon().path()) .path(self.icon.icon().path())
.text_color(self.style.icon) .text_color(self.style.icon)
@ -139,7 +139,7 @@ impl RenderOnce for WindowControl {
.cursor_pointer() .cursor_pointer()
.justify_center() .justify_center()
.content_center() .content_center()
.rounded_md() .rounded_2xl()
.w_5() .w_5()
.h_5() .h_5()
.hover(|this| this.bg(self.style.background_hover)) .hover(|this| this.bg(self.style.background_hover))

View file

@ -1,9 +1,10 @@
use crate::{ItemHandle, Pane}; use crate::{ItemHandle, Pane};
use gpui::{ use gpui::{
AnyView, IntoElement, ParentElement, Render, Styled, Subscription, View, ViewContext, AnyView, Decorations, IntoElement, ParentElement, Render, Styled, Subscription, View,
WindowContext, ViewContext, WindowContext,
}; };
use std::any::TypeId; use std::any::TypeId;
use theme::CLIENT_SIDE_DECORATION_ROUNDING;
use ui::{h_flex, prelude::*}; use ui::{h_flex, prelude::*};
use util::ResultExt; use util::ResultExt;
@ -40,8 +41,17 @@ impl Render for StatusBar {
.gap(Spacing::Large.rems(cx)) .gap(Spacing::Large.rems(cx))
.py(Spacing::Small.rems(cx)) .py(Spacing::Small.rems(cx))
.px(Spacing::Large.rems(cx)) .px(Spacing::Large.rems(cx))
// .h_8()
.bg(cx.theme().colors().status_bar_background) .bg(cx.theme().colors().status_bar_background)
.map(|el| match cx.window_decorations() {
Decorations::Server => el,
Decorations::Client { tiling, .. } => el
.when(!(tiling.bottom || tiling.right), |el| {
el.rounded_br(CLIENT_SIDE_DECORATION_ROUNDING)
})
.when(!(tiling.bottom || tiling.left), |el| {
el.rounded_bl(CLIENT_SIDE_DECORATION_ROUNDING)
}),
})
.child(self.render_left_tools(cx)) .child(self.render_left_tools(cx))
.child(self.render_right_tools(cx)) .child(self.render_right_tools(cx))
} }

View file

@ -27,11 +27,13 @@ use futures::{
Future, FutureExt, StreamExt, Future, FutureExt, StreamExt,
}; };
use gpui::{ use gpui::{
action_as, actions, canvas, impl_action_as, impl_actions, point, relative, size, Action, action_as, actions, canvas, impl_action_as, impl_actions, point, relative, size,
AnyElement, AnyView, AnyWeakView, AppContext, AsyncAppContext, AsyncWindowContext, Bounds, transparent_black, Action, AnyElement, AnyView, AnyWeakView, AppContext, AsyncAppContext,
DragMoveEvent, Entity as _, EntityId, EventEmitter, FocusHandle, FocusableView, Global, AsyncWindowContext, Bounds, CursorStyle, Decorations, DragMoveEvent, Entity as _, EntityId,
KeyContext, Keystroke, ManagedView, Model, ModelContext, PathPromptOptions, Point, PromptLevel, EventEmitter, FocusHandle, FocusableView, Global, Hsla, KeyContext, Keystroke, ManagedView,
Render, Size, Subscription, Task, View, WeakView, WindowBounds, WindowHandle, WindowOptions, Model, ModelContext, MouseButton, PathPromptOptions, Point, PromptLevel, Render, ResizeEdge,
Size, Stateful, Subscription, Task, Tiling, View, WeakView, WindowBounds, WindowHandle,
WindowOptions,
}; };
use item::{ use item::{
FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, PreviewTabsSettings, FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, PreviewTabsSettings,
@ -4165,156 +4167,162 @@ impl Render for Workspace {
let theme = cx.theme().clone(); let theme = cx.theme().clone();
let colors = theme.colors(); let colors = theme.colors();
self.actions(div(), cx) client_side_decorations(
.key_context(context) self.actions(div(), cx)
.relative() .key_context(context)
.size_full() .relative()
.flex() .size_full()
.flex_col() .flex()
.font(ui_font) .flex_col()
.gap_0() .font(ui_font)
.justify_start() .gap_0()
.items_start() .justify_start()
.text_color(colors.text) .items_start()
.bg(colors.background) .text_color(colors.text)
.children(self.titlebar_item.clone()) .overflow_hidden()
.child( .children(self.titlebar_item.clone())
div() .child(
.id("workspace") div()
.relative() .id("workspace")
.flex_1() .bg(colors.background)
.w_full() .relative()
.flex() .flex_1()
.flex_col() .w_full()
.overflow_hidden() .flex()
.border_t_1() .flex_col()
.border_b_1() .overflow_hidden()
.border_color(colors.border) .border_t_1()
.child({ .border_b_1()
let this = cx.view().clone(); .border_color(colors.border)
canvas( .child({
move |bounds, cx| this.update(cx, |this, _cx| this.bounds = bounds), let this = cx.view().clone();
|_, _, _| {}, canvas(
) move |bounds, cx| this.update(cx, |this, _cx| this.bounds = bounds),
.absolute() |_, _, _| {},
.size_full() )
}) .absolute()
.when(self.zoomed.is_none(), |this| { .size_full()
this.on_drag_move(cx.listener( })
|workspace, e: &DragMoveEvent<DraggedDock>, cx| match e.drag(cx).0 { .when(self.zoomed.is_none(), |this| {
DockPosition::Left => { this.on_drag_move(cx.listener(
let size = workspace.bounds.left() + e.event.position.x; |workspace, e: &DragMoveEvent<DraggedDock>, cx| match e.drag(cx).0 {
workspace.left_dock.update(cx, |left_dock, cx| { DockPosition::Left => {
left_dock.resize_active_panel(Some(size), cx); let size = e.event.position.x - workspace.bounds.left();
}); workspace.left_dock.update(cx, |left_dock, cx| {
} left_dock.resize_active_panel(Some(size), cx);
DockPosition::Right => { });
let size = workspace.bounds.right() - e.event.position.x; }
workspace.right_dock.update(cx, |right_dock, cx| { DockPosition::Right => {
right_dock.resize_active_panel(Some(size), cx); let size = workspace.bounds.right() - e.event.position.x;
}); workspace.right_dock.update(cx, |right_dock, cx| {
} right_dock.resize_active_panel(Some(size), cx);
DockPosition::Bottom => { });
let size = workspace.bounds.bottom() - e.event.position.y; }
workspace.bottom_dock.update(cx, |bottom_dock, cx| { DockPosition::Bottom => {
bottom_dock.resize_active_panel(Some(size), cx); let size = workspace.bounds.bottom() - e.event.position.y;
}); workspace.bottom_dock.update(cx, |bottom_dock, cx| {
} bottom_dock.resize_active_panel(Some(size), cx);
}, });
)) }
})
.child(
div()
.flex()
.flex_row()
.h_full()
// Left Dock
.children(self.zoomed_position.ne(&Some(DockPosition::Left)).then(
|| {
div()
.flex()
.flex_none()
.overflow_hidden()
.child(self.left_dock.clone())
}, },
)) ))
// Panes })
.child( .child(
div() div()
.flex() .flex()
.flex_col() .flex_row()
.flex_1() .h_full()
.overflow_hidden() // Left Dock
.child( .children(self.zoomed_position.ne(&Some(DockPosition::Left)).then(
h_flex() || {
.flex_1() div()
.when_some(paddings.0, |this, p| { .flex()
this.child(p.border_r_1()) .flex_none()
}) .overflow_hidden()
.child(self.center.render( .child(self.left_dock.clone())
&self.project, },
&self.follower_states, ))
self.active_call(), // Panes
&self.active_pane, .child(
self.zoomed.as_ref(),
&self.app_state,
cx,
))
.when_some(paddings.1, |this, p| {
this.child(p.border_l_1())
}),
)
.children(
self.zoomed_position
.ne(&Some(DockPosition::Bottom))
.then(|| self.bottom_dock.clone()),
),
)
// Right Dock
.children(self.zoomed_position.ne(&Some(DockPosition::Right)).then(
|| {
div() div()
.flex() .flex()
.flex_none() .flex_col()
.flex_1()
.overflow_hidden() .overflow_hidden()
.child(self.right_dock.clone()) .child(
}, h_flex()
)), .flex_1()
) .when_some(paddings.0, |this, p| {
.children(self.zoomed.as_ref().and_then(|view| { this.child(p.border_r_1())
let zoomed_view = view.upgrade()?; })
let div = div() .child(self.center.render(
.occlude() &self.project,
.absolute() &self.follower_states,
.overflow_hidden() self.active_call(),
.border_color(colors.border) &self.active_pane,
.bg(colors.background) self.zoomed.as_ref(),
.child(zoomed_view) &self.app_state,
.inset_0() cx,
.shadow_lg(); ))
.when_some(paddings.1, |this, p| {
this.child(p.border_l_1())
}),
)
.children(
self.zoomed_position
.ne(&Some(DockPosition::Bottom))
.then(|| self.bottom_dock.clone()),
),
)
// Right Dock
.children(
self.zoomed_position.ne(&Some(DockPosition::Right)).then(
|| {
div()
.flex()
.flex_none()
.overflow_hidden()
.child(self.right_dock.clone())
},
),
),
)
.children(self.zoomed.as_ref().and_then(|view| {
let zoomed_view = view.upgrade()?;
let div = div()
.occlude()
.absolute()
.overflow_hidden()
.border_color(colors.border)
.bg(colors.background)
.child(zoomed_view)
.inset_0()
.shadow_lg();
Some(match self.zoomed_position { Some(match self.zoomed_position {
Some(DockPosition::Left) => div.right_2().border_r_1(), Some(DockPosition::Left) => div.right_2().border_r_1(),
Some(DockPosition::Right) => div.left_2().border_l_1(), Some(DockPosition::Right) => div.left_2().border_l_1(),
Some(DockPosition::Bottom) => div.top_2().border_t_1(), Some(DockPosition::Bottom) => div.top_2().border_t_1(),
None => div.top_2().bottom_2().left_2().right_2().border_1(), None => div.top_2().bottom_2().left_2().right_2().border_1(),
}) })
})) }))
.child(self.modal_layer.clone()) .child(self.modal_layer.clone())
.children(self.render_notifications(cx)), .children(self.render_notifications(cx)),
) )
.child(self.status_bar.clone()) .child(self.status_bar.clone())
.children(if self.project.read(cx).is_disconnected() { .children(if self.project.read(cx).is_disconnected() {
if let Some(render) = self.render_disconnected_overlay.take() { if let Some(render) = self.render_disconnected_overlay.take() {
let result = render(self, cx); let result = render(self, cx);
self.render_disconnected_overlay = Some(render); self.render_disconnected_overlay = Some(render);
Some(result) Some(result)
} else {
None
}
} else { } else {
None None
} }),
} else { cx,
None )
})
} }
} }
@ -6474,3 +6482,267 @@ mod tests {
}); });
} }
} }
pub fn client_side_decorations(element: impl IntoElement, cx: &mut WindowContext) -> Stateful<Div> {
const BORDER_SIZE: Pixels = px(1.0);
let decorations = cx.window_decorations();
if matches!(decorations, Decorations::Client { .. }) {
cx.set_client_inset(theme::CLIENT_SIDE_DECORATION_SHADOW);
}
struct GlobalResizeEdge(ResizeEdge);
impl Global for GlobalResizeEdge {}
div()
.id("window-backdrop")
.bg(transparent_black())
.map(|div| match decorations {
Decorations::Server => div,
Decorations::Client { tiling, .. } => div
.child(
canvas(
|_bounds, cx| {
cx.insert_hitbox(
Bounds::new(
point(px(0.0), px(0.0)),
cx.window_bounds().get_bounds().size,
),
false,
)
},
move |_bounds, hitbox, cx| {
let mouse = cx.mouse_position();
let size = cx.window_bounds().get_bounds().size;
let Some(edge) = resize_edge(
mouse,
theme::CLIENT_SIDE_DECORATION_SHADOW,
size,
tiling,
) else {
return;
};
cx.set_global(GlobalResizeEdge(edge));
cx.set_cursor_style(
match edge {
ResizeEdge::Top | ResizeEdge::Bottom => {
CursorStyle::ResizeUpDown
}
ResizeEdge::Left | ResizeEdge::Right => {
CursorStyle::ResizeLeftRight
}
ResizeEdge::TopLeft | ResizeEdge::BottomRight => {
CursorStyle::ResizeUpLeftDownRight
}
ResizeEdge::TopRight | ResizeEdge::BottomLeft => {
CursorStyle::ResizeUpRightDownLeft
}
},
&hitbox,
);
},
)
.size_full()
.absolute(),
)
.when(!(tiling.top || tiling.right), |div| {
div.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING)
})
.when(!(tiling.top || tiling.left), |div| {
div.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
})
.when(!(tiling.bottom || tiling.right), |div| {
div.rounded_br(theme::CLIENT_SIDE_DECORATION_ROUNDING)
})
.when(!(tiling.bottom || tiling.left), |div| {
div.rounded_bl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
})
.when(!tiling.top, |div| {
div.pt(theme::CLIENT_SIDE_DECORATION_SHADOW)
})
.when(!tiling.bottom, |div| {
div.pb(theme::CLIENT_SIDE_DECORATION_SHADOW)
})
.when(!tiling.left, |div| {
div.pl(theme::CLIENT_SIDE_DECORATION_SHADOW)
})
.when(!tiling.right, |div| {
div.pr(theme::CLIENT_SIDE_DECORATION_SHADOW)
})
.on_mouse_move(move |e, cx| {
let size = cx.window_bounds().get_bounds().size;
let pos = e.position;
let new_edge =
resize_edge(pos, theme::CLIENT_SIDE_DECORATION_SHADOW, size, tiling);
let edge = cx.try_global::<GlobalResizeEdge>();
if new_edge != edge.map(|edge| edge.0) {
cx.window_handle()
.update(cx, |workspace, cx| cx.notify(workspace.entity_id()))
.ok();
}
})
.on_mouse_down(MouseButton::Left, move |e, cx| {
let size = cx.window_bounds().get_bounds().size;
let pos = e.position;
let edge = match resize_edge(
pos,
theme::CLIENT_SIDE_DECORATION_SHADOW,
size,
tiling,
) {
Some(value) => value,
None => return,
};
cx.start_window_resize(edge);
}),
})
.size_full()
.child(
div()
.cursor(CursorStyle::Arrow)
.map(|div| match decorations {
Decorations::Server => div,
Decorations::Client { tiling } => div
.border_color(cx.theme().colors().border)
.when(!(tiling.top || tiling.right), |div| {
div.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING)
})
.when(!(tiling.top || tiling.left), |div| {
div.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
})
.when(!(tiling.bottom || tiling.right), |div| {
div.rounded_br(theme::CLIENT_SIDE_DECORATION_ROUNDING)
})
.when(!(tiling.bottom || tiling.left), |div| {
div.rounded_bl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
})
.when(!tiling.top, |div| div.border_t(BORDER_SIZE))
.when(!tiling.bottom, |div| div.border_b(BORDER_SIZE))
.when(!tiling.left, |div| div.border_l(BORDER_SIZE))
.when(!tiling.right, |div| div.border_r(BORDER_SIZE))
.when(!tiling.is_tiled(), |div| {
div.shadow(smallvec::smallvec![gpui::BoxShadow {
color: Hsla {
h: 0.,
s: 0.,
l: 0.,
a: 0.4,
},
blur_radius: theme::CLIENT_SIDE_DECORATION_SHADOW / 2.,
spread_radius: px(0.),
offset: point(px(0.0), px(0.0)),
}])
}),
})
.on_mouse_move(|_e, cx| {
cx.stop_propagation();
})
.bg(cx.theme().colors().border)
.size_full()
.child(element),
)
.map(|div| match decorations {
Decorations::Server => div,
Decorations::Client { tiling, .. } => div.child(
canvas(
|_bounds, cx| {
cx.insert_hitbox(
Bounds::new(
point(px(0.0), px(0.0)),
cx.window_bounds().get_bounds().size,
),
false,
)
},
move |_bounds, hitbox, cx| {
let mouse = cx.mouse_position();
let size = cx.window_bounds().get_bounds().size;
let Some(edge) =
resize_edge(mouse, theme::CLIENT_SIDE_DECORATION_SHADOW, size, tiling)
else {
return;
};
cx.set_global(GlobalResizeEdge(edge));
cx.set_cursor_style(
match edge {
ResizeEdge::Top | ResizeEdge::Bottom => CursorStyle::ResizeUpDown,
ResizeEdge::Left | ResizeEdge::Right => {
CursorStyle::ResizeLeftRight
}
ResizeEdge::TopLeft | ResizeEdge::BottomRight => {
CursorStyle::ResizeUpLeftDownRight
}
ResizeEdge::TopRight | ResizeEdge::BottomLeft => {
CursorStyle::ResizeUpRightDownLeft
}
},
&hitbox,
);
},
)
.size_full()
.absolute(),
),
})
}
fn resize_edge(
pos: Point<Pixels>,
shadow_size: Pixels,
window_size: Size<Pixels>,
tiling: Tiling,
) -> Option<ResizeEdge> {
let bounds = Bounds::new(Point::default(), window_size).inset(shadow_size * 1.5);
if bounds.contains(&pos) {
return None;
}
let corner_size = size(shadow_size * 1.5, shadow_size * 1.5);
let top_left_bounds = Bounds::new(Point::new(px(0.), px(0.)), corner_size);
if top_left_bounds.contains(&pos) {
return Some(ResizeEdge::TopLeft);
}
let top_right_bounds = Bounds::new(
Point::new(window_size.width - corner_size.width, px(0.)),
corner_size,
);
if top_right_bounds.contains(&pos) {
return Some(ResizeEdge::TopRight);
}
let bottom_left_bounds = Bounds::new(
Point::new(px(0.), window_size.height - corner_size.height),
corner_size,
);
if bottom_left_bounds.contains(&pos) {
return Some(ResizeEdge::BottomLeft);
}
let bottom_right_bounds = Bounds::new(
Point::new(
window_size.width - corner_size.width,
window_size.height - corner_size.height,
),
corner_size,
);
if bottom_right_bounds.contains(&pos) {
return Some(ResizeEdge::BottomRight);
}
if !tiling.top && pos.y < shadow_size {
Some(ResizeEdge::Top)
} else if !tiling.bottom && pos.y > window_size.height - shadow_size {
Some(ResizeEdge::Bottom)
} else if !tiling.left && pos.x < shadow_size {
Some(ResizeEdge::Left)
} else if !tiling.right && pos.x > window_size.width - shadow_size {
Some(ResizeEdge::Right)
} else {
None
}
}

View file

@ -105,6 +105,7 @@ pub fn build_window_options(display_uuid: Option<Uuid>, cx: &mut AppContext) ->
display_id: display.map(|display| display.id()), display_id: display.map(|display| display.id()),
window_background: cx.theme().window_background_appearance(), window_background: cx.theme().window_background_appearance(),
app_id: Some(app_id.to_owned()), app_id: Some(app_id.to_owned()),
window_decorations: Some(gpui::WindowDecorations::Client),
window_min_size: Some(gpui::Size { window_min_size: Some(gpui::Size {
width: px(360.0), width: px(360.0),
height: px(240.0), height: px(240.0),

View file

@ -1,7 +1,7 @@
use gpui::{ use gpui::{
div, opaque_grey, AppContext, EventEmitter, FocusHandle, FocusableView, FontWeight, div, AppContext, EventEmitter, FocusHandle, FocusableView, FontWeight, InteractiveElement,
InteractiveElement, IntoElement, ParentElement, PromptHandle, PromptLevel, PromptResponse, IntoElement, ParentElement, PromptHandle, PromptLevel, PromptResponse, Render,
Render, RenderablePromptHandle, Styled, ViewContext, VisualContext, WindowContext, RenderablePromptHandle, Styled, ViewContext, VisualContext, WindowContext,
}; };
use settings::Settings; use settings::Settings;
use theme::ThemeSettings; use theme::ThemeSettings;
@ -101,35 +101,24 @@ impl Render for FallbackPromptRenderer {
}), }),
)); ));
div() div().size_full().occlude().child(
.size_full() div()
.occlude() .size_full()
.child( .absolute()
div() .top_0()
.size_full() .left_0()
.bg(opaque_grey(0.5, 0.6)) .flex()
.absolute() .flex_col()
.top_0() .justify_around()
.left_0(), .child(
) div()
.child( .w_full()
div() .flex()
.size_full() .flex_row()
.absolute() .justify_around()
.top_0() .child(prompt),
.left_0() ),
.flex() )
.flex_col()
.justify_around()
.child(
div()
.w_full()
.flex()
.flex_row()
.justify_around()
.child(prompt),
),
)
} }
} }