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) {