From def87a8d76d8902b2551fa0cc88598f192dde6b0 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Mon, 8 Apr 2024 16:40:35 -0700 Subject: [PATCH] WIP: Refactor Linux platform implementation (#10227) This puts the Linux platform implementation at a similar code style and quality to the macOS platform. The largest change is that I collapsed the `LinuxPlatform` -> `[Backend]` -> `[Backend]State` -> `[Backend]StateInner` to just `[Backend]` and `[Backend]State`, and in the process removed most of the `Rc`s and `RefCell`s. TODO: - [x] Make sure that this is on-par with the existing implementation - [x] Review in detail, now that the large changes are done. - [ ] Update the roadmap Release Notes: - N/A --- Cargo.lock | 4 +- Cargo.toml | 5 +- crates/client/src/client.rs | 1 - crates/gpui/src/interactive.rs | 1 - crates/gpui/src/platform.rs | 10 +- crates/gpui/src/platform/linux.rs | 8 +- crates/gpui/src/platform/linux/client.rs | 21 - crates/gpui/src/platform/linux/dispatcher.rs | 8 +- crates/gpui/src/platform/linux/platform.rs | 402 ++++++--- crates/gpui/src/platform/linux/util.rs | 128 --- crates/gpui/src/platform/linux/wayland.rs | 7 +- .../gpui/src/platform/linux/wayland/client.rs | 798 ++++++++---------- .../gpui/src/platform/linux/wayland/cursor.rs | 42 +- .../gpui/src/platform/linux/wayland/window.rs | 388 ++++++--- crates/gpui/src/platform/linux/x11/client.rs | 299 ++++--- crates/gpui/src/platform/linux/x11/window.rs | 216 ++--- crates/gpui/src/window.rs | 10 + crates/workspace/src/persistence.rs | 3 +- 18 files changed, 1256 insertions(+), 1095 deletions(-) delete mode 100644 crates/gpui/src/platform/linux/client.rs delete mode 100644 crates/gpui/src/platform/linux/util.rs diff --git a/Cargo.lock b/Cargo.lock index 0b9f0185ae..66c8f7b873 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1434,7 +1434,7 @@ dependencies = [ [[package]] name = "blade-graphics" version = "0.3.0" -source = "git+https://github.com/kvark/blade?rev=61cbd6b2c224791d52b150fe535cee665cc91bb2#61cbd6b2c224791d52b150fe535cee665cc91bb2" +source = "git+https://github.com/zed-industries/blade?rev=85981c0f4890a5fcd08da2a53cc4a0459247af44#85981c0f4890a5fcd08da2a53cc4a0459247af44" dependencies = [ "ash", "ash-window", @@ -1464,7 +1464,7 @@ dependencies = [ [[package]] name = "blade-macros" version = "0.2.1" -source = "git+https://github.com/kvark/blade?rev=61cbd6b2c224791d52b150fe535cee665cc91bb2#61cbd6b2c224791d52b150fe535cee665cc91bb2" +source = "git+https://github.com/zed-industries/blade?rev=85981c0f4890a5fcd08da2a53cc4a0459247af44#85981c0f4890a5fcd08da2a53cc4a0459247af44" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 92a20f4b0b..9cbc7088dd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -230,8 +230,9 @@ async-recursion = "1.0.0" async-tar = "0.4.2" async-trait = "0.1" bitflags = "2.4.2" -blade-graphics = { git = "https://github.com/kvark/blade", rev = "61cbd6b2c224791d52b150fe535cee665cc91bb2" } -blade-macros = { git = "https://github.com/kvark/blade", rev = "61cbd6b2c224791d52b150fe535cee665cc91bb2" } +# todo(linux): Remove these once https://github.com/kvark/blade/pull/107 is merged and we've upgraded our renderer +blade-graphics = { git = "https://github.com/zed-industries/blade", rev = "85981c0f4890a5fcd08da2a53cc4a0459247af44" } +blade-macros = { git = "https://github.com/zed-industries/blade", rev = "85981c0f4890a5fcd08da2a53cc4a0459247af44" } blade-rwh = { package = "raw-window-handle", version = "0.5" } cap-std = "3.0" chrono = { version = "0.4", features = ["serde"] } diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index 157b7ce425..5d06fccb7a 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -784,7 +784,6 @@ impl Client { } Status::UpgradeRequired => return Err(EstablishConnectionError::UpgradeRequired)?, }; - if was_disconnected { self.set_status(Status::Authenticating, cx); } else { diff --git a/crates/gpui/src/interactive.rs b/crates/gpui/src/interactive.rs index c7b946b439..c92b58bebc 100644 --- a/crates/gpui/src/interactive.rs +++ b/crates/gpui/src/interactive.rs @@ -307,7 +307,6 @@ impl ScrollDelta { } /// A mouse exit event from the platform, generated when the mouse leaves the window. -/// The position generated should be just outside of the window's bounds. #[derive(Clone, Debug, Default)] pub struct MouseExitEvent { /// The position of the mouse relative to the window. diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index e111f3078f..d65eb6deb9 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -66,7 +66,14 @@ pub(crate) fn current_platform() -> Rc { } #[cfg(target_os = "linux")] pub(crate) fn current_platform() -> Rc { - Rc::new(LinuxPlatform::new()) + let wayland_display = std::env::var_os("WAYLAND_DISPLAY"); + let use_wayland = wayland_display.is_some_and(|display| !display.is_empty()); + + if use_wayland { + Rc::new(WaylandClient::new()) + } else { + Rc::new(X11Client::new()) + } } // todo("windows") #[cfg(target_os = "windows")] @@ -207,6 +214,7 @@ pub(crate) trait PlatformWindow: HasWindowHandle + HasDisplayHandle { fn on_appearance_changed(&self, callback: Box); fn is_topmost_for_position(&self, position: Point) -> bool; fn draw(&self, scene: &Scene); + fn completed_frame(&self) {} fn sprite_atlas(&self) -> Arc; #[cfg(target_os = "windows")] diff --git a/crates/gpui/src/platform/linux.rs b/crates/gpui/src/platform/linux.rs index f334b23399..f12c5432f5 100644 --- a/crates/gpui/src/platform/linux.rs +++ b/crates/gpui/src/platform/linux.rs @@ -1,12 +1,14 @@ -mod client; +// todo(linux): remove +#![allow(unused)] + mod dispatcher; mod platform; mod text_system; -mod util; mod wayland; mod x11; pub(crate) use dispatcher::*; pub(crate) use platform::*; pub(crate) use text_system::*; -// pub(crate) use x11::*; +pub(crate) use wayland::*; +pub(crate) use x11::*; diff --git a/crates/gpui/src/platform/linux/client.rs b/crates/gpui/src/platform/linux/client.rs deleted file mode 100644 index acc5a2d955..0000000000 --- a/crates/gpui/src/platform/linux/client.rs +++ /dev/null @@ -1,21 +0,0 @@ -use std::cell::RefCell; -use std::rc::Rc; - -use copypasta::ClipboardProvider; - -use crate::platform::PlatformWindow; -use crate::{AnyWindowHandle, CursorStyle, DisplayId, PlatformDisplay, WindowParams}; - -pub trait Client { - fn displays(&self) -> Vec>; - fn primary_display(&self) -> Option>; - fn display(&self, id: DisplayId) -> Option>; - fn open_window( - &self, - handle: AnyWindowHandle, - options: WindowParams, - ) -> Box; - fn set_cursor_style(&self, style: CursorStyle); - fn get_clipboard(&self) -> Rc>; - fn get_primary(&self) -> Rc>; -} diff --git a/crates/gpui/src/platform/linux/dispatcher.rs b/crates/gpui/src/platform/linux/dispatcher.rs index 9dc0442035..1557b87ac1 100644 --- a/crates/gpui/src/platform/linux/dispatcher.rs +++ b/crates/gpui/src/platform/linux/dispatcher.rs @@ -101,15 +101,13 @@ impl PlatformDispatcher for LinuxDispatcher { } fn dispatch_on_main_thread(&self, runnable: Runnable) { - self.main_sender - .send(runnable) - .expect("Main thread is gone"); + self.main_sender.send(runnable).ok(); } fn dispatch_after(&self, duration: Duration, runnable: Runnable) { self.timer_sender .send(TimerAfter { duration, runnable }) - .expect("Timer thread has died"); + .ok(); } fn tick(&self, background_only: bool) -> bool { @@ -117,7 +115,7 @@ impl PlatformDispatcher for LinuxDispatcher { } fn park(&self) { - self.parker.lock().park() + self.parker.lock().park(); } fn unparker(&self) -> Unparker { diff --git a/crates/gpui/src/platform/linux/platform.rs b/crates/gpui/src/platform/linux/platform.rs index c059183d87..0a962b6751 100644 --- a/crates/gpui/src/platform/linux/platform.rs +++ b/crates/gpui/src/platform/linux/platform.rs @@ -1,7 +1,10 @@ #![allow(unused)] -use std::cell::RefCell; +use std::any::{type_name, Any}; +use std::cell::{self, RefCell}; use std::env; +use std::ops::{Deref, DerefMut}; +use std::panic::Location; use std::{ path::{Path, PathBuf}, process::Command, @@ -13,140 +16,176 @@ use std::{ use anyhow::anyhow; use ashpd::desktop::file_chooser::{OpenFileRequest, SaveFileRequest}; use async_task::Runnable; +use calloop::channel::Channel; use calloop::{EventLoop, LoopHandle, LoopSignal}; +use copypasta::ClipboardProvider; use flume::{Receiver, Sender}; use futures::channel::oneshot; use parking_lot::Mutex; use time::UtcOffset; use wayland_client::Connection; +use xkbcommon::xkb::{self, Keycode, Keysym, State}; -use crate::platform::linux::client::Client; use crate::platform::linux::wayland::WaylandClient; use crate::{ px, Action, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DisplayId, - ForegroundExecutor, Keymap, LinuxDispatcher, LinuxTextSystem, Menu, PathPromptOptions, Pixels, - Platform, PlatformDisplay, PlatformInput, PlatformTextSystem, PlatformWindow, Result, - SemanticVersion, Task, WindowOptions, WindowParams, + ForegroundExecutor, Keymap, Keystroke, LinuxDispatcher, LinuxTextSystem, Menu, Modifiers, + PathPromptOptions, Pixels, Platform, PlatformDisplay, PlatformInput, PlatformInputHandler, + PlatformTextSystem, PlatformWindow, Point, PromptLevel, Result, SemanticVersion, Size, Task, + WindowAppearance, WindowOptions, WindowParams, }; use super::x11::X11Client; -pub(super) const SCROLL_LINES: f64 = 3.0; +pub(crate) const SCROLL_LINES: f64 = 3.0; // Values match the defaults on GTK. // Taken from https://github.com/GNOME/gtk/blob/main/gtk/gtksettings.c#L320 -pub(super) const DOUBLE_CLICK_INTERVAL: Duration = Duration::from_millis(400); -pub(super) const DOUBLE_CLICK_DISTANCE: Pixels = px(5.0); +pub(crate) const DOUBLE_CLICK_INTERVAL: Duration = Duration::from_millis(400); +pub(crate) const DOUBLE_CLICK_DISTANCE: Pixels = px(5.0); +pub(crate) const KEYRING_LABEL: &str = "zed-github-account"; -#[derive(Default)] -pub(crate) struct Callbacks { - open_urls: Option)>>, - become_active: Option>, - resign_active: Option>, - quit: Option>, - reopen: Option>, - event: Option bool>>, - app_menu_action: Option>, - will_open_app_menu: Option>, - validate_app_menu_command: Option bool>>, -} +pub struct RcRefCell(Rc>); -pub(crate) struct LinuxPlatformInner { - pub(crate) event_loop: RefCell>, - pub(crate) loop_handle: Rc>, - pub(crate) loop_signal: LoopSignal, - pub(crate) background_executor: BackgroundExecutor, - pub(crate) foreground_executor: ForegroundExecutor, - pub(crate) text_system: Arc, - pub(crate) callbacks: RefCell, -} +impl RcRefCell { + pub fn new(value: T) -> Self { + RcRefCell(Rc::new(RefCell::new(value))) + } -pub(crate) struct LinuxPlatform { - client: Rc, - inner: Rc, -} + #[inline] + #[track_caller] + pub fn borrow_mut(&self) -> std::cell::RefMut<'_, T> { + #[cfg(debug_assertions)] + { + if option_env!("TRACK_BORROW_MUT").is_some() { + eprintln!( + "borrow_mut-ing {} at {}", + type_name::(), + Location::caller() + ); + } + } -impl Default for LinuxPlatform { - fn default() -> Self { - Self::new() + self.0.borrow_mut() + } + + #[inline] + #[track_caller] + pub fn borrow(&self) -> std::cell::Ref<'_, T> { + #[cfg(debug_assertions)] + { + if option_env!("TRACK_BORROW_MUT").is_some() { + eprintln!("borrow-ing {} at {}", type_name::(), Location::caller()); + } + } + + self.0.borrow() } } -impl LinuxPlatform { - pub(crate) fn new() -> Self { - let wayland_display = env::var_os("WAYLAND_DISPLAY"); - let use_wayland = wayland_display.is_some_and(|display| !display.is_empty()); +impl Deref for RcRefCell { + type Target = Rc>; + fn deref(&self) -> &Self::Target { + &self.0 + } +} +impl DerefMut for RcRefCell { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl Clone for RcRefCell { + fn clone(&self) -> Self { + RcRefCell(self.0.clone()) + } +} + +pub trait LinuxClient { + fn with_common(&self, f: impl FnOnce(&mut LinuxCommon) -> R) -> R; + fn displays(&self) -> Vec>; + fn primary_display(&self) -> Option>; + fn display(&self, id: DisplayId) -> Option>; + fn open_window( + &self, + handle: AnyWindowHandle, + options: WindowParams, + ) -> Box; + fn set_cursor_style(&self, style: CursorStyle); + fn write_to_clipboard(&self, item: ClipboardItem); + fn read_from_clipboard(&self) -> Option; + fn run(&self); +} + +#[derive(Default)] +pub(crate) struct PlatformHandlers { + pub(crate) open_urls: Option)>>, + pub(crate) become_active: Option>, + pub(crate) resign_active: Option>, + pub(crate) quit: Option>, + pub(crate) reopen: Option>, + pub(crate) event: Option bool>>, + pub(crate) app_menu_action: Option>, + pub(crate) will_open_app_menu: Option>, + pub(crate) validate_app_menu_command: Option bool>>, +} + +pub(crate) struct LinuxCommon { + pub(crate) background_executor: BackgroundExecutor, + pub(crate) foreground_executor: ForegroundExecutor, + pub(crate) text_system: Arc, + pub(crate) callbacks: PlatformHandlers, + pub(crate) signal: LoopSignal, +} + +impl LinuxCommon { + pub fn new(signal: LoopSignal) -> (Self, Channel) { let (main_sender, main_receiver) = calloop::channel::channel::(); let text_system = Arc::new(LinuxTextSystem::new()); - let callbacks = RefCell::new(Callbacks::default()); - - let event_loop = EventLoop::try_new().unwrap(); - event_loop - .handle() - .insert_source(main_receiver, |event, _, _| { - if let calloop::channel::Event::Msg(runnable) = event { - runnable.run(); - } - }); + let callbacks = PlatformHandlers::default(); let dispatcher = Arc::new(LinuxDispatcher::new(main_sender)); - let inner = Rc::new(LinuxPlatformInner { - loop_handle: Rc::new(event_loop.handle()), - loop_signal: event_loop.get_signal(), - event_loop: RefCell::new(event_loop), + let common = LinuxCommon { background_executor: BackgroundExecutor::new(dispatcher.clone()), foreground_executor: ForegroundExecutor::new(dispatcher.clone()), text_system, callbacks, - }); + signal, + }; - if use_wayland { - Self { - client: Rc::new(WaylandClient::new(Rc::clone(&inner))), - inner, - } - } else { - Self { - client: X11Client::new(Rc::clone(&inner)), - inner, - } - } + (common, main_receiver) } } -const KEYRING_LABEL: &str = "zed-github-account"; - -impl Platform for LinuxPlatform { +impl Platform for P { fn background_executor(&self) -> BackgroundExecutor { - self.inner.background_executor.clone() + self.with_common(|common| common.background_executor.clone()) } fn foreground_executor(&self) -> ForegroundExecutor { - self.inner.foreground_executor.clone() + self.with_common(|common| common.foreground_executor.clone()) } fn text_system(&self) -> Arc { - self.inner.text_system.clone() + self.with_common(|common| common.text_system.clone()) } fn run(&self, on_finish_launching: Box) { on_finish_launching(); - self.inner - .event_loop - .borrow_mut() - .run(None, &mut (), |&mut ()| {}) - .expect("Run loop failed"); + LinuxClient::run(self); - if let Some(mut fun) = self.inner.callbacks.borrow_mut().quit.take() { - fun(); - } + self.with_common(|common| { + if let Some(mut fun) = common.callbacks.quit.take() { + fun(); + } + }); } fn quit(&self) { - self.inner.loop_signal.stop(); + self.with_common(|common| common.signal.stop()); } fn restart(&self) { @@ -194,22 +233,23 @@ impl Platform for LinuxPlatform { // todo(linux) fn hide(&self) {} - // todo(linux) - fn hide_other_apps(&self) {} + fn hide_other_apps(&self) { + log::warn!("hide_other_apps is not implemented on Linux, ignoring the call") + } // todo(linux) fn unhide_other_apps(&self) {} fn primary_display(&self) -> Option> { - self.client.primary_display() + self.primary_display() } fn displays(&self) -> Vec> { - self.client.displays() + self.displays() } fn display(&self, id: DisplayId) -> Option> { - self.client.display(id) + self.display(id) } // todo(linux) @@ -222,7 +262,7 @@ impl Platform for LinuxPlatform { handle: AnyWindowHandle, options: WindowParams, ) -> Box { - self.client.open_window(handle, options) + self.open_window(handle, options) } fn open_url(&self, url: &str) { @@ -230,7 +270,7 @@ impl Platform for LinuxPlatform { } fn on_open_urls(&self, callback: Box)>) { - self.inner.callbacks.borrow_mut().open_urls = Some(callback); + self.with_common(|common| common.callbacks.open_urls = Some(callback)); } fn prompt_for_paths( @@ -238,8 +278,7 @@ impl Platform for LinuxPlatform { options: PathPromptOptions, ) -> oneshot::Receiver>> { let (done_tx, done_rx) = oneshot::channel(); - self.inner - .foreground_executor + self.foreground_executor() .spawn(async move { let title = if options.multiple { if !options.files { @@ -282,8 +321,7 @@ impl Platform for LinuxPlatform { fn prompt_for_new_path(&self, directory: &Path) -> oneshot::Receiver> { let (done_tx, done_rx) = oneshot::channel(); let directory = directory.to_owned(); - self.inner - .foreground_executor + self.foreground_executor() .spawn(async move { let result = SaveFileRequest::default() .modal(true) @@ -303,6 +341,7 @@ impl Platform for LinuxPlatform { done_tx.send(result); }) .detach(); + done_rx } @@ -317,35 +356,51 @@ impl Platform for LinuxPlatform { } fn on_become_active(&self, callback: Box) { - self.inner.callbacks.borrow_mut().become_active = Some(callback); + self.with_common(|common| { + common.callbacks.become_active = Some(callback); + }); } fn on_resign_active(&self, callback: Box) { - self.inner.callbacks.borrow_mut().resign_active = Some(callback); + self.with_common(|common| { + common.callbacks.resign_active = Some(callback); + }); } fn on_quit(&self, callback: Box) { - self.inner.callbacks.borrow_mut().quit = Some(callback); + self.with_common(|common| { + common.callbacks.quit = Some(callback); + }); } fn on_reopen(&self, callback: Box) { - self.inner.callbacks.borrow_mut().reopen = Some(callback); + self.with_common(|common| { + common.callbacks.reopen = Some(callback); + }); } fn on_event(&self, callback: Box bool>) { - self.inner.callbacks.borrow_mut().event = Some(callback); + self.with_common(|common| { + common.callbacks.event = Some(callback); + }); } fn on_app_menu_action(&self, callback: Box) { - self.inner.callbacks.borrow_mut().app_menu_action = Some(callback); + self.with_common(|common| { + common.callbacks.app_menu_action = Some(callback); + }); } fn on_will_open_app_menu(&self, callback: Box) { - self.inner.callbacks.borrow_mut().will_open_app_menu = Some(callback); + self.with_common(|common| { + common.callbacks.will_open_app_menu = Some(callback); + }); } fn on_validate_app_menu_command(&self, callback: Box bool>) { - self.inner.callbacks.borrow_mut().validate_app_menu_command = Some(callback); + self.with_common(|common| { + common.callbacks.validate_app_menu_command = Some(callback); + }); } fn os_name(&self) -> &'static str { @@ -381,7 +436,7 @@ impl Platform for LinuxPlatform { } fn set_cursor_style(&self, style: CursorStyle) { - self.client.set_cursor_style(style) + self.set_cursor_style(style) } // todo(linux) @@ -389,23 +444,6 @@ impl Platform for LinuxPlatform { false } - fn write_to_clipboard(&self, item: ClipboardItem) { - let clipboard = self.client.get_clipboard(); - clipboard.borrow_mut().set_contents(item.text); - } - - fn read_from_clipboard(&self) -> Option { - let clipboard = self.client.get_clipboard(); - let contents = clipboard.borrow_mut().get_contents(); - match contents { - Ok(text) => Some(ClipboardItem { - metadata: None, - text, - }), - _ => None, - } - } - fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Task> { let url = url.to_string(); let username = username.to_string(); @@ -479,14 +517,136 @@ impl Platform for LinuxPlatform { fn register_url_scheme(&self, _: &str) -> Task> { Task::ready(Err(anyhow!("register_url_scheme unimplemented"))) } + + fn write_to_clipboard(&self, item: ClipboardItem) { + self.write_to_clipboard(item) + } + + fn read_from_clipboard(&self) -> Option { + self.read_from_clipboard() + } +} + +pub(super) fn is_within_click_distance(a: Point, b: Point) -> bool { + let diff = a - b; + diff.x.abs() <= DOUBLE_CLICK_DISTANCE && diff.y.abs() <= DOUBLE_CLICK_DISTANCE +} + +impl Keystroke { + pub(super) fn from_xkb(state: &State, modifiers: Modifiers, keycode: Keycode) -> Self { + let mut modifiers = modifiers; + + let key_utf32 = state.key_get_utf32(keycode); + let key_utf8 = state.key_get_utf8(keycode); + let key_sym = state.key_get_one_sym(keycode); + + // The logic here tries to replicate the logic in `../mac/events.rs` + // "Consumed" modifiers are modifiers that have been used to translate a key, for example + // pressing "shift" and "1" on US layout produces the key `!` but "consumes" the shift. + // Notes: + // - macOS gets the key character directly ("."), xkb gives us the key name ("period") + // - macOS logic removes consumed shift modifier for symbols: "{", not "shift-{" + // - macOS logic keeps consumed shift modifiers for letters: "shift-a", not "a" or "A" + + let mut handle_consumed_modifiers = true; + let key = match key_sym { + Keysym::Return => "enter".to_owned(), + Keysym::Prior => "pageup".to_owned(), + Keysym::Next => "pagedown".to_owned(), + + Keysym::comma => ",".to_owned(), + Keysym::period => ".".to_owned(), + Keysym::less => "<".to_owned(), + Keysym::greater => ">".to_owned(), + Keysym::slash => "/".to_owned(), + Keysym::question => "?".to_owned(), + + Keysym::semicolon => ";".to_owned(), + Keysym::colon => ":".to_owned(), + Keysym::apostrophe => "'".to_owned(), + Keysym::quotedbl => "\"".to_owned(), + + Keysym::bracketleft => "[".to_owned(), + Keysym::braceleft => "{".to_owned(), + Keysym::bracketright => "]".to_owned(), + Keysym::braceright => "}".to_owned(), + Keysym::backslash => "\\".to_owned(), + Keysym::bar => "|".to_owned(), + + Keysym::grave => "`".to_owned(), + Keysym::asciitilde => "~".to_owned(), + Keysym::exclam => "!".to_owned(), + Keysym::at => "@".to_owned(), + Keysym::numbersign => "#".to_owned(), + Keysym::dollar => "$".to_owned(), + Keysym::percent => "%".to_owned(), + Keysym::asciicircum => "^".to_owned(), + Keysym::ampersand => "&".to_owned(), + Keysym::asterisk => "*".to_owned(), + Keysym::parenleft => "(".to_owned(), + Keysym::parenright => ")".to_owned(), + Keysym::minus => "-".to_owned(), + Keysym::underscore => "_".to_owned(), + Keysym::equal => "=".to_owned(), + Keysym::plus => "+".to_owned(), + + Keysym::ISO_Left_Tab => { + handle_consumed_modifiers = false; + "tab".to_owned() + } + + _ => { + handle_consumed_modifiers = false; + xkb::keysym_get_name(key_sym).to_lowercase() + } + }; + + // Ignore control characters (and DEL) for the purposes of ime_key, + // but if key_utf32 is 0 then assume it isn't one + let ime_key = ((key_utf32 == 0 || (key_utf32 >= 32 && key_utf32 != 127)) + && !key_utf8.is_empty()) + .then_some(key_utf8); + + if handle_consumed_modifiers { + let mod_shift_index = state.get_keymap().mod_get_index(xkb::MOD_NAME_SHIFT); + let is_shift_consumed = state.mod_index_is_consumed(keycode, mod_shift_index); + + if modifiers.shift && is_shift_consumed { + modifiers.shift = false; + } + } + + Keystroke { + modifiers, + key, + ime_key, + } + } } #[cfg(test)] mod tests { use super::*; + use crate::{px, Point}; - fn build_platform() -> LinuxPlatform { - let platform = LinuxPlatform::new(); - platform + #[test] + fn test_is_within_click_distance() { + let zero = Point::new(px(0.0), px(0.0)); + assert_eq!( + is_within_click_distance(zero, Point::new(px(5.0), px(5.0))), + true + ); + assert_eq!( + is_within_click_distance(zero, Point::new(px(-4.9), px(5.0))), + true + ); + assert_eq!( + is_within_click_distance(Point::new(px(3.0), px(2.0)), Point::new(px(-2.0), px(-2.0))), + true + ); + assert_eq!( + is_within_click_distance(zero, Point::new(px(5.0), px(5.1))), + false + ); } } diff --git a/crates/gpui/src/platform/linux/util.rs b/crates/gpui/src/platform/linux/util.rs deleted file mode 100644 index a485fb4b17..0000000000 --- a/crates/gpui/src/platform/linux/util.rs +++ /dev/null @@ -1,128 +0,0 @@ -use xkbcommon::xkb::{self, Keycode, Keysym, State}; - -use super::DOUBLE_CLICK_DISTANCE; -use crate::{Keystroke, Modifiers, Pixels, Point}; - -impl Keystroke { - pub(super) fn from_xkb(state: &State, modifiers: Modifiers, keycode: Keycode) -> Self { - let mut modifiers = modifiers; - - let key_utf32 = state.key_get_utf32(keycode); - let key_utf8 = state.key_get_utf8(keycode); - let key_sym = state.key_get_one_sym(keycode); - - // The logic here tries to replicate the logic in `../mac/events.rs` - // "Consumed" modifiers are modifiers that have been used to translate a key, for example - // pressing "shift" and "1" on US layout produces the key `!` but "consumes" the shift. - // Notes: - // - macOS gets the key character directly ("."), xkb gives us the key name ("period") - // - macOS logic removes consumed shift modifier for symbols: "{", not "shift-{" - // - macOS logic keeps consumed shift modifiers for letters: "shift-a", not "a" or "A" - - let mut handle_consumed_modifiers = true; - let key = match key_sym { - Keysym::Return => "enter".to_owned(), - Keysym::Prior => "pageup".to_owned(), - Keysym::Next => "pagedown".to_owned(), - - Keysym::comma => ",".to_owned(), - Keysym::period => ".".to_owned(), - Keysym::less => "<".to_owned(), - Keysym::greater => ">".to_owned(), - Keysym::slash => "/".to_owned(), - Keysym::question => "?".to_owned(), - - Keysym::semicolon => ";".to_owned(), - Keysym::colon => ":".to_owned(), - Keysym::apostrophe => "'".to_owned(), - Keysym::quotedbl => "\"".to_owned(), - - Keysym::bracketleft => "[".to_owned(), - Keysym::braceleft => "{".to_owned(), - Keysym::bracketright => "]".to_owned(), - Keysym::braceright => "}".to_owned(), - Keysym::backslash => "\\".to_owned(), - Keysym::bar => "|".to_owned(), - - Keysym::grave => "`".to_owned(), - Keysym::asciitilde => "~".to_owned(), - Keysym::exclam => "!".to_owned(), - Keysym::at => "@".to_owned(), - Keysym::numbersign => "#".to_owned(), - Keysym::dollar => "$".to_owned(), - Keysym::percent => "%".to_owned(), - Keysym::asciicircum => "^".to_owned(), - Keysym::ampersand => "&".to_owned(), - Keysym::asterisk => "*".to_owned(), - Keysym::parenleft => "(".to_owned(), - Keysym::parenright => ")".to_owned(), - Keysym::minus => "-".to_owned(), - Keysym::underscore => "_".to_owned(), - Keysym::equal => "=".to_owned(), - Keysym::plus => "+".to_owned(), - - Keysym::ISO_Left_Tab => { - handle_consumed_modifiers = false; - "tab".to_owned() - } - - _ => { - handle_consumed_modifiers = false; - xkb::keysym_get_name(key_sym).to_lowercase() - } - }; - - // Ignore control characters (and DEL) for the purposes of ime_key, - // but if key_utf32 is 0 then assume it isn't one - let ime_key = ((key_utf32 == 0 || (key_utf32 >= 32 && key_utf32 != 127)) - && !key_utf8.is_empty()) - .then_some(key_utf8); - - if handle_consumed_modifiers { - let mod_shift_index = state.get_keymap().mod_get_index(xkb::MOD_NAME_SHIFT); - let is_shift_consumed = state.mod_index_is_consumed(keycode, mod_shift_index); - - if modifiers.shift && is_shift_consumed { - modifiers.shift = false; - } - } - - Keystroke { - modifiers, - key, - ime_key, - } - } -} - -pub(super) fn is_within_click_distance(a: Point, b: Point) -> bool { - let diff = a - b; - diff.x.abs() <= DOUBLE_CLICK_DISTANCE && diff.y.abs() <= DOUBLE_CLICK_DISTANCE -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::{px, Point}; - - #[test] - fn test_is_within_click_distance() { - let zero = Point::new(px(0.0), px(0.0)); - assert_eq!( - is_within_click_distance(zero, Point::new(px(5.0), px(5.0))), - true - ); - assert_eq!( - is_within_click_distance(zero, Point::new(px(-4.9), px(5.0))), - true - ); - assert_eq!( - is_within_click_distance(Point::new(px(3.0), px(2.0)), Point::new(px(-2.0), px(-2.0))), - true - ); - assert_eq!( - is_within_click_distance(zero, Point::new(px(5.0), px(5.1))), - false - ); - } -} diff --git a/crates/gpui/src/platform/linux/wayland.rs b/crates/gpui/src/platform/linux/wayland.rs index ebb592d375..89a4851d63 100644 --- a/crates/gpui/src/platform/linux/wayland.rs +++ b/crates/gpui/src/platform/linux/wayland.rs @@ -1,9 +1,6 @@ -// todo(linux): remove this once the relevant functionality has been implemented -#![allow(unused_variables)] - -pub(crate) use client::*; - mod client; mod cursor; mod display; mod window; + +pub(crate) use client::*; diff --git a/crates/gpui/src/platform/linux/wayland/client.rs b/crates/gpui/src/platform/linux/wayland/client.rs index 95ab6b9342..069b5abc2b 100644 --- a/crates/gpui/src/platform/linux/wayland/client.rs +++ b/crates/gpui/src/platform/linux/wayland/client.rs @@ -1,27 +1,25 @@ use std::cell::RefCell; -use std::mem; -use std::num::NonZeroU32; use std::rc::Rc; -use std::sync::Arc; use std::time::{Duration, Instant}; use calloop::timer::{TimeoutAction, Timer}; -use calloop::LoopHandle; +use calloop::{EventLoop, LoopHandle}; use calloop_wayland_source::WaylandSource; +use collections::HashMap; use copypasta::wayland_clipboard::{create_clipboards_from_external, Clipboard, Primary}; use copypasta::ClipboardProvider; +use util::ResultExt; use wayland_backend::client::ObjectId; use wayland_backend::protocol::WEnum; -use wayland_client::globals::{registry_queue_init, GlobalListContents}; -use wayland_client::protocol::wl_callback::WlCallback; +use wayland_client::globals::{registry_queue_init, GlobalList, GlobalListContents}; +use wayland_client::protocol::wl_callback::{self, WlCallback}; use wayland_client::protocol::wl_output; use wayland_client::protocol::wl_pointer::{AxisRelativeDirection, AxisSource}; use wayland_client::{ delegate_noop, protocol::{ - wl_buffer, wl_callback, wl_compositor, wl_keyboard, wl_pointer, wl_registry, wl_seat, - wl_shm, wl_shm_pool, - wl_surface::{self, WlSurface}, + wl_buffer, wl_compositor, wl_keyboard, wl_pointer, wl_registry, wl_seat, wl_shm, + wl_shm_pool, wl_surface, }, Connection, Dispatch, Proxy, QueueHandle, }; @@ -37,60 +35,83 @@ use xkbcommon::xkb::ffi::XKB_KEYMAP_FORMAT_TEXT_V1; use xkbcommon::xkb::{self, Keycode, KEYMAP_COMPILE_NO_FLAGS}; use super::super::DOUBLE_CLICK_INTERVAL; -use crate::platform::linux::client::Client; -use crate::platform::linux::util::is_within_click_distance; +use crate::platform::linux::is_within_click_distance; use crate::platform::linux::wayland::cursor::Cursor; -use crate::platform::linux::wayland::window::{WaylandDecorationState, WaylandWindow}; -use crate::platform::{LinuxPlatformInner, PlatformWindow}; -use crate::WindowParams; +use crate::platform::linux::wayland::window::WaylandWindow; +use crate::platform::linux::LinuxClient; +use crate::platform::PlatformWindow; +use crate::{point, px, MouseExitEvent}; use crate::{ - platform::linux::wayland::window::WaylandWindowState, AnyWindowHandle, CursorStyle, DisplayId, - KeyDownEvent, KeyUpEvent, Keystroke, Modifiers, ModifiersChangedEvent, MouseButton, - MouseDownEvent, MouseMoveEvent, MouseUpEvent, NavigationDirection, Pixels, PlatformDisplay, - PlatformInput, Point, ScrollDelta, ScrollWheelEvent, TouchPhase, + AnyWindowHandle, CursorStyle, DisplayId, KeyDownEvent, KeyUpEvent, Keystroke, Modifiers, + ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, + NavigationDirection, Pixels, PlatformDisplay, PlatformInput, Point, ScrollDelta, + ScrollWheelEvent, TouchPhase, }; -use crate::{point, px}; +use crate::{LinuxCommon, WindowParams}; /// Used to convert evdev scancode to xkb scancode const MIN_KEYCODE: u32 = 8; -pub(crate) struct WaylandClientStateInner { - compositor: wl_compositor::WlCompositor, - wm_base: xdg_wm_base::XdgWmBase, - shm: wl_shm::WlShm, - viewporter: Option, - fractional_scale_manager: Option, - decoration_manager: Option, - windows: Vec<(xdg_surface::XdgSurface, Rc)>, - outputs: Vec<(wl_output::WlOutput, Rc>)>, - platform_inner: Rc, +#[derive(Debug, Clone)] +pub struct Globals { + pub qh: QueueHandle, + pub compositor: wl_compositor::WlCompositor, + pub wm_base: xdg_wm_base::XdgWmBase, + pub shm: wl_shm::WlShm, + pub viewporter: Option, + pub fractional_scale_manager: + Option, + pub decoration_manager: Option, +} + +impl Globals { + fn new(globals: GlobalList, qh: QueueHandle) -> Self { + Globals { + compositor: globals + .bind( + &qh, + wl_surface::REQ_SET_BUFFER_SCALE_SINCE + ..=wl_surface::EVT_PREFERRED_BUFFER_SCALE_SINCE, + (), + ) + .unwrap(), + shm: globals.bind(&qh, 1..=1, ()).unwrap(), + wm_base: globals.bind(&qh, 1..=1, ()).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(), + qh, + } + } +} + +pub(crate) struct WaylandClientState { + globals: Globals, + // Surface to Window mapping + windows: HashMap, + // Output to scale mapping + output_scales: HashMap, keymap_state: Option, - click_state: ClickState, + click: ClickState, repeat: KeyRepeat, modifiers: Modifiers, scroll_direction: f64, axis_source: AxisSource, - mouse_location: Point, + mouse_location: Option>, + enter_token: Option<()>, button_pressed: Option, - mouse_focused_window: Option>, - keyboard_focused_window: Option>, - loop_handle: Rc>, -} - -pub(crate) struct CursorState { + mouse_focused_window: Option, + keyboard_focused_window: Option, + loop_handle: LoopHandle<'static, WaylandClient>, cursor_icon_name: String, - cursor: Option, + cursor: Cursor, + clipboard: Clipboard, + primary: Primary, + event_loop: Option>, + common: LinuxCommon, } -#[derive(Clone)] -pub(crate) struct WaylandClientState { - client_state_inner: Rc>, - cursor_state: Rc>, - clipboard: Rc>, - primary: Rc>, -} - -struct ClickState { +pub struct ClickState { last_click: Instant, last_location: Point, current_count: usize, @@ -103,11 +124,8 @@ pub(crate) struct KeyRepeat { current_keysym: Option, } -pub(crate) struct WaylandClient { - platform_inner: Rc, - state: WaylandClientState, - qh: Arc>, -} +#[derive(Clone)] +pub struct WaylandClient(Rc>); const WL_SEAT_MIN_VERSION: u32 = 4; const WL_OUTPUT_VERSION: u32 = 2; @@ -126,12 +144,12 @@ fn wl_seat_version(version: u32) -> u32 { } impl WaylandClient { - pub(crate) fn new(linux_platform_inner: Rc) -> Self { + pub(crate) fn new() -> Self { let conn = Connection::connect_to_env().unwrap(); - let (globals, mut event_queue) = registry_queue_init::(&conn).unwrap(); + let (globals, mut event_queue) = registry_queue_init::(&conn).unwrap(); let qh = event_queue.handle(); - let mut outputs = Vec::new(); + let mut outputs = HashMap::default(); globals.contents().with_list(|list| { for global in list { @@ -144,15 +162,15 @@ impl WaylandClient { (), ); } - "wl_output" => outputs.push(( - globals.registry().bind::( + "wl_output" => { + let output = globals.registry().bind::( global.name, WL_OUTPUT_VERSION, &qh, (), - ), - Rc::new(RefCell::new(OutputState::default())), - )), + ); + outputs.insert(output.id(), 1); + } _ => {} } } @@ -161,20 +179,29 @@ impl WaylandClient { let display = conn.backend().display_ptr() as *mut std::ffi::c_void; let (primary, clipboard) = unsafe { create_clipboards_from_external(display) }; - let mut state_inner = Rc::new(RefCell::new(WaylandClientStateInner { - compositor: globals - .bind(&qh, 1..=wl_surface::EVT_PREFERRED_BUFFER_SCALE_SINCE, ()) - .unwrap(), - wm_base: globals.bind(&qh, 1..=1, ()).unwrap(), - shm: globals.bind(&qh, 1..=1, ()).unwrap(), - outputs, - viewporter: globals.bind(&qh, 1..=1, ()).ok(), - fractional_scale_manager: globals.bind(&qh, 1..=1, ()).ok(), - decoration_manager: globals.bind(&qh, 1..=1, ()).ok(), - windows: Vec::new(), - platform_inner: Rc::clone(&linux_platform_inner), + let event_loop = EventLoop::try_new().unwrap(); + + let (common, main_receiver) = LinuxCommon::new(event_loop.get_signal()); + + let handle = event_loop.handle(); + + handle.insert_source(main_receiver, |event, _, _: &mut WaylandClient| { + if let calloop::channel::Event::Msg(runnable) = event { + runnable.run(); + } + }); + + let globals = Globals::new(globals, qh); + + let cursor = Cursor::new(&conn, &globals, 24); + + let mut state = Rc::new(RefCell::new(WaylandClientState { + globals, + output_scales: outputs, + windows: HashMap::default(), + common, keymap_state: None, - click_state: ClickState { + click: ClickState { last_click: Instant::now(), last_location: Point::new(px(0.0), px(0.0)), current_count: 0, @@ -194,43 +221,26 @@ impl WaylandClient { }, scroll_direction: -1.0, axis_source: AxisSource::Wheel, - mouse_location: point(px(0.0), px(0.0)), + mouse_location: None, button_pressed: None, mouse_focused_window: None, keyboard_focused_window: None, - loop_handle: Rc::clone(&linux_platform_inner.loop_handle), - })); - - let mut cursor_state = Rc::new(RefCell::new(CursorState { + loop_handle: handle.clone(), cursor_icon_name: "arrow".to_string(), - cursor: None, + enter_token: None, + cursor, + clipboard, + primary, + event_loop: Some(event_loop), })); - let source = WaylandSource::new(conn, event_queue); + WaylandSource::new(conn, event_queue).insert(handle); - let mut state = WaylandClientState { - client_state_inner: Rc::clone(&state_inner), - cursor_state: Rc::clone(&cursor_state), - clipboard: Rc::new(RefCell::new(clipboard)), - primary: Rc::new(RefCell::new(primary)), - }; - let mut state_loop = state.clone(); - linux_platform_inner - .loop_handle - .insert_source(source, move |_, queue, _| { - queue.dispatch_pending(&mut state_loop) - }) - .unwrap(); - - Self { - platform_inner: linux_platform_inner, - state, - qh: Arc::new(qh), - } + Self(state) } } -impl Client for WaylandClient { +impl LinuxClient for WaylandClient { fn displays(&self) -> Vec> { Vec::new() } @@ -246,61 +256,14 @@ impl Client for WaylandClient { fn open_window( &self, handle: AnyWindowHandle, - options: WindowParams, + params: WindowParams, ) -> Box { - let mut state = self.state.client_state_inner.borrow_mut(); + let mut state = self.0.borrow_mut(); - let wl_surface = state.compositor.create_surface(&self.qh, ()); - let xdg_surface = state.wm_base.get_xdg_surface(&wl_surface, &self.qh, ()); - let toplevel = xdg_surface.get_toplevel(&self.qh, ()); - let wl_surface = Arc::new(wl_surface); + let (window, surface_id) = WaylandWindow::new(state.globals.clone(), params); + state.windows.insert(surface_id, window.clone()); - // Attempt to set up window decorations based on the requested configuration - // - // Note that wayland compositors may either not support decorations at all, or may - // support them but not allow clients to choose whether they are enabled or not. - // We attempt to account for these cases here. - - if let Some(decoration_manager) = state.decoration_manager.as_ref() { - // The protocol for managing decorations is present at least, but that doesn't - // mean that the compositor will allow us to use it. - - let decoration = - decoration_manager.get_toplevel_decoration(&toplevel, &self.qh, xdg_surface.id()); - - // todo(linux) - options.titlebar is lacking information required for wayland. - // Especially, whether a titlebar is wanted in itself. - // - // Removing the titlebar also removes the entire window frame (ie. the ability to - // close, move and resize the window [snapping still works]). This needs additional - // handling in Zed, in order to implement drag handlers on a titlebar element. - // - // Since all of this handling is not present, we request server-side decorations - // for now as a stopgap solution. - decoration.set_mode(zxdg_toplevel_decoration_v1::Mode::ServerSide); - } - - let viewport = state - .viewporter - .as_ref() - .map(|viewporter| viewporter.get_viewport(&wl_surface, &self.qh, ())); - - wl_surface.frame(&self.qh, wl_surface.clone()); - wl_surface.commit(); - - let window_state: Rc = Rc::new(WaylandWindowState::new( - wl_surface.clone(), - viewport, - Arc::new(toplevel), - options, - )); - - if let Some(fractional_scale_manager) = state.fractional_scale_manager.as_ref() { - fractional_scale_manager.get_fractional_scale(&wl_surface, &self.qh, xdg_surface.id()); - } - - state.windows.push((xdg_surface, Rc::clone(&window_state))); - Box::new(WaylandWindow(window_state)) + Box::new(window) } fn set_cursor_style(&self, style: CursorStyle) { @@ -329,19 +292,42 @@ impl Client for WaylandClient { } .to_string(); - self.state.cursor_state.borrow_mut().cursor_icon_name = cursor_icon_name; + self.0.borrow_mut().cursor_icon_name = cursor_icon_name; } - fn get_clipboard(&self) -> Rc> { - self.state.clipboard.clone() + fn with_common(&self, f: impl FnOnce(&mut LinuxCommon) -> R) -> R { + f(&mut self.0.borrow_mut().common) } - fn get_primary(&self) -> Rc> { - self.state.primary.clone() + fn run(&self) { + let mut event_loop = self + .0 + .borrow_mut() + .event_loop + .take() + .expect("App is already running"); + + event_loop.run(None, &mut self.clone(), |_| {}).log_err(); + } + + fn write_to_clipboard(&self, item: crate::ClipboardItem) { + self.0.borrow_mut().clipboard.set_contents(item.text); + } + + fn read_from_clipboard(&self) -> Option { + self.0 + .borrow_mut() + .clipboard + .get_contents() + .ok() + .map(|s| crate::ClipboardItem { + text: s, + metadata: None, + }) } } -impl Dispatch for WaylandClientState { +impl Dispatch for WaylandClient { fn event( state: &mut Self, registry: &wl_registry::WlRegistry, @@ -350,7 +336,7 @@ impl Dispatch for WaylandClientStat _: &Connection, qh: &QueueHandle, ) { - let mut state = state.client_state_inner.borrow_mut(); + let mut state = state.0.borrow_mut(); match event { wl_registry::Event::Global { name, @@ -361,10 +347,10 @@ impl Dispatch for WaylandClientStat registry.bind::(name, wl_seat_version(version), qh, ()); } "wl_output" => { - state.outputs.push(( - registry.bind::(name, WL_OUTPUT_VERSION, qh, ()), - Rc::new(RefCell::new(OutputState::default())), - )); + let output = + registry.bind::(name, WL_OUTPUT_VERSION, qh, ()); + + state.output_scales.insert(output.id(), 1); } _ => {} }, @@ -374,38 +360,40 @@ impl Dispatch for WaylandClientStat } } -delegate_noop!(WaylandClientState: ignore wl_compositor::WlCompositor); -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 wp_fractional_scale_manager_v1::WpFractionalScaleManagerV1); -delegate_noop!(WaylandClientState: ignore zxdg_decoration_manager_v1::ZxdgDecorationManagerV1); -delegate_noop!(WaylandClientState: ignore wp_viewporter::WpViewporter); -delegate_noop!(WaylandClientState: ignore wp_viewport::WpViewport); +delegate_noop!(WaylandClient: ignore wl_compositor::WlCompositor); +delegate_noop!(WaylandClient: ignore wl_shm::WlShm); +delegate_noop!(WaylandClient: ignore wl_shm_pool::WlShmPool); +delegate_noop!(WaylandClient: ignore wl_buffer::WlBuffer); +delegate_noop!(WaylandClient: ignore wp_fractional_scale_manager_v1::WpFractionalScaleManagerV1); +delegate_noop!(WaylandClient: ignore zxdg_decoration_manager_v1::ZxdgDecorationManagerV1); +delegate_noop!(WaylandClient: ignore wp_viewporter::WpViewporter); +delegate_noop!(WaylandClient: ignore wp_viewport::WpViewport); -impl Dispatch> for WaylandClientState { +impl Dispatch for WaylandClient { fn event( - state: &mut Self, - _: &WlCallback, + this: &mut WaylandClient, + _: &wl_callback::WlCallback, event: wl_callback::Event, - surf: &Arc, + surface_id: &ObjectId, _: &Connection, qh: &QueueHandle, ) { - let mut state = state.client_state_inner.borrow_mut(); - if let wl_callback::Event::Done { .. } = event { - for window in &state.windows { - if window.1.surface.id() == surf.id() { - window.1.surface.frame(qh, surf.clone()); - window.1.update(); - window.1.surface.commit(); - } + let state = this.0.borrow_mut(); + let Some(window) = state.windows.get(surface_id).cloned() else { + return; + }; + + drop(state); + match event { + wl_callback::Event::Done { callback_data } => { + window.frame(); } + _ => {} } } } -impl Dispatch for WaylandClientState { +impl Dispatch for WaylandClient { fn event( state: &mut Self, surface: &wl_surface::WlSurface, @@ -414,85 +402,18 @@ impl Dispatch for WaylandClientState { _: &Connection, _: &QueueHandle, ) { - let mut state = state.client_state_inner.borrow_mut(); - - // We use `WpFractionalScale` instead to set the scale if it's available - // or give up on scaling if `WlSurface::set_buffer_scale` isn't available - if state.fractional_scale_manager.is_some() - || state.compositor.version() < wl_surface::REQ_SET_BUFFER_SCALE_SINCE - { - return; - } - - let Some(window) = state - .windows - .iter() - .map(|(_, state)| state) - .find(|state| &*state.surface == surface) - else { + let mut state = state.0.borrow_mut(); + let Some(window) = state.windows.get(&surface.id()).cloned() else { return; }; + let scales = state.output_scales.clone(); + drop(state); - let mut outputs = window.outputs.borrow_mut(); - - match event { - wl_surface::Event::Enter { output } => { - // We use `PreferredBufferScale` instead to set the scale if it's available - if surface.version() >= wl_surface::EVT_PREFERRED_BUFFER_SCALE_SINCE { - return; - } - let mut scale = 1; - for global_output in &state.outputs { - if output == global_output.0 { - outputs.insert(output.id()); - scale = scale.max(global_output.1.borrow().scale.get()); - } else if outputs.contains(&global_output.0.id()) { - scale = scale.max(global_output.1.borrow().scale.get()); - } - } - window.rescale(scale as f32); - window.surface.set_buffer_scale(scale as i32); - } - wl_surface::Event::Leave { output } => { - // We use `PreferredBufferScale` instead to set the scale if it's available - if surface.version() >= wl_surface::EVT_PREFERRED_BUFFER_SCALE_SINCE { - return; - } - - outputs.remove(&output.id()); - - let mut scale = 1; - for global_output in &state.outputs { - if outputs.contains(&global_output.0.id()) { - scale = scale.max(global_output.1.borrow().scale.get()); - } - } - window.rescale(scale as f32); - window.surface.set_buffer_scale(scale as i32); - } - wl_surface::Event::PreferredBufferScale { factor } => { - window.rescale(factor as f32); - surface.set_buffer_scale(factor); - } - _ => {} - } + window.handle_surface_event(event, scales); } } -#[derive(Debug, Clone)] -struct OutputState { - scale: NonZeroU32, -} - -impl Default for OutputState { - fn default() -> Self { - Self { - scale: NonZeroU32::new(1).unwrap(), - } - } -} - -impl Dispatch for WaylandClientState { +impl Dispatch for WaylandClient { fn event( state: &mut Self, output: &wl_output::WlOutput, @@ -501,90 +422,64 @@ impl Dispatch for WaylandClientState { _: &Connection, _: &QueueHandle, ) { - let mut state = state.client_state_inner.borrow_mut(); - let mut output_state = state - .outputs - .iter_mut() - .find(|(o, _)| o == output) - .map(|(_, state)| state) - .unwrap() - .borrow_mut(); + let mut state = state.0.borrow_mut(); + let Some(mut output_scale) = state.output_scales.get_mut(&output.id()) else { + return; + }; + match event { wl_output::Event::Scale { factor } => { - if factor > 0 { - output_state.scale = NonZeroU32::new(factor as u32).unwrap(); - } + *output_scale = factor; } _ => {} } } } -impl Dispatch for WaylandClientState { +impl Dispatch for WaylandClient { fn event( state: &mut Self, xdg_surface: &xdg_surface::XdgSurface, event: xdg_surface::Event, - _: &(), + surface_id: &ObjectId, _: &Connection, _: &QueueHandle, ) { - let mut state = state.client_state_inner.borrow_mut(); + let mut state = state.0.borrow_mut(); + + // todo(linux): Apply the configuration changes as we go if let xdg_surface::Event::Configure { serial, .. } = event { xdg_surface.ack_configure(serial); - for window in &state.windows { - if &window.0 == xdg_surface { - window.1.update(); - window.1.surface.commit(); - return; - } - } } } } -impl Dispatch for WaylandClientState { +impl Dispatch for WaylandClient { fn event( - state: &mut Self, + this: &mut Self, xdg_toplevel: &xdg_toplevel::XdgToplevel, event: ::Event, - _: &(), + surface_id: &ObjectId, _: &Connection, _: &QueueHandle, ) { - let mut state = state.client_state_inner.borrow_mut(); - if let xdg_toplevel::Event::Configure { - width, - height, - states, - } = event - { - let width = NonZeroU32::new(width as u32); - let height = NonZeroU32::new(height as u32); - let fullscreen = states.contains(&(xdg_toplevel::State::Fullscreen as u8)); - for window in &state.windows { - if window.1.toplevel.id() == xdg_toplevel.id() { - window.1.resize(width, height); - window.1.set_fullscreen(fullscreen); - window.1.surface.commit(); - return; - } - } - } else if let xdg_toplevel::Event::Close = event { - state.windows.retain(|(_, window)| { - if window.toplevel.id() == xdg_toplevel.id() { - window.toplevel.destroy(); - false - } else { - true - } - }); - state.platform_inner.loop_signal.stop(); + let mut state = this.0.borrow_mut(); + + let Some(window) = state.windows.get(surface_id).cloned() else { + return; + }; + + drop(state); + let should_close = window.handle_toplevel_event(event); + + if should_close { + let mut state = this.0.borrow_mut(); + state.windows.remove(surface_id); } } } -impl Dispatch for WaylandClientState { +impl Dispatch for WaylandClient { fn event( _: &mut Self, wm_base: &xdg_wm_base::XdgWmBase, @@ -599,7 +494,7 @@ impl Dispatch for WaylandClientState { } } -impl Dispatch for WaylandClientState { +impl Dispatch for WaylandClient { fn event( state: &mut Self, seat: &wl_seat::WlSeat, @@ -622,7 +517,7 @@ impl Dispatch for WaylandClientState { } } -impl Dispatch for WaylandClientState { +impl Dispatch for WaylandClient { fn event( this: &mut Self, keyboard: &wl_keyboard::WlKeyboard, @@ -631,7 +526,7 @@ impl Dispatch for WaylandClientState { conn: &Connection, qh: &QueueHandle, ) { - let mut state = this.client_state_inner.borrow_mut(); + let mut state = this.0.borrow_mut(); match event { wl_keyboard::Event::RepeatInfo { rate, delay } => { state.repeat.characters_per_second = rate as u32; @@ -656,34 +551,28 @@ impl Dispatch for WaylandClientState { XKB_KEYMAP_FORMAT_TEXT_V1, KEYMAP_COMPILE_NO_FLAGS, ) - .unwrap() - } - .unwrap(); + .log_err() + .flatten() + .expect("Failed to create keymap") + }; state.keymap_state = Some(xkb::State::new(&keymap)); } wl_keyboard::Event::Enter { surface, .. } => { - state.keyboard_focused_window = state - .windows - .iter() - .find(|&w| w.1.surface.id() == surface.id()) - .map(|w| w.1.clone()); + state.keyboard_focused_window = state.windows.get(&surface.id()).cloned(); - if let Some(window) = &state.keyboard_focused_window { + if let Some(window) = state.keyboard_focused_window.clone() { + drop(state); window.set_focused(true); } } wl_keyboard::Event::Leave { surface, .. } => { - let keyboard_focused_window = state - .windows - .iter() - .find(|&w| w.1.surface.id() == surface.id()) - .map(|w| w.1.clone()); + let keyboard_focused_window = state.windows.get(&surface.id()).cloned(); + state.keyboard_focused_window = None; if let Some(window) = keyboard_focused_window { + drop(state); window.set_focused(false); } - - state.keyboard_focused_window = None; } wl_keyboard::Event::Modifiers { mods_depressed, @@ -719,7 +608,6 @@ impl Dispatch for WaylandClientState { }); drop(state); - focused_window.handle_input(input); } wl_keyboard::Event::Key { @@ -748,38 +636,33 @@ impl Dispatch for WaylandClientState { state.repeat.current_keysym = Some(keysym); let rate = state.repeat.characters_per_second; - let delay = state.repeat.delay; let id = state.repeat.current_id; - let this = this.clone(); - - let timer = Timer::from_duration(delay); - let state_ = Rc::clone(&this.client_state_inner); - let input_ = input.clone(); state .loop_handle - .insert_source(timer, move |event, _metadata, shared_data| { - let state_ = state_.borrow_mut(); - let is_repeating = id == state_.repeat.current_id - && state_.repeat.current_keysym.is_some() - && state_.keyboard_focused_window.is_some(); + .insert_source(Timer::from_duration(state.repeat.delay), { + let input = input.clone(); + move |event, _metadata, client| { + let state = client.0.borrow_mut(); + let is_repeating = id == state.repeat.current_id + && state.repeat.current_keysym.is_some() + && state.keyboard_focused_window.is_some(); - if !is_repeating { - return TimeoutAction::Drop; + if !is_repeating { + return TimeoutAction::Drop; + } + + let focused_window = + state.keyboard_focused_window.as_ref().unwrap().clone(); + + drop(state); + focused_window.handle_input(input.clone()); + + TimeoutAction::ToDuration(Duration::from_secs(1) / rate) } - - let focused_window = - state_.keyboard_focused_window.as_ref().unwrap().clone(); - - drop(state_); - - focused_window.handle_input(input_.clone()); - - TimeoutAction::ToDuration(Duration::from_secs(1) / rate) }) .unwrap(); drop(state); - focused_window.handle_input(input); } wl_keyboard::KeyState::Released if !keysym.is_modifier_key() => { @@ -790,7 +673,6 @@ impl Dispatch for WaylandClientState { state.repeat.current_keysym = None; drop(state); - focused_window.handle_input(input); } _ => {} @@ -821,23 +703,16 @@ fn linux_button_to_gpui(button: u32) -> Option { }) } -impl Dispatch for WaylandClientState { +impl Dispatch for WaylandClient { fn event( - self_state: &mut Self, + client: &mut Self, wl_pointer: &wl_pointer::WlPointer, event: wl_pointer::Event, data: &(), conn: &Connection, qh: &QueueHandle, ) { - let mut state = self_state.client_state_inner.borrow_mut(); - { - let mut cursor_state = self_state.cursor_state.borrow_mut(); - if cursor_state.cursor.is_none() { - cursor_state.cursor = - Some(Cursor::new(&conn, &state.compositor, &qh, &state.shm, 24)); - } - } + let mut state = client.0.borrow_mut(); match event { wl_pointer::Event::Enter { @@ -847,21 +722,32 @@ impl Dispatch for WaylandClientState { surface_y, .. } => { - let windows = mem::take(&mut state.windows); - for window in windows.iter() { - if window.1.surface.id() == surface.id() { - window.1.set_focused(true); - state.mouse_focused_window = Some(window.1.clone()); - let mut cursor_state = self_state.cursor_state.borrow_mut(); - let cursor_icon_name = cursor_state.cursor_icon_name.clone(); - if let Some(mut cursor) = cursor_state.cursor.as_mut() { - cursor.set_serial_id(serial); - cursor.set_icon(&wl_pointer, cursor_icon_name); - } - } + state.mouse_location = Some(point(px(surface_x as f32), px(surface_y as f32))); + + if let Some(window) = state.windows.get(&surface.id()).cloned() { + state.enter_token = Some(()); + state.mouse_focused_window = Some(window.clone()); + state.cursor.set_serial_id(serial); + state.cursor.set_icon(&wl_pointer, None); + drop(state); + window.set_focused(true); + } + } + wl_pointer::Event::Leave { surface, .. } => { + if let Some(focused_window) = state.mouse_focused_window.clone() { + state.enter_token.take(); + let input = PlatformInput::MouseExited(MouseExitEvent { + position: state.mouse_location.unwrap(), + pressed_button: state.button_pressed, + modifiers: state.modifiers, + }); + state.mouse_focused_window = None; + state.mouse_location = None; + + drop(state); + focused_window.handle_input(input); + focused_window.set_focused(false); } - state.windows = windows; - state.mouse_location = point(px(surface_x as f32), px(surface_y as f32)); } wl_pointer::Event::Motion { time, @@ -872,19 +758,17 @@ impl Dispatch for WaylandClientState { if state.mouse_focused_window.is_none() { return; } - state.mouse_location = point(px(surface_x as f32), px(surface_y as f32)); + state.mouse_location = Some(point(px(surface_x as f32), px(surface_y as f32))); + state.cursor.set_icon(&wl_pointer, None); - state.mouse_focused_window.as_ref().unwrap().handle_input( - PlatformInput::MouseMove(MouseMoveEvent { - position: state.mouse_location, + if let Some(window) = state.mouse_focused_window.clone() { + let input = PlatformInput::MouseMove(MouseMoveEvent { + position: state.mouse_location.unwrap(), pressed_button: state.button_pressed, modifiers: state.modifiers, - }), - ); - let mut cursor_state = self_state.cursor_state.borrow_mut(); - let cursor_icon_name = cursor_state.cursor_icon_name.clone(); - if let Some(mut cursor) = cursor_state.cursor.as_mut() { - cursor.set_icon(&wl_pointer, cursor_icon_name); + }); + drop(state); + window.handle_input(input); } } wl_pointer::Event::Button { @@ -899,43 +783,49 @@ impl Dispatch for WaylandClientState { } match button_state { wl_pointer::ButtonState::Pressed => { - let click_elapsed = state.click_state.last_click.elapsed(); + let click_elapsed = state.click.last_click.elapsed(); if click_elapsed < DOUBLE_CLICK_INTERVAL && is_within_click_distance( - state.click_state.last_location, - state.mouse_location, + state.click.last_location, + state.mouse_location.unwrap(), ) { - state.click_state.current_count += 1; + state.click.current_count += 1; } else { - state.click_state.current_count = 1; + state.click.current_count = 1; } - state.click_state.last_click = Instant::now(); - state.click_state.last_location = state.mouse_location; + state.click.last_click = Instant::now(); + state.click.last_location = state.mouse_location.unwrap(); state.button_pressed = Some(button); - state.mouse_focused_window.as_ref().unwrap().handle_input( - PlatformInput::MouseDown(MouseDownEvent { + + if let Some(window) = state.mouse_focused_window.clone() { + let input = PlatformInput::MouseDown(MouseDownEvent { button, - position: state.mouse_location, + position: state.mouse_location.unwrap(), modifiers: state.modifiers, - click_count: state.click_state.current_count, - first_mouse: false, - }), - ); + click_count: state.click.current_count, + first_mouse: state.enter_token.take().is_some(), + }); + drop(state); + window.handle_input(input); + } } wl_pointer::ButtonState::Released => { state.button_pressed = None; - state.mouse_focused_window.as_ref().unwrap().handle_input( - PlatformInput::MouseUp(MouseUpEvent { + + if let Some(window) = state.mouse_focused_window.clone() { + let input = PlatformInput::MouseUp(MouseUpEvent { button, - position: state.mouse_location, + position: state.mouse_location.unwrap(), modifiers: state.modifiers, - click_count: state.click_state.current_count, - }), - ); + click_count: state.click.current_count, + }); + drop(state); + window.handle_input(input); + } } _ => {} } @@ -959,10 +849,11 @@ impl Dispatch for WaylandClientState { axis: WEnum::Value(axis), value120, } => { - if let Some(focused_window) = &state.mouse_focused_window { + if let Some(focused_window) = state.mouse_focused_window.clone() { let value = value120 as f64 * state.scroll_direction; - focused_window.handle_input(PlatformInput::ScrollWheel(ScrollWheelEvent { - position: state.mouse_location, + + let input = PlatformInput::ScrollWheel(ScrollWheelEvent { + position: state.mouse_location.unwrap(), delta: match axis { wl_pointer::Axis::VerticalScroll => { ScrollDelta::Pixels(point(px(0.0), px(value as f32))) @@ -974,7 +865,9 @@ impl Dispatch for WaylandClientState { }, modifiers: state.modifiers, touch_phase: TouchPhase::Moved, - })) + }); + drop(state); + focused_window.handle_input(input) } } wl_pointer::Event::Axis { @@ -989,10 +882,11 @@ impl Dispatch for WaylandClientState { { return; } - if let Some(focused_window) = &state.mouse_focused_window { + if let Some(focused_window) = state.mouse_focused_window.clone() { let value = value * state.scroll_direction; - focused_window.handle_input(PlatformInput::ScrollWheel(ScrollWheelEvent { - position: state.mouse_location, + + let input = PlatformInput::ScrollWheel(ScrollWheelEvent { + position: state.mouse_location.unwrap(), delta: match axis { wl_pointer::Axis::VerticalScroll => { ScrollDelta::Pixels(point(px(0.0), px(value as f32))) @@ -1004,49 +898,37 @@ impl Dispatch for WaylandClientState { }, modifiers: state.modifiers, touch_phase: TouchPhase::Moved, - })) + }); + drop(state); + focused_window.handle_input(input) } } - wl_pointer::Event::Leave { surface, .. } => { - if let Some(focused_window) = &state.mouse_focused_window { - focused_window.handle_input(PlatformInput::MouseMove(MouseMoveEvent { - position: Point::default(), - pressed_button: None, - modifiers: Modifiers::default(), - })); - focused_window.set_focused(false); - } - state.mouse_focused_window = None; - } _ => {} } } } -impl Dispatch for WaylandClientState { +impl Dispatch for WaylandClient { fn event( state: &mut Self, _: &wp_fractional_scale_v1::WpFractionalScaleV1, event: ::Event, - id: &ObjectId, + surface_id: &ObjectId, _: &Connection, _: &QueueHandle, ) { - let mut state = state.client_state_inner.borrow_mut(); - if let wp_fractional_scale_v1::Event::PreferredScale { scale, .. } = event { - for window in &state.windows { - if window.0.id() == *id { - window.1.rescale(scale as f32 / 120.0); - return; - } - } - } + let mut state = state.0.borrow_mut(); + + let Some(window) = state.windows.get(surface_id).cloned() else { + return; + }; + + drop(state); + window.handle_fractional_scale_event(event); } } -impl Dispatch - for WaylandClientState -{ +impl Dispatch for WaylandClient { fn event( state: &mut Self, _: &zxdg_toplevel_decoration_v1::ZxdgToplevelDecorationV1, @@ -1055,31 +937,13 @@ impl Dispatch _: &Connection, _: &QueueHandle, ) { - let mut state = state.client_state_inner.borrow_mut(); - if let zxdg_toplevel_decoration_v1::Event::Configure { mode, .. } = event { - for window in &state.windows { - if window.0.id() == *surface_id { - match mode { - WEnum::Value(zxdg_toplevel_decoration_v1::Mode::ServerSide) => { - window - .1 - .set_decoration_state(WaylandDecorationState::Server); - } - WEnum::Value(zxdg_toplevel_decoration_v1::Mode::ClientSide) => { - window - .1 - .set_decoration_state(WaylandDecorationState::Client); - } - WEnum::Value(_) => { - log::warn!("Unknown decoration mode"); - } - WEnum::Unknown(v) => { - log::warn!("Unknown decoration mode: {}", v); - } - } - return; - } - } - } + let mut state = state.0.borrow_mut(); + + let Some(window) = state.windows.get(surface_id).cloned() else { + return; + }; + + drop(state); + window.handle_toplevel_decoration_event(event); } } diff --git a/crates/gpui/src/platform/linux/wayland/cursor.rs b/crates/gpui/src/platform/linux/wayland/cursor.rs index 8ea66168c8..4a1de5f2e0 100644 --- a/crates/gpui/src/platform/linux/wayland/cursor.rs +++ b/crates/gpui/src/platform/linux/wayland/cursor.rs @@ -1,31 +1,31 @@ -use crate::platform::linux::wayland::WaylandClientState; -use wayland_backend::client::InvalidId; -use wayland_client::protocol::wl_compositor::WlCompositor; +use crate::Globals; +use util::ResultExt; + use wayland_client::protocol::wl_pointer::WlPointer; -use wayland_client::protocol::wl_shm::WlShm; use wayland_client::protocol::wl_surface::WlSurface; -use wayland_client::{Connection, QueueHandle}; +use wayland_client::Connection; use wayland_cursor::{CursorImageBuffer, CursorTheme}; pub(crate) struct Cursor { - theme: Result, + theme: Option, current_icon_name: String, surface: WlSurface, serial_id: u32, } +impl Drop for Cursor { + fn drop(&mut self) { + self.theme.take(); + self.surface.destroy(); + } +} + impl Cursor { - pub fn new( - connection: &Connection, - compositor: &WlCompositor, - qh: &QueueHandle, - shm: &WlShm, - size: u32, - ) -> Self { + pub fn new(connection: &Connection, globals: &Globals, size: u32) -> Self { Self { - theme: CursorTheme::load(&connection, shm.clone(), size), - current_icon_name: "".to_string(), - surface: compositor.create_surface(qh, ()), + theme: CursorTheme::load(&connection, globals.shm.clone(), size).log_err(), + current_icon_name: "default".to_string(), + surface: globals.compositor.create_surface(&globals.qh, ()), serial_id: 0, } } @@ -34,17 +34,17 @@ impl Cursor { self.serial_id = serial_id; } - pub fn set_icon(&mut self, wl_pointer: &WlPointer, cursor_icon_name: String) { - let mut cursor_icon_name = cursor_icon_name.clone(); + pub fn set_icon(&mut self, wl_pointer: &WlPointer, mut cursor_icon_name: Option<&str>) { + let mut cursor_icon_name = cursor_icon_name.unwrap_or("default"); if self.current_icon_name != cursor_icon_name { - if let Ok(theme) = &mut self.theme { + if let Some(theme) = &mut self.theme { let mut buffer: Option<&CursorImageBuffer>; if let Some(cursor) = theme.get_cursor(&cursor_icon_name) { buffer = Some(&cursor[0]); } else if let Some(cursor) = theme.get_cursor("default") { buffer = Some(&cursor[0]); - cursor_icon_name = "default".to_string(); + cursor_icon_name = "default"; log::warn!( "Linux: Wayland: Unable to get cursor icon: {}. Using default cursor icon", cursor_icon_name @@ -68,7 +68,7 @@ impl Cursor { self.surface.damage(0, 0, width as i32, height as i32); self.surface.commit(); - self.current_icon_name = cursor_icon_name; + self.current_icon_name = cursor_icon_name.to_string(); } } else { log::warn!("Linux: Wayland: Unable to load cursor themes"); diff --git a/crates/gpui/src/platform/linux/wayland/window.rs b/crates/gpui/src/platform/linux/wayland/window.rs index 3938256673..dd6884ad64 100644 --- a/crates/gpui/src/platform/linux/wayland/window.rs +++ b/crates/gpui/src/platform/linux/wayland/window.rs @@ -7,23 +7,28 @@ use std::sync::Arc; use blade_graphics as gpu; use blade_rwh::{HasRawDisplayHandle, HasRawWindowHandle, RawDisplayHandle, RawWindowHandle}; -use collections::HashSet; +use collections::{HashMap, HashSet}; use futures::channel::oneshot::Receiver; use raw_window_handle::{ DisplayHandle, HandleError, HasDisplayHandle, HasWindowHandle, WindowHandle, }; use wayland_backend::client::ObjectId; +use wayland_client::WEnum; use wayland_client::{protocol::wl_surface, Proxy}; +use wayland_protocols::wp::fractional_scale::v1::client::wp_fractional_scale_v1; use wayland_protocols::wp::viewporter::client::wp_viewport; -use wayland_protocols::xdg::shell::client::xdg_toplevel; +use wayland_protocols::xdg::decoration::zv1::client::zxdg_toplevel_decoration_v1; +use wayland_protocols::xdg::shell::client::xdg_surface; +use wayland_protocols::xdg::shell::client::xdg_toplevel::{self, WmCapabilities}; use crate::platform::blade::BladeRenderer; use crate::platform::linux::wayland::display::WaylandDisplay; use crate::platform::{PlatformAtlas, PlatformInputHandler, PlatformWindow}; use crate::scene::Scene; use crate::{ - px, size, Bounds, DevicePixels, Modifiers, Pixels, PlatformDisplay, PlatformInput, Point, - PromptLevel, Size, WindowAppearance, WindowBackgroundAppearance, WindowParams, + px, size, Bounds, DevicePixels, Globals, Modifiers, Pixels, PlatformDisplay, PlatformInput, + Point, PromptLevel, RcRefCell, Size, WindowAppearance, WindowBackgroundAppearance, + WindowParams, }; #[derive(Default)] @@ -39,14 +44,6 @@ pub(crate) struct Callbacks { appearance_changed: Option>, } -struct WaylandWindowInner { - renderer: BladeRenderer, - bounds: Bounds, - scale: f32, - input_handler: Option, - decoration_state: WaylandDecorationState, -} - struct RawWindow { window: *mut c_void, display: *mut c_void, @@ -68,11 +65,36 @@ unsafe impl HasRawDisplayHandle for RawWindow { } } -impl WaylandWindowInner { - fn new(wl_surf: &Arc, bounds: Bounds) -> Self { +pub struct WaylandWindowState { + xdg_surface: xdg_surface::XdgSurface, + surface: wl_surface::WlSurface, + toplevel: xdg_toplevel::XdgToplevel, + viewport: Option, + outputs: HashSet, + globals: Globals, + renderer: BladeRenderer, + bounds: Bounds, + scale: f32, + input_handler: Option, + decoration_state: WaylandDecorationState, + fullscreen: bool, + maximized: bool, +} + +impl WaylandWindowState { + pub(crate) fn new( + surface: wl_surface::WlSurface, + xdg_surface: xdg_surface::XdgSurface, + viewport: Option, + toplevel: xdg_toplevel::XdgToplevel, + globals: Globals, + options: WindowParams, + ) -> Self { + let bounds = options.bounds.map(|p| p.0 as u32); + let raw = RawWindow { - window: wl_surf.id().as_ptr().cast::(), - display: wl_surf + window: surface.id().as_ptr().cast::(), + display: surface .backend() .upgrade() .unwrap() @@ -97,54 +119,213 @@ impl WaylandWindowInner { height: bounds.size.height, depth: 1, }; + Self { + xdg_surface, + surface, + toplevel, + viewport, + globals, + + outputs: HashSet::default(), + renderer: BladeRenderer::new(gpu, extent), bounds, scale: 1.0, input_handler: None, - - // On wayland, decorations are by default provided by the client decoration_state: WaylandDecorationState::Client, + fullscreen: false, + maximized: false, } } } -pub(crate) struct WaylandWindowState { - inner: RefCell, - pub(crate) callbacks: RefCell, - pub(crate) surface: Arc, - pub(crate) toplevel: Arc, - pub(crate) outputs: RefCell>, - viewport: Option, - fullscreen: RefCell, +#[derive(Clone)] +pub(crate) struct WaylandWindow { + pub(crate) state: RcRefCell, + pub(crate) callbacks: Rc>, } -impl WaylandWindowState { - pub(crate) fn new( - wl_surf: Arc, - viewport: Option, - toplevel: Arc, - options: WindowParams, - ) -> Self { - let bounds = options.bounds.map(|p| p.0 as u32); +impl WaylandWindow { + pub fn ptr_eq(&self, other: &Self) -> bool { + Rc::ptr_eq(&self.state, &other.state) + } - Self { - surface: Arc::clone(&wl_surf), - inner: RefCell::new(WaylandWindowInner::new(&wl_surf, bounds)), - callbacks: RefCell::new(Callbacks::default()), - outputs: RefCell::new(HashSet::default()), - toplevel, + pub fn new(globals: Globals, params: WindowParams) -> (Self, ObjectId) { + let surface = globals.compositor.create_surface(&globals.qh, ()); + let xdg_surface = globals + .wm_base + .get_xdg_surface(&surface, &globals.qh, surface.id()); + let toplevel = xdg_surface.get_toplevel(&globals.qh, surface.id()); + + if let Some(fractional_scale_manager) = globals.fractional_scale_manager.as_ref() { + fractional_scale_manager.get_fractional_scale(&surface, &globals.qh, surface.id()); + } + + // Attempt to set up window decorations based on the requested configuration + if let Some(decoration_manager) = globals.decoration_manager.as_ref() { + let decoration = + decoration_manager.get_toplevel_decoration(&toplevel, &globals.qh, surface.id()); + + // Request client side decorations if possible + decoration.set_mode(zxdg_toplevel_decoration_v1::Mode::ClientSide); + } + + let viewport = globals + .viewporter + .as_ref() + .map(|viewporter| viewporter.get_viewport(&surface, &globals.qh, ())); + + surface.frame(&globals.qh, surface.id()); + + let window_state = RcRefCell::new(WaylandWindowState::new( + surface.clone(), + xdg_surface, viewport, - fullscreen: RefCell::new(false), + toplevel, + globals, + params, + )); + + let this = Self { + state: window_state, + callbacks: Rc::new(RefCell::new(Callbacks::default())), + }; + + // Kick things off + surface.commit(); + + (this, surface.id()) + } + + pub fn frame(&self) { + let state = self.state.borrow_mut(); + state.surface.frame(&state.globals.qh, state.surface.id()); + drop(state); + + let mut cb = self.callbacks.borrow_mut(); + if let Some(fun) = cb.request_frame.as_mut() { + fun(); } } - pub fn update(&self) { - let mut cb = self.callbacks.borrow_mut(); - if let Some(mut fun) = cb.request_frame.take() { - drop(cb); - fun(); - self.callbacks.borrow_mut().request_frame = Some(fun); + pub fn handle_toplevel_decoration_event(&self, event: zxdg_toplevel_decoration_v1::Event) { + match event { + zxdg_toplevel_decoration_v1::Event::Configure { mode } => match mode { + WEnum::Value(zxdg_toplevel_decoration_v1::Mode::ServerSide) => { + self.set_decoration_state(WaylandDecorationState::Server) + } + WEnum::Value(zxdg_toplevel_decoration_v1::Mode::ClientSide) => { + self.set_decoration_state(WaylandDecorationState::Server) + } + WEnum::Value(_) => { + log::warn!("Unknown decoration mode"); + } + WEnum::Unknown(v) => { + log::warn!("Unknown decoration mode: {}", v); + } + }, + _ => {} + } + } + + pub fn handle_fractional_scale_event(&self, event: wp_fractional_scale_v1::Event) { + match event { + wp_fractional_scale_v1::Event::PreferredScale { scale } => { + self.rescale(scale as f32 / 120.0); + } + _ => {} + } + } + + pub fn handle_toplevel_event(&self, event: xdg_toplevel::Event) -> bool { + match event { + xdg_toplevel::Event::Configure { + width, + height, + states, + } => { + let width = NonZeroU32::new(width as u32); + let height = NonZeroU32::new(height as u32); + let fullscreen = states.contains(&(xdg_toplevel::State::Fullscreen as u8)); + let maximized = states.contains(&(xdg_toplevel::State::Maximized as u8)); + self.resize(width, height); + self.set_fullscreen(fullscreen); + let mut state = self.state.borrow_mut(); + state.maximized = true; + false + } + xdg_toplevel::Event::Close => { + let mut cb = self.callbacks.borrow_mut(); + if let Some(mut should_close) = cb.should_close.take() { + let result = (should_close)(); + cb.should_close = Some(should_close); + result + } else { + false + } + } + _ => false, + } + } + + pub fn handle_surface_event( + &self, + event: wl_surface::Event, + output_scales: HashMap, + ) { + let mut state = self.state.borrow_mut(); + + // We use `WpFractionalScale` instead to set the scale if it's available + if state.globals.fractional_scale_manager.is_some() { + return; + } + + match event { + wl_surface::Event::Enter { output } => { + // We use `PreferredBufferScale` instead to set the scale if it's available + if state.surface.version() >= wl_surface::EVT_PREFERRED_BUFFER_SCALE_SINCE { + return; + } + + state.outputs.insert(output.id()); + + let mut scale = 1; + for output in state.outputs.iter() { + if let Some(s) = output_scales.get(output) { + scale = scale.max(*s) + } + } + + state.surface.set_buffer_scale(scale); + drop(state); + self.rescale(scale as f32); + } + wl_surface::Event::Leave { output } => { + // We use `PreferredBufferScale` instead to set the scale if it's available + if state.surface.version() >= wl_surface::EVT_PREFERRED_BUFFER_SCALE_SINCE { + return; + } + + state.outputs.remove(&output.id()); + + let mut scale = 1; + for output in state.outputs.iter() { + if let Some(s) = output_scales.get(output) { + scale = scale.max(*s) + } + } + + state.surface.set_buffer_scale(scale); + drop(state); + self.rescale(scale as f32); + } + wl_surface::Event::PreferredBufferScale { factor } => { + state.surface.set_buffer_scale(factor); + drop(state); + self.rescale(factor as f32); + } + _ => {} } } @@ -155,26 +336,26 @@ impl WaylandWindowState { scale: Option, ) { let (width, height, scale) = { - let mut inner = self.inner.borrow_mut(); - if width.map_or(true, |width| width.get() == inner.bounds.size.width) - && height.map_or(true, |height| height.get() == inner.bounds.size.height) - && scale.map_or(true, |scale| scale == inner.scale) + let mut state = self.state.borrow_mut(); + if width.map_or(true, |width| width.get() == state.bounds.size.width) + && height.map_or(true, |height| height.get() == state.bounds.size.height) + && scale.map_or(true, |scale| scale == state.scale) { return; } if let Some(width) = width { - inner.bounds.size.width = width.get(); + state.bounds.size.width = width.get(); } if let Some(height) = height { - inner.bounds.size.height = height.get(); + state.bounds.size.height = height.get(); } if let Some(scale) = scale { - inner.scale = scale; + state.scale = scale; } - let width = inner.bounds.size.width; - let height = inner.bounds.size.height; - let scale = inner.scale; - inner.renderer.update_drawable_size(size( + let width = state.bounds.size.width; + let height = state.bounds.size.height; + let scale = state.scale; + state.renderer.update_drawable_size(size( width as f64 * scale as f64, height as f64 * scale as f64, )); @@ -191,8 +372,11 @@ impl WaylandWindowState { ); } - if let Some(viewport) = &self.viewport { - viewport.set_destination(width as i32, height as i32); + { + let state = self.state.borrow(); + if let Some(viewport) = &state.viewport { + viewport.set_destination(width as i32, height as i32); + } } } @@ -205,11 +389,13 @@ impl WaylandWindowState { } pub fn set_fullscreen(&self, fullscreen: bool) { + let mut state = self.state.borrow_mut(); + state.fullscreen = fullscreen; + let mut callbacks = self.callbacks.borrow_mut(); if let Some(ref mut fun) = callbacks.fullscreen { fun(fullscreen) } - self.fullscreen.replace(fullscreen); } /// Notifies the window of the state of the decorations. @@ -221,9 +407,7 @@ impl WaylandWindowState { /// of the decorations. This is because the state of the decorations /// is managed by the compositor and not the client. pub fn set_decoration_state(&self, state: WaylandDecorationState) { - self.inner.borrow_mut().decoration_state = state; - log::trace!("Window decorations are now handled by {:?}", state); - // todo(linux) - Handle this properly + self.state.borrow_mut().decoration_state = state; } pub fn close(&self) { @@ -231,7 +415,7 @@ impl WaylandWindowState { if let Some(fun) = callbacks.close.take() { fun() } - self.toplevel.destroy(); + self.state.borrow_mut().toplevel.destroy(); } pub fn handle_input(&self, input: PlatformInput) { @@ -241,10 +425,13 @@ impl WaylandWindowState { } } if let PlatformInput::KeyDown(event) = input { - let mut inner = self.inner.borrow_mut(); - if let Some(ref mut input_handler) = inner.input_handler { + let mut state = self.state.borrow_mut(); + if let Some(mut input_handler) = state.input_handler.take() { if let Some(ime_key) = &event.keystroke.ime_key { + drop(state); input_handler.replace_text_in_range(None, ime_key); + let mut state = self.state.borrow_mut(); + state.input_handler = Some(input_handler); } } } @@ -257,9 +444,6 @@ impl WaylandWindowState { } } -#[derive(Clone)] -pub(crate) struct WaylandWindow(pub(crate) Rc); - impl HasWindowHandle for WaylandWindow { fn window_handle(&self) -> Result, HandleError> { unimplemented!() @@ -273,31 +457,29 @@ impl HasDisplayHandle for WaylandWindow { } impl PlatformWindow for WaylandWindow { - // todo(linux) fn bounds(&self) -> Bounds { - unimplemented!() + self.state.borrow().bounds.map(|p| DevicePixels(p as i32)) } - // todo(linux) fn is_maximized(&self) -> bool { - false + self.state.borrow().maximized } - // todo(linux) fn is_minimized(&self) -> bool { + // This cannot be determined by the client false } fn content_size(&self) -> Size { - let inner = self.0.inner.borrow(); + let state = self.state.borrow(); Size { - width: Pixels(inner.bounds.size.width as f32), - height: Pixels(inner.bounds.size.height as f32), + width: Pixels(state.bounds.size.width as f32), + height: Pixels(state.bounds.size.height as f32), } } fn scale_factor(&self) -> f32 { - self.0.inner.borrow().scale + self.state.borrow().scale } // todo(linux) @@ -325,11 +507,11 @@ impl PlatformWindow for WaylandWindow { } fn set_input_handler(&mut self, input_handler: PlatformInputHandler) { - self.0.inner.borrow_mut().input_handler = Some(input_handler); + self.state.borrow_mut().input_handler = Some(input_handler); } fn take_input_handler(&mut self) -> Option { - self.0.inner.borrow_mut().input_handler.take() + self.state.borrow_mut().input_handler.take() } fn prompt( @@ -352,7 +534,10 @@ impl PlatformWindow for WaylandWindow { } fn set_title(&mut self, title: &str) { - self.0.toplevel.set_title(title.to_string()); + self.state + .borrow_mut() + .toplevel + .set_title(title.to_string()); } fn set_background_appearance(&mut self, _background_appearance: WindowBackgroundAppearance) { @@ -368,7 +553,7 @@ impl PlatformWindow for WaylandWindow { } fn minimize(&self) { - self.0.toplevel.set_minimized(); + self.state.borrow_mut().toplevel.set_minimized(); } fn zoom(&self) { @@ -376,47 +561,48 @@ impl PlatformWindow for WaylandWindow { } fn toggle_fullscreen(&self) { - if !(*self.0.fullscreen.borrow()) { - self.0.toplevel.set_fullscreen(None); + let state = self.state.borrow_mut(); + if !state.fullscreen { + state.toplevel.set_fullscreen(None); } else { - self.0.toplevel.unset_fullscreen(); + state.toplevel.unset_fullscreen(); } } fn is_fullscreen(&self) -> bool { - *self.0.fullscreen.borrow() + self.state.borrow().fullscreen } fn on_request_frame(&self, callback: Box) { - self.0.callbacks.borrow_mut().request_frame = Some(callback); + self.callbacks.borrow_mut().request_frame = Some(callback); } fn on_input(&self, callback: Box crate::DispatchEventResult>) { - self.0.callbacks.borrow_mut().input = Some(callback); + self.callbacks.borrow_mut().input = Some(callback); } fn on_active_status_change(&self, callback: Box) { - self.0.callbacks.borrow_mut().active_status_change = Some(callback); + self.callbacks.borrow_mut().active_status_change = Some(callback); } fn on_resize(&self, callback: Box, f32)>) { - self.0.callbacks.borrow_mut().resize = Some(callback); + self.callbacks.borrow_mut().resize = Some(callback); } fn on_fullscreen(&self, callback: Box) { - self.0.callbacks.borrow_mut().fullscreen = Some(callback); + self.callbacks.borrow_mut().fullscreen = Some(callback); } fn on_moved(&self, callback: Box) { - self.0.callbacks.borrow_mut().moved = Some(callback); + self.callbacks.borrow_mut().moved = Some(callback); } fn on_should_close(&self, callback: Box bool>) { - self.0.callbacks.borrow_mut().should_close = Some(callback); + self.callbacks.borrow_mut().should_close = Some(callback); } fn on_close(&self, callback: Box) { - self.0.callbacks.borrow_mut().close = Some(callback); + self.callbacks.borrow_mut().close = Some(callback); } fn on_appearance_changed(&self, callback: Box) { @@ -429,12 +615,18 @@ impl PlatformWindow for WaylandWindow { } fn draw(&self, scene: &Scene) { - self.0.inner.borrow_mut().renderer.draw(scene); + let mut state = self.state.borrow_mut(); + state.renderer.draw(scene); + } + + fn completed_frame(&self) { + let mut state = self.state.borrow_mut(); + state.surface.commit(); } fn sprite_atlas(&self) -> Arc { - let inner = self.0.inner.borrow(); - inner.renderer.sprite_atlas().clone() + let state = self.state.borrow(); + state.renderer.sprite_atlas().clone() } } diff --git a/crates/gpui/src/platform/linux/x11/client.rs b/crates/gpui/src/platform/linux/x11/client.rs index 337456d5f8..210a0e4853 100644 --- a/crates/gpui/src/platform/linux/x11/client.rs +++ b/crates/gpui/src/platform/linux/x11/client.rs @@ -1,11 +1,14 @@ use std::cell::RefCell; +use std::ops::Deref; use std::rc::Rc; use std::time::{Duration, Instant}; +use calloop::{EventLoop, LoopHandle}; use collections::HashMap; use copypasta::x11_clipboard::{Clipboard, Primary, X11ClipboardContext}; use copypasta::ClipboardProvider; +use util::ResultExt; use x11rb::connection::{Connection, RequestConnection}; use x11rb::errors::ConnectionError; use x11rb::protocol::randr::ConnectionExt as _; @@ -16,50 +19,71 @@ use x11rb::xcb_ffi::XCBConnection; use xkbc::x11::ffi::{XKB_X11_MIN_MAJOR_XKB_VERSION, XKB_X11_MIN_MINOR_XKB_VERSION}; use xkbcommon::xkb as xkbc; -use crate::platform::linux::client::Client; -use crate::platform::{LinuxPlatformInner, PlatformWindow}; +use crate::platform::linux::LinuxClient; +use crate::platform::{LinuxCommon, PlatformWindow}; use crate::{ px, AnyWindowHandle, Bounds, CursorStyle, DisplayId, Pixels, PlatformDisplay, PlatformInput, Point, ScrollDelta, Size, TouchPhase, WindowParams, }; -use super::{super::SCROLL_LINES, X11Display, X11Window, X11WindowState, XcbAtoms}; +use super::{super::SCROLL_LINES, X11Display, X11Window, XcbAtoms}; +use super::{button_of_key, modifiers_from_state}; +use crate::platform::linux::is_within_click_distance; use crate::platform::linux::platform::DOUBLE_CLICK_INTERVAL; -use crate::platform::linux::util::is_within_click_distance; use calloop::{ generic::{FdWrapper, Generic}, RegistrationToken, }; -struct WindowRef { - state: Rc, +pub(crate) struct WindowRef { + window: X11Window, refresh_event_token: RegistrationToken, } -struct X11ClientState { - windows: HashMap, - xkb: xkbc::State, - clipboard: Rc>>, - primary: Rc>>, - click_state: ClickState, +impl Deref for WindowRef { + type Target = X11Window; + + fn deref(&self) -> &Self::Target { + &self.window + } } -struct ClickState { - last_click: Instant, - last_location: Point, - current_count: usize, +pub struct X11ClientState { + pub(crate) loop_handle: LoopHandle<'static, X11Client>, + pub(crate) event_loop: Option>, + + pub(crate) last_click: Instant, + pub(crate) last_location: Point, + pub(crate) current_count: usize, + + pub(crate) xcb_connection: Rc, + pub(crate) x_root_index: usize, + pub(crate) atoms: XcbAtoms, + pub(crate) windows: HashMap, + pub(crate) xkb: xkbc::State, + + pub(crate) common: LinuxCommon, + pub(crate) clipboard: X11ClipboardContext, + pub(crate) primary: X11ClipboardContext, } -pub(crate) struct X11Client { - platform_inner: Rc, - xcb_connection: Rc, - x_root_index: usize, - atoms: XcbAtoms, - state: RefCell, -} +#[derive(Clone)] +pub(crate) struct X11Client(Rc>); impl X11Client { - pub(crate) fn new(inner: Rc) -> Rc { + pub(crate) fn new() -> Self { + let event_loop = EventLoop::try_new().unwrap(); + + let (common, main_receiver) = LinuxCommon::new(event_loop.get_signal()); + + let handle = event_loop.handle(); + + handle.insert_source(main_receiver, |event, _, _: &mut X11Client| { + if let calloop::channel::Event::Msg(runnable) = event { + runnable.run(); + } + }); + let (xcb_connection, x_root_index) = XCBConnection::connect(None).unwrap(); xcb_connection .prefetch_extension_information(xkb::X11_EXTENSION_NAME) @@ -94,30 +118,10 @@ impl X11Client { let xcb_connection = Rc::new(xcb_connection); - let click_state = ClickState { - last_click: Instant::now(), - last_location: Point::new(px(0.0), px(0.0)), - current_count: 0, - }; - let client: Rc = Rc::new(Self { - platform_inner: inner.clone(), - xcb_connection: Rc::clone(&xcb_connection), - x_root_index, - atoms, - state: RefCell::new(X11ClientState { - windows: HashMap::default(), - xkb: xkb_state, - clipboard: Rc::new(RefCell::new(clipboard)), - primary: Rc::new(RefCell::new(primary)), - click_state, - }), - }); - // Safety: Safe if xcb::Connection always returns a valid fd let fd = unsafe { FdWrapper::new(Rc::clone(&xcb_connection)) }; - inner - .loop_handle + handle .insert_source( Generic::new_with_error::( fd, @@ -125,8 +129,8 @@ impl X11Client { calloop::Mode::Level, ), { - let client = Rc::clone(&client); - move |_readiness, _, _| { + let xcb_connection = xcb_connection.clone(); + move |_readiness, _, client| { while let Some(event) = xcb_connection.poll_for_event()? { client.handle_event(event); } @@ -136,34 +140,47 @@ impl X11Client { ) .expect("Failed to initialize x11 event source"); - client + X11Client(Rc::new(RefCell::new(X11ClientState { + event_loop: Some(event_loop), + loop_handle: handle, + common, + last_click: Instant::now(), + last_location: Point::new(px(0.0), px(0.0)), + current_count: 0, + + xcb_connection, + x_root_index, + atoms, + windows: HashMap::default(), + xkb: xkb_state, + clipboard, + primary, + }))) } - fn get_window(&self, win: xproto::Window) -> Option> { - let state = self.state.borrow(); - state.windows.get(&win).map(|wr| Rc::clone(&wr.state)) + fn get_window(&self, win: xproto::Window) -> Option { + let state = self.0.borrow(); + state + .windows + .get(&win) + .map(|window_reference| window_reference.window.clone()) } fn handle_event(&self, event: Event) -> Option<()> { match event { Event::ClientMessage(event) => { let [atom, ..] = event.data.as_data32(); - if atom == self.atoms.WM_DELETE_WINDOW { + let mut state = self.0.borrow_mut(); + + if atom == state.atoms.WM_DELETE_WINDOW { // window "x" button clicked by user, we gracefully exit - let window_ref = self - .state - .borrow_mut() - .windows - .remove(&event.window) - .unwrap(); + let window_ref = state.windows.remove(&event.window)?; - self.platform_inner - .loop_handle - .remove(window_ref.refresh_event_token); - window_ref.state.destroy(); + state.loop_handle.remove(window_ref.refresh_event_token); + window_ref.window.destroy(); - if self.state.borrow().windows.is_empty() { - self.platform_inner.loop_signal.stop(); + if state.windows.is_empty() { + state.common.signal.stop(); } } } @@ -195,15 +212,17 @@ impl X11Client { } Event::KeyPress(event) => { let window = self.get_window(event.event)?; - let modifiers = super::modifiers_from_state(event.state); + let mut state = self.0.borrow_mut(); + + let modifiers = modifiers_from_state(event.state); let keystroke = { let code = event.detail.into(); - let mut state = self.state.borrow_mut(); let keystroke = crate::Keystroke::from_xkb(&state.xkb, modifiers, code); state.xkb.update_key(code, xkbc::KeyDirection::Down); keystroke }; + drop(state); window.handle_input(PlatformInput::KeyDown(crate::KeyDownEvent { keystroke, is_held: false, @@ -211,48 +230,54 @@ impl X11Client { } Event::KeyRelease(event) => { let window = self.get_window(event.event)?; - let modifiers = super::modifiers_from_state(event.state); + let mut state = self.0.borrow_mut(); + + let modifiers = modifiers_from_state(event.state); let keystroke = { let code = event.detail.into(); - let mut state = self.state.borrow_mut(); let keystroke = crate::Keystroke::from_xkb(&state.xkb, modifiers, code); state.xkb.update_key(code, xkbc::KeyDirection::Up); keystroke }; - + drop(state); window.handle_input(PlatformInput::KeyUp(crate::KeyUpEvent { keystroke })); } Event::ButtonPress(event) => { let window = self.get_window(event.event)?; - let modifiers = super::modifiers_from_state(event.state); + let mut state = self.0.borrow_mut(); + + let modifiers = modifiers_from_state(event.state); let position = Point::new((event.event_x as f32).into(), (event.event_y as f32).into()); - if let Some(button) = super::button_of_key(event.detail) { - let mut state = self.state.borrow_mut(); - let click_elapsed = state.click_state.last_click.elapsed(); + if let Some(button) = button_of_key(event.detail) { + let click_elapsed = state.last_click.elapsed(); if click_elapsed < DOUBLE_CLICK_INTERVAL - && is_within_click_distance(state.click_state.last_location, position) + && is_within_click_distance(state.last_location, position) { - state.click_state.current_count += 1; + state.current_count += 1; } else { - state.click_state.current_count = 1; + state.current_count = 1; } - state.click_state.last_click = Instant::now(); - state.click_state.last_location = position; + state.last_click = Instant::now(); + state.last_location = position; + let current_count = state.current_count; + drop(state); window.handle_input(PlatformInput::MouseDown(crate::MouseDownEvent { button, position, modifiers, - click_count: state.click_state.current_count, + click_count: current_count, first_mouse: false, })); } else if event.detail >= 4 && event.detail <= 5 { // https://stackoverflow.com/questions/15510472/scrollwheel-event-in-x11 let scroll_direction = if event.detail == 4 { 1.0 } else { -1.0 }; let scroll_y = SCROLL_LINES * scroll_direction; + + drop(state); window.handle_input(PlatformInput::ScrollWheel(crate::ScrollWheelEvent { position, delta: ScrollDelta::Lines(Point::new(0.0, scroll_y as f32)), @@ -265,16 +290,18 @@ impl X11Client { } Event::ButtonRelease(event) => { let window = self.get_window(event.event)?; - let modifiers = super::modifiers_from_state(event.state); + let state = self.0.borrow(); + let modifiers = modifiers_from_state(event.state); let position = Point::new((event.event_x as f32).into(), (event.event_y as f32).into()); - let state = self.state.borrow(); - if let Some(button) = super::button_of_key(event.detail) { + if let Some(button) = button_of_key(event.detail) { + let click_count = state.current_count; + drop(state); window.handle_input(PlatformInput::MouseUp(crate::MouseUpEvent { button, position, modifiers, - click_count: state.click_state.current_count, + click_count, })); } } @@ -283,7 +310,7 @@ impl X11Client { let pressed_button = super::button_from_state(event.state); let position = Point::new((event.event_x as f32).into(), (event.event_y as f32).into()); - let modifiers = super::modifiers_from_state(event.state); + let modifiers = modifiers_from_state(event.state); window.handle_input(PlatformInput::MouseMove(crate::MouseMoveEvent { pressed_button, position, @@ -295,7 +322,7 @@ impl X11Client { let pressed_button = super::button_from_state(event.state); let position = Point::new((event.event_x as f32).into(), (event.event_y as f32).into()); - let modifiers = super::modifiers_from_state(event.state); + let modifiers = modifiers_from_state(event.state); window.handle_input(PlatformInput::MouseExited(crate::MouseExitEvent { pressed_button, position, @@ -309,61 +336,71 @@ impl X11Client { } } -impl Client for X11Client { +impl LinuxClient for X11Client { + fn with_common(&self, f: impl FnOnce(&mut LinuxCommon) -> R) -> R { + f(&mut self.0.borrow_mut().common) + } + fn displays(&self) -> Vec> { - let setup = self.xcb_connection.setup(); + let state = self.0.borrow(); + let setup = state.xcb_connection.setup(); setup .roots .iter() .enumerate() .filter_map(|(root_id, _)| { - Some(Rc::new(X11Display::new(&self.xcb_connection, root_id)?) + Some(Rc::new(X11Display::new(&state.xcb_connection, root_id)?) as Rc) }) .collect() } - fn display(&self, id: DisplayId) -> Option> { - Some(Rc::new(X11Display::new( - &self.xcb_connection, - id.0 as usize, - )?)) - } - fn primary_display(&self) -> Option> { + let state = self.0.borrow(); + Some(Rc::new( - X11Display::new(&self.xcb_connection, self.x_root_index) + X11Display::new(&state.xcb_connection, state.x_root_index) .expect("There should always be a root index"), )) } + fn display(&self, id: DisplayId) -> Option> { + let state = self.0.borrow(); + + Some(Rc::new(X11Display::new( + &state.xcb_connection, + id.0 as usize, + )?)) + } + fn open_window( &self, _handle: AnyWindowHandle, - options: WindowParams, + params: WindowParams, ) -> Box { - let x_window = self.xcb_connection.generate_id().unwrap(); + let mut state = self.0.borrow_mut(); + let x_window = state.xcb_connection.generate_id().unwrap(); - let window_ptr = Rc::new(X11WindowState::new( - options, - &self.xcb_connection, - self.x_root_index, + let window = X11Window::new( + params, + &state.xcb_connection, + state.x_root_index, x_window, - &self.atoms, - )); + &state.atoms, + ); - let screen_resources = self + let screen_resources = state .xcb_connection .randr_get_screen_resources(x_window) .unwrap() .reply() - .expect("TODO"); + .expect("Could not find available screens"); let mode = screen_resources .crtcs .iter() .find_map(|crtc| { - let crtc_info = self + let crtc_info = state .xcb_connection .randr_get_crtc_info(*crtc, x11rb::CURRENT_TIME) .ok()? @@ -377,16 +414,14 @@ impl Client for X11Client { }) .expect("Unable to find screen refresh rate"); - // .expect("Missing screen mode for crtc specified mode id"); - - let refresh_event_token = self - .platform_inner + let refresh_event_token = state .loop_handle .insert_source(calloop::timer::Timer::immediate(), { let refresh_duration = mode_refresh_rate(mode); - let xcb_connection = Rc::clone(&self.xcb_connection); - move |mut instant, (), _| { - xcb_connection + move |mut instant, (), client| { + let state = client.0.borrow_mut(); + state + .xcb_connection .send_event( false, x_window, @@ -403,7 +438,7 @@ impl Client for X11Client { }, ) .unwrap(); - let _ = xcb_connection.flush().unwrap(); + let _ = state.xcb_connection.flush().unwrap(); // Take into account that some frames have been skipped let now = time::Instant::now(); while instant < now { @@ -415,22 +450,42 @@ impl Client for X11Client { .expect("Failed to initialize refresh timer"); let window_ref = WindowRef { - state: Rc::clone(&window_ptr), + window: window.clone(), refresh_event_token, }; - self.state.borrow_mut().windows.insert(x_window, window_ref); - Box::new(X11Window(window_ptr)) + + state.windows.insert(x_window, window_ref); + Box::new(window) } //todo(linux) fn set_cursor_style(&self, _style: CursorStyle) {} - fn get_clipboard(&self) -> Rc> { - self.state.borrow().clipboard.clone() + fn write_to_clipboard(&self, item: crate::ClipboardItem) { + self.0.borrow_mut().clipboard.set_contents(item.text); } - fn get_primary(&self) -> Rc> { - self.state.borrow().primary.clone() + fn read_from_clipboard(&self) -> Option { + self.0 + .borrow_mut() + .clipboard + .get_contents() + .ok() + .map(|text| crate::ClipboardItem { + text, + metadata: None, + }) + } + + fn run(&self) { + let mut event_loop = self + .0 + .borrow_mut() + .event_loop + .take() + .expect("App is already running"); + + event_loop.run(None, &mut self.clone(), |_| {}).log_err(); } } diff --git a/crates/gpui/src/platform/linux/x11/window.rs b/crates/gpui/src/platform/linux/x11/window.rs index 9bd0635031..fe0f94cda7 100644 --- a/crates/gpui/src/platform/linux/x11/window.rs +++ b/crates/gpui/src/platform/linux/x11/window.rs @@ -5,10 +5,12 @@ use crate::{ platform::blade::BladeRenderer, size, Bounds, DevicePixels, Modifiers, Pixels, PlatformAtlas, PlatformDisplay, PlatformInput, PlatformInputHandler, PlatformWindow, Point, PromptLevel, Scene, Size, WindowAppearance, WindowBackgroundAppearance, WindowOptions, WindowParams, + X11Client, X11ClientState, }; use blade_graphics as gpu; use parking_lot::Mutex; use raw_window_handle as rwh; +use util::ResultExt; use x11rb::{ connection::Connection, protocol::xproto::{self, ConnectionExt as _, CreateWindowAux}, @@ -17,8 +19,9 @@ use x11rb::{ }; use std::{ - cell::RefCell, + cell::{Ref, RefCell, RefMut}, ffi::c_void, + iter::Zip, mem, num::NonZeroU32, ptr::NonNull, @@ -28,19 +31,6 @@ use std::{ use super::X11Display; -#[derive(Default)] -struct Callbacks { - request_frame: Option>, - input: Option crate::DispatchEventResult>>, - active_status_change: Option>, - resize: Option, f32)>>, - fullscreen: Option>, - moved: Option>, - should_close: Option bool>>, - close: Option>, - appearance_changed: Option>, -} - x11rb::atom_manager! { pub XcbAtoms: AtomsCookie { WM_PROTOCOLS, @@ -51,23 +41,6 @@ x11rb::atom_manager! { } } -struct LinuxWindowInner { - bounds: Bounds, - scale_factor: f32, - renderer: BladeRenderer, - input_handler: Option, -} - -impl LinuxWindowInner { - fn content_size(&self) -> Size { - let size = self.renderer.viewport_size(); - Size { - width: size.width.into(), - height: size.height.into(), - } - } -} - fn query_render_extent(xcb_connection: &XCBConnection, x_window: xproto::Window) -> gpu::Extent { let reply = xcb_connection .get_geometry(x_window) @@ -88,17 +61,37 @@ struct RawWindow { visual_id: u32, } +#[derive(Default)] +pub struct Callbacks { + request_frame: Option>, + input: Option crate::DispatchEventResult>>, + active_status_change: Option>, + resize: Option, f32)>>, + fullscreen: Option>, + moved: Option>, + should_close: Option bool>>, + close: Option>, + appearance_changed: Option>, +} + pub(crate) struct X11WindowState { - xcb_connection: Rc, - display: Rc, raw: RawWindow, - x_window: xproto::Window, - callbacks: RefCell, - inner: RefCell, + + bounds: Bounds, + scale_factor: f32, + renderer: BladeRenderer, + display: Rc, + + input_handler: Option, } #[derive(Clone)] -pub(crate) struct X11Window(pub(crate) Rc); +pub(crate) struct X11Window { + pub(crate) state: Rc>, + pub(crate) callbacks: Rc>, + xcb_connection: Rc, + x_window: xproto::Window, +} // todo(linux): Remove other RawWindowHandle implementation unsafe impl blade_rwh::HasRawWindowHandle for RawWindow { @@ -121,7 +114,7 @@ unsafe impl blade_rwh::HasRawDisplayHandle for RawWindow { impl rwh::HasWindowHandle for X11Window { fn window_handle(&self) -> Result { Ok(unsafe { - let non_zero = NonZeroU32::new(self.0.raw.window_id).unwrap(); + let non_zero = NonZeroU32::new(self.state.borrow().raw.window_id).unwrap(); let handle = rwh::XcbWindowHandle::new(non_zero); rwh::WindowHandle::borrow_raw(handle.into()) }) @@ -130,8 +123,9 @@ impl rwh::HasWindowHandle for X11Window { impl rwh::HasDisplayHandle for X11Window { fn display_handle(&self) -> Result { Ok(unsafe { - let non_zero = NonNull::new(self.0.raw.connection).unwrap(); - let handle = rwh::XcbDisplayHandle::new(Some(non_zero), self.0.raw.screen_id as i32); + let this = self.state.borrow(); + let non_zero = NonNull::new(this.raw.connection).unwrap(); + let handle = rwh::XcbDisplayHandle::new(Some(non_zero), this.raw.screen_id as i32); rwh::DisplayHandle::borrow_raw(handle.into()) }) } @@ -239,22 +233,52 @@ impl X11WindowState { let gpu_extent = query_render_extent(xcb_connection, x_window); Self { - xcb_connection: xcb_connection.clone(), display: Rc::new(X11Display::new(xcb_connection, x_screen_index).unwrap()), raw, + bounds: params.bounds.map(|v| v.0), + scale_factor: 1.0, + renderer: BladeRenderer::new(gpu, gpu_extent), + + input_handler: None, + } + } + + fn content_size(&self) -> Size { + let size = self.renderer.viewport_size(); + Size { + width: size.width.into(), + height: size.height.into(), + } + } +} + +impl X11Window { + pub fn new( + params: WindowParams, + xcb_connection: &Rc, + x_main_screen_index: usize, + x_window: xproto::Window, + atoms: &XcbAtoms, + ) -> Self { + X11Window { + state: Rc::new(RefCell::new(X11WindowState::new( + params, + xcb_connection, + x_main_screen_index, + x_window, + atoms, + ))), + callbacks: Rc::new(RefCell::new(Callbacks::default())), + xcb_connection: xcb_connection.clone(), x_window, - callbacks: RefCell::new(Callbacks::default()), - inner: RefCell::new(LinuxWindowInner { - bounds: params.bounds.map(|v| v.0), - scale_factor: 1.0, - renderer: BladeRenderer::new(gpu, gpu_extent), - input_handler: None, - }), } } pub fn destroy(&self) { - self.inner.borrow_mut().renderer.destroy(); + let mut state = self.state.borrow_mut(); + state.renderer.destroy(); + drop(state); + self.xcb_connection.unmap_window(self.x_window).unwrap(); self.xcb_connection.destroy_window(self.x_window).unwrap(); if let Some(fun) = self.callbacks.borrow_mut().close.take() { @@ -270,21 +294,40 @@ impl X11WindowState { } } + pub fn handle_input(&self, input: PlatformInput) { + if let Some(ref mut fun) = self.callbacks.borrow_mut().input { + if !fun(input.clone()).propagate { + return; + } + } + if let PlatformInput::KeyDown(event) = input { + let mut state = self.state.borrow_mut(); + if let Some(mut input_handler) = state.input_handler.take() { + if let Some(ime_key) = &event.keystroke.ime_key { + drop(state); + input_handler.replace_text_in_range(None, ime_key); + state = self.state.borrow_mut(); + } + state.input_handler = Some(input_handler); + } + } + } + pub fn configure(&self, bounds: Bounds) { let mut resize_args = None; let do_move; { - let mut inner = self.inner.borrow_mut(); - let old_bounds = mem::replace(&mut inner.bounds, bounds); + let mut state = self.state.borrow_mut(); + let old_bounds = mem::replace(&mut state.bounds, bounds); do_move = old_bounds.origin != bounds.origin; // todo(linux): use normal GPUI types here, refactor out the double // viewport check and extra casts ( ) let gpu_size = query_render_extent(&self.xcb_connection, self.x_window); - if inner.renderer.viewport_size() != gpu_size { - inner + if state.renderer.viewport_size() != gpu_size { + state .renderer .update_drawable_size(size(gpu_size.width as f64, gpu_size.height as f64)); - resize_args = Some((inner.content_size(), inner.scale_factor)); + resize_args = Some((state.content_size(), state.scale_factor)); } } @@ -301,22 +344,6 @@ impl X11WindowState { } } - pub fn handle_input(&self, input: PlatformInput) { - if let Some(ref mut fun) = self.callbacks.borrow_mut().input { - if !fun(input.clone()).propagate { - return; - } - } - if let PlatformInput::KeyDown(event) = input { - let mut inner = self.inner.borrow_mut(); - if let Some(ref mut input_handler) = inner.input_handler { - if let Some(ime_key) = &event.keystroke.ime_key { - input_handler.replace_text_in_range(None, ime_key); - } - } - } - } - pub fn set_focused(&self, focus: bool) { if let Some(ref mut fun) = self.callbacks.borrow_mut().active_status_change { fun(focus); @@ -326,7 +353,7 @@ impl X11WindowState { impl PlatformWindow for X11Window { fn bounds(&self) -> Bounds { - self.0.inner.borrow_mut().bounds.map(|v| v.into()) + self.state.borrow_mut().bounds.map(|v| v.into()) } // todo(linux) @@ -340,11 +367,11 @@ impl PlatformWindow for X11Window { } fn content_size(&self) -> Size { - self.0.inner.borrow_mut().content_size() + self.state.borrow_mut().content_size() } fn scale_factor(&self) -> f32 { - self.0.inner.borrow_mut().scale_factor + self.state.borrow_mut().scale_factor } // todo(linux) @@ -353,14 +380,13 @@ impl PlatformWindow for X11Window { } fn display(&self) -> Rc { - Rc::clone(&self.0.display) + self.state.borrow().display.clone() } fn mouse_position(&self) -> Point { let reply = self - .0 .xcb_connection - .query_pointer(self.0.x_window) + .query_pointer(self.x_window) .unwrap() .reply() .unwrap(); @@ -377,11 +403,11 @@ impl PlatformWindow for X11Window { } fn set_input_handler(&mut self, input_handler: PlatformInputHandler) { - self.0.inner.borrow_mut().input_handler = Some(input_handler); + self.state.borrow_mut().input_handler = Some(input_handler); } fn take_input_handler(&mut self) -> Option { - self.0.inner.borrow_mut().input_handler.take() + self.state.borrow_mut().input_handler.take() } fn prompt( @@ -396,10 +422,9 @@ impl PlatformWindow for X11Window { fn activate(&self) { let win_aux = xproto::ConfigureWindowAux::new().stack_mode(xproto::StackMode::ABOVE); - self.0 - .xcb_connection - .configure_window(self.0.x_window, &win_aux) - .unwrap(); + self.xcb_connection + .configure_window(self.x_window, &win_aux) + .log_err(); } // todo(linux) @@ -408,11 +433,10 @@ impl PlatformWindow for X11Window { } fn set_title(&mut self, title: &str) { - self.0 - .xcb_connection + self.xcb_connection .change_property8( xproto::PropMode::REPLACE, - self.0.x_window, + self.x_window, xproto::AtomEnum::WM_NAME, xproto::AtomEnum::STRING, title.as_bytes(), @@ -458,39 +482,39 @@ impl PlatformWindow for X11Window { } fn on_request_frame(&self, callback: Box) { - self.0.callbacks.borrow_mut().request_frame = Some(callback); + self.callbacks.borrow_mut().request_frame = Some(callback); } fn on_input(&self, callback: Box crate::DispatchEventResult>) { - self.0.callbacks.borrow_mut().input = Some(callback); + self.callbacks.borrow_mut().input = Some(callback); } fn on_active_status_change(&self, callback: Box) { - self.0.callbacks.borrow_mut().active_status_change = Some(callback); + self.callbacks.borrow_mut().active_status_change = Some(callback); } fn on_resize(&self, callback: Box, f32)>) { - self.0.callbacks.borrow_mut().resize = Some(callback); + self.callbacks.borrow_mut().resize = Some(callback); } fn on_fullscreen(&self, callback: Box) { - self.0.callbacks.borrow_mut().fullscreen = Some(callback); + self.callbacks.borrow_mut().fullscreen = Some(callback); } fn on_moved(&self, callback: Box) { - self.0.callbacks.borrow_mut().moved = Some(callback); + self.callbacks.borrow_mut().moved = Some(callback); } fn on_should_close(&self, callback: Box bool>) { - self.0.callbacks.borrow_mut().should_close = Some(callback); + self.callbacks.borrow_mut().should_close = Some(callback); } fn on_close(&self, callback: Box) { - self.0.callbacks.borrow_mut().close = Some(callback); + self.callbacks.borrow_mut().close = Some(callback); } fn on_appearance_changed(&self, callback: Box) { - self.0.callbacks.borrow_mut().appearance_changed = Some(callback); + self.callbacks.borrow_mut().appearance_changed = Some(callback); } // todo(linux) @@ -499,12 +523,12 @@ impl PlatformWindow for X11Window { } fn draw(&self, scene: &Scene) { - let mut inner = self.0.inner.borrow_mut(); + let mut inner = self.state.borrow_mut(); inner.renderer.draw(scene); } fn sprite_atlas(&self) -> sync::Arc { - let inner = self.0.inner.borrow_mut(); + let inner = self.state.borrow(); inner.renderer.sprite_atlas().clone() } } diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index a9a473b84f..c01c813e61 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -476,6 +476,12 @@ impl Window { } else if needs_present { handle.update(&mut cx, |_, cx| cx.present()).log_err(); } + + handle + .update(&mut cx, |_, cx| { + cx.complete_frame(); + }) + .log_err(); } })); platform_window.on_resize(Box::new({ @@ -1004,6 +1010,10 @@ impl<'a> WindowContext<'a> { self.window.modifiers } + fn complete_frame(&self) { + self.window.platform_window.completed_frame(); + } + /// Produces a new frame and assigns it to `rendered_frame`. To actually show /// the contents of the new [Scene], use [present]. #[profiling::function] diff --git a/crates/workspace/src/persistence.rs b/crates/workspace/src/persistence.rs index 2e66322f05..b719b1f703 100644 --- a/crates/workspace/src/persistence.rs +++ b/crates/workspace/src/persistence.rs @@ -352,7 +352,8 @@ impl WorkspaceDb { // Clear out panes and pane_groups conn.exec_bound(sql!( DELETE FROM pane_groups WHERE workspace_id = ?1; - DELETE FROM panes WHERE workspace_id = ?1;))?(workspace.id)?; + DELETE FROM panes WHERE workspace_id = ?1;))?(workspace.id) + .context("Clearing old panes")?; conn.exec_bound(sql!( DELETE FROM workspaces WHERE workspace_location = ? AND workspace_id != ?