From d8605c86145e8c002a04bf92cc0173e47d925ba4 Mon Sep 17 00:00:00 2001 From: apricotbucket28 <71973804+apricotbucket28@users.noreply.github.com> Date: Sun, 26 May 2024 20:43:24 -0300 Subject: [PATCH] x11: Implement various window functions (#12070) This (mostly) allows the CSD added in https://github.com/zed-industries/zed/pull/11525 to work in X11. It's still a bit buggy as it detects a second window drag right after the first one finishes, but it's probably better to change the way window drags are detected in the title bar itself (as that causes other issues). The CSD can be tested by changing the return value of `should_render_window_controls` to true. Also fixes F11 crashing. Release Notes: - N/A --- crates/gpui/src/platform/linux/x11/client.rs | 10 +- crates/gpui/src/platform/linux/x11/event.rs | 2 +- crates/gpui/src/platform/linux/x11/window.rs | 214 ++++++++++++++++--- crates/gpui/src/window.rs | 4 +- 4 files changed, 195 insertions(+), 35 deletions(-) diff --git a/crates/gpui/src/platform/linux/x11/client.rs b/crates/gpui/src/platform/linux/x11/client.rs index f464889e1a31942a35e36c1dd17e38634eb5905d..77da484a7e00dd656590d29287e741ebcfbb97e5 100644 --- a/crates/gpui/src/platform/linux/x11/client.rs +++ b/crates/gpui/src/platform/linux/x11/client.rs @@ -38,11 +38,13 @@ use super::{ super::{open_uri_internal, SCROLL_LINES}, X11Display, X11WindowStatePtr, XcbAtoms, }; -use super::{button_from_mask, button_of_key, modifiers_from_state}; +use super::{button_of_key, modifiers_from_state, pressed_button_from_mask}; use super::{XimCallbackEvent, XimHandler}; use crate::platform::linux::is_within_click_distance; use crate::platform::linux::platform::DOUBLE_CLICK_INTERVAL; +pub(super) const XINPUT_MASTER_DEVICE: u16 = 1; + pub(crate) struct WindowRef { window: X11WindowStatePtr, refresh_event_token: RegistrationToken, @@ -183,7 +185,7 @@ impl X11Client { ); let master_device_query = xcb_connection - .xinput_xi_query_device(1_u16) + .xinput_xi_query_device(XINPUT_MASTER_DEVICE) .unwrap() .reply() .unwrap(); @@ -591,7 +593,7 @@ impl X11Client { Event::XinputMotion(event) => { let window = self.get_window(event.event)?; let state = self.0.borrow(); - let pressed_button = button_from_mask(event.button_mask[0]); + let pressed_button = pressed_button_from_mask(event.button_mask[0]); let position = point( px(event.event_x as f32 / u16::MAX as f32 / state.scale_factor), px(event.event_y as f32 / u16::MAX as f32 / state.scale_factor), @@ -674,7 +676,7 @@ impl X11Client { let window = self.get_window(event.event)?; let state = self.0.borrow(); - let pressed_button = button_from_mask(event.buttons[0]); + let pressed_button = pressed_button_from_mask(event.buttons[0]); let position = point( px(event.event_x as f32 / u16::MAX as f32 / state.scale_factor), px(event.event_y as f32 / u16::MAX as f32 / state.scale_factor), diff --git a/crates/gpui/src/platform/linux/x11/event.rs b/crates/gpui/src/platform/linux/x11/event.rs index fb16a854909b6bc0e30e5f3427c04d4c2ff5f844..18ec392fc657efefe8cdcea4df9dcef27d8db4be 100644 --- a/crates/gpui/src/platform/linux/x11/event.rs +++ b/crates/gpui/src/platform/linux/x11/event.rs @@ -37,7 +37,7 @@ pub(crate) fn modifiers_from_xinput_info(modifier_info: xinput::ModifierInfo) -> } } -pub(crate) fn button_from_mask(button_mask: u32) -> Option { +pub(crate) fn pressed_button_from_mask(button_mask: u32) -> Option { Some(if button_mask & 2 == 2 { MouseButton::Left } else if button_mask & 4 == 4 { diff --git a/crates/gpui/src/platform/linux/x11/window.rs b/crates/gpui/src/platform/linux/x11/window.rs index 7f500b4ed459bb7b934d8f4bbc1b79760b4a5d80..9eb63f4377d2cce27ce26ef86a642328ff427fdc 100644 --- a/crates/gpui/src/platform/linux/x11/window.rs +++ b/crates/gpui/src/platform/linux/x11/window.rs @@ -16,9 +16,12 @@ use util::ResultExt; use x11rb::{ connection::{Connection as _, RequestConnection as _}, protocol::{ - render::{self, ConnectionExt as _}, + render, xinput::{self, ConnectionExt as _}, - xproto::{self, ConnectionExt as _, CreateWindowAux}, + xproto::{ + self, Atom, ClientMessageEvent, ConnectionExt as _, CreateWindowAux, EventMask, + TranslateCoordinatesReply, + }, }, resource_manager::Database, wrapper::ConnectionExt as _, @@ -38,17 +41,23 @@ use std::{ sync::{self, Arc}, }; -use super::X11Display; +use super::{X11Display, XINPUT_MASTER_DEVICE}; x11rb::atom_manager! { pub XcbAtoms: AtomsCookie { UTF8_STRING, WM_PROTOCOLS, WM_DELETE_WINDOW, + WM_CHANGE_STATE, _NET_WM_NAME, _NET_WM_STATE, _NET_WM_STATE_MAXIMIZED_VERT, _NET_WM_STATE_MAXIMIZED_HORZ, + _NET_WM_STATE_FULLSCREEN, + _NET_WM_STATE_HIDDEN, + _NET_WM_STATE_FOCUSED, + _NET_WM_MOVERESIZE, + _GTK_SHOW_WINDOW_MENU, } } @@ -158,6 +167,7 @@ pub(crate) struct X11WindowState { client: X11ClientStatePtr, executor: ForegroundExecutor, atoms: XcbAtoms, + x_root_window: xproto::Window, raw: RawWindow, bounds: Bounds, scale_factor: f32, @@ -311,7 +321,7 @@ impl X11WindowState { .xinput_xi_select_events( x_window, &[xinput::EventMask { - deviceid: 1, + deviceid: XINPUT_MASTER_DEVICE, mask: vec![ xinput::XIEventMask::MOTION | xinput::XIEventMask::BUTTON_PRESS @@ -359,6 +369,7 @@ impl X11WindowState { executor, display: Rc::new(X11Display::new(xcb_connection, x_screen_index).unwrap()), raw, + x_root_window: visual_set.root, bounds: params.bounds.map(|v| v.0), scale_factor, renderer: BladeRenderer::new(gpu, config), @@ -403,6 +414,12 @@ impl Drop for X11Window { } } +enum WmHintPropertyState { + Remove = 0, + Add = 1, + Toggle = 2, +} + impl X11Window { #[allow(clippy::too_many_arguments)] pub fn new( @@ -431,6 +448,63 @@ impl X11Window { x_window, }) } + + fn set_wm_hints(&self, wm_hint_property_state: WmHintPropertyState, prop1: u32, prop2: u32) { + let state = self.0.state.borrow(); + let message = ClientMessageEvent::new( + 32, + self.0.x_window, + state.atoms._NET_WM_STATE, + [wm_hint_property_state as u32, prop1, prop2, 1, 0], + ); + self.0 + .xcb_connection + .send_event( + false, + state.x_root_window, + EventMask::SUBSTRUCTURE_REDIRECT | EventMask::SUBSTRUCTURE_NOTIFY, + message, + ) + .unwrap(); + } + + fn get_wm_hints(&self) -> Vec { + let reply = self + .0 + .xcb_connection + .get_property( + false, + self.0.x_window, + self.0.state.borrow().atoms._NET_WM_STATE, + xproto::AtomEnum::ATOM, + 0, + u32::MAX, + ) + .unwrap() + .reply() + .unwrap(); + // Reply is in u8 but atoms are represented as u32 + reply + .value + .chunks_exact(4) + .map(|chunk| u32::from_ne_bytes([chunk[0], chunk[1], chunk[2], chunk[3]])) + .collect() + } + + fn get_root_position(&self, position: Point) -> TranslateCoordinatesReply { + let state = self.0.state.borrow(); + self.0 + .xcb_connection + .translate_coordinates( + self.0.x_window, + state.x_root_window, + (position.x.0 * state.scale_factor) as i16, + (position.y.0 * state.scale_factor) as i16, + ) + .unwrap() + .reply() + .unwrap() + } } impl X11WindowStatePtr { @@ -555,12 +629,15 @@ impl PlatformWindow for X11Window { self.0.state.borrow().bounds.map(|v| v.into()) } - // todo(linux) fn is_maximized(&self) -> bool { - false + 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) } - // todo(linux) fn window_bounds(&self) -> WindowBounds { let state = self.0.state.borrow(); WindowBounds::Windowed(state.bounds.map(|p| DevicePixels(p))) @@ -630,9 +707,10 @@ impl PlatformWindow for X11Window { .log_err(); } - // todo(linux) fn is_active(&self) -> bool { - false + let state = self.0.state.borrow(); + self.get_wm_hints() + .contains(&state.atoms._NET_WM_STATE_FOCUSED) } fn set_title(&mut self, title: &str) { @@ -665,13 +743,16 @@ impl PlatformWindow for X11Window { data.push(b'\0'); data.extend(app_id.bytes()); // class - self.0.xcb_connection.change_property8( - xproto::PropMode::REPLACE, - self.0.x_window, - xproto::AtomEnum::WM_CLASS, - xproto::AtomEnum::STRING, - &data, - ); + self.0 + .xcb_connection + .change_property8( + xproto::PropMode::REPLACE, + self.0.x_window, + xproto::AtomEnum::WM_CLASS, + xproto::AtomEnum::STRING, + &data, + ) + .unwrap(); } // todo(linux) @@ -693,24 +774,48 @@ impl PlatformWindow for X11Window { unimplemented!() } - // todo(linux) fn minimize(&self) { - unimplemented!() + let state = self.0.state.borrow(); + const WINDOW_ICONIC_STATE: u32 = 3; + let message = ClientMessageEvent::new( + 32, + self.0.x_window, + state.atoms.WM_CHANGE_STATE, + [WINDOW_ICONIC_STATE, 0, 0, 0, 0], + ); + self.0 + .xcb_connection + .send_event( + false, + state.x_root_window, + EventMask::SUBSTRUCTURE_REDIRECT | EventMask::SUBSTRUCTURE_NOTIFY, + message, + ) + .unwrap(); } - // todo(linux) fn zoom(&self) { - unimplemented!() + let state = self.0.state.borrow(); + self.set_wm_hints( + WmHintPropertyState::Toggle, + state.atoms._NET_WM_STATE_MAXIMIZED_VERT, + state.atoms._NET_WM_STATE_MAXIMIZED_HORZ, + ); } - // todo(linux) fn toggle_fullscreen(&self) { - unimplemented!() + let state = self.0.state.borrow(); + self.set_wm_hints( + WmHintPropertyState::Toggle, + state.atoms._NET_WM_STATE_FULLSCREEN, + xproto::AtomEnum::NONE.into(), + ); } - // todo(linux) fn is_fullscreen(&self) -> bool { - false + let state = self.0.state.borrow(); + self.get_wm_hints() + .contains(&state.atoms._NET_WM_STATE_FULLSCREEN) } fn on_request_frame(&self, callback: Box) { @@ -755,11 +860,64 @@ impl PlatformWindow for X11Window { inner.renderer.sprite_atlas().clone() } - // todo(linux) - fn show_window_menu(&self, _position: Point) {} + fn show_window_menu(&self, position: Point) { + let state = self.0.state.borrow(); + let coords = self.get_root_position(position); + let message = ClientMessageEvent::new( + 32, + self.0.x_window, + state.atoms._GTK_SHOW_WINDOW_MENU, + [ + XINPUT_MASTER_DEVICE as u32, + coords.dst_x as u32, + coords.dst_y as u32, + 0, + 0, + ], + ); + self.0 + .xcb_connection + .send_event( + false, + state.x_root_window, + EventMask::SUBSTRUCTURE_REDIRECT | EventMask::SUBSTRUCTURE_NOTIFY, + message, + ) + .unwrap(); + } - // todo(linux) - fn start_system_move(&self) {} + 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(); + 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(); + } fn should_render_window_controls(&self) -> bool { false diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index 370a082cb86f9b4a6936e19e848b0fe201e99bb5..253bb3639665664ea0f0c0d4dbd8151b6fd77c27 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -1147,12 +1147,12 @@ impl<'a> WindowContext<'a> { self.window.platform_window.zoom(); } - /// Opens the native title bar context menu, useful when implementing client side decorations (Wayland only) + /// Opens the native title bar context menu, useful when implementing client side decorations (Wayland and X11) 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) + /// 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) {