mirror of
https://github.com/zed-industries/zed.git
synced 2024-11-07 20:39:04 +03:00
Linux window decorations (#13611)
This PR adds support for full client side decorations on X11 and Wayland TODO: - [x] Adjust GPUI APIs to expose CSD related information - [x] Implement remaining CSD features (Resizing, window border, window shadow) - [x] Integrate with existing background appearance and window transparency - [x] Figure out how to check if the window is tiled on X11 - [x] Implement in Zed - [x] Repeatedly maximizing and unmaximizing can panic - [x] Resizing is strangely slow - [x] X11 resizing and movement doesn't work for this: https://discord.com/channels/869392257814519848/1204679850208657418/1256816908519604305 - [x] The top corner can clip with current styling - [x] Pressing titlebar buttons doesn't work - [x] Not showing maximize / unmaximize buttons - [x] Noisy transparency logs / surface transparency problem https://github.com/zed-industries/zed/pull/13611#issuecomment-2201685030 - [x] Strange offsets when dragging the project panel https://github.com/zed-industries/zed/pull/13611#pullrequestreview-2154606261 - [x] Shadow inset with `_GTK_FRAME_EXTENTS` doesn't respect tiling on X11 (observe by snapping an X11 window in any direction) Release Notes: - N/A --------- Co-authored-by: conrad <conrad@zed.dev> Co-authored-by: Owen Law <81528246+someone13574@users.noreply.github.com> Co-authored-by: apricotbucket28 <71973804+apricotbucket28@users.noreply.github.com> Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
This commit is contained in:
parent
98699a65c1
commit
47aa761ca9
@ -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,
|
||||
}
|
||||
};
|
||||
|
||||
|
222
crates/gpui/examples/window_shadow.rs
Normal file
222
crates/gpui/examples/window_shadow.rs
Normal file
@ -0,0 +1,222 @@
|
||||
use gpui::*;
|
||||
use prelude::FluentBuilder;
|
||||
|
||||
struct WindowShadow {}
|
||||
|
||||
/*
|
||||
Things to do:
|
||||
1. We need a way of calculating which edge or corner the mouse is on,
|
||||
and then dispatch on that
|
||||
2. We need to improve the shadow rendering significantly
|
||||
3. We need to implement the techniques in here in Zed
|
||||
*/
|
||||
|
||||
impl Render for WindowShadow {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let decorations = cx.window_decorations();
|
||||
let rounding = px(10.0);
|
||||
let shadow_size = px(10.0);
|
||||
let border_size = px(1.0);
|
||||
let grey = rgb(0x808080);
|
||||
cx.set_client_inset(shadow_size);
|
||||
|
||||
div()
|
||||
.id("window-backdrop")
|
||||
.bg(transparent_black())
|
||||
.map(|div| match decorations {
|
||||
Decorations::Server => div,
|
||||
Decorations::Client { tiling, .. } => div
|
||||
.bg(gpui::transparent_black())
|
||||
.child(
|
||||
canvas(
|
||||
|_bounds, cx| {
|
||||
cx.insert_hitbox(
|
||||
Bounds::new(
|
||||
point(px(0.0), px(0.0)),
|
||||
cx.window_bounds().get_bounds().size,
|
||||
),
|
||||
false,
|
||||
)
|
||||
},
|
||||
move |_bounds, hitbox, cx| {
|
||||
let mouse = cx.mouse_position();
|
||||
let size = cx.window_bounds().get_bounds().size;
|
||||
let Some(edge) = resize_edge(mouse, shadow_size, size) else {
|
||||
return;
|
||||
};
|
||||
cx.set_cursor_style(
|
||||
match edge {
|
||||
ResizeEdge::Top | ResizeEdge::Bottom => {
|
||||
CursorStyle::ResizeUpDown
|
||||
}
|
||||
ResizeEdge::Left | ResizeEdge::Right => {
|
||||
CursorStyle::ResizeLeftRight
|
||||
}
|
||||
ResizeEdge::TopLeft | ResizeEdge::BottomRight => {
|
||||
CursorStyle::ResizeUpLeftDownRight
|
||||
}
|
||||
ResizeEdge::TopRight | ResizeEdge::BottomLeft => {
|
||||
CursorStyle::ResizeUpRightDownLeft
|
||||
}
|
||||
},
|
||||
&hitbox,
|
||||
);
|
||||
},
|
||||
)
|
||||
.size_full()
|
||||
.absolute(),
|
||||
)
|
||||
.when(!(tiling.top || tiling.right), |div| {
|
||||
div.rounded_tr(rounding)
|
||||
})
|
||||
.when(!(tiling.top || tiling.left), |div| div.rounded_tl(rounding))
|
||||
.when(!tiling.top, |div| div.pt(shadow_size))
|
||||
.when(!tiling.bottom, |div| div.pb(shadow_size))
|
||||
.when(!tiling.left, |div| div.pl(shadow_size))
|
||||
.when(!tiling.right, |div| div.pr(shadow_size))
|
||||
.on_mouse_move(|_e, cx| cx.refresh())
|
||||
.on_mouse_down(MouseButton::Left, move |e, cx| {
|
||||
let size = cx.window_bounds().get_bounds().size;
|
||||
let pos = e.position;
|
||||
|
||||
match resize_edge(pos, shadow_size, size) {
|
||||
Some(edge) => cx.start_window_resize(edge),
|
||||
None => cx.start_window_move(),
|
||||
};
|
||||
}),
|
||||
})
|
||||
.size_full()
|
||||
.child(
|
||||
div()
|
||||
.cursor(CursorStyle::Arrow)
|
||||
.map(|div| match decorations {
|
||||
Decorations::Server => div,
|
||||
Decorations::Client { tiling } => div
|
||||
.border_color(grey)
|
||||
.when(!(tiling.top || tiling.right), |div| {
|
||||
div.rounded_tr(rounding)
|
||||
})
|
||||
.when(!(tiling.top || tiling.left), |div| div.rounded_tl(rounding))
|
||||
.when(!tiling.top, |div| div.border_t(border_size))
|
||||
.when(!tiling.bottom, |div| div.border_b(border_size))
|
||||
.when(!tiling.left, |div| div.border_l(border_size))
|
||||
.when(!tiling.right, |div| div.border_r(border_size))
|
||||
.when(!tiling.is_tiled(), |div| {
|
||||
div.shadow(smallvec::smallvec![gpui::BoxShadow {
|
||||
color: Hsla {
|
||||
h: 0.,
|
||||
s: 0.,
|
||||
l: 0.,
|
||||
a: 0.4,
|
||||
},
|
||||
blur_radius: shadow_size / 2.,
|
||||
spread_radius: px(0.),
|
||||
offset: point(px(0.0), px(0.0)),
|
||||
}])
|
||||
}),
|
||||
})
|
||||
.on_mouse_move(|_e, cx| {
|
||||
cx.stop_propagation();
|
||||
})
|
||||
.bg(gpui::rgb(0xCCCCFF))
|
||||
.size_full()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.justify_around()
|
||||
.child(
|
||||
div().w_full().flex().flex_row().justify_around().child(
|
||||
div()
|
||||
.flex()
|
||||
.bg(white())
|
||||
.size(Length::Definite(Pixels(300.0).into()))
|
||||
.justify_center()
|
||||
.items_center()
|
||||
.shadow_lg()
|
||||
.border_1()
|
||||
.border_color(rgb(0x0000ff))
|
||||
.text_xl()
|
||||
.text_color(rgb(0xffffff))
|
||||
.child(
|
||||
div()
|
||||
.id("hello")
|
||||
.w(px(200.0))
|
||||
.h(px(100.0))
|
||||
.bg(green())
|
||||
.shadow(smallvec::smallvec![gpui::BoxShadow {
|
||||
color: Hsla {
|
||||
h: 0.,
|
||||
s: 0.,
|
||||
l: 0.,
|
||||
a: 1.0,
|
||||
},
|
||||
blur_radius: px(20.0),
|
||||
spread_radius: px(0.0),
|
||||
offset: point(px(0.0), px(0.0)),
|
||||
}])
|
||||
.map(|div| match decorations {
|
||||
Decorations::Server => div,
|
||||
Decorations::Client { .. } => div
|
||||
.on_mouse_down(MouseButton::Left, |_e, cx| {
|
||||
cx.start_window_move();
|
||||
})
|
||||
.on_click(|e, cx| {
|
||||
if e.down.button == MouseButton::Right {
|
||||
cx.show_window_menu(e.up.position);
|
||||
}
|
||||
})
|
||||
.text_color(black())
|
||||
.child("this is the custom titlebar"),
|
||||
}),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn resize_edge(pos: Point<Pixels>, shadow_size: Pixels, size: Size<Pixels>) -> Option<ResizeEdge> {
|
||||
let edge = if pos.y < shadow_size && pos.x < shadow_size {
|
||||
ResizeEdge::TopLeft
|
||||
} else if pos.y < shadow_size && pos.x > size.width - shadow_size {
|
||||
ResizeEdge::TopRight
|
||||
} else if pos.y < shadow_size {
|
||||
ResizeEdge::Top
|
||||
} else if pos.y > size.height - shadow_size && pos.x < shadow_size {
|
||||
ResizeEdge::BottomLeft
|
||||
} else if pos.y > size.height - shadow_size && pos.x > size.width - shadow_size {
|
||||
ResizeEdge::BottomRight
|
||||
} else if pos.y > size.height - shadow_size {
|
||||
ResizeEdge::Bottom
|
||||
} else if pos.x < shadow_size {
|
||||
ResizeEdge::Left
|
||||
} else if pos.x > size.width - shadow_size {
|
||||
ResizeEdge::Right
|
||||
} else {
|
||||
return None;
|
||||
};
|
||||
Some(edge)
|
||||
}
|
||||
|
||||
fn main() {
|
||||
App::new().run(|cx: &mut AppContext| {
|
||||
let bounds = Bounds::centered(None, size(px(600.0), px(600.0)), cx);
|
||||
cx.open_window(
|
||||
WindowOptions {
|
||||
window_bounds: Some(WindowBounds::Windowed(bounds)),
|
||||
window_background: WindowBackgroundAppearance::Opaque,
|
||||
window_decorations: Some(WindowDecorations::Client),
|
||||
..Default::default()
|
||||
},
|
||||
|cx| {
|
||||
cx.new_view(|cx| {
|
||||
cx.observe_window_appearance(|_, cx| {
|
||||
cx.refresh();
|
||||
})
|
||||
.detach();
|
||||
WindowShadow {}
|
||||
})
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
});
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
|
||||
pub enum WaylandDecorationState {
|
||||
/// Decorations are to be provided by the client
|
||||
Client,
|
||||
fn update_window(mut state: RefMut<WaylandWindowState>) {
|
||||
let opaque = !state.is_transparent();
|
||||
|
||||
/// Decorations are provided by the server
|
||||
Server,
|
||||
state.renderer.update_transparency(!opaque);
|
||||
let mut opaque_area = state.window_bounds.map(|v| v.0 as i32);
|
||||
if let Some(inset) = state.inset {
|
||||
opaque_area.inset(inset.0 as i32);
|
||||
}
|
||||
|
||||
let region = state
|
||||
.globals
|
||||
.compositor
|
||||
.create_region(&state.globals.qh, ());
|
||||
region.add(
|
||||
opaque_area.origin.x,
|
||||
opaque_area.origin.y,
|
||||
opaque_area.size.width,
|
||||
opaque_area.size.height,
|
||||
);
|
||||
|
||||
// Note that rounded corners make this rectangle API hard to work with.
|
||||
// As this is common when using CSD, let's just disable this API.
|
||||
if state.background_appearance == WindowBackgroundAppearance::Opaque
|
||||
&& state.decorations == WindowDecorations::Server
|
||||
{
|
||||
// Promise the compositor that this region of the window surface
|
||||
// contains no transparent pixels. This allows the compositor to
|
||||
// do skip whatever is behind the surface for better performance.
|
||||
state.surface.set_opaque_region(Some(®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<Pixels>,
|
||||
new_size: Option<Size<Pixels>>,
|
||||
tiling: Tiling,
|
||||
) -> Option<Size<Pixels>> {
|
||||
let Some(inset) = inset else { return new_size };
|
||||
|
||||
new_size.map(|mut new_size| {
|
||||
if !tiling.top {
|
||||
new_size.height += inset;
|
||||
}
|
||||
if !tiling.bottom {
|
||||
new_size.height += inset;
|
||||
}
|
||||
if !tiling.left {
|
||||
new_size.width += inset;
|
||||
}
|
||||
if !tiling.right {
|
||||
new_size.width += inset;
|
||||
}
|
||||
|
||||
new_size
|
||||
})
|
||||
}
|
||||
|
||||
fn inset_by_tiling(mut bounds: Bounds<Pixels>, inset: Pixels, tiling: Tiling) -> Bounds<Pixels> {
|
||||
if !tiling.top {
|
||||
bounds.origin.y += inset;
|
||||
bounds.size.height -= inset;
|
||||
}
|
||||
if !tiling.bottom {
|
||||
bounds.size.height -= inset;
|
||||
}
|
||||
if !tiling.left {
|
||||
bounds.origin.x += inset;
|
||||
bounds.size.width -= inset;
|
||||
}
|
||||
if !tiling.right {
|
||||
bounds.size.width -= inset;
|
||||
}
|
||||
|
||||
bounds
|
||||
}
|
||||
|
@ -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.0
|
||||
.xcb_connection
|
||||
.send_event(
|
||||
false,
|
||||
state.x_root_window,
|
||||
EventMask::SUBSTRUCTURE_REDIRECT | EventMask::SUBSTRUCTURE_NOTIFY,
|
||||
message,
|
||||
)
|
||||
.unwrap();
|
||||
self.send_moveresize(MOVERESIZE_MOVE);
|
||||
}
|
||||
|
||||
fn should_render_window_controls(&self) -> bool {
|
||||
false
|
||||
fn start_window_resize(&self, edge: ResizeEdge) {
|
||||
self.send_moveresize(edge.to_moveresize());
|
||||
}
|
||||
|
||||
fn window_decorations(&self) -> crate::Decorations {
|
||||
let state = self.0.state.borrow();
|
||||
|
||||
match state.decorations {
|
||||
WindowDecorations::Server => Decorations::Server,
|
||||
WindowDecorations::Client => {
|
||||
// https://source.chromium.org/chromium/chromium/src/+/main:ui/ozone/platform/x11/x11_window.cc;l=2519;drc=1f14cc876cc5bf899d13284a12c451498219bb2d
|
||||
Decorations::Client {
|
||||
tiling: Tiling {
|
||||
top: state.maximized_vertical,
|
||||
bottom: state.maximized_vertical,
|
||||
left: state.maximized_horizontal,
|
||||
right: state.maximized_horizontal,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn set_client_inset(&self, inset: Pixels) {
|
||||
let mut state = self.0.state.borrow_mut();
|
||||
|
||||
let dp = (inset.0 * state.scale_factor) as u32;
|
||||
|
||||
let (left, right) = if state.maximized_horizontal {
|
||||
(0, 0)
|
||||
} else {
|
||||
(dp, dp)
|
||||
};
|
||||
let (top, bottom) = if state.maximized_vertical {
|
||||
(0, 0)
|
||||
} else {
|
||||
(dp, dp)
|
||||
};
|
||||
let insets = [left, right, top, bottom];
|
||||
|
||||
if state.last_insets != insets {
|
||||
state.last_insets = insets;
|
||||
|
||||
self.0
|
||||
.xcb_connection
|
||||
.change_property(
|
||||
xproto::PropMode::REPLACE,
|
||||
self.0.x_window,
|
||||
state.atoms._GTK_FRAME_EXTENTS,
|
||||
xproto::AtomEnum::CARDINAL,
|
||||
size_of::<u32>() as u8 * 8,
|
||||
4,
|
||||
bytemuck::cast_slice::<u32, u8>(&insets),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
fn request_decorations(&self, decorations: crate::WindowDecorations) {
|
||||
// https://github.com/rust-windowing/winit/blob/master/src/platform_impl/linux/x11/util/hint.rs#L53-L87
|
||||
let hints_data: [u32; 5] = match decorations {
|
||||
WindowDecorations::Server => [1 << 1, 0, 1, 0, 0],
|
||||
WindowDecorations::Client => [1 << 1, 0, 0, 0, 0],
|
||||
};
|
||||
|
||||
let mut state = self.0.state.borrow_mut();
|
||||
|
||||
self.0
|
||||
.xcb_connection
|
||||
.change_property(
|
||||
xproto::PropMode::REPLACE,
|
||||
self.0.x_window,
|
||||
state.atoms._MOTIF_WM_HINTS,
|
||||
state.atoms._MOTIF_WM_HINTS,
|
||||
std::mem::size_of::<u32>() as u8 * 8,
|
||||
5,
|
||||
bytemuck::cast_slice::<u32, u8>(&hints_data),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
match decorations {
|
||||
WindowDecorations::Server => {
|
||||
state.decorations = WindowDecorations::Server;
|
||||
let is_transparent = state.is_transparent();
|
||||
state.renderer.update_transparency(is_transparent);
|
||||
}
|
||||
WindowDecorations::Client => {
|
||||
state.decorations = WindowDecorations::Client;
|
||||
let is_transparent = state.is_transparent();
|
||||
state.renderer.update_transparency(is_transparent);
|
||||
}
|
||||
}
|
||||
|
||||
drop(state);
|
||||
let mut callbacks = self.0.callbacks.borrow_mut();
|
||||
if let Some(appearance_changed) = callbacks.appearance_changed.as_mut() {
|
||||
appearance_changed();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
.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,
|
||||
|
@ -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();
|
||||
.child(
|
||||
h_flex()
|
||||
.flex_1()
|
||||
.when_some(paddings.0, |this, p| {
|
||||
this.child(p.border_r_1())
|
||||
})
|
||||
.child(self.center.render(
|
||||
&self.project,
|
||||
&self.follower_states,
|
||||
self.active_call(),
|
||||
&self.active_pane,
|
||||
self.zoomed.as_ref(),
|
||||
&self.app_state,
|
||||
cx,
|
||||
))
|
||||
.when_some(paddings.1, |this, p| {
|
||||
this.child(p.border_l_1())
|
||||
}),
|
||||
)
|
||||
.children(
|
||||
self.zoomed_position
|
||||
.ne(&Some(DockPosition::Bottom))
|
||||
.then(|| self.bottom_dock.clone()),
|
||||
),
|
||||
)
|
||||
// Right Dock
|
||||
.children(
|
||||
self.zoomed_position.ne(&Some(DockPosition::Right)).then(
|
||||
|| {
|
||||
div()
|
||||
.flex()
|
||||
.flex_none()
|
||||
.overflow_hidden()
|
||||
.child(self.right_dock.clone())
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
.children(self.zoomed.as_ref().and_then(|view| {
|
||||
let zoomed_view = view.upgrade()?;
|
||||
let div = div()
|
||||
.occlude()
|
||||
.absolute()
|
||||
.overflow_hidden()
|
||||
.border_color(colors.border)
|
||||
.bg(colors.background)
|
||||
.child(zoomed_view)
|
||||
.inset_0()
|
||||
.shadow_lg();
|
||||
|
||||
Some(match self.zoomed_position {
|
||||
Some(DockPosition::Left) => div.right_2().border_r_1(),
|
||||
Some(DockPosition::Right) => div.left_2().border_l_1(),
|
||||
Some(DockPosition::Bottom) => div.top_2().border_t_1(),
|
||||
None => div.top_2().bottom_2().left_2().right_2().border_1(),
|
||||
})
|
||||
}))
|
||||
.child(self.modal_layer.clone())
|
||||
.children(self.render_notifications(cx)),
|
||||
)
|
||||
.child(self.status_bar.clone())
|
||||
.children(if self.project.read(cx).is_disconnected() {
|
||||
if let Some(render) = self.render_disconnected_overlay.take() {
|
||||
let result = render(self, cx);
|
||||
self.render_disconnected_overlay = Some(render);
|
||||
Some(result)
|
||||
Some(match self.zoomed_position {
|
||||
Some(DockPosition::Left) => div.right_2().border_r_1(),
|
||||
Some(DockPosition::Right) => div.left_2().border_l_1(),
|
||||
Some(DockPosition::Bottom) => div.top_2().border_t_1(),
|
||||
None => div.top_2().bottom_2().left_2().right_2().border_1(),
|
||||
})
|
||||
}))
|
||||
.child(self.modal_layer.clone())
|
||||
.children(self.render_notifications(cx)),
|
||||
)
|
||||
.child(self.status_bar.clone())
|
||||
.children(if self.project.read(cx).is_disconnected() {
|
||||
if let Some(render) = self.render_disconnected_overlay.take() {
|
||||
let result = render(self, cx);
|
||||
self.render_disconnected_overlay = Some(render);
|
||||
Some(result)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
})
|
||||
}),
|
||||
cx,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -6474,3 +6482,267 @@ mod tests {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub fn client_side_decorations(element: impl IntoElement, cx: &mut WindowContext) -> Stateful<Div> {
|
||||
const BORDER_SIZE: Pixels = px(1.0);
|
||||
let decorations = cx.window_decorations();
|
||||
|
||||
if matches!(decorations, Decorations::Client { .. }) {
|
||||
cx.set_client_inset(theme::CLIENT_SIDE_DECORATION_SHADOW);
|
||||
}
|
||||
|
||||
struct GlobalResizeEdge(ResizeEdge);
|
||||
impl Global for GlobalResizeEdge {}
|
||||
|
||||
div()
|
||||
.id("window-backdrop")
|
||||
.bg(transparent_black())
|
||||
.map(|div| match decorations {
|
||||
Decorations::Server => div,
|
||||
Decorations::Client { tiling, .. } => div
|
||||
.child(
|
||||
canvas(
|
||||
|_bounds, cx| {
|
||||
cx.insert_hitbox(
|
||||
Bounds::new(
|
||||
point(px(0.0), px(0.0)),
|
||||
cx.window_bounds().get_bounds().size,
|
||||
),
|
||||
false,
|
||||
)
|
||||
},
|
||||
move |_bounds, hitbox, cx| {
|
||||
let mouse = cx.mouse_position();
|
||||
let size = cx.window_bounds().get_bounds().size;
|
||||
let Some(edge) = resize_edge(
|
||||
mouse,
|
||||
theme::CLIENT_SIDE_DECORATION_SHADOW,
|
||||
size,
|
||||
tiling,
|
||||
) else {
|
||||
return;
|
||||
};
|
||||
cx.set_global(GlobalResizeEdge(edge));
|
||||
cx.set_cursor_style(
|
||||
match edge {
|
||||
ResizeEdge::Top | ResizeEdge::Bottom => {
|
||||
CursorStyle::ResizeUpDown
|
||||
}
|
||||
ResizeEdge::Left | ResizeEdge::Right => {
|
||||
CursorStyle::ResizeLeftRight
|
||||
}
|
||||
ResizeEdge::TopLeft | ResizeEdge::BottomRight => {
|
||||
CursorStyle::ResizeUpLeftDownRight
|
||||
}
|
||||
ResizeEdge::TopRight | ResizeEdge::BottomLeft => {
|
||||
CursorStyle::ResizeUpRightDownLeft
|
||||
}
|
||||
},
|
||||
&hitbox,
|
||||
);
|
||||
},
|
||||
)
|
||||
.size_full()
|
||||
.absolute(),
|
||||
)
|
||||
.when(!(tiling.top || tiling.right), |div| {
|
||||
div.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING)
|
||||
})
|
||||
.when(!(tiling.top || tiling.left), |div| {
|
||||
div.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
|
||||
})
|
||||
.when(!(tiling.bottom || tiling.right), |div| {
|
||||
div.rounded_br(theme::CLIENT_SIDE_DECORATION_ROUNDING)
|
||||
})
|
||||
.when(!(tiling.bottom || tiling.left), |div| {
|
||||
div.rounded_bl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
|
||||
})
|
||||
.when(!tiling.top, |div| {
|
||||
div.pt(theme::CLIENT_SIDE_DECORATION_SHADOW)
|
||||
})
|
||||
.when(!tiling.bottom, |div| {
|
||||
div.pb(theme::CLIENT_SIDE_DECORATION_SHADOW)
|
||||
})
|
||||
.when(!tiling.left, |div| {
|
||||
div.pl(theme::CLIENT_SIDE_DECORATION_SHADOW)
|
||||
})
|
||||
.when(!tiling.right, |div| {
|
||||
div.pr(theme::CLIENT_SIDE_DECORATION_SHADOW)
|
||||
})
|
||||
.on_mouse_move(move |e, cx| {
|
||||
let size = cx.window_bounds().get_bounds().size;
|
||||
let pos = e.position;
|
||||
|
||||
let new_edge =
|
||||
resize_edge(pos, theme::CLIENT_SIDE_DECORATION_SHADOW, size, tiling);
|
||||
|
||||
let edge = cx.try_global::<GlobalResizeEdge>();
|
||||
if new_edge != edge.map(|edge| edge.0) {
|
||||
cx.window_handle()
|
||||
.update(cx, |workspace, cx| cx.notify(workspace.entity_id()))
|
||||
.ok();
|
||||
}
|
||||
})
|
||||
.on_mouse_down(MouseButton::Left, move |e, cx| {
|
||||
let size = cx.window_bounds().get_bounds().size;
|
||||
let pos = e.position;
|
||||
|
||||
let edge = match resize_edge(
|
||||
pos,
|
||||
theme::CLIENT_SIDE_DECORATION_SHADOW,
|
||||
size,
|
||||
tiling,
|
||||
) {
|
||||
Some(value) => value,
|
||||
None => return,
|
||||
};
|
||||
|
||||
cx.start_window_resize(edge);
|
||||
}),
|
||||
})
|
||||
.size_full()
|
||||
.child(
|
||||
div()
|
||||
.cursor(CursorStyle::Arrow)
|
||||
.map(|div| match decorations {
|
||||
Decorations::Server => div,
|
||||
Decorations::Client { tiling } => div
|
||||
.border_color(cx.theme().colors().border)
|
||||
.when(!(tiling.top || tiling.right), |div| {
|
||||
div.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING)
|
||||
})
|
||||
.when(!(tiling.top || tiling.left), |div| {
|
||||
div.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
|
||||
})
|
||||
.when(!(tiling.bottom || tiling.right), |div| {
|
||||
div.rounded_br(theme::CLIENT_SIDE_DECORATION_ROUNDING)
|
||||
})
|
||||
.when(!(tiling.bottom || tiling.left), |div| {
|
||||
div.rounded_bl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
|
||||
})
|
||||
.when(!tiling.top, |div| div.border_t(BORDER_SIZE))
|
||||
.when(!tiling.bottom, |div| div.border_b(BORDER_SIZE))
|
||||
.when(!tiling.left, |div| div.border_l(BORDER_SIZE))
|
||||
.when(!tiling.right, |div| div.border_r(BORDER_SIZE))
|
||||
.when(!tiling.is_tiled(), |div| {
|
||||
div.shadow(smallvec::smallvec![gpui::BoxShadow {
|
||||
color: Hsla {
|
||||
h: 0.,
|
||||
s: 0.,
|
||||
l: 0.,
|
||||
a: 0.4,
|
||||
},
|
||||
blur_radius: theme::CLIENT_SIDE_DECORATION_SHADOW / 2.,
|
||||
spread_radius: px(0.),
|
||||
offset: point(px(0.0), px(0.0)),
|
||||
}])
|
||||
}),
|
||||
})
|
||||
.on_mouse_move(|_e, cx| {
|
||||
cx.stop_propagation();
|
||||
})
|
||||
.bg(cx.theme().colors().border)
|
||||
.size_full()
|
||||
.child(element),
|
||||
)
|
||||
.map(|div| match decorations {
|
||||
Decorations::Server => div,
|
||||
Decorations::Client { tiling, .. } => div.child(
|
||||
canvas(
|
||||
|_bounds, cx| {
|
||||
cx.insert_hitbox(
|
||||
Bounds::new(
|
||||
point(px(0.0), px(0.0)),
|
||||
cx.window_bounds().get_bounds().size,
|
||||
),
|
||||
false,
|
||||
)
|
||||
},
|
||||
move |_bounds, hitbox, cx| {
|
||||
let mouse = cx.mouse_position();
|
||||
let size = cx.window_bounds().get_bounds().size;
|
||||
let Some(edge) =
|
||||
resize_edge(mouse, theme::CLIENT_SIDE_DECORATION_SHADOW, size, tiling)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
cx.set_global(GlobalResizeEdge(edge));
|
||||
cx.set_cursor_style(
|
||||
match edge {
|
||||
ResizeEdge::Top | ResizeEdge::Bottom => CursorStyle::ResizeUpDown,
|
||||
ResizeEdge::Left | ResizeEdge::Right => {
|
||||
CursorStyle::ResizeLeftRight
|
||||
}
|
||||
ResizeEdge::TopLeft | ResizeEdge::BottomRight => {
|
||||
CursorStyle::ResizeUpLeftDownRight
|
||||
}
|
||||
ResizeEdge::TopRight | ResizeEdge::BottomLeft => {
|
||||
CursorStyle::ResizeUpRightDownLeft
|
||||
}
|
||||
},
|
||||
&hitbox,
|
||||
);
|
||||
},
|
||||
)
|
||||
.size_full()
|
||||
.absolute(),
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
fn resize_edge(
|
||||
pos: Point<Pixels>,
|
||||
shadow_size: Pixels,
|
||||
window_size: Size<Pixels>,
|
||||
tiling: Tiling,
|
||||
) -> Option<ResizeEdge> {
|
||||
let bounds = Bounds::new(Point::default(), window_size).inset(shadow_size * 1.5);
|
||||
if bounds.contains(&pos) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let corner_size = size(shadow_size * 1.5, shadow_size * 1.5);
|
||||
let top_left_bounds = Bounds::new(Point::new(px(0.), px(0.)), corner_size);
|
||||
if top_left_bounds.contains(&pos) {
|
||||
return Some(ResizeEdge::TopLeft);
|
||||
}
|
||||
|
||||
let top_right_bounds = Bounds::new(
|
||||
Point::new(window_size.width - corner_size.width, px(0.)),
|
||||
corner_size,
|
||||
);
|
||||
if top_right_bounds.contains(&pos) {
|
||||
return Some(ResizeEdge::TopRight);
|
||||
}
|
||||
|
||||
let bottom_left_bounds = Bounds::new(
|
||||
Point::new(px(0.), window_size.height - corner_size.height),
|
||||
corner_size,
|
||||
);
|
||||
if bottom_left_bounds.contains(&pos) {
|
||||
return Some(ResizeEdge::BottomLeft);
|
||||
}
|
||||
|
||||
let bottom_right_bounds = Bounds::new(
|
||||
Point::new(
|
||||
window_size.width - corner_size.width,
|
||||
window_size.height - corner_size.height,
|
||||
),
|
||||
corner_size,
|
||||
);
|
||||
if bottom_right_bounds.contains(&pos) {
|
||||
return Some(ResizeEdge::BottomRight);
|
||||
}
|
||||
|
||||
if !tiling.top && pos.y < shadow_size {
|
||||
Some(ResizeEdge::Top)
|
||||
} else if !tiling.bottom && pos.y > window_size.height - shadow_size {
|
||||
Some(ResizeEdge::Bottom)
|
||||
} else if !tiling.left && pos.x < shadow_size {
|
||||
Some(ResizeEdge::Left)
|
||||
} else if !tiling.right && pos.x > window_size.width - shadow_size {
|
||||
Some(ResizeEdge::Right)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
@ -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),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user