Detailed changes
@@ -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;
@@ -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();
+ }
+ })
+ },
+ )
}
}