use std::{ cell::{RefCell, RefMut}, hash::Hash, os::fd::{AsRawFd, BorrowedFd}, path::PathBuf, rc::{Rc, Weak}, time::{Duration, Instant},}; use ashpd::WindowIdentifier;use calloop::{ EventLoop, LoopHandle, timer::{TimeoutAction, Timer},};use calloop_wayland_source::WaylandSource;use collections::HashMap;use filedescriptor::Pipe;use gpui_util::ResultExt as _;use http_client::Url;use smallvec::SmallVec;use wayland_backend::client::ObjectId;use wayland_backend::protocol::WEnum;use wayland_client::event_created_child;use wayland_client::globals::{GlobalList, GlobalListContents, registry_queue_init};use wayland_client::protocol::wl_callback::{self, WlCallback};use wayland_client::protocol::wl_data_device_manager::DndAction;use wayland_client::protocol::wl_data_offer::WlDataOffer;use wayland_client::protocol::wl_pointer::AxisSource;use wayland_client::protocol::{ wl_data_device, wl_data_device_manager, wl_data_offer, wl_data_source, wl_output, wl_region,};use wayland_client::{ Connection, Dispatch, Proxy, QueueHandle, delegate_noop, protocol::{ wl_buffer, wl_compositor, wl_keyboard, wl_pointer, wl_registry, wl_seat, wl_shm, wl_shm_pool, wl_surface, },};use wayland_protocols::wp::pointer_gestures::zv1::client::{ zwp_pointer_gesture_pinch_v1, zwp_pointer_gestures_v1,};use wayland_protocols::wp::primary_selection::zv1::client::zwp_primary_selection_offer_v1::{ self, ZwpPrimarySelectionOfferV1,};use wayland_protocols::wp::primary_selection::zv1::client::{ zwp_primary_selection_device_manager_v1, zwp_primary_selection_device_v1, zwp_primary_selection_source_v1,};use wayland_protocols::wp::text_input::zv3::client::zwp_text_input_v3::{ ContentHint, ContentPurpose,};use wayland_protocols::wp::text_input::zv3::client::{ zwp_text_input_manager_v3, zwp_text_input_v3,};use wayland_protocols::wp::viewporter::client::{wp_viewport, wp_viewporter};use wayland_protocols::xdg::activation::v1::client::{xdg_activation_token_v1, xdg_activation_v1};use wayland_protocols::xdg::decoration::zv1::client::{ zxdg_decoration_manager_v1, zxdg_toplevel_decoration_v1,};use wayland_protocols::xdg::shell::client::{ xdg_popup, xdg_positioner, xdg_surface, xdg_toplevel, xdg_wm_base,};use wayland_protocols::xdg::system_bell::v1::client::xdg_system_bell_v1;use wayland_protocols::{ wp::cursor_shape::v1::client::{wp_cursor_shape_device_v1, wp_cursor_shape_manager_v1}, xdg::dialog::v1::client::xdg_wm_dialog_v1::{self, XdgWmDialogV1},};use wayland_protocols::{ wp::fractional_scale::v1::client::{wp_fractional_scale_manager_v1, wp_fractional_scale_v1}, xdg::dialog::v1::client::xdg_dialog_v1::XdgDialogV1,};use wayland_protocols_plasma::blur::client::{org_kde_kwin_blur, org_kde_kwin_blur_manager};use wayland_protocols_wlr::layer_shell::v1::client::{zwlr_layer_shell_v1, zwlr_layer_surface_v1};use xkbcommon::xkb::ffi::XKB_KEYMAP_FORMAT_TEXT_V1;use xkbcommon::xkb::{self, KEYMAP_COMPILE_NO_FLAGS, Keycode}; use super::{ display::WaylandDisplay, window::{ImeInput, WaylandWindowStatePtr},}; use crate::linux::{ DOUBLE_CLICK_INTERVAL, LinuxClient, LinuxCommon, LinuxKeyboardLayout, PIPE_READ_TIMEOUT, SCROLL_LINES, capslock_from_xkb, cursor_style_to_icon_names, get_xkb_compose_state, is_within_click_distance, keystroke_from_xkb, keystroke_underlying_dead_key, modifiers_from_xkb, open_uri_internal, read_fd_with_timeout, reveal_path_internal, wayland::{ clipboard::{Clipboard, DataOffer, FILE_LIST_MIME_TYPE, TEXT_MIME_TYPES}, cursor::Cursor, serial::{Serial, SerialKind, SerialTracker}, to_shape, window::WaylandWindow, }, xdg_desktop_portal::{Event as XDPEvent, XDPEventSource},};use gpui::{ AnyWindowHandle, Bounds, Capslock, CursorStyle, DevicePixels, DisplayId, FileDropEvent, ForegroundExecutor, KeyDownEvent, KeyUpEvent, Keystroke, Modifiers, ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseExitEvent, MouseMoveEvent, MouseUpEvent, NavigationDirection, Pixels, PlatformDisplay, PlatformInput, PlatformKeyboardLayout, PlatformWindow, Point, ScrollDelta, ScrollWheelEvent, SharedString, Size, TouchPhase, WindowButtonLayout, WindowKind, WindowParams, point, profiler, px, size,};use gpui_wgpu::{CompositorGpuHint, GpuContext};use wayland_protocols::wp::linux_dmabuf::zv1::client::{ zwp_linux_dmabuf_feedback_v1, zwp_linux_dmabuf_v1,}; /// Used to convert evdev scancode to xkb scancodeconst MIN_KEYCODE: u32 = 8; const UNKNOWN_KEYBOARD_LAYOUT_NAME: SharedString = SharedString::new_static("unknown");const XDG_ACTIVATION_TOKEN_ENV_VAR: &str = "XDG_ACTIVATION_TOKEN"; #[derive(Clone, Copy, Debug, PartialEq, Eq)]struct ImeCursorRectangle { x: i32, y: i32, width: i32, height: i32,} impl From<Bounds<Pixels>> for ImeCursorRectangle { fn from(bounds: Bounds<Pixels>) -> Self { Self { x: bounds.origin.x.as_f32() as i32, y: bounds.origin.y.as_f32() as i32, width: bounds.size.width.as_f32() as i32, height: bounds.size.height.as_f32() as i32, } }} trait ImeCursorRectangleSink { fn set_ime_cursor_rectangle(&self, x: i32, y: i32, width: i32, height: i32); fn commit_ime_state(&self);} impl ImeCursorRectangleSink for zwp_text_input_v3::ZwpTextInputV3 { fn set_ime_cursor_rectangle(&self, x: i32, y: i32, width: i32, height: i32) { self.set_cursor_rectangle(x, y, width, height); } fn commit_ime_state(&self) { self.commit(); }} fn set_ime_cursor_rectangle( text_input: &impl ImeCursorRectangleSink, cursor_rectangle: ImeCursorRectangle,) { text_input.set_ime_cursor_rectangle( cursor_rectangle.x, cursor_rectangle.y, cursor_rectangle.width, cursor_rectangle.height, );} fn update_ime_cursor_rectangle( text_input: &impl ImeCursorRectangleSink, last_ime_cursor_rectangle: &mut Option<ImeCursorRectangle>, bounds: Bounds<Pixels>,) { let cursor_rectangle = ImeCursorRectangle::from(bounds); if *last_ime_cursor_rectangle == Some(cursor_rectangle) { return; } *last_ime_cursor_rectangle = Some(cursor_rectangle); set_ime_cursor_rectangle(text_input, cursor_rectangle); text_input.commit_ime_state();} fn set_ime_cursor_rectangle_after_done( text_input: &impl ImeCursorRectangleSink, last_ime_cursor_rectangle: &mut Option<ImeCursorRectangle>, bounds: Bounds<Pixels>, should_commit: bool,) { if should_commit { update_ime_cursor_rectangle(text_input, last_ime_cursor_rectangle, bounds); } else { set_ime_cursor_rectangle(text_input, ImeCursorRectangle::from(bounds)); }} fn take_startup_activation_token_from_environment() -> Option<String> { let startup_activation_token = std::env::var(XDG_ACTIVATION_TOKEN_ENV_VAR) .ok() .filter(|token| !token.is_empty()); // The token must be removed from the environment so it isn't inherited by child // processes we spawn, per the xdg-activation spec: https://wayland.app/protocols/xdg-activation-v1 // SAFETY: This runs during Wayland platform initialization before GPUI starts // concurrent environment access or spawning child processes. unsafe { std::env::remove_var(XDG_ACTIVATION_TOKEN_ENV_VAR) }; startup_activation_token} #[derive(Clone)]pub struct Globals { pub qh: QueueHandle<WaylandClientStatePtr>, pub activation: Option<xdg_activation_v1::XdgActivationV1>, pub compositor: wl_compositor::WlCompositor, pub cursor_shape_manager: Option<wp_cursor_shape_manager_v1::WpCursorShapeManagerV1>, pub data_device_manager: Option<wl_data_device_manager::WlDataDeviceManager>, pub primary_selection_manager: Option<zwp_primary_selection_device_manager_v1::ZwpPrimarySelectionDeviceManagerV1>, 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>, pub decoration_manager: Option<zxdg_decoration_manager_v1::ZxdgDecorationManagerV1>, pub layer_shell: Option<zwlr_layer_shell_v1::ZwlrLayerShellV1>, pub blur_manager: Option<org_kde_kwin_blur_manager::OrgKdeKwinBlurManager>, pub text_input_manager: Option<zwp_text_input_manager_v3::ZwpTextInputManagerV3>, pub gesture_manager: Option<zwp_pointer_gestures_v1::ZwpPointerGesturesV1>, pub dialog: Option<xdg_wm_dialog_v1::XdgWmDialogV1>, pub system_bell: Option<xdg_system_bell_v1::XdgSystemBellV1>, pub executor: ForegroundExecutor,} impl Globals { fn new( globals: GlobalList, executor: ForegroundExecutor, qh: QueueHandle<WaylandClientStatePtr>, seat: wl_seat::WlSeat, ) -> Self { let dialog_v = XdgWmDialogV1::interface().version; Globals { activation: globals.bind(&qh, 1..=1, ()).ok(), compositor: globals .bind( &qh, wl_surface::REQ_SET_BUFFER_SCALE_SINCE ..=wl_surface::EVT_PREFERRED_BUFFER_SCALE_SINCE, (), ) .unwrap(), cursor_shape_manager: globals.bind(&qh, 1..=1, ()).ok(), data_device_manager: globals .bind( &qh, WL_DATA_DEVICE_MANAGER_VERSION..=WL_DATA_DEVICE_MANAGER_VERSION, (), ) .ok(), primary_selection_manager: globals.bind(&qh, 1..=1, ()).ok(), shm: globals.bind(&qh, 1..=1, ()).unwrap(), seat, wm_base: globals.bind(&qh, 1..=5, ()).unwrap(), viewporter: globals.bind(&qh, 1..=1, ()).ok(), fractional_scale_manager: globals.bind(&qh, 1..=1, ()).ok(), decoration_manager: globals.bind(&qh, 1..=1, ()).ok(), layer_shell: globals.bind(&qh, 1..=5, ()).ok(), blur_manager: globals.bind(&qh, 1..=1, ()).ok(), text_input_manager: globals.bind(&qh, 1..=1, ()).ok(), gesture_manager: globals.bind(&qh, 1..=3, ()).ok(), dialog: globals.bind(&qh, dialog_v..=dialog_v, ()).ok(), system_bell: globals.bind(&qh, 1..=1, ()).ok(), executor, qh, } }} #[derive(Default, Debug, Clone, PartialEq, Eq, Hash)]pub struct InProgressOutput { name: Option<String>, scale: Option<i32>, position: Option<Point<DevicePixels>>, size: Option<Size<DevicePixels>>, subpixel: Option<wl_output::Subpixel>,} impl InProgressOutput { fn complete(&self) -> Option<Output> { if let Some((position, size)) = self.position.zip(self.size) { let scale = self.scale.unwrap_or(1); Some(Output { name: self.name.clone(), scale, bounds: Bounds::new(position, size), subpixel: self.subpixel, }) } else { None } }} #[derive(Debug, Clone, Eq, PartialEq, Hash)]pub struct Output { pub name: Option<String>, pub scale: i32, pub bounds: Bounds<DevicePixels>, pub subpixel: Option<wl_output::Subpixel>,} pub(crate) struct WaylandClientState { serial_tracker: SerialTracker, globals: Globals, pub gpu_context: GpuContext, pub compositor_gpu: Option<CompositorGpuHint>, wl_seat: wl_seat::WlSeat, // TODO: Multi seat support wl_pointer: Option<wl_pointer::WlPointer>, pinch_gesture: Option<zwp_pointer_gesture_pinch_v1::ZwpPointerGesturePinchV1>, pinch_scale: f32, wl_keyboard: Option<wl_keyboard::WlKeyboard>, cursor_shape_device: Option<wp_cursor_shape_device_v1::WpCursorShapeDeviceV1>, data_device: Option<wl_data_device::WlDataDevice>, primary_selection: Option<zwp_primary_selection_device_v1::ZwpPrimarySelectionDeviceV1>, text_input: Option<zwp_text_input_v3::ZwpTextInputV3>, pre_edit_text: Option<String>, ime_pre_edit: Option<String>, composing: bool, last_ime_cursor_rectangle: Option<ImeCursorRectangle>, // Surface to Window mapping windows: HashMap<ObjectId, WaylandWindowStatePtr>, // Output to scale mapping outputs: HashMap<ObjectId, Output>, in_progress_outputs: HashMap<ObjectId, InProgressOutput>, wl_outputs: HashMap<ObjectId, wl_output::WlOutput>, keyboard_layout: LinuxKeyboardLayout, keymap_state: Option<xkb::State>, compose_state: Option<xkb::compose::State>, drag: DragState, click: ClickState, repeat: KeyRepeat, pub modifiers: Modifiers, pub capslock: Capslock, axis_source: AxisSource, pub mouse_location: Option<Point<Pixels>>, continuous_scroll_delta: Option<Point<Pixels>>, discrete_scroll_delta: Option<Point<f32>>, vertical_modifier: f32, horizontal_modifier: f32, scroll_event_received: bool, enter_token: Option<()>, button_pressed: Option<MouseButton>, mouse_focused_window: Option<WaylandWindowStatePtr>, keyboard_focused_window: Option<WaylandWindowStatePtr>, loop_handle: LoopHandle<'static, WaylandClientStatePtr>, cursor_style: Option<CursorStyle>, cursor_hidden_window: Option<WaylandWindowStatePtr>, clipboard: Clipboard, data_offers: Vec<DataOffer<WlDataOffer>>, primary_data_offer: Option<DataOffer<ZwpPrimarySelectionOfferV1>>, cursor: Cursor, pending_activation: Option<PendingActivation>, startup_activation_token: Option<String>, event_loop: Option<EventLoop<'static, WaylandClientStatePtr>>, pub common: LinuxCommon, ime_enabled: Option<bool>,} pub struct DragState { data_offer: Option<wl_data_offer::WlDataOffer>, window: Option<WaylandWindowStatePtr>, position: Point<Pixels>,} pub struct ClickState { last_mouse_button: Option<MouseButton>, last_click: Instant, last_location: Point<Pixels>, current_count: usize,} pub(crate) struct KeyRepeat { characters_per_second: u32, delay: Duration, current_id: u64, current_keycode: Option<xkb::Keycode>,} pub(crate) enum PendingActivation { /// URI to open in the web browser. Uri(String), /// Path to open in the file explorer. Path(PathBuf), /// A window from ourselves to raise. Window(ObjectId),} impl WaylandClientState { fn consume_startup_activation_token(&mut self, surface: &wl_surface::WlSurface) { let Some(startup_activation_token) = self.startup_activation_token.take() else { return; }; let Some(activation) = self.globals.activation.as_ref() else { return; }; activation.activate(startup_activation_token, surface); }} /// This struct is required to conform to Rust's orphan rules, so we can dispatch on the state but hand the/// window to GPUI.#[derive(Clone)]pub struct WaylandClientStatePtr(Weak<RefCell<WaylandClientState>>); impl WaylandClientStatePtr { pub fn get_client(&self) -> Rc<RefCell<WaylandClientState>> { self.0 .upgrade() .expect("The pointer should always be valid when dispatching in wayland") } pub fn get_serial(&self, kind: SerialKind) -> Serial { self.0.upgrade().unwrap().borrow().serial_tracker.get(kind) } pub fn set_pending_activation(&self, window: ObjectId) { self.0.upgrade().unwrap().borrow_mut().pending_activation = Some(PendingActivation::Window(window)); } pub fn enable_ime(&self) { let client = self.get_client(); let mut state = client.borrow_mut(); state.ime_enabled = Some(true); state.last_ime_cursor_rectangle = None; let Some(text_input) = state.text_input.take() else { return; }; text_input.enable(); text_input.set_content_type(ContentHint::None, ContentPurpose::Normal); let mut cursor_rectangle = None; if let Some(window) = state.keyboard_focused_window.clone() { drop(state); if let Some(area) = window.get_ime_area() { let area = ImeCursorRectangle::from(area); set_ime_cursor_rectangle(&text_input, area); cursor_rectangle = Some(area); } state = client.borrow_mut(); } text_input.commit(); state.last_ime_cursor_rectangle = cursor_rectangle; state.text_input = Some(text_input); } pub fn disable_ime(&self) { let client = self.get_client(); let mut state = client.borrow_mut(); state.ime_enabled = Some(false); state.composing = false; if let Some(text_input) = &state.text_input { text_input.disable(); text_input.commit(); } } pub fn ime_enabled(&self) -> Option<bool> { let client = self.get_client(); client.borrow().ime_enabled } pub fn update_ime_position(&self, bounds: Bounds<Pixels>) { let client = self.get_client(); let mut state = client.borrow_mut(); if state.pre_edit_text.is_some() { return; } let Some(text_input) = state.text_input.clone() else { return; }; update_ime_cursor_rectangle(&text_input, &mut state.last_ime_cursor_rectangle, bounds); } pub fn handle_keyboard_layout_change(&self) { let client = self.get_client(); let mut state = client.borrow_mut(); let changed = if let Some(keymap_state) = &state.keymap_state { let layout_idx = keymap_state.serialize_layout(xkbcommon::xkb::STATE_LAYOUT_EFFECTIVE); let keymap = keymap_state.get_keymap(); let layout_name = keymap.layout_get_name(layout_idx); let changed = layout_name != state.keyboard_layout.name(); if changed { state.keyboard_layout = LinuxKeyboardLayout::new(layout_name.to_string().into()); } changed } else { let changed = &UNKNOWN_KEYBOARD_LAYOUT_NAME != state.keyboard_layout.name(); if changed { state.keyboard_layout = LinuxKeyboardLayout::new(UNKNOWN_KEYBOARD_LAYOUT_NAME); } changed }; if changed && let Some(mut callback) = state.common.callbacks.keyboard_layout_change.take() { drop(state); callback();Lines 1–500 of 2839. Load more
use std::{ cell::{RefCell, RefMut}, hash::Hash, os::fd::{AsRawFd, BorrowedFd}, path::PathBuf, rc::{Rc, Weak}, time::{Duration, Instant}, }; use ashpd::WindowIdentifier; use calloop::{ EventLoop, LoopHandle, timer::{TimeoutAction, Timer}, }; use calloop_wayland_source::WaylandSource; use collections::HashMap; use filedescriptor::Pipe; use gpui_util::ResultExt as _; use http_client::Url; use smallvec::SmallVec; use wayland_backend::client::ObjectId; use wayland_backend::protocol::WEnum; use wayland_client::event_created_child; use wayland_client::globals::{GlobalList, GlobalListContents, registry_queue_init}; use wayland_client::protocol::wl_callback::{self, WlCallback}; use wayland_client::protocol::wl_data_device_manager::DndAction; use wayland_client::protocol::wl_data_offer::WlDataOffer; use wayland_client::protocol::wl_pointer::AxisSource; use wayland_client::protocol::{ wl_data_device, wl_data_device_manager, wl_data_offer, wl_data_source, wl_output, wl_region, }; use wayland_client::{ Connection, Dispatch, Proxy, QueueHandle, delegate_noop, protocol::{ wl_buffer, wl_compositor, wl_keyboard, wl_pointer, wl_registry, wl_seat, wl_shm, wl_shm_pool, wl_surface, }, }; use wayland_protocols::wp::pointer_gestures::zv1::client::{ zwp_pointer_gesture_pinch_v1, zwp_pointer_gestures_v1, }; use wayland_protocols::wp::primary_selection::zv1::client::zwp_primary_selection_offer_v1::{ self, ZwpPrimarySelectionOfferV1, }; use wayland_protocols::wp::primary_selection::zv1::client::{ zwp_primary_selection_device_manager_v1, zwp_primary_selection_device_v1, zwp_primary_selection_source_v1, }; use wayland_protocols::wp::text_input::zv3::client::zwp_text_input_v3::{ ContentHint, ContentPurpose, }; use wayland_protocols::wp::text_input::zv3::client::{ zwp_text_input_manager_v3, zwp_text_input_v3, }; use wayland_protocols::wp::viewporter::client::{wp_viewport, wp_viewporter}; use wayland_protocols::xdg::activation::v1::client::{xdg_activation_token_v1, xdg_activation_v1}; use wayland_protocols::xdg::decoration::zv1::client::{ zxdg_decoration_manager_v1, zxdg_toplevel_decoration_v1, }; use wayland_protocols::xdg::shell::client::{ xdg_popup, xdg_positioner, xdg_surface, xdg_toplevel, xdg_wm_base, }; use wayland_protocols::xdg::system_bell::v1::client::xdg_system_bell_v1; use wayland_protocols::{ wp::cursor_shape::v1::client::{wp_cursor_shape_device_v1, wp_cursor_shape_manager_v1}, xdg::dialog::v1::client::xdg_wm_dialog_v1::{self, XdgWmDialogV1}, }; use wayland_protocols::{ wp::fractional_scale::v1::client::{wp_fractional_scale_manager_v1, wp_fractional_scale_v1}, xdg::dialog::v1::client::xdg_dialog_v1::XdgDialogV1, }; use wayland_protocols_plasma::blur::client::{org_kde_kwin_blur, org_kde_kwin_blur_manager}; use wayland_protocols_wlr::layer_shell::v1::client::{zwlr_layer_shell_v1, zwlr_layer_surface_v1}; use xkbcommon::xkb::ffi::XKB_KEYMAP_FORMAT_TEXT_V1; use xkbcommon::xkb::{self, KEYMAP_COMPILE_NO_FLAGS, Keycode}; use super::{ display::WaylandDisplay, window::{ImeInput, WaylandWindowStatePtr}, }; use crate::linux::{ DOUBLE_CLICK_INTERVAL, LinuxClient, LinuxCommon, LinuxKeyboardLayout, PIPE_READ_TIMEOUT, SCROLL_LINES, capslock_from_xkb, cursor_style_to_icon_names, get_xkb_compose_state, is_within_click_distance, keystroke_from_xkb, keystroke_underlying_dead_key, modifiers_from_xkb, open_uri_internal, read_fd_with_timeout, reveal_path_internal, wayland::{ clipboard::{Clipboard, DataOffer, FILE_LIST_MIME_TYPE, TEXT_MIME_TYPES}, cursor::Cursor, serial::{Serial, SerialKind, SerialTracker}, to_shape, window::WaylandWindow, }, xdg_desktop_portal::{Event as XDPEvent, XDPEventSource}, }; use gpui::{ AnyWindowHandle, Bounds, Capslock, CursorStyle, DevicePixels, DisplayId, FileDropEvent, ForegroundExecutor, KeyDownEvent, KeyUpEvent, Keystroke, Modifiers, ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseExitEvent, MouseMoveEvent, MouseUpEvent, NavigationDirection, Pixels, PlatformDisplay, PlatformInput, PlatformKeyboardLayout, PlatformWindow, Point, ScrollDelta, ScrollWheelEvent, SharedString, Size, TouchPhase, WindowButtonLayout, WindowKind, WindowParams, point, profiler, px, size, }; use gpui_wgpu::{CompositorGpuHint, GpuContext}; use wayland_protocols::wp::linux_dmabuf::zv1::client::{ zwp_linux_dmabuf_feedback_v1, zwp_linux_dmabuf_v1, }; /// Used to convert evdev scancode to xkb scancode const MIN_KEYCODE: u32 = 8; const UNKNOWN_KEYBOARD_LAYOUT_NAME: SharedString = SharedString::new_static("unknown"); const XDG_ACTIVATION_TOKEN_ENV_VAR: &str = "XDG_ACTIVATION_TOKEN"; #[derive(Clone, Copy, Debug, PartialEq, Eq)] struct ImeCursorRectangle { x: i32, y: i32, width: i32, height: i32, } impl From<Bounds<Pixels>> for ImeCursorRectangle { fn from(bounds: Bounds<Pixels>) -> Self { Self { x: bounds.origin.x.as_f32() as i32, y: bounds.origin.y.as_f32() as i32, width: bounds.size.width.as_f32() as i32, height: bounds.size.height.as_f32() as i32, } } } trait ImeCursorRectangleSink { fn set_ime_cursor_rectangle(&self, x: i32, y: i32, width: i32, height: i32); fn commit_ime_state(&self); } impl ImeCursorRectangleSink for zwp_text_input_v3::ZwpTextInputV3 { fn set_ime_cursor_rectangle(&self, x: i32, y: i32, width: i32, height: i32) { self.set_cursor_rectangle(x, y, width, height); } fn commit_ime_state(&self) { self.commit(); } } fn set_ime_cursor_rectangle( text_input: &impl ImeCursorRectangleSink, cursor_rectangle: ImeCursorRectangle, ) { text_input.set_ime_cursor_rectangle( cursor_rectangle.x, cursor_rectangle.y, cursor_rectangle.width, cursor_rectangle.height, ); } fn update_ime_cursor_rectangle( text_input: &impl ImeCursorRectangleSink, last_ime_cursor_rectangle: &mut Option<ImeCursorRectangle>, bounds: Bounds<Pixels>, ) { let cursor_rectangle = ImeCursorRectangle::from(bounds); if *last_ime_cursor_rectangle == Some(cursor_rectangle) { return; } *last_ime_cursor_rectangle = Some(cursor_rectangle); set_ime_cursor_rectangle(text_input, cursor_rectangle); text_input.commit_ime_state(); } fn set_ime_cursor_rectangle_after_done( text_input: &impl ImeCursorRectangleSink, last_ime_cursor_rectangle: &mut Option<ImeCursorRectangle>, bounds: Bounds<Pixels>, should_commit: bool, ) { if should_commit { update_ime_cursor_rectangle(text_input, last_ime_cursor_rectangle, bounds); } else { set_ime_cursor_rectangle(text_input, ImeCursorRectangle::from(bounds)); } } fn take_startup_activation_token_from_environment() -> Option<String> { let startup_activation_token = std::env::var(XDG_ACTIVATION_TOKEN_ENV_VAR) .ok() .filter(|token| !token.is_empty()); // The token must be removed from the environment so it isn't inherited by child // processes we spawn, per the xdg-activation spec: https://wayland.app/protocols/xdg-activation-v1 // SAFETY: This runs during Wayland platform initialization before GPUI starts // concurrent environment access or spawning child processes. unsafe { std::env::remove_var(XDG_ACTIVATION_TOKEN_ENV_VAR) }; startup_activation_token } #[derive(Clone)] pub struct Globals { pub qh: QueueHandle<WaylandClientStatePtr>, pub activation: Option<xdg_activation_v1::XdgActivationV1>, pub compositor: wl_compositor::WlCompositor, pub cursor_shape_manager: Option<wp_cursor_shape_manager_v1::WpCursorShapeManagerV1>, pub data_device_manager: Option<wl_data_device_manager::WlDataDeviceManager>, pub primary_selection_manager: Option<zwp_primary_selection_device_manager_v1::ZwpPrimarySelectionDeviceManagerV1>, 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>, pub decoration_manager: Option<zxdg_decoration_manager_v1::ZxdgDecorationManagerV1>, pub layer_shell: Option<zwlr_layer_shell_v1::ZwlrLayerShellV1>, pub blur_manager: Option<org_kde_kwin_blur_manager::OrgKdeKwinBlurManager>, pub text_input_manager: Option<zwp_text_input_manager_v3::ZwpTextInputManagerV3>, pub gesture_manager: Option<zwp_pointer_gestures_v1::ZwpPointerGesturesV1>, pub dialog: Option<xdg_wm_dialog_v1::XdgWmDialogV1>, pub system_bell: Option<xdg_system_bell_v1::XdgSystemBellV1>, pub executor: ForegroundExecutor, } impl Globals { fn new( globals: GlobalList, executor: ForegroundExecutor, qh: QueueHandle<WaylandClientStatePtr>, seat: wl_seat::WlSeat, ) -> Self { let dialog_v = XdgWmDialogV1::interface().version; Globals { activation: globals.bind(&qh, 1..=1, ()).ok(), compositor: globals .bind( &qh, wl_surface::REQ_SET_BUFFER_SCALE_SINCE ..=wl_surface::EVT_PREFERRED_BUFFER_SCALE_SINCE, (), ) .unwrap(), cursor_shape_manager: globals.bind(&qh, 1..=1, ()).ok(), data_device_manager: globals .bind( &qh, WL_DATA_DEVICE_MANAGER_VERSION..=WL_DATA_DEVICE_MANAGER_VERSION, (), ) .ok(), primary_selection_manager: globals.bind(&qh, 1..=1, ()).ok(), shm: globals.bind(&qh, 1..=1, ()).unwrap(), seat, wm_base: globals.bind(&qh, 1..=5, ()).unwrap(), viewporter: globals.bind(&qh, 1..=1, ()).ok(), fractional_scale_manager: globals.bind(&qh, 1..=1, ()).ok(), decoration_manager: globals.bind(&qh, 1..=1, ()).ok(), layer_shell: globals.bind(&qh, 1..=5, ()).ok(), blur_manager: globals.bind(&qh, 1..=1, ()).ok(), text_input_manager: globals.bind(&qh, 1..=1, ()).ok(), gesture_manager: globals.bind(&qh, 1..=3, ()).ok(), dialog: globals.bind(&qh, dialog_v..=dialog_v, ()).ok(), system_bell: globals.bind(&qh, 1..=1, ()).ok(), executor, qh, } } } #[derive(Default, Debug, Clone, PartialEq, Eq, Hash)] pub struct InProgressOutput { name: Option<String>, scale: Option<i32>, position: Option<Point<DevicePixels>>, size: Option<Size<DevicePixels>>, subpixel: Option<wl_output::Subpixel>, } impl InProgressOutput { fn complete(&self) -> Option<Output> { if let Some((position, size)) = self.position.zip(self.size) { let scale = self.scale.unwrap_or(1); Some(Output { name: self.name.clone(), scale, bounds: Bounds::new(position, size), subpixel: self.subpixel, }) } else { None } } } #[derive(Debug, Clone, Eq, PartialEq, Hash)] pub struct Output { pub name: Option<String>, pub scale: i32, pub bounds: Bounds<DevicePixels>, pub subpixel: Option<wl_output::Subpixel>, } pub(crate) struct WaylandClientState { serial_tracker: SerialTracker, globals: Globals, pub gpu_context: GpuContext, pub compositor_gpu: Option<CompositorGpuHint>, wl_seat: wl_seat::WlSeat, // TODO: Multi seat support wl_pointer: Option<wl_pointer::WlPointer>, pinch_gesture: Option<zwp_pointer_gesture_pinch_v1::ZwpPointerGesturePinchV1>, pinch_scale: f32, wl_keyboard: Option<wl_keyboard::WlKeyboard>, cursor_shape_device: Option<wp_cursor_shape_device_v1::WpCursorShapeDeviceV1>, data_device: Option<wl_data_device::WlDataDevice>, primary_selection: Option<zwp_primary_selection_device_v1::ZwpPrimarySelectionDeviceV1>, text_input: Option<zwp_text_input_v3::ZwpTextInputV3>, pre_edit_text: Option<String>, ime_pre_edit: Option<String>, composing: bool, last_ime_cursor_rectangle: Option<ImeCursorRectangle>, // Surface to Window mapping windows: HashMap<ObjectId, WaylandWindowStatePtr>, // Output to scale mapping outputs: HashMap<ObjectId, Output>, in_progress_outputs: HashMap<ObjectId, InProgressOutput>, wl_outputs: HashMap<ObjectId, wl_output::WlOutput>, keyboard_layout: LinuxKeyboardLayout, keymap_state: Option<xkb::State>, compose_state: Option<xkb::compose::State>, drag: DragState, click: ClickState, repeat: KeyRepeat, pub modifiers: Modifiers, pub capslock: Capslock, axis_source: AxisSource, pub mouse_location: Option<Point<Pixels>>, continuous_scroll_delta: Option<Point<Pixels>>, discrete_scroll_delta: Option<Point<f32>>, vertical_modifier: f32, horizontal_modifier: f32, scroll_event_received: bool, enter_token: Option<()>, button_pressed: Option<MouseButton>, mouse_focused_window: Option<WaylandWindowStatePtr>, keyboard_focused_window: Option<WaylandWindowStatePtr>, loop_handle: LoopHandle<'static, WaylandClientStatePtr>, cursor_style: Option<CursorStyle>, cursor_hidden_window: Option<WaylandWindowStatePtr>, clipboard: Clipboard, data_offers: Vec<DataOffer<WlDataOffer>>, primary_data_offer: Option<DataOffer<ZwpPrimarySelectionOfferV1>>, cursor: Cursor, pending_activation: Option<PendingActivation>, startup_activation_token: Option<String>, event_loop: Option<EventLoop<'static, WaylandClientStatePtr>>, pub common: LinuxCommon, ime_enabled: Option<bool>, } pub struct DragState { data_offer: Option<wl_data_offer::WlDataOffer>, window: Option<WaylandWindowStatePtr>, position: Point<Pixels>, } pub struct ClickState { last_mouse_button: Option<MouseButton>, last_click: Instant, last_location: Point<Pixels>, current_count: usize, } pub(crate) struct KeyRepeat { characters_per_second: u32, delay: Duration, current_id: u64, current_keycode: Option<xkb::Keycode>, } pub(crate) enum PendingActivation { /// URI to open in the web browser. Uri(String), /// Path to open in the file explorer. Path(PathBuf), /// A window from ourselves to raise. Window(ObjectId), } impl WaylandClientState { fn consume_startup_activation_token(&mut self, surface: &wl_surface::WlSurface) { let Some(startup_activation_token) = self.startup_activation_token.take() else { return; }; let Some(activation) = self.globals.activation.as_ref() else { return; }; activation.activate(startup_activation_token, surface); } } /// This struct is required to conform to Rust's orphan rules, so we can dispatch on the state but hand the /// window to GPUI. #[derive(Clone)] pub struct WaylandClientStatePtr(Weak<RefCell<WaylandClientState>>); impl WaylandClientStatePtr { pub fn get_client(&self) -> Rc<RefCell<WaylandClientState>> { self.0 .upgrade() .expect("The pointer should always be valid when dispatching in wayland") } pub fn get_serial(&self, kind: SerialKind) -> Serial { self.0.upgrade().unwrap().borrow().serial_tracker.get(kind) } pub fn set_pending_activation(&self, window: ObjectId) { self.0.upgrade().unwrap().borrow_mut().pending_activation = Some(PendingActivation::Window(window)); } pub fn enable_ime(&self) { let client = self.get_client(); let mut state = client.borrow_mut(); state.ime_enabled = Some(true); state.last_ime_cursor_rectangle = None; let Some(text_input) = state.text_input.take() else { return; }; text_input.enable(); text_input.set_content_type(ContentHint::None, ContentPurpose::Normal); let mut cursor_rectangle = None; if let Some(window) = state.keyboard_focused_window.clone() { drop(state); if let Some(area) = window.get_ime_area() { let area = ImeCursorRectangle::from(area); set_ime_cursor_rectangle(&text_input, area); cursor_rectangle = Some(area); } state = client.borrow_mut(); } text_input.commit(); state.last_ime_cursor_rectangle = cursor_rectangle; state.text_input = Some(text_input); } pub fn disable_ime(&self) { let client = self.get_client(); let mut state = client.borrow_mut(); state.ime_enabled = Some(false); state.composing = false; if let Some(text_input) = &state.text_input { text_input.disable(); text_input.commit(); } } pub fn ime_enabled(&self) -> Option<bool> { let client = self.get_client(); client.borrow().ime_enabled } pub fn update_ime_position(&self, bounds: Bounds<Pixels>) { let client = self.get_client(); let mut state = client.borrow_mut(); if state.pre_edit_text.is_some() { return; } let Some(text_input) = state.text_input.clone() else { return; }; update_ime_cursor_rectangle(&text_input, &mut state.last_ime_cursor_rectangle, bounds); } pub fn handle_keyboard_layout_change(&self) { let client = self.get_client(); let mut state = client.borrow_mut(); let changed = if let Some(keymap_state) = &state.keymap_state { let layout_idx = keymap_state.serialize_layout(xkbcommon::xkb::STATE_LAYOUT_EFFECTIVE); let keymap = keymap_state.get_keymap(); let layout_name = keymap.layout_get_name(layout_idx); let changed = layout_name != state.keyboard_layout.name(); if changed { state.keyboard_layout = LinuxKeyboardLayout::new(layout_name.to_string().into()); } changed } else { let changed = &UNKNOWN_KEYBOARD_LAYOUT_NAME != state.keyboard_layout.name(); if changed { state.keyboard_layout = LinuxKeyboardLayout::new(UNKNOWN_KEYBOARD_LAYOUT_NAME); } changed }; if changed && let Some(mut callback) = state.common.callbacks.keyboard_layout_change.take() { drop(state); callback();