mirror of
https://github.com/zed-industries/zed.git
synced 2024-11-07 20:39:04 +03:00
wayland: Window controls and drag (#11525)
Based on https://github.com/zed-industries/zed/pull/11046 - Partially fixes #10346 - Fixes https://github.com/zed-industries/zed/issues/9964 ## Features Window buttons ![image](https://github.com/zed-industries/zed/assets/71973804/1b7e0504-3925-45ba-90b5-5adb55e0d739) Window drag ![image](https://github.com/zed-industries/zed/assets/71973804/9c509a37-e5a5-484c-9f80-c722aeee4380) Native window context menu ![image](https://github.com/zed-industries/zed/assets/71973804/048ecf52-e277-49bb-a106-91cad226fd8a) ### Limitations - No resizing - Wayland only (though X11 always has window decorations) ### Technical This PR adds three APIs to gpui. 1. `show_window_menu`: Triggers the native title bar context menu. 2. `start_system_move`: Tells the compositor to start dragging the window. 3. `should_render_window_controls`: Whether the compositor doesn't support server side decorations. These APIs have only been implemented for Wayland, but they should be portable to other platforms. Release Notes: - N/A --------- Co-authored-by: Akilan Elango <akilan1997@gmail.com>
This commit is contained in:
parent
db89353193
commit
d1ee2d0749
@ -58,7 +58,7 @@ impl Render for CollabTitlebarItem {
|
||||
let project_id = self.project.read(cx).remote_id();
|
||||
let workspace = self.workspace.upgrade();
|
||||
|
||||
TitleBar::new("collab-titlebar")
|
||||
TitleBar::new("collab-titlebar", Box::new(workspace::CloseWindow))
|
||||
// note: on windows titlebar behaviour is handled by the platform implementation
|
||||
.when(cfg!(not(windows)), |this| {
|
||||
this.on_click(|event, cx| {
|
||||
@ -73,7 +73,8 @@ impl Render for CollabTitlebarItem {
|
||||
.gap_1()
|
||||
.children(self.render_project_host(cx))
|
||||
.child(self.render_project_name(cx))
|
||||
.children(self.render_project_branch(cx)),
|
||||
.children(self.render_project_branch(cx))
|
||||
.on_mouse_move(|_, cx| cx.stop_propagation()),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
@ -105,6 +106,7 @@ impl Render for CollabTitlebarItem {
|
||||
|
||||
this.children(current_user_face_pile.map(|face_pile| {
|
||||
v_flex()
|
||||
.on_mouse_move(|_, cx| cx.stop_propagation())
|
||||
.child(face_pile)
|
||||
.child(render_color_ribbon(player_colors.local().cursor))
|
||||
}))
|
||||
@ -167,6 +169,7 @@ impl Render for CollabTitlebarItem {
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.pr_1()
|
||||
.on_mouse_move(|_, cx| cx.stop_propagation())
|
||||
.when_some(room, |this, room| {
|
||||
let room = room.read(cx);
|
||||
let project = self.project.read(cx);
|
||||
|
@ -226,6 +226,10 @@ pub(crate) trait PlatformWindow: HasWindowHandle + HasDisplayHandle {
|
||||
#[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;
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
fn as_test(&mut self) -> Option<&mut TestWindow> {
|
||||
None
|
||||
|
@ -23,6 +23,7 @@ use wayland_client::globals::{registry_queue_init, GlobalList, GlobalListContent
|
||||
use wayland_client::protocol::wl_callback::{self, WlCallback};
|
||||
use wayland_client::protocol::wl_data_device_manager::DndAction;
|
||||
use wayland_client::protocol::wl_pointer::{AxisRelativeDirection, AxisSource};
|
||||
use wayland_client::protocol::wl_seat::WlSeat;
|
||||
use wayland_client::protocol::{
|
||||
wl_data_device, wl_data_device_manager, wl_data_offer, wl_data_source, wl_output, wl_region,
|
||||
};
|
||||
@ -80,6 +81,7 @@ pub struct Globals {
|
||||
pub data_device_manager: Option<wl_data_device_manager::WlDataDeviceManager>,
|
||||
pub wm_base: xdg_wm_base::XdgWmBase,
|
||||
pub shm: wl_shm::WlShm,
|
||||
pub seat: wl_seat::WlSeat,
|
||||
pub viewporter: Option<wp_viewporter::WpViewporter>,
|
||||
pub fractional_scale_manager:
|
||||
Option<wp_fractional_scale_manager_v1::WpFractionalScaleManagerV1>,
|
||||
@ -93,6 +95,7 @@ impl Globals {
|
||||
globals: GlobalList,
|
||||
executor: ForegroundExecutor,
|
||||
qh: QueueHandle<WaylandClientStatePtr>,
|
||||
seat: wl_seat::WlSeat,
|
||||
) -> Self {
|
||||
Globals {
|
||||
activation: globals.bind(&qh, 1..=1, ()).ok(),
|
||||
@ -113,6 +116,7 @@ impl Globals {
|
||||
)
|
||||
.ok(),
|
||||
shm: globals.bind(&qh, 1..=1, ()).unwrap(),
|
||||
seat,
|
||||
wm_base: globals.bind(&qh, 1..=1, ()).unwrap(),
|
||||
viewporter: globals.bind(&qh, 1..=1, ()).ok(),
|
||||
fractional_scale_manager: globals.bind(&qh, 1..=1, ()).ok(),
|
||||
@ -193,6 +197,10 @@ impl WaylandClientStatePtr {
|
||||
.expect("The pointer should always be valid when dispatching in wayland")
|
||||
}
|
||||
|
||||
pub fn get_serial(&self, kind: SerialKind) -> u32 {
|
||||
self.0.upgrade().unwrap().borrow().serial_tracker.get(kind)
|
||||
}
|
||||
|
||||
pub fn drop_window(&self, surface_id: &ObjectId) {
|
||||
let mut client = self.get_client();
|
||||
let mut state = client.borrow_mut();
|
||||
@ -303,7 +311,12 @@ impl WaylandClient {
|
||||
});
|
||||
|
||||
let seat = seat.unwrap();
|
||||
let globals = Globals::new(globals, common.foreground_executor.clone(), qh.clone());
|
||||
let globals = Globals::new(
|
||||
globals,
|
||||
common.foreground_executor.clone(),
|
||||
qh.clone(),
|
||||
seat.clone(),
|
||||
);
|
||||
|
||||
let data_device = globals
|
||||
.data_device_manager
|
||||
@ -962,6 +975,7 @@ impl Dispatch<wl_pointer::WlPointer, ()> for WaylandClientStatePtr {
|
||||
} => {
|
||||
state.serial_tracker.update(SerialKind::MouseEnter, serial);
|
||||
state.mouse_location = Some(point(px(surface_x as f32), px(surface_y as f32)));
|
||||
state.button_pressed = None;
|
||||
|
||||
if let Some(window) = get_window(&mut state, &surface.id()) {
|
||||
state.mouse_focused_window = Some(window.clone());
|
||||
@ -990,6 +1004,7 @@ impl Dispatch<wl_pointer::WlPointer, ()> for WaylandClientStatePtr {
|
||||
});
|
||||
state.mouse_focused_window = None;
|
||||
state.mouse_location = None;
|
||||
state.button_pressed = None;
|
||||
|
||||
drop(state);
|
||||
focused_window.handle_input(input);
|
||||
|
@ -23,12 +23,13 @@ use wayland_protocols_plasma::blur::client::{org_kde_kwin_blur, org_kde_kwin_blu
|
||||
|
||||
use crate::platform::blade::{BladeRenderer, BladeSurfaceConfig};
|
||||
use crate::platform::linux::wayland::display::WaylandDisplay;
|
||||
use crate::platform::linux::wayland::serial::SerialKind;
|
||||
use crate::platform::{PlatformAtlas, PlatformInputHandler, PlatformWindow};
|
||||
use crate::scene::Scene;
|
||||
use crate::{
|
||||
px, size, Bounds, DevicePixels, Globals, Modifiers, Pixels, PlatformDisplay, PlatformInput,
|
||||
Point, PromptLevel, Size, WaylandClientState, WaylandClientStatePtr, WindowAppearance,
|
||||
WindowBackgroundAppearance, WindowBounds, WindowParams,
|
||||
Point, PromptLevel, Size, WaylandClientStatePtr, WindowAppearance, WindowBackgroundAppearance,
|
||||
WindowBounds, WindowParams,
|
||||
};
|
||||
|
||||
#[derive(Default)]
|
||||
@ -753,6 +754,27 @@ impl PlatformWindow for WaylandWindow {
|
||||
let state = self.borrow();
|
||||
state.renderer.sprite_atlas().clone()
|
||||
}
|
||||
|
||||
fn show_window_menu(&self, position: Point<Pixels>) {
|
||||
let state = self.borrow();
|
||||
let serial = state.client.get_serial(SerialKind::MousePress);
|
||||
state.toplevel.show_window_menu(
|
||||
&state.globals.seat,
|
||||
serial,
|
||||
position.x.0 as i32,
|
||||
position.y.0 as i32,
|
||||
);
|
||||
}
|
||||
|
||||
fn start_system_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
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
|
||||
|
@ -8,6 +8,7 @@ use crate::{
|
||||
Scene, Size, WindowAppearance, WindowBackgroundAppearance, WindowBounds, WindowOptions,
|
||||
WindowParams, X11Client, X11ClientState, X11ClientStatePtr,
|
||||
};
|
||||
|
||||
use blade_graphics as gpu;
|
||||
use parking_lot::Mutex;
|
||||
use raw_window_handle as rwh;
|
||||
@ -719,4 +720,14 @@ impl PlatformWindow for X11Window {
|
||||
let inner = self.0.state.borrow();
|
||||
inner.renderer.sprite_atlas().clone()
|
||||
}
|
||||
|
||||
// todo(linux)
|
||||
fn show_window_menu(&self, _position: Point<Pixels>) {}
|
||||
|
||||
// todo(linux)
|
||||
fn start_system_move(&self) {}
|
||||
|
||||
fn should_render_window_controls(&self) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
@ -1100,6 +1100,14 @@ 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 {
|
||||
|
@ -257,6 +257,18 @@ impl PlatformWindow for TestWindow {
|
||||
fn get_raw_handle(&self) -> windows::Win32::Foundation::HWND {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn show_window_menu(&self, _position: Point<Pixels>) {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn start_system_move(&self) {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn should_render_window_controls(&self) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct TestAtlasState {
|
||||
|
@ -628,6 +628,14 @@ 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)]
|
||||
|
@ -1143,6 +1143,23 @@ impl<'a> WindowContext<'a> {
|
||||
self.window.platform_window.zoom();
|
||||
}
|
||||
|
||||
/// Opens the native title bar context menu, useful when implementing client side decorations (Wayland only)
|
||||
pub fn show_window_menu(&self, position: Point<Pixels>) {
|
||||
self.window.platform_window.show_window_menu(position)
|
||||
}
|
||||
|
||||
/// Tells the compositor to take control of window movement (Wayland only)
|
||||
///
|
||||
/// Events may not be received during a move operation.
|
||||
pub fn start_system_move(&self) {
|
||||
self.window.platform_window.start_system_move()
|
||||
}
|
||||
|
||||
/// 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()
|
||||
}
|
||||
|
||||
/// Updates the window's title at the platform level.
|
||||
pub fn set_window_title(&mut self, title: &str) {
|
||||
self.window.platform_window.set_title(title);
|
||||
|
@ -1,4 +1,4 @@
|
||||
use gpui::Render;
|
||||
use gpui::{NoAction, Render};
|
||||
use story::{StoryContainer, StoryItem, StorySection};
|
||||
|
||||
use crate::{prelude::*, PlatformStyle, TitleBar};
|
||||
@ -19,7 +19,7 @@ impl Render for TitleBarStory {
|
||||
StorySection::new().child(
|
||||
StoryItem::new(
|
||||
"Default (macOS)",
|
||||
TitleBar::new("macos")
|
||||
TitleBar::new("macos", Box::new(NoAction))
|
||||
.platform_style(PlatformStyle::Mac)
|
||||
.map(add_sample_children),
|
||||
)
|
||||
@ -31,7 +31,7 @@ impl Render for TitleBarStory {
|
||||
StorySection::new().child(
|
||||
StoryItem::new(
|
||||
"Default (Linux)",
|
||||
TitleBar::new("linux")
|
||||
TitleBar::new("linux", Box::new(NoAction))
|
||||
.platform_style(PlatformStyle::Linux)
|
||||
.map(add_sample_children),
|
||||
)
|
||||
@ -43,7 +43,7 @@ impl Render for TitleBarStory {
|
||||
StorySection::new().child(
|
||||
StoryItem::new(
|
||||
"Default (Windows)",
|
||||
TitleBar::new("windows")
|
||||
TitleBar::new("windows", Box::new(NoAction))
|
||||
.platform_style(PlatformStyle::Windows)
|
||||
.map(add_sample_children),
|
||||
)
|
||||
|
@ -1,3 +1,4 @@
|
||||
mod linux_window_controls;
|
||||
mod title_bar;
|
||||
mod windows_window_controls;
|
||||
|
||||
|
145
crates/ui/src/components/title_bar/linux_window_controls.rs
Normal file
145
crates/ui/src/components/title_bar/linux_window_controls.rs
Normal file
@ -0,0 +1,145 @@
|
||||
use gpui::{prelude::*, Action, Rgba, WindowAppearance};
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct LinuxWindowControls {
|
||||
button_height: Pixels,
|
||||
close_window_action: Box<dyn Action>,
|
||||
}
|
||||
|
||||
impl LinuxWindowControls {
|
||||
pub fn new(button_height: Pixels, close_window_action: Box<dyn Action>) -> Self {
|
||||
Self {
|
||||
button_height,
|
||||
close_window_action,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for LinuxWindowControls {
|
||||
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
|
||||
let close_button_hover_color = Rgba {
|
||||
r: 232.0 / 255.0,
|
||||
g: 17.0 / 255.0,
|
||||
b: 32.0 / 255.0,
|
||||
a: 1.0,
|
||||
};
|
||||
|
||||
let button_hover_color = match cx.appearance() {
|
||||
WindowAppearance::Light | WindowAppearance::VibrantLight => Rgba {
|
||||
r: 0.1,
|
||||
g: 0.1,
|
||||
b: 0.1,
|
||||
a: 0.2,
|
||||
},
|
||||
WindowAppearance::Dark | WindowAppearance::VibrantDark => Rgba {
|
||||
r: 0.9,
|
||||
g: 0.9,
|
||||
b: 0.9,
|
||||
a: 0.1,
|
||||
},
|
||||
};
|
||||
|
||||
div()
|
||||
.id("linux-window-controls")
|
||||
.flex()
|
||||
.flex_row()
|
||||
.justify_center()
|
||||
.content_stretch()
|
||||
.max_h(self.button_height)
|
||||
.min_h(self.button_height)
|
||||
.child(TitlebarButton::new(
|
||||
"minimize",
|
||||
TitlebarButtonType::Minimize,
|
||||
button_hover_color,
|
||||
self.close_window_action.boxed_clone(),
|
||||
))
|
||||
.child(TitlebarButton::new(
|
||||
"maximize-or-restore",
|
||||
if cx.is_maximized() {
|
||||
TitlebarButtonType::Restore
|
||||
} else {
|
||||
TitlebarButtonType::Maximize
|
||||
},
|
||||
button_hover_color,
|
||||
self.close_window_action.boxed_clone(),
|
||||
))
|
||||
.child(TitlebarButton::new(
|
||||
"close",
|
||||
TitlebarButtonType::Close,
|
||||
close_button_hover_color,
|
||||
self.close_window_action,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
|
||||
enum TitlebarButtonType {
|
||||
Minimize,
|
||||
Restore,
|
||||
Maximize,
|
||||
Close,
|
||||
}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
struct TitlebarButton {
|
||||
id: ElementId,
|
||||
icon: TitlebarButtonType,
|
||||
hover_background_color: Rgba,
|
||||
close_window_action: Box<dyn Action>,
|
||||
}
|
||||
|
||||
impl TitlebarButton {
|
||||
pub fn new(
|
||||
id: impl Into<ElementId>,
|
||||
icon: TitlebarButtonType,
|
||||
hover_background_color: Rgba,
|
||||
close_window_action: Box<dyn Action>,
|
||||
) -> Self {
|
||||
Self {
|
||||
id: id.into(),
|
||||
icon,
|
||||
hover_background_color,
|
||||
close_window_action,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for TitlebarButton {
|
||||
fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
|
||||
let width = px(36.);
|
||||
|
||||
h_flex()
|
||||
.id(self.id)
|
||||
.justify_center()
|
||||
.content_center()
|
||||
.w(width)
|
||||
.h_full()
|
||||
.hover(|style| style.bg(self.hover_background_color))
|
||||
.active(|style| {
|
||||
let mut active_color = self.hover_background_color;
|
||||
active_color.a *= 0.2;
|
||||
|
||||
style.bg(active_color)
|
||||
})
|
||||
.child(Icon::new(match self.icon {
|
||||
TitlebarButtonType::Minimize => IconName::Dash,
|
||||
TitlebarButtonType::Restore => IconName::Minimize,
|
||||
TitlebarButtonType::Maximize => IconName::Maximize,
|
||||
TitlebarButtonType::Close => IconName::Close,
|
||||
}))
|
||||
.on_mouse_move(|_, cx| cx.stop_propagation())
|
||||
.on_click(move |_, cx| {
|
||||
cx.stop_propagation();
|
||||
match self.icon {
|
||||
TitlebarButtonType::Minimize => cx.minimize_window(),
|
||||
TitlebarButtonType::Restore => cx.zoom_window(),
|
||||
TitlebarButtonType::Maximize => cx.zoom_window(),
|
||||
TitlebarButtonType::Close => {
|
||||
cx.dispatch_action(self.close_window_action.boxed_clone())
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
use gpui::{AnyElement, Interactivity, Stateful};
|
||||
use gpui::{Action, AnyElement, Interactivity, Stateful};
|
||||
use smallvec::SmallVec;
|
||||
|
||||
use crate::components::title_bar::linux_window_controls::LinuxWindowControls;
|
||||
use crate::components::title_bar::windows_window_controls::WindowsWindowControls;
|
||||
use crate::prelude::*;
|
||||
|
||||
@ -9,6 +10,7 @@ pub struct TitleBar {
|
||||
platform_style: PlatformStyle,
|
||||
content: Stateful<Div>,
|
||||
children: SmallVec<[AnyElement; 2]>,
|
||||
close_window_action: Box<dyn Action>,
|
||||
}
|
||||
|
||||
impl TitleBar {
|
||||
@ -45,11 +47,12 @@ impl TitleBar {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new(id: impl Into<ElementId>) -> Self {
|
||||
pub fn new(id: impl Into<ElementId>, close_window_action: Box<dyn Action>) -> Self {
|
||||
Self {
|
||||
platform_style: PlatformStyle::platform(),
|
||||
content: div().id(id.into()),
|
||||
children: SmallVec::new(),
|
||||
close_window_action,
|
||||
}
|
||||
}
|
||||
|
||||
@ -111,5 +114,22 @@ impl RenderOnce for TitleBar {
|
||||
self.platform_style == PlatformStyle::Windows && !cx.is_fullscreen(),
|
||||
|title_bar| title_bar.child(WindowsWindowControls::new(height)),
|
||||
)
|
||||
.when(
|
||||
self.platform_style == PlatformStyle::Linux
|
||||
&& !cx.is_fullscreen()
|
||||
&& cx.should_render_window_controls(),
|
||||
|title_bar| {
|
||||
title_bar
|
||||
.child(LinuxWindowControls::new(height, self.close_window_action))
|
||||
.on_mouse_down(gpui::MouseButton::Right, move |ev, cx| {
|
||||
cx.show_window_menu(ev.position)
|
||||
})
|
||||
.on_mouse_move(move |ev, cx| {
|
||||
if ev.dragging() {
|
||||
cx.start_system_move();
|
||||
}
|
||||
})
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user