Detailed changes
@@ -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),
}
}
@@ -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"
@@ -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)),
@@ -52,6 +52,7 @@ fn main() {
is_movable: false,
app_id: None,
window_min_size: None,
+ window_decorations: None,
}
};
@@ -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();
+ });
+}
@@ -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 {
@@ -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.
@@ -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,
@@ -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",
@@ -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(),
@@ -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(®ion));
- } 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(®ion));
- 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);
+ }
+ }
+}
+
+fn update_window(mut state: RefMut<WaylandWindowState>) {
+ let opaque = !state.is_transparent();
+
+ 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(®ion));
+ } 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(®ion));
+ 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,
+ }
}
}
-#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
-pub enum WaylandDecorationState {
- /// Decorations are to be provided by the client
- Client,
+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;
+ }
- /// Decorations are provided by the server
- Server,
+ bounds
}
@@ -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();
@@ -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.send_moveresize(MOVERESIZE_MOVE);
+ }
+
+ 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
- .send_event(
- false,
- state.x_root_window,
- EventMask::SUBSTRUCTURE_REDIRECT | EventMask::SUBSTRUCTURE_NOTIFY,
- message,
+ .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();
- }
- fn should_render_window_controls(&self) -> bool {
- false
+ 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();
+ }
}
}
@@ -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]
}
@@ -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 {
@@ -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 {
@@ -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)]
@@ -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);
@@ -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 {
@@ -1,4 +1,3 @@
-pub mod platform_generic;
pub mod platform_linux;
pub mod platform_mac;
pub mod platform_windows;
@@ -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,
- ))
- }
-}
@@ -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,
+ ))
}
}
@@ -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)
- })
- .on_mouse_move(move |ev, cx| {
- if ev.dragging() {
- cx.start_system_move();
- }
+ .when(supported_controls.window_menu, |titlebar| {
+ titlebar.on_mouse_down(gpui::MouseButton::Right, move |ev, cx| {
+ cx.show_window_menu(ev.position)
+ })
})
- },
- );
- 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,
@@ -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))
@@ -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))
}
@@ -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();
-
- 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)
+ .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)
+ } 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
+ }
+}
@@ -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),
@@ -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),
+ ),
+ )
}
}