diff --git a/crates/collab_ui/src/collab_titlebar_item.rs b/crates/collab_ui/src/collab_titlebar_item.rs index fd1aa74779c9..314c6e5dc4e3 100644 --- a/crates/collab_ui/src/collab_titlebar_item.rs +++ b/crates/collab_ui/src/collab_titlebar_item.rs @@ -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); diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index 083c6d5fe11d..49c0b7ce7d4d 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -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); + 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 diff --git a/crates/gpui/src/platform/linux/wayland/client.rs b/crates/gpui/src/platform/linux/wayland/client.rs index b5aed4e941a2..4339aec9e947 100644 --- a/crates/gpui/src/platform/linux/wayland/client.rs +++ b/crates/gpui/src/platform/linux/wayland/client.rs @@ -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, pub wm_base: xdg_wm_base::XdgWmBase, pub shm: wl_shm::WlShm, + pub seat: wl_seat::WlSeat, pub viewporter: Option, pub fractional_scale_manager: Option, @@ -93,6 +95,7 @@ impl Globals { globals: GlobalList, executor: ForegroundExecutor, qh: QueueHandle, + 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 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 for WaylandClientStatePtr { }); state.mouse_focused_window = None; state.mouse_location = None; + state.button_pressed = None; drop(state); focused_window.handle_input(input); diff --git a/crates/gpui/src/platform/linux/wayland/window.rs b/crates/gpui/src/platform/linux/wayland/window.rs index 58e4641d6455..c09f2192bd06 100644 --- a/crates/gpui/src/platform/linux/wayland/window.rs +++ b/crates/gpui/src/platform/linux/wayland/window.rs @@ -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) { + 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)] diff --git a/crates/gpui/src/platform/linux/x11/window.rs b/crates/gpui/src/platform/linux/x11/window.rs index 77c050ac58e4..783af95d2ac5 100644 --- a/crates/gpui/src/platform/linux/x11/window.rs +++ b/crates/gpui/src/platform/linux/x11/window.rs @@ -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) {} + + // todo(linux) + fn start_system_move(&self) {} + + fn should_render_window_controls(&self) -> bool { + false + } } diff --git a/crates/gpui/src/platform/mac/window.rs b/crates/gpui/src/platform/mac/window.rs index eab3eddd2540..c4b8cb9f8c28 100644 --- a/crates/gpui/src/platform/mac/window.rs +++ b/crates/gpui/src/platform/mac/window.rs @@ -1100,6 +1100,14 @@ impl PlatformWindow for MacWindow { fn sprite_atlas(&self) -> Arc { self.0.lock().renderer.sprite_atlas().clone() } + + fn show_window_menu(&self, _position: Point) {} + + fn start_system_move(&self) {} + + fn should_render_window_controls(&self) -> bool { + false + } } impl rwh::HasWindowHandle for MacWindow { diff --git a/crates/gpui/src/platform/test/window.rs b/crates/gpui/src/platform/test/window.rs index 1b9654c56127..3b48ea6d87c7 100644 --- a/crates/gpui/src/platform/test/window.rs +++ b/crates/gpui/src/platform/test/window.rs @@ -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) { + unimplemented!() + } + + fn start_system_move(&self) { + unimplemented!() + } + + fn should_render_window_controls(&self) -> bool { + false + } } pub(crate) struct TestAtlasState { diff --git a/crates/gpui/src/platform/windows/window.rs b/crates/gpui/src/platform/windows/window.rs index 91e6af0fb5a6..4f31c5d97e2c 100644 --- a/crates/gpui/src/platform/windows/window.rs +++ b/crates/gpui/src/platform/windows/window.rs @@ -628,6 +628,14 @@ impl PlatformWindow for WindowsWindow { fn get_raw_handle(&self) -> HWND { self.0.hwnd } + + fn show_window_menu(&self, _position: Point) {} + + fn start_system_move(&self) {} + + fn should_render_window_controls(&self) -> bool { + false + } } #[implement(IDropTarget)] diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index 17236c07c6a1..34f7e0cc228b 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -1131,6 +1131,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) { + 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); diff --git a/crates/ui/src/components/stories/title_bar.rs b/crates/ui/src/components/stories/title_bar.rs index 78e0d85e308d..254e92ecaf6f 100644 --- a/crates/ui/src/components/stories/title_bar.rs +++ b/crates/ui/src/components/stories/title_bar.rs @@ -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), ) diff --git a/crates/ui/src/components/title_bar.rs b/crates/ui/src/components/title_bar.rs index 2eb181f9b2f8..28aac32c73f9 100644 --- a/crates/ui/src/components/title_bar.rs +++ b/crates/ui/src/components/title_bar.rs @@ -1,3 +1,4 @@ +mod linux_window_controls; mod title_bar; mod windows_window_controls; diff --git a/crates/ui/src/components/title_bar/linux_window_controls.rs b/crates/ui/src/components/title_bar/linux_window_controls.rs new file mode 100644 index 000000000000..a3cbebbcf13a --- /dev/null +++ b/crates/ui/src/components/title_bar/linux_window_controls.rs @@ -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, +} + +impl LinuxWindowControls { + pub fn new(button_height: Pixels, close_window_action: Box) -> 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, +} + +impl TitlebarButton { + pub fn new( + id: impl Into, + icon: TitlebarButtonType, + hover_background_color: Rgba, + close_window_action: Box, + ) -> 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()) + } + } + }) + } +} diff --git a/crates/ui/src/components/title_bar/title_bar.rs b/crates/ui/src/components/title_bar/title_bar.rs index ae083e6f9006..f80a4648f8e0 100644 --- a/crates/ui/src/components/title_bar/title_bar.rs +++ b/crates/ui/src/components/title_bar/title_bar.rs @@ -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
, children: SmallVec<[AnyElement; 2]>, + close_window_action: Box, } impl TitleBar { @@ -45,11 +47,12 @@ impl TitleBar { } } - pub fn new(id: impl Into) -> Self { + pub fn new(id: impl Into, close_window_action: Box) -> 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(); + } + }) + }, + ) } }