From f82b2741cda64836ea5e29aaeb90a8334e00b3f3 Mon Sep 17 00:00:00 2001 From: Roman <40907255+witelokk@users.noreply.github.com> Date: Sat, 17 Feb 2024 00:49:34 +0300 Subject: [PATCH] Wayland input handling (#7857) Adds initial keyboard and mouse input for Wayland (thanks to @gabydd and @kvark for reference). Release Notes: - N/A --------- Co-authored-by: Mikayla Maki --- crates/gpui/Cargo.toml | 2 +- .../gpui/src/platform/linux/wayland/client.rs | 360 +++++++++++++++++- .../gpui/src/platform/linux/wayland/window.rs | 22 +- 3 files changed, 374 insertions(+), 10 deletions(-) diff --git a/crates/gpui/Cargo.toml b/crates/gpui/Cargo.toml index 7272fd4e43..02c16b841b 100644 --- a/crates/gpui/Cargo.toml +++ b/crates/gpui/Cargo.toml @@ -107,6 +107,7 @@ xcb = { version = "1.3", features = ["as-raw-xcb-connection", "present", "randr" wayland-client= { version = "0.31.2" } wayland-protocols = { version = "0.31.2", features = ["client"] } wayland-backend = { version = "0.3.3", features = ["client_system"] } +xkbcommon = { version = "0.7", features = ["wayland", "x11"] } as-raw-xcb-connection = "1" #TODO: use these on all platforms blade-graphics.workspace = true @@ -114,4 +115,3 @@ blade-macros.workspace = true blade-rwh.workspace = true bytemuck = "1" cosmic-text = "0.10.0" -xkbcommon = { version = "0.7", features = ["x11"] } diff --git a/crates/gpui/src/platform/linux/wayland/client.rs b/crates/gpui/src/platform/linux/wayland/client.rs index f058833f4d..1d64d96e40 100644 --- a/crates/gpui/src/platform/linux/wayland/client.rs +++ b/crates/gpui/src/platform/linux/wayland/client.rs @@ -2,32 +2,50 @@ use std::rc::Rc; use std::sync::Arc; use parking_lot::Mutex; +use wayland_backend::protocol::WEnum; use wayland_client::protocol::wl_callback::WlCallback; +use wayland_client::protocol::wl_pointer::AxisRelativeDirection; use wayland_client::{ delegate_noop, protocol::{ - wl_buffer, wl_callback, wl_compositor, wl_keyboard, wl_registry, wl_seat, wl_shm, - wl_shm_pool, + wl_buffer, wl_callback, wl_compositor, wl_keyboard, wl_pointer, wl_registry, wl_seat, + wl_shm, wl_shm_pool, wl_surface::{self, WlSurface}, }, Connection, Dispatch, EventQueue, Proxy, QueueHandle, }; use wayland_protocols::xdg::shell::client::{xdg_surface, xdg_toplevel, xdg_wm_base}; +use xkbcommon::xkb; +use xkbcommon::xkb::ffi::XKB_KEYMAP_FORMAT_TEXT_V1; +use xkbcommon::xkb::{Keycode, KEYMAP_COMPILE_NO_FLAGS}; use crate::platform::linux::client::Client; use crate::platform::linux::wayland::window::WaylandWindow; use crate::platform::{LinuxPlatformInner, PlatformWindow}; +use crate::PlatformInput::KeyDown; +use crate::ScrollDelta; use crate::{ - platform::linux::wayland::window::WaylandWindowState, AnyWindowHandle, DisplayId, - PlatformDisplay, WindowOptions, + platform::linux::wayland::window::WaylandWindowState, AnyWindowHandle, DisplayId, KeyDownEvent, + KeyUpEvent, Keystroke, Modifiers, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, + Pixels, PlatformDisplay, PlatformInput, Point, ScrollWheelEvent, TouchPhase, WindowOptions, }; +const MIN_KEYCODE: u32 = 8; // used to convert evdev scancode to xkb scancode + pub(crate) struct WaylandClientState { compositor: Option, buffer: Option, wm_base: Option, windows: Vec<(xdg_surface::XdgSurface, Rc)>, platform_inner: Rc, + wl_seat: Option, + keymap_state: Option, + modifiers: Modifiers, + scroll_direction: f64, + mouse_location: Option>, + button_pressed: Option, + mouse_focused_window: Option>, + keyboard_focused_window: Option>, } pub(crate) struct WaylandClient { @@ -46,6 +64,20 @@ impl WaylandClient { wm_base: None, windows: Vec::new(), platform_inner: Rc::clone(&linux_platform_inner), + wl_seat: None, + keymap_state: None, + modifiers: Modifiers { + shift: false, + control: false, + alt: false, + function: false, + command: false, + }, + scroll_direction: -1.0, + mouse_location: None, + button_pressed: None, + mouse_focused_window: None, + keyboard_focused_window: None, }; let event_queue: EventQueue = conn.new_event_queue(); let qh = event_queue.handle(); @@ -141,6 +173,10 @@ impl Dispatch for WaylandClientState { let wm_base = registry.bind::(name, 1, qh, ()); state.wm_base = Some(wm_base); } + "wl_seat" => { + let seat = registry.bind::(name, 1, qh, ()); + state.wl_seat = Some(seat); + } _ => {} }; } @@ -152,8 +188,6 @@ delegate_noop!(WaylandClientState: ignore wl_surface::WlSurface); delegate_noop!(WaylandClientState: ignore wl_shm::WlShm); delegate_noop!(WaylandClientState: ignore wl_shm_pool::WlShmPool); delegate_noop!(WaylandClientState: ignore wl_buffer::WlBuffer); -delegate_noop!(WaylandClientState: ignore wl_seat::WlSeat); -delegate_noop!(WaylandClientState: ignore wl_keyboard::WlKeyboard); impl Dispatch> for WaylandClientState { fn event( @@ -251,3 +285,317 @@ impl Dispatch for WaylandClientState { } } } + +impl Dispatch for WaylandClientState { + fn event( + state: &mut Self, + seat: &wl_seat::WlSeat, + event: wl_seat::Event, + data: &(), + conn: &Connection, + qh: &QueueHandle, + ) { + if let wl_seat::Event::Capabilities { + capabilities: WEnum::Value(capabilities), + } = event + { + if capabilities.contains(wl_seat::Capability::Keyboard) { + seat.get_keyboard(qh, ()); + } + if capabilities.contains(wl_seat::Capability::Pointer) { + seat.get_pointer(qh, ()); + } + } + } +} + +impl Dispatch for WaylandClientState { + fn event( + state: &mut Self, + keyboard: &wl_keyboard::WlKeyboard, + event: wl_keyboard::Event, + data: &(), + conn: &Connection, + qh: &QueueHandle, + ) { + match event { + wl_keyboard::Event::Keymap { + format: WEnum::Value(format), + fd, + size, + .. + } => { + assert_eq!( + format, + wl_keyboard::KeymapFormat::XkbV1, + "Unsupported keymap format" + ); + let keymap = unsafe { + xkb::Keymap::new_from_fd( + &xkb::Context::new(xkb::CONTEXT_NO_FLAGS), + fd, + size as usize, + XKB_KEYMAP_FORMAT_TEXT_V1, + KEYMAP_COMPILE_NO_FLAGS, + ) + .unwrap() + } + .unwrap(); + state.keymap_state = Some(xkb::State::new(&keymap)); + } + wl_keyboard::Event::Enter { surface, .. } => { + for window in &state.windows { + if window.1.surface.id() == surface.id() { + state.keyboard_focused_window = Some(Rc::clone(&window.1)); + } + } + } + wl_keyboard::Event::Modifiers { + mods_depressed, + mods_latched, + mods_locked, + group, + .. + } => { + state.keymap_state.as_mut().unwrap().update_mask( + mods_depressed, + mods_latched, + mods_locked, + 0, + 0, + group, + ); + } + wl_keyboard::Event::Key { + key, + state: WEnum::Value(key_state), + .. + } => { + let keymap_state = state.keymap_state.as_ref().unwrap(); + let key_utf8 = keymap_state.key_get_utf8(Keycode::from(key + MIN_KEYCODE)); + let key_sym = keymap_state.key_get_one_sym(Keycode::from(key + MIN_KEYCODE)); + + let key = if matches!( + key_sym, + xkb::Keysym::BackSpace + | xkb::Keysym::Left + | xkb::Keysym::Right + | xkb::Keysym::Down + | xkb::Keysym::Up + | xkb::Keysym::Super_L + | xkb::Keysym::Super_R + ) { + xkb::keysym_get_name(key_sym).to_lowercase() + } else { + key_utf8.clone() + }; + + let focused_window = &state.keyboard_focused_window; + if let Some(focused_window) = focused_window { + match key_state { + wl_keyboard::KeyState::Pressed => { + if key_sym == xkb::Keysym::Shift_L || key_sym == xkb::Keysym::Shift_R { + state.modifiers.shift = true; + } else if key_sym == xkb::Keysym::Control_L + || key_sym == xkb::Keysym::Control_R + { + state.modifiers.control = true; + } else if key_sym == xkb::Keysym::Alt_L || key_sym == xkb::Keysym::Alt_R + { + state.modifiers.alt = true; + } else { + focused_window.handle_input(KeyDown(KeyDownEvent { + keystroke: Keystroke { + modifiers: state.modifiers, + key, + ime_key: None, + }, + is_held: false, // todo!(linux) + })); + } + } + wl_keyboard::KeyState::Released => { + if key_sym == xkb::Keysym::Shift_L || key_sym == xkb::Keysym::Shift_R { + state.modifiers.shift = false; + } else if key_sym == xkb::Keysym::Control_L + || key_sym == xkb::Keysym::Control_R + { + state.modifiers.control = false; + } else if key_sym == xkb::Keysym::Alt_L || key_sym == xkb::Keysym::Alt_R + { + state.modifiers.alt = false; + } else { + focused_window.handle_input(PlatformInput::KeyUp(KeyUpEvent { + keystroke: Keystroke { + modifiers: state.modifiers, + key, + ime_key: None, + }, + })); + } + } + _ => {} + } + } + } + wl_keyboard::Event::Leave { .. } => { + state.modifiers = Modifiers { + control: false, + alt: false, + shift: false, + command: false, + function: false, + }; + } + _ => {} + } + } +} + +fn linux_button_to_gpui(button: u32) -> MouseButton { + match button { + 0x110 => MouseButton::Left, + 0x111 => MouseButton::Right, + 0x112 => MouseButton::Middle, + _ => unimplemented!(), // todo!(linux) + } +} + +impl Dispatch for WaylandClientState { + fn event( + state: &mut Self, + wl_pointer: &wl_pointer::WlPointer, + event: wl_pointer::Event, + data: &(), + conn: &Connection, + qh: &QueueHandle, + ) { + match event { + wl_pointer::Event::Enter { + surface, + surface_x, + surface_y, + .. + } => { + for window in &state.windows { + if window.1.surface.id() == surface.id() { + state.mouse_focused_window = Some(Rc::clone(&window.1)); + } + } + state.mouse_location = Some(Point { + x: Pixels::from(surface_x), + y: Pixels::from(surface_y), + }); + } + wl_pointer::Event::Motion { + time, + surface_x, + surface_y, + .. + } => { + let focused_window = &state.mouse_focused_window; + if let Some(focused_window) = focused_window { + state.mouse_location = Some(Point { + x: Pixels::from(surface_x), + y: Pixels::from(surface_y), + }); + focused_window.handle_input(PlatformInput::MouseMove(MouseMoveEvent { + position: state.mouse_location.unwrap(), + pressed_button: state.button_pressed, + modifiers: state.modifiers, + })) + } + } + wl_pointer::Event::Button { + button, + state: WEnum::Value(button_state), + .. + } => { + let focused_window = &state.mouse_focused_window; + let mouse_location = &state.mouse_location; + if let (Some(focused_window), Some(mouse_location)) = + (focused_window, mouse_location) + { + match button_state { + wl_pointer::ButtonState::Pressed => { + state.button_pressed = Some(linux_button_to_gpui(button)); + focused_window.handle_input(PlatformInput::MouseDown(MouseDownEvent { + button: linux_button_to_gpui(button), + position: *mouse_location, + modifiers: state.modifiers, + click_count: 1, + })); + } + wl_pointer::ButtonState::Released => { + state.button_pressed = None; + focused_window.handle_input(PlatformInput::MouseUp(MouseUpEvent { + button: linux_button_to_gpui(button), + position: *mouse_location, + modifiers: Modifiers { + shift: false, + control: false, + alt: false, + function: false, + command: false, + }, + click_count: 1, + })); + } + _ => {} + } + } + } + wl_pointer::Event::AxisRelativeDirection { + direction: WEnum::Value(direction), + .. + } => { + state.scroll_direction = match direction { + AxisRelativeDirection::Identical => -1.0, + AxisRelativeDirection::Inverted => 1.0, + _ => -1.0, + } + } + wl_pointer::Event::Axis { + time, + axis: WEnum::Value(axis), + value, + .. + } => { + let focused_window = &state.mouse_focused_window; + let mouse_location = &state.mouse_location; + if let (Some(focused_window), Some(mouse_location)) = + (focused_window, mouse_location) + { + let value = value * state.scroll_direction; + focused_window.handle_input(PlatformInput::ScrollWheel(ScrollWheelEvent { + position: *mouse_location, + delta: match axis { + wl_pointer::Axis::VerticalScroll => { + ScrollDelta::Pixels(Point::new(Pixels(0.0), Pixels(value as f32))) + } + wl_pointer::Axis::HorizontalScroll => { + ScrollDelta::Pixels(Point::new(Pixels(value as f32), Pixels(0.0))) + } + _ => unimplemented!(), + }, + modifiers: state.modifiers, + touch_phase: TouchPhase::Started, + })) + } + } + wl_pointer::Event::Leave { surface, .. } => { + let focused_window = &state.mouse_focused_window; + if let Some(focused_window) = focused_window { + focused_window.handle_input(PlatformInput::MouseMove(MouseMoveEvent { + position: Point::::default(), + pressed_button: None, + modifiers: Modifiers::default(), + })); + } + state.mouse_focused_window = None; + state.mouse_location = None; + } + _ => {} + } + } +} diff --git a/crates/gpui/src/platform/linux/wayland/window.rs b/crates/gpui/src/platform/linux/wayland/window.rs index 247c142b11..18eb5aad55 100644 --- a/crates/gpui/src/platform/linux/wayland/window.rs +++ b/crates/gpui/src/platform/linux/wayland/window.rs @@ -38,6 +38,7 @@ pub(crate) struct Callbacks { struct WaylandWindowInner { renderer: BladeRenderer, bounds: Bounds, + input_handler: Option, } struct RawWindow { @@ -91,6 +92,7 @@ impl WaylandWindowInner { Self { renderer: BladeRenderer::new(gpu, extent), bounds, + input_handler: None, } } } @@ -176,6 +178,20 @@ impl WaylandWindowState { } self.toplevel.destroy(); } + + pub fn handle_input(&self, input: PlatformInput) { + if let Some(ref mut fun) = self.callbacks.lock().input { + if fun(input.clone()) { + return; + } + } + if let PlatformInput::KeyDown(event) = input { + let mut inner = self.inner.lock(); + if let Some(ref mut input_handler) = inner.input_handler { + input_handler.replace_text_in_range(None, &event.keystroke.key); + } + } + } } #[derive(Clone)] @@ -244,12 +260,12 @@ impl PlatformWindow for WaylandWindow { } fn set_input_handler(&mut self, input_handler: PlatformInputHandler) { - //todo!(linux) + self.0.inner.lock().input_handler = Some(input_handler); } //todo!(linux) fn take_input_handler(&mut self) -> Option { - None + self.0.inner.lock().input_handler.take() } //todo!(linux) @@ -296,7 +312,7 @@ impl PlatformWindow for WaylandWindow { } fn on_input(&self, callback: Box bool>) { - //todo!(linux) + self.0.callbacks.lock().input = Some(callback); } fn on_active_status_change(&self, callback: Box) {