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;
use gpui::{
point, AppContext, Pixels, PlatformDisplay, Size, WindowBackgroundAppearance, WindowBounds,
WindowKind, WindowOptions,
WindowDecorations, WindowKind, WindowOptions,
};
use panel_settings::MessageEditorSettings;
pub use panel_settings::{
@ -63,8 +63,9 @@ fn notification_window_options(
kind: WindowKind::PopUp,
is_movable: false,
display_id: Some(screen.id()),
window_background: WindowBackgroundAppearance::default(),
window_background: WindowBackgroundAppearance::Transparent,
app_id: Some(app_id.to_owned()),
window_min_size: None,
window_decorations: Some(WindowDecorations::Client),
}
}

View File

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

View File

@ -23,7 +23,7 @@ impl Render for HelloWorld {
fn main() {
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(
WindowOptions {
window_bounds: Some(WindowBounds::Windowed(bounds)),

View File

@ -52,6 +52,7 @@ fn main() {
is_movable: false,
app_id: 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]
pub fn opaque_grey(lightness: f32, opacity: f32) -> Hsla {
Hsla {

View File

@ -883,6 +883,14 @@ where
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.
///
/// 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 },
/// });
/// ```
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 {
origin: f(self.origin),
origin: self.origin.map(f),
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.

View File

@ -210,6 +210,83 @@ impl Debug 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 {
fn bounds(&self) -> Bounds<Pixels>;
fn is_maximized(&self) -> bool;
@ -232,10 +309,7 @@ pub(crate) trait PlatformWindow: HasWindowHandle + HasDisplayHandle {
fn activate(&self);
fn is_active(&self) -> bool;
fn set_title(&mut self, title: &str);
fn set_app_id(&mut self, app_id: &str);
fn set_background_appearance(&mut self, background_appearance: WindowBackgroundAppearance);
fn set_edited(&mut self, edited: bool);
fn show_character_palette(&self);
fn set_background_appearance(&self, background_appearance: WindowBackgroundAppearance);
fn minimize(&self);
fn zoom(&self);
fn toggle_fullscreen(&self);
@ -252,12 +326,31 @@ pub(crate) trait PlatformWindow: HasWindowHandle + HasDisplayHandle {
fn completed_frame(&self) {}
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")]
fn get_raw_handle(&self) -> windows::HWND;
fn show_window_menu(&self, position: Point<Pixels>);
fn start_system_move(&self);
fn should_render_window_controls(&self) -> bool;
// Linux specific methods
fn request_decorations(&self, _decorations: WindowDecorations) {}
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"))]
fn as_test(&mut self) -> Option<&mut TestWindow> {
@ -570,6 +663,10 @@ pub struct WindowOptions {
/// Window minimum size
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
@ -596,8 +693,6 @@ pub(crate) struct WindowParams {
pub display_id: Option<DisplayId>,
pub window_background: WindowBackgroundAppearance,
#[cfg_attr(target_os = "linux", allow(dead_code))]
pub window_min_size: Option<Size<Pixels>>,
}
@ -649,6 +744,7 @@ impl Default for WindowOptions {
window_background: WindowBackgroundAppearance::default(),
app_id: None,
window_min_size: None,
window_decorations: None,
}
}
}
@ -659,7 +755,7 @@ pub struct TitlebarOptions {
/// The initial title of the window
pub title: Option<SharedString>,
/// Whether the titlebar should appear transparent
/// Whether the titlebar should appear transparent (macOS only)
pub appears_transparent: bool,
/// The position of the macOS traffic light buttons
@ -805,6 +901,14 @@ pub enum CursorStyle {
/// corresponds to the CSS cursor value `ns-resize`
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.
/// corresponds to the CSS curosr value `col-resize`
ResizeColumn,

View File

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

View File

@ -138,7 +138,7 @@ impl Globals {
primary_selection_manager: globals.bind(&qh, 1..=1, ()).ok(),
shm: globals.bind(&qh, 1..=1, ()).unwrap(),
seat,
wm_base: globals.bind(&qh, 1..=1, ()).unwrap(),
wm_base: globals.bind(&qh, 2..=5, ()).unwrap(),
viewporter: globals.bind(&qh, 1..=1, ()).ok(),
fractional_scale_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::scene::Scene;
use crate::{
px, size, AnyWindowHandle, Bounds, Globals, Modifiers, Output, Pixels, PlatformDisplay,
PlatformInput, Point, PromptLevel, Size, WaylandClientStatePtr, WindowAppearance,
WindowBackgroundAppearance, WindowBounds, WindowParams,
px, size, AnyWindowHandle, Bounds, Decorations, Globals, Modifiers, Output, Pixels,
PlatformDisplay, PlatformInput, Point, PromptLevel, ResizeEdge, Size, Tiling,
WaylandClientStatePtr, WindowAppearance, WindowBackgroundAppearance, WindowBounds,
WindowControls, WindowDecorations, WindowParams,
};
#[derive(Default)]
@ -62,10 +63,12 @@ impl rwh::HasDisplayHandle for RawWindow {
}
}
#[derive(Debug)]
struct InProgressConfigure {
size: Option<Size<Pixels>>,
fullscreen: bool,
maximized: bool,
tiling: Tiling,
}
pub struct WaylandWindowState {
@ -84,14 +87,20 @@ pub struct WaylandWindowState {
bounds: Bounds<Pixels>,
scale: f32,
input_handler: Option<PlatformInputHandler>,
decoration_state: WaylandDecorationState,
decorations: WindowDecorations,
background_appearance: WindowBackgroundAppearance,
fullscreen: bool,
maximized: bool,
windowed_bounds: Bounds<Pixels>,
tiling: Tiling,
window_bounds: Bounds<Pixels>,
client: WaylandClientStatePtr,
handle: AnyWindowHandle,
active: bool,
in_progress_configure: Option<InProgressConfigure>,
in_progress_window_controls: Option<WindowControls>,
window_controls: WindowControls,
inset: Option<Pixels>,
requested_inset: Option<Pixels>,
}
#[derive(Clone)]
@ -142,7 +151,7 @@ impl WaylandWindowState {
height: options.bounds.size.height.0 as u32,
depth: 1,
},
transparent: options.window_background != WindowBackgroundAppearance::Opaque,
transparent: true,
};
Ok(Self {
@ -160,17 +169,34 @@ impl WaylandWindowState {
bounds: options.bounds,
scale: 1.0,
input_handler: None,
decoration_state: WaylandDecorationState::Client,
decorations: WindowDecorations::Client,
background_appearance: WindowBackgroundAppearance::Opaque,
fullscreen: false,
maximized: false,
windowed_bounds: options.bounds,
tiling: Tiling::default(),
window_bounds: options.bounds,
in_progress_configure: None,
client,
appearance,
handle,
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);
@ -235,7 +261,7 @@ impl WaylandWindow {
.wm_base
.get_xdg_surface(&surface, &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() {
fractional_scale_manager.get_fractional_scale(&surface, &globals.qh, surface.id());
@ -246,13 +272,7 @@ impl WaylandWindow {
.decoration_manager
.as_ref()
.map(|decoration_manager| {
let decoration = decoration_manager.get_toplevel_decoration(
&toplevel,
&globals.qh,
surface.id(),
);
decoration.set_mode(zxdg_toplevel_decoration_v1::Mode::ClientSide);
decoration
decoration_manager.get_toplevel_decoration(&toplevel, &globals.qh, surface.id())
});
let viewport = globals
@ -298,7 +318,7 @@ impl WaylandWindowStatePtr {
pub fn frame(&self, request_frame_callback: bool) {
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());
drop(state);
}
@ -311,6 +331,18 @@ impl WaylandWindowStatePtr {
pub fn handle_xdg_surface_event(&self, event: xdg_surface::Event) {
match event {
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();
@ -318,18 +350,21 @@ impl WaylandWindowStatePtr {
let got_unmaximized = state.maximized && !configure.maximized;
state.fullscreen = configure.fullscreen;
state.maximized = configure.maximized;
state.tiling = configure.tiling;
if got_unmaximized {
configure.size = Some(state.windowed_bounds.size);
} else if !configure.fullscreen && !configure.maximized {
configure.size = Some(state.window_bounds.size);
} 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 {
state.windowed_bounds = Bounds {
state.window_bounds = Bounds {
origin: Point::default(),
size,
};
}
}
drop(state);
if let Some(size) = configure.size {
self.resize(size);
@ -340,8 +375,11 @@ impl WaylandWindowStatePtr {
state.xdg_surface.ack_configure(serial);
let request_frame_callback = !state.acknowledged_first_configure;
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 {
zxdg_toplevel_decoration_v1::Event::Configure { mode } => match mode {
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) => {
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(_) => {
log::warn!("Unknown decoration mode");
@ -389,14 +438,44 @@ impl WaylandWindowStatePtr {
Some(size(px(width as f32), px(height as f32)))
};
let fullscreen = states.contains(&(xdg_toplevel::State::Fullscreen as u8));
let maximized = states.contains(&(xdg_toplevel::State::Maximized as u8));
let states = extract_states::<xdg_toplevel::State>(&states);
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();
state.in_progress_configure = Some(InProgressConfigure {
size,
fullscreen,
maximized,
tiling,
});
false
@ -415,6 +494,33 @@ impl WaylandWindowStatePtr {
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,
}
}
@ -545,18 +651,6 @@ impl WaylandWindowStatePtr {
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) {
let mut callbacks = self.callbacks.borrow_mut();
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 {
let mut scale = 1;
let mut current_output = state.display.take();
@ -639,9 +744,9 @@ impl PlatformWindow for WaylandWindow {
fn window_bounds(&self) -> WindowBounds {
let state = self.borrow();
if state.fullscreen {
WindowBounds::Fullscreen(state.windowed_bounds)
WindowBounds::Fullscreen(state.window_bounds)
} else if state.maximized {
WindowBounds::Maximized(state.windowed_bounds)
WindowBounds::Maximized(state.window_bounds)
} else {
drop(state);
WindowBounds::Windowed(self.bounds())
@ -718,52 +823,10 @@ impl PlatformWindow for WaylandWindow {
self.borrow().toplevel.set_app_id(app_id.to_owned());
}
fn set_background_appearance(&mut self, background_appearance: WindowBackgroundAppearance) {
let opaque = background_appearance == WindowBackgroundAppearance::Opaque;
fn set_background_appearance(&self, background_appearance: WindowBackgroundAppearance) {
let mut state = self.borrow_mut();
state.renderer.update_transparency(!opaque);
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");
state.background_appearance = background_appearance;
update_window(state);
}
fn minimize(&self) {
@ -831,6 +894,25 @@ impl PlatformWindow for WaylandWindow {
fn completed_frame(&self) {
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();
}
@ -850,22 +932,173 @@ impl PlatformWindow for WaylandWindow {
);
}
fn start_system_move(&self) {
fn start_window_move(&self) {
let state = self.borrow();
let serial = state.client.get_serial(SerialKind::MousePress);
state.toplevel._move(&state.globals.seat, serial);
}
fn should_render_window_controls(&self) -> bool {
self.borrow().decoration_state == WaylandDecorationState::Client
fn start_window_resize(&self, edge: crate::ResizeEdge) {
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)]
pub enum WaylandDecorationState {
/// Decorations are to be provided by the client
Client,
fn update_window(mut state: RefMut<WaylandWindowState>) {
let opaque = !state.is_transparent();
/// Decorations are provided by the server
Server,
state.renderer.update_transparency(!opaque);
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 {
Event::ClientMessage(event) => {
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();
if atom == state.atoms.WM_DELETE_WINDOW {
@ -521,6 +521,12 @@ impl X11Client {
// Rest of the close logic is handled in drop_window()
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) => {
@ -537,6 +543,10 @@ impl X11Client {
let window = self.get_window(event.window)?;
window.configure(bounds);
}
Event::PropertyNotify(event) => {
let window = self.get_window(event.window)?;
window.property_notify(event);
}
Event::Expose(event) => {
let window = self.get_window(event.window)?;
window.refresh();

View File

@ -2,10 +2,11 @@ use anyhow::Context;
use crate::{
platform::blade::{BladeRenderer, BladeSurfaceConfig},
px, size, AnyWindowHandle, Bounds, DevicePixels, ForegroundExecutor, Modifiers, Pixels,
PlatformAtlas, PlatformDisplay, PlatformInput, PlatformInputHandler, PlatformWindow, Point,
PromptLevel, Scene, Size, WindowAppearance, WindowBackgroundAppearance, WindowBounds,
WindowKind, WindowParams, X11ClientStatePtr,
px, size, AnyWindowHandle, Bounds, Decorations, DevicePixels, ForegroundExecutor, Modifiers,
Pixels, PlatformAtlas, PlatformDisplay, PlatformInput, PlatformInputHandler, PlatformWindow,
Point, PromptLevel, ResizeEdge, Scene, Size, Tiling, WindowAppearance,
WindowBackgroundAppearance, WindowBounds, WindowDecorations, WindowKind, WindowParams,
X11ClientStatePtr,
};
use blade_graphics as gpu;
@ -15,24 +16,17 @@ use x11rb::{
connection::Connection,
protocol::{
randr::{self, ConnectionExt as _},
sync,
xinput::{self, ConnectionExt as _},
xproto::{
self, ClientMessageEvent, ConnectionExt as _, EventMask, TranslateCoordinatesReply,
},
xproto::{self, ClientMessageEvent, ConnectionExt, EventMask, TranslateCoordinatesReply},
},
wrapper::ConnectionExt as _,
xcb_ffi::XCBConnection,
};
use std::{
cell::RefCell,
ffi::c_void,
num::NonZeroU32,
ops::Div,
ptr::NonNull,
rc::Rc,
sync::{self, Arc},
time::Duration,
cell::RefCell, ffi::c_void, mem::size_of, num::NonZeroU32, ops::Div, ptr::NonNull, rc::Rc,
sync::Arc, time::Duration,
};
use super::{X11Display, XINPUT_MASTER_DEVICE};
@ -50,10 +44,16 @@ x11rb::atom_manager! {
_NET_WM_STATE_HIDDEN,
_NET_WM_STATE_FOCUSED,
_NET_ACTIVE_WINDOW,
_NET_WM_SYNC_REQUEST,
_NET_WM_SYNC_REQUEST_COUNTER,
_NET_WM_BYPASS_COMPOSITOR,
_NET_WM_MOVERESIZE,
_NET_WM_WINDOW_TYPE,
_NET_WM_WINDOW_TYPE_NOTIFICATION,
_NET_WM_SYNC,
_MOTIF_WM_HINTS,
_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)]
struct Visual {
id: xproto::Visualid,
@ -166,6 +181,8 @@ pub struct X11WindowState {
executor: ForegroundExecutor,
atoms: XcbAtoms,
x_root_window: xproto::Window,
pub(crate) counter_id: sync::Counter,
pub(crate) last_sync_counter: Option<sync::Int64>,
_raw: RawWindow,
bounds: Bounds<Pixels>,
scale_factor: f32,
@ -173,7 +190,22 @@ pub struct X11WindowState {
display: Rc<dyn PlatformDisplay>,
input_handler: Option<PlatformInputHandler>,
appearance: WindowAppearance,
background_appearance: WindowBackgroundAppearance,
maximized_vertical: bool,
maximized_horizontal: bool,
hidden: bool,
active: bool,
fullscreen: bool,
decorations: WindowDecorations,
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)]
@ -230,19 +262,11 @@ impl X11WindowState {
.map_or(x_main_screen_index, |did| did.0 as usize);
let visual_set = find_visuals(&xcb_connection, x_screen_index);
let visual_maybe = match params.window_background {
WindowBackgroundAppearance::Opaque => visual_set.opaque,
WindowBackgroundAppearance::Transparent | WindowBackgroundAppearance::Blurred => {
visual_set.transparent
}
};
let visual = match visual_maybe {
let visual = match visual_set.transparent {
Some(visual) => visual,
None => {
log::warn!(
"Unable to find a matching visual for {:?}",
params.window_background
);
log::warn!("Unable to find a transparent visual",);
visual_set.inherit
}
};
@ -269,7 +293,8 @@ impl X11WindowState {
| xproto::EventMask::STRUCTURE_NOTIFY
| xproto::EventMask::FOCUS_CHANGE
| 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);
@ -349,7 +374,26 @@ impl X11WindowState {
x_window,
atoms.WM_PROTOCOLS,
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();
@ -396,7 +440,8 @@ impl X11WindowState {
// Note: this has to be done after the GPU init, or otherwise
// the sizes are immediately invalidated.
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();
@ -438,9 +483,19 @@ impl X11WindowState {
renderer: BladeRenderer::new(gpu, config),
atoms: *atoms,
input_handler: None,
active: false,
fullscreen: false,
maximized_vertical: false,
maximized_horizontal: false,
hidden: false,
appearance,
handle,
background_appearance: WindowBackgroundAppearance::Opaque,
destroyed: false,
decorations: WindowDecorations::Server,
last_insets: [0, 0, 0, 0],
counter_id: sync_request_counter,
last_sync_counter: None,
refresh_rate,
})
}
@ -511,7 +566,7 @@ impl X11Window {
scale_factor: f32,
appearance: WindowAppearance,
) -> anyhow::Result<Self> {
Ok(Self(X11WindowStatePtr {
let ptr = X11WindowStatePtr {
state: Rc::new(RefCell::new(X11WindowState::new(
handle,
client,
@ -527,7 +582,12 @@ impl X11Window {
callbacks: Rc::new(RefCell::new(Callbacks::default())),
xcb_connection: xcb_connection.clone(),
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) {
@ -549,29 +609,6 @@ impl X11Window {
.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 {
let state = self.0.state.borrow();
self.0
@ -586,6 +623,48 @@ impl X11Window {
.reply()
.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 {
@ -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) {
let mut callbacks = self.callbacks.borrow_mut();
if let Some(fun) = callbacks.close.take() {
@ -715,6 +842,9 @@ impl X11WindowStatePtr {
));
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();
@ -737,8 +867,12 @@ impl X11WindowStatePtr {
}
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();
if let Some(ref mut fun) = callbacks.appearance_changed {
(fun)()
@ -757,11 +891,9 @@ impl PlatformWindow for X11Window {
fn is_maximized(&self) -> bool {
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.
!wm_hints.contains(&state.atoms._NET_WM_STATE_HIDDEN)
&& wm_hints.contains(&state.atoms._NET_WM_STATE_MAXIMIZED_VERT)
&& wm_hints.contains(&state.atoms._NET_WM_STATE_MAXIMIZED_HORZ)
!state.hidden && state.maximized_vertical && state.maximized_horizontal
}
fn window_bounds(&self) -> WindowBounds {
@ -862,9 +994,7 @@ impl PlatformWindow for X11Window {
}
fn is_active(&self) -> bool {
let state = self.0.state.borrow();
self.get_wm_hints()
.contains(&state.atoms._NET_WM_STATE_FOCUSED)
self.0.state.borrow().active
}
fn set_title(&mut self, title: &str) {
@ -913,10 +1043,11 @@ impl PlatformWindow for X11Window {
log::info!("ignoring macOS specific set_edited");
}
fn set_background_appearance(&mut self, background_appearance: WindowBackgroundAppearance) {
let mut inner = self.0.state.borrow_mut();
let transparent = background_appearance != WindowBackgroundAppearance::Opaque;
inner.renderer.update_transparency(transparent);
fn set_background_appearance(&self, background_appearance: WindowBackgroundAppearance) {
let mut state = self.0.state.borrow_mut();
state.background_appearance = background_appearance;
let transparent = state.is_transparent();
state.renderer.update_transparency(transparent);
}
fn show_character_palette(&self) {
@ -962,9 +1093,7 @@ impl PlatformWindow for X11Window {
}
fn is_fullscreen(&self) -> bool {
let state = self.0.state.borrow();
self.get_wm_hints()
.contains(&state.atoms._NET_WM_STATE_FULLSCREEN)
self.0.state.borrow().fullscreen
}
fn on_request_frame(&self, callback: Box<dyn FnMut()>) {
@ -1004,7 +1133,7 @@ impl PlatformWindow for X11Window {
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();
inner.renderer.sprite_atlas().clone()
}
@ -1035,41 +1164,109 @@ impl PlatformWindow for X11Window {
.unwrap();
}
fn start_system_move(&self) {
let state = self.0.state.borrow();
let pointer = self
.0
.xcb_connection
.query_pointer(self.0.x_window)
.unwrap()
.reply()
.unwrap();
fn start_window_move(&self) {
const MOVERESIZE_MOVE: u32 = 8;
let message = ClientMessageEvent::new(
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();
self.send_moveresize(MOVERESIZE_MOVE);
}
fn should_render_window_controls(&self) -> bool {
false
fn start_window_resize(&self, edge: ResizeEdge) {
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::OpenHand => msg_send![class!(NSCursor), openHandCursor],
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::ResizeRight => msg_send![class!(NSCursor), resizeRightCursor],
CursorStyle::ResizeLeftRight => 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::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 => {
msg_send![class!(NSCursor), IBeamCursorForVerticalLayout]
}

View File

@ -497,7 +497,6 @@ impl MacWindow {
pub fn open(
handle: AnyWindowHandle,
WindowParams {
window_background,
bounds,
titlebar,
kind,
@ -603,7 +602,7 @@ impl MacWindow {
native_window as *mut _,
native_view as *mut _,
bounds.size.map(|pixels| pixels.0),
window_background != WindowBackgroundAppearance::Opaque,
false,
),
request_frame_callback: None,
event_callback: None,
@ -676,8 +675,6 @@ impl MacWindow {
native_window.setContentView_(native_view.autorelease());
native_window.makeFirstResponder_(native_view);
window.set_background_appearance(window_background);
match kind {
WindowKind::Normal => {
native_window.setLevel_(NSNormalWindowLevel);
@ -956,7 +953,7 @@ impl PlatformWindow for MacWindow {
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();
this.renderer
.update_transparency(background_appearance != WindowBackgroundAppearance::Opaque);
@ -1092,14 +1089,6 @@ impl PlatformWindow for MacWindow {
fn sprite_atlas(&self) -> Arc<dyn PlatformAtlas> {
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 {

View File

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

View File

@ -274,7 +274,7 @@ impl WindowsWindow {
handle,
hide_title_bar,
display,
transparent: params.window_background != WindowBackgroundAppearance::Opaque,
transparent: true,
executor,
current_cursor,
};
@ -511,9 +511,7 @@ impl PlatformWindow for WindowsWindow {
.ok();
}
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) {
self.0
.state
.borrow_mut()
@ -521,12 +519,6 @@ impl PlatformWindow for WindowsWindow {
.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) {
unsafe { ShowWindowAsync(self.0.hwnd, SW_MINIMIZE).ok().log_err() };
}
@ -645,14 +637,6 @@ impl PlatformWindow for WindowsWindow {
fn get_raw_handle(&self) -> 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)]

View File

@ -1,19 +1,20 @@
use crate::{
hash, point, prelude::*, px, size, transparent_black, Action, AnyDrag, AnyElement, AnyTooltip,
AnyView, AppContext, Arena, Asset, AsyncWindowContext, AvailableSpace, Bounds, BoxShadow,
Context, Corners, CursorStyle, DevicePixels, DispatchActionListener, DispatchNodeId,
DispatchTree, DisplayId, Edges, Effect, Entity, EntityId, EventEmitter, FileDropEvent, Flatten,
FontId, Global, GlobalElementId, GlyphId, Hsla, ImageData, InputHandler, IsZero, KeyBinding,
KeyContext, KeyDownEvent, KeyEvent, KeyMatch, KeymatchResult, Keystroke, KeystrokeEvent,
LayoutId, LineLayoutIndex, Model, ModelContext, Modifiers, ModifiersChangedEvent,
MonochromeSprite, MouseButton, MouseEvent, MouseMoveEvent, MouseUpEvent, Path, Pixels,
PlatformAtlas, PlatformDisplay, PlatformInput, PlatformInputHandler, PlatformWindow, Point,
PolychromeSprite, PromptLevel, Quad, Render, RenderGlyphParams, RenderImageParams,
RenderSvgParams, ScaledPixels, Scene, Shadow, SharedString, Size, StrikethroughStyle, Style,
SubscriberSet, Subscription, TaffyLayoutEngine, Task, TextStyle, TextStyleRefinement,
TransformationMatrix, Underline, UnderlineStyle, View, VisualContext, WeakView,
WindowAppearance, WindowBackgroundAppearance, WindowBounds, WindowOptions, WindowParams,
WindowTextSystem, SUBPIXEL_VARIANTS,
Context, Corners, CursorStyle, Decorations, DevicePixels, DispatchActionListener,
DispatchNodeId, DispatchTree, DisplayId, Edges, Effect, Entity, EntityId, EventEmitter,
FileDropEvent, Flatten, FontId, Global, GlobalElementId, GlyphId, Hsla, ImageData,
InputHandler, IsZero, KeyBinding, KeyContext, KeyDownEvent, KeyEvent, KeyMatch, KeymatchResult,
Keystroke, KeystrokeEvent, LayoutId, LineLayoutIndex, Model, ModelContext, Modifiers,
ModifiersChangedEvent, MonochromeSprite, MouseButton, MouseEvent, MouseMoveEvent, MouseUpEvent,
Path, Pixels, PlatformAtlas, PlatformDisplay, PlatformInput, PlatformInputHandler,
PlatformWindow, Point, PolychromeSprite, PromptLevel, Quad, Render, RenderGlyphParams,
RenderImageParams, RenderSvgParams, ResizeEdge, ScaledPixels, Scene, Shadow, SharedString,
Size, StrikethroughStyle, Style, SubscriberSet, Subscription, TaffyLayoutEngine, Task,
TextStyle, TextStyleRefinement, TransformationMatrix, Underline, UnderlineStyle, View,
VisualContext, WeakView, WindowAppearance, WindowBackgroundAppearance, WindowBounds,
WindowControls, WindowDecorations, WindowOptions, WindowParams, WindowTextSystem,
SUBPIXEL_VARIANTS,
};
use anyhow::{anyhow, Context as _, Result};
use collections::{FxHashMap, FxHashSet};
@ -610,7 +611,10 @@ fn default_bounds(display_id: Option<DisplayId>, cx: &mut AppContext) -> Bounds<
cx.active_window()
.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(|| {
let display = display_id
.map(|id| cx.find_display(id))
@ -639,6 +643,7 @@ impl Window {
window_background,
app_id,
window_min_size,
window_decorations,
} = options;
let bounds = window_bounds
@ -654,7 +659,6 @@ impl Window {
focus,
show,
display_id,
window_background,
window_min_size,
},
)?;
@ -672,6 +676,10 @@ impl Window {
let next_frame_callbacks: Rc<RefCell<Vec<FrameCallback>>> = Default::default();
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 {
match window_open_state {
WindowBounds::Fullscreen(_) => platform_window.toggle_fullscreen(),
@ -990,6 +998,16 @@ impl<'a> WindowContext<'a> {
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
/// after it has been closed
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)
///
/// Events may not be received during a move operation.
pub fn start_system_move(&self) {
self.window.platform_window.start_system_move()
pub fn start_window_move(&self) {
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)
pub fn should_render_window_controls(&self) -> bool {
self.window.platform_window.should_render_window_controls()
pub fn window_decorations(&self) -> Decorations {
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.
@ -1237,7 +1265,7 @@ impl<'a> WindowContext<'a> {
}
/// 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
.platform_window
.set_background_appearance(background_appearance);

View File

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

View File

@ -1,4 +1,3 @@
pub mod platform_generic;
pub mod platform_linux;
pub mod platform_mac;
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 super::platform_generic::GenericWindowControls;
use crate::window_controls::{WindowControl, WindowControlType};
#[derive(IntoElement)]
pub struct LinuxWindowControls {
@ -18,7 +18,30 @@ impl LinuxWindowControls {
}
impl RenderOnce for LinuxWindowControls {
fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
GenericWindowControls::new(self.close_window_action.boxed_clone()).into_any_element()
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
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 collab::render_color_ribbon;
use gpui::{
actions, div, px, Action, AnyElement, AppContext, Element, InteractiveElement, Interactivity,
IntoElement, Model, ParentElement, Render, Stateful, StatefulInteractiveElement, Styled,
Subscription, ViewContext, VisualContext, WeakView,
actions, div, px, Action, AnyElement, AppContext, Decorations, Element, InteractiveElement,
Interactivity, IntoElement, Model, MouseButton, ParentElement, Render, Stateful,
StatefulInteractiveElement, Styled, Subscription, ViewContext, VisualContext, WeakView,
};
use project::{Project, RepositoryEntry};
use recent_projects::RecentProjects;
@ -58,6 +58,7 @@ pub struct TitleBar {
user_store: Model<UserStore>,
client: Arc<Client>,
workspace: WeakView<Workspace>,
should_move: bool,
_subscriptions: Vec<Subscription>,
}
@ -73,8 +74,10 @@ impl Render for TitleBar {
let platform_supported = cfg!(target_os = "macos");
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")
.w_full()
.pt(Self::top_padding(cx))
@ -88,6 +91,16 @@ impl Render for TitleBar {
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)
.content_stretch()
.child(
@ -113,7 +126,7 @@ impl Render for TitleBar {
.children(self.render_project_host(cx))
.child(self.render_project_name(cx))
.children(self.render_project_branch(cx))
.on_mouse_move(|_, cx| cx.stop_propagation()),
.on_mouse_down(MouseButton::Left, |_, cx| cx.stop_propagation())
)
.child(
h_flex()
@ -145,7 +158,7 @@ impl Render for TitleBar {
this.children(current_user_face_pile.map(|face_pile| {
v_flex()
.on_mouse_move(|_, cx| cx.stop_propagation())
.on_mouse_down(MouseButton::Left, |_, cx| cx.stop_propagation())
.child(face_pile)
.child(render_color_ribbon(player_colors.local().cursor))
}))
@ -208,7 +221,7 @@ impl Render for TitleBar {
h_flex()
.gap_1()
.pr_1()
.on_mouse_move(|_, cx| cx.stop_propagation())
.on_mouse_down(MouseButton::Left, |_, cx| cx.stop_propagation())
.when_some(room, |this, room| {
let room = room.read(cx);
let project = self.project.read(cx);
@ -373,34 +386,38 @@ impl Render for TitleBar {
}
}),
)
);
// Windows Window Controls
title_bar = title_bar.when(
).when(
self.platform_style == PlatformStyle::Windows && !cx.is_fullscreen(),
|title_bar| title_bar.child(platform_windows::WindowsWindowControls::new(height)),
);
// Linux Window Controls
title_bar = title_bar.when(
).when(
self.platform_style == PlatformStyle::Linux
&& !cx.is_fullscreen()
&& cx.should_render_window_controls(),
&& matches!(decorations, Decorations::Client { .. }),
|title_bar| {
title_bar
.child(platform_linux::LinuxWindowControls::new(close_action))
.on_mouse_down(gpui::MouseButton::Right, move |ev, cx| {
cx.show_window_menu(ev.position)
.when(supported_controls.window_menu, |titlebar| {
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()),
children: SmallVec::new(),
workspace: workspace.weak_handle(),
should_move: false,
project,
user_store,
client,

View File

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

View File

@ -1,9 +1,10 @@
use crate::{ItemHandle, Pane};
use gpui::{
AnyView, IntoElement, ParentElement, Render, Styled, Subscription, View, ViewContext,
WindowContext,
AnyView, Decorations, IntoElement, ParentElement, Render, Styled, Subscription, View,
ViewContext, WindowContext,
};
use std::any::TypeId;
use theme::CLIENT_SIDE_DECORATION_ROUNDING;
use ui::{h_flex, prelude::*};
use util::ResultExt;
@ -40,8 +41,17 @@ impl Render for StatusBar {
.gap(Spacing::Large.rems(cx))
.py(Spacing::Small.rems(cx))
.px(Spacing::Large.rems(cx))
// .h_8()
.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_right_tools(cx))
}

View File

@ -27,11 +27,13 @@ use futures::{
Future, FutureExt, StreamExt,
};
use gpui::{
action_as, actions, canvas, impl_action_as, impl_actions, point, relative, size, Action,
AnyElement, AnyView, AnyWeakView, AppContext, AsyncAppContext, AsyncWindowContext, Bounds,
DragMoveEvent, Entity as _, EntityId, EventEmitter, FocusHandle, FocusableView, Global,
KeyContext, Keystroke, ManagedView, Model, ModelContext, PathPromptOptions, Point, PromptLevel,
Render, Size, Subscription, Task, View, WeakView, WindowBounds, WindowHandle, WindowOptions,
action_as, actions, canvas, impl_action_as, impl_actions, point, relative, size,
transparent_black, Action, AnyElement, AnyView, AnyWeakView, AppContext, AsyncAppContext,
AsyncWindowContext, Bounds, CursorStyle, Decorations, DragMoveEvent, Entity as _, EntityId,
EventEmitter, FocusHandle, FocusableView, Global, Hsla, KeyContext, Keystroke, ManagedView,
Model, ModelContext, MouseButton, PathPromptOptions, Point, PromptLevel, Render, ResizeEdge,
Size, Stateful, Subscription, Task, Tiling, View, WeakView, WindowBounds, WindowHandle,
WindowOptions,
};
use item::{
FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, PreviewTabsSettings,
@ -4165,156 +4167,162 @@ impl Render for Workspace {
let theme = cx.theme().clone();
let colors = theme.colors();
self.actions(div(), cx)
.key_context(context)
.relative()
.size_full()
.flex()
.flex_col()
.font(ui_font)
.gap_0()
.justify_start()
.items_start()
.text_color(colors.text)
.bg(colors.background)
.children(self.titlebar_item.clone())
.child(
div()
.id("workspace")
.relative()
.flex_1()
.w_full()
.flex()
.flex_col()
.overflow_hidden()
.border_t_1()
.border_b_1()
.border_color(colors.border)
.child({
let this = cx.view().clone();
canvas(
move |bounds, cx| this.update(cx, |this, _cx| this.bounds = bounds),
|_, _, _| {},
)
.absolute()
.size_full()
})
.when(self.zoomed.is_none(), |this| {
this.on_drag_move(cx.listener(
|workspace, e: &DragMoveEvent<DraggedDock>, cx| match e.drag(cx).0 {
DockPosition::Left => {
let size = workspace.bounds.left() + e.event.position.x;
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| {
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| {
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())
client_side_decorations(
self.actions(div(), cx)
.key_context(context)
.relative()
.size_full()
.flex()
.flex_col()
.font(ui_font)
.gap_0()
.justify_start()
.items_start()
.text_color(colors.text)
.overflow_hidden()
.children(self.titlebar_item.clone())
.child(
div()
.id("workspace")
.bg(colors.background)
.relative()
.flex_1()
.w_full()
.flex()
.flex_col()
.overflow_hidden()
.border_t_1()
.border_b_1()
.border_color(colors.border)
.child({
let this = cx.view().clone();
canvas(
move |bounds, cx| this.update(cx, |this, _cx| this.bounds = bounds),
|_, _, _| {},
)
.absolute()
.size_full()
})
.when(self.zoomed.is_none(), |this| {
this.on_drag_move(cx.listener(
|workspace, e: &DragMoveEvent<DraggedDock>, cx| match e.drag(cx).0 {
DockPosition::Left => {
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| {
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| {
bottom_dock.resize_active_panel(Some(size), cx);
});
}
},
))
// Panes
.child(
div()
.flex()
.flex_col()
.flex_1()
.overflow_hidden()
.child(
h_flex()
.flex_1()
.when_some(paddings.0, |this, p| {
this.child(p.border_r_1())
})
.child(self.center.render(
&self.project,
&self.follower_states,
self.active_call(),
&self.active_pane,
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(
|| {
})
.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(
div()
.flex()
.flex_none()
.flex_col()
.flex_1()
.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();
.child(
h_flex()
.flex_1()
.when_some(paddings.0, |this, p| {
this.child(p.border_r_1())
})
.child(self.center.render(
&self.project,
&self.follower_states,
self.active_call(),
&self.active_pane,
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()
.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(DockPosition::Left) => div.right_2().border_r_1(),
Some(DockPosition::Right) => div.left_2().border_l_1(),
Some(DockPosition::Bottom) => div.top_2().border_t_1(),
None => div.top_2().bottom_2().left_2().right_2().border_1(),
})
}))
.child(self.modal_layer.clone())
.children(self.render_notifications(cx)),
)
.child(self.status_bar.clone())
.children(if self.project.read(cx).is_disconnected() {
if let Some(render) = self.render_disconnected_overlay.take() {
let result = render(self, cx);
self.render_disconnected_overlay = Some(render);
Some(result)
Some(match self.zoomed_position {
Some(DockPosition::Left) => div.right_2().border_r_1(),
Some(DockPosition::Right) => div.left_2().border_l_1(),
Some(DockPosition::Bottom) => div.top_2().border_t_1(),
None => div.top_2().bottom_2().left_2().right_2().border_1(),
})
}))
.child(self.modal_layer.clone())
.children(self.render_notifications(cx)),
)
.child(self.status_bar.clone())
.children(if self.project.read(cx).is_disconnected() {
if let Some(render) = self.render_disconnected_overlay.take() {
let result = render(self, cx);
self.render_disconnected_overlay = Some(render);
Some(result)
} else {
None
}
} else {
None
}
} else {
None
})
}),
cx,
)
}
}
@ -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()),
window_background: cx.theme().window_background_appearance(),
app_id: Some(app_id.to_owned()),
window_decorations: Some(gpui::WindowDecorations::Client),
window_min_size: Some(gpui::Size {
width: px(360.0),
height: px(240.0),

View File

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