diff --git a/crates/collab_ui/src/collab_ui.rs b/crates/collab_ui/src/collab_ui.rs index fe177603cb..8b704b6a05 100644 --- a/crates/collab_ui/src/collab_ui.rs +++ b/crates/collab_ui/src/collab_ui.rs @@ -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), } } diff --git a/crates/gpui/Cargo.toml b/crates/gpui/Cargo.toml index c7c3a92a95..04f62f28e7 100644 --- a/crates/gpui/Cargo.toml +++ b/crates/gpui/Cargo.toml @@ -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" diff --git a/crates/gpui/examples/hello_world.rs b/crates/gpui/examples/hello_world.rs index 96ab335b08..961212fa62 100644 --- a/crates/gpui/examples/hello_world.rs +++ b/crates/gpui/examples/hello_world.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)), diff --git a/crates/gpui/examples/window_positioning.rs b/crates/gpui/examples/window_positioning.rs index b22dc58974..0c5f216c2e 100644 --- a/crates/gpui/examples/window_positioning.rs +++ b/crates/gpui/examples/window_positioning.rs @@ -52,6 +52,7 @@ fn main() { is_movable: false, app_id: None, window_min_size: None, + window_decorations: None, } }; diff --git a/crates/gpui/examples/window_shadow.rs b/crates/gpui/examples/window_shadow.rs new file mode 100644 index 0000000000..122231f6b5 --- /dev/null +++ b/crates/gpui/examples/window_shadow.rs @@ -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) -> 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, shadow_size: Pixels, size: Size) -> Option { + 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(); + }); +} diff --git a/crates/gpui/src/color.rs b/crates/gpui/src/color.rs index 2cf2ad55f2..585255b450 100644 --- a/crates/gpui/src/color.rs +++ b/crates/gpui/src/color.rs @@ -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 { diff --git a/crates/gpui/src/geometry.rs b/crates/gpui/src/geometry.rs index e6cde4fce2..f3cd933e2b 100644 --- a/crates/gpui/src/geometry.rs +++ b/crates/gpui/src/geometry.rs @@ -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) -> Point) -> Bounds { + pub fn map_origin(self, f: impl Fn(T) -> T) -> Bounds { 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 { + Bounds { + origin: self.origin, + size: self.size.map(f), + } + } } /// Checks if the bounds represent an empty area. diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index 238ccb87d1..3d81e88fb4 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -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; 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; + // 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); - 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) {} + 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>, + + /// Whether to use client or server side decorations. Wayland only + /// Note that this may be ignored. + pub window_decorations: Option, } /// The variables that can be configured when creating a new window @@ -596,8 +693,6 @@ pub(crate) struct WindowParams { pub display_id: Option, - pub window_background: WindowBackgroundAppearance, - #[cfg_attr(target_os = "linux", allow(dead_code))] pub window_min_size: Option>, } @@ -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, - /// 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, diff --git a/crates/gpui/src/platform/linux/platform.rs b/crates/gpui/src/platform/linux/platform.rs index 54fc4aa17d..9eef459d5d 100644 --- a/crates/gpui/src/platform/linux/platform.rs +++ b/crates/gpui/src/platform/linux/platform.rs @@ -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", diff --git a/crates/gpui/src/platform/linux/wayland/client.rs b/crates/gpui/src/platform/linux/wayland/client.rs index 22750d1814..2728a141ee 100644 --- a/crates/gpui/src/platform/linux/wayland/client.rs +++ b/crates/gpui/src/platform/linux/wayland/client.rs @@ -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(), diff --git a/crates/gpui/src/platform/linux/wayland/window.rs b/crates/gpui/src/platform/linux/wayland/window.rs index e82a5fc842..5ab64e9e1e 100644 --- a/crates/gpui/src/platform/linux/wayland/window.rs +++ b/crates/gpui/src/platform/linux/wayland/window.rs @@ -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>, fullscreen: bool, maximized: bool, + tiling: Tiling, } pub struct WaylandWindowState { @@ -84,14 +87,20 @@ pub struct WaylandWindowState { bounds: Bounds, scale: f32, input_handler: Option, - decoration_state: WaylandDecorationState, + decorations: WindowDecorations, + background_appearance: WindowBackgroundAppearance, fullscreen: bool, maximized: bool, - windowed_bounds: Bounds, + tiling: Tiling, + window_bounds: Bounds, client: WaylandClientStatePtr, handle: AnyWindowHandle, active: bool, in_progress_configure: Option, + in_progress_window_controls: Option, + window_controls: WindowControls, + inset: Option, + requested_inset: Option, } #[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::(&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::(&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 + 'a>(states: &'a [u8]) -> impl Iterator + 'a +where + >::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) -> 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); + } } } -#[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) { + 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(®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, + } + } +} + +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, + new_size: Option>, + tiling: Tiling, +) -> Option> { + 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, inset: Pixels, tiling: Tiling) -> Bounds { + 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 } diff --git a/crates/gpui/src/platform/linux/x11/client.rs b/crates/gpui/src/platform/linux/x11/client.rs index ba42bc3fa4..39c0b0fd6d 100644 --- a/crates/gpui/src/platform/linux/x11/client.rs +++ b/crates/gpui/src/platform/linux/x11/client.rs @@ -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(); diff --git a/crates/gpui/src/platform/linux/x11/window.rs b/crates/gpui/src/platform/linux/x11/window.rs index b15de3df73..52fccaf272 100644 --- a/crates/gpui/src/platform/linux/x11/window.rs +++ b/crates/gpui/src/platform/linux/x11/window.rs @@ -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, _raw: RawWindow, bounds: Bounds, scale_factor: f32, @@ -173,7 +190,22 @@ pub struct X11WindowState { display: Rc, input_handler: Option, 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 { - 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 { - 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) -> 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) { + 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) { @@ -1004,7 +1133,7 @@ impl PlatformWindow for X11Window { inner.renderer.draw(scene); } - fn sprite_atlas(&self) -> sync::Arc { + fn sprite_atlas(&self) -> Arc { 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::() as u8 * 8, + 4, + bytemuck::cast_slice::(&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::() as u8 * 8, + 5, + bytemuck::cast_slice::(&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(); + } } } diff --git a/crates/gpui/src/platform/mac/platform.rs b/crates/gpui/src/platform/mac/platform.rs index d6fc06b43c..332cf74bd5 100644 --- a/crates/gpui/src/platform/mac/platform.rs +++ b/crates/gpui/src/platform/mac/platform.rs @@ -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] } diff --git a/crates/gpui/src/platform/mac/window.rs b/crates/gpui/src/platform/mac/window.rs index 5eb9caba2e..e98e8f0f74 100644 --- a/crates/gpui/src/platform/mac/window.rs +++ b/crates/gpui/src/platform/mac/window.rs @@ -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 { self.0.lock().renderer.sprite_atlas().clone() } - - fn show_window_menu(&self, _position: Point) {} - - fn start_system_move(&self) {} - - fn should_render_window_controls(&self) -> bool { - false - } } impl rwh::HasWindowHandle for MacWindow { diff --git a/crates/gpui/src/platform/test/window.rs b/crates/gpui/src/platform/test/window.rs index 5946256a8d..7ffaf77250 100644 --- a/crates/gpui/src/platform/test/window.rs +++ b/crates/gpui/src/platform/test/window.rs @@ -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 { diff --git a/crates/gpui/src/platform/windows/window.rs b/crates/gpui/src/platform/windows/window.rs index eedb32cc2e..8c648661ca 100644 --- a/crates/gpui/src/platform/windows/window.rs +++ b/crates/gpui/src/platform/windows/window.rs @@ -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) {} - - fn start_system_move(&self) {} - - fn should_render_window_controls(&self) -> bool { - false - } } #[implement(IDropTarget)] diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index 08c4a8753b..a5f2af6035 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -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, 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>> = 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); diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index fa54159f61..7bbe7c9885 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -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 { diff --git a/crates/title_bar/src/platforms.rs b/crates/title_bar/src/platforms.rs index 2f0f9a5392..67e87d45ea 100644 --- a/crates/title_bar/src/platforms.rs +++ b/crates/title_bar/src/platforms.rs @@ -1,4 +1,3 @@ -pub mod platform_generic; pub mod platform_linux; pub mod platform_mac; pub mod platform_windows; diff --git a/crates/title_bar/src/platforms/platform_generic.rs b/crates/title_bar/src/platforms/platform_generic.rs deleted file mode 100644 index 42e32de4e9..0000000000 --- a/crates/title_bar/src/platforms/platform_generic.rs +++ /dev/null @@ -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, -} - -impl GenericWindowControls { - pub fn new(close_action: Box) -> 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, - )) - } -} diff --git a/crates/title_bar/src/platforms/platform_linux.rs b/crates/title_bar/src/platforms/platform_linux.rs index c2142fc8d5..dd71e59625 100644 --- a/crates/title_bar/src/platforms/platform_linux.rs +++ b/crates/title_bar/src/platforms/platform_linux.rs @@ -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, + )) } } diff --git a/crates/title_bar/src/title_bar.rs b/crates/title_bar/src/title_bar.rs index e218305905..59854ab572 100644 --- a/crates/title_bar/src/title_bar.rs +++ b/crates/title_bar/src/title_bar.rs @@ -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, client: Arc, workspace: WeakView, + should_move: bool, _subscriptions: Vec, } @@ -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, diff --git a/crates/title_bar/src/window_controls.rs b/crates/title_bar/src/window_controls.rs index 5b44f0c446..21b3811668 100644 --- a/crates/title_bar/src/window_controls.rs +++ b/crates/title_bar/src/window_controls.rs @@ -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)) diff --git a/crates/workspace/src/status_bar.rs b/crates/workspace/src/status_bar.rs index 0b80126163..f2d2b73854 100644 --- a/crates/workspace/src/status_bar.rs +++ b/crates/workspace/src/status_bar.rs @@ -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)) } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index f156669956..afc494a6ea 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -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, 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, 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
{ + 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::(); + 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, + shadow_size: Pixels, + window_size: Size, + tiling: Tiling, +) -> Option { + 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 + } +} diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 15fef998b0..48009666f7 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -105,6 +105,7 @@ pub fn build_window_options(display_uuid: Option, 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), diff --git a/crates/zed/src/zed/linux_prompts.rs b/crates/zed/src/zed/linux_prompts.rs index fdcfbdc124..949bba4936 100644 --- a/crates/zed/src/zed/linux_prompts.rs +++ b/crates/zed/src/zed/linux_prompts.rs @@ -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), + ), + ) } }