WIP: Refactor Linux platform implementation (#10227)

Mikayla Maki created

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

Change summary

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       | 400 +++++++---
crates/gpui/src/platform/linux/util.rs           | 128 ---
crates/gpui/src/platform/linux/wayland.rs        |   7 
crates/gpui/src/platform/linux/wayland/client.rs | 702 +++++++----------
crates/gpui/src/platform/linux/wayland/cursor.rs |  42 
crates/gpui/src/platform/linux/wayland/window.rs | 386 +++++++--
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, 1,219 insertions(+), 1,032 deletions(-)

Detailed changes

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",

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"] }

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 {

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.

crates/gpui/src/platform.rs 🔗

@@ -66,7 +66,14 @@ pub(crate) fn current_platform() -> Rc<dyn Platform> {
 }
 #[cfg(target_os = "linux")]
 pub(crate) fn current_platform() -> Rc<dyn Platform> {
-    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<dyn FnMut()>);
     fn is_topmost_for_position(&self, position: Point<Pixels>) -> bool;
     fn draw(&self, scene: &Scene);
+    fn completed_frame(&self) {}
     fn sprite_atlas(&self) -> Arc<dyn PlatformAtlas>;
 
     #[cfg(target_os = "windows")]

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::*;

crates/gpui/src/platform/linux/client.rs 🔗

@@ -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<Rc<dyn PlatformDisplay>>;
-    fn primary_display(&self) -> Option<Rc<dyn PlatformDisplay>>;
-    fn display(&self, id: DisplayId) -> Option<Rc<dyn PlatformDisplay>>;
-    fn open_window(
-        &self,
-        handle: AnyWindowHandle,
-        options: WindowParams,
-    ) -> Box<dyn PlatformWindow>;
-    fn set_cursor_style(&self, style: CursorStyle);
-    fn get_clipboard(&self) -> Rc<RefCell<dyn ClipboardProvider>>;
-    fn get_primary(&self) -> Rc<RefCell<dyn ClipboardProvider>>;
-}

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 {

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";
+
+pub struct RcRefCell<T>(Rc<RefCell<T>>);
+
+impl<T> RcRefCell<T> {
+    pub fn new(value: T) -> Self {
+        RcRefCell(Rc::new(RefCell::new(value)))
+    }
+
+    #[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::<T>(),
+                    Location::caller()
+                );
+            }
+        }
 
-#[derive(Default)]
-pub(crate) struct Callbacks {
-    open_urls: Option<Box<dyn FnMut(Vec<String>)>>,
-    become_active: Option<Box<dyn FnMut()>>,
-    resign_active: Option<Box<dyn FnMut()>>,
-    quit: Option<Box<dyn FnMut()>>,
-    reopen: Option<Box<dyn FnMut()>>,
-    event: Option<Box<dyn FnMut(PlatformInput) -> bool>>,
-    app_menu_action: Option<Box<dyn FnMut(&dyn Action)>>,
-    will_open_app_menu: Option<Box<dyn FnMut()>>,
-    validate_app_menu_command: Option<Box<dyn FnMut(&dyn Action) -> bool>>,
-}
+        self.0.borrow_mut()
+    }
 
-pub(crate) struct LinuxPlatformInner {
-    pub(crate) event_loop: RefCell<EventLoop<'static, ()>>,
-    pub(crate) loop_handle: Rc<LoopHandle<'static, ()>>,
-    pub(crate) loop_signal: LoopSignal,
-    pub(crate) background_executor: BackgroundExecutor,
-    pub(crate) foreground_executor: ForegroundExecutor,
-    pub(crate) text_system: Arc<LinuxTextSystem>,
-    pub(crate) callbacks: RefCell<Callbacks>,
+    #[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::<T>(), Location::caller());
+            }
+        }
+
+        self.0.borrow()
+    }
 }
 
-pub(crate) struct LinuxPlatform {
-    client: Rc<dyn Client>,
-    inner: Rc<LinuxPlatformInner>,
+impl<T> Deref for RcRefCell<T> {
+    type Target = Rc<RefCell<T>>;
+
+    fn deref(&self) -> &Self::Target {
+        &self.0
+    }
+}
+impl<T> DerefMut for RcRefCell<T> {
+    fn deref_mut(&mut self) -> &mut Self::Target {
+        &mut self.0
+    }
 }
 
-impl Default for LinuxPlatform {
-    fn default() -> Self {
-        Self::new()
+impl<T> Clone for RcRefCell<T> {
+    fn clone(&self) -> Self {
+        RcRefCell(self.0.clone())
     }
 }
 
-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());
+pub trait LinuxClient {
+    fn with_common<R>(&self, f: impl FnOnce(&mut LinuxCommon) -> R) -> R;
+    fn displays(&self) -> Vec<Rc<dyn PlatformDisplay>>;
+    fn primary_display(&self) -> Option<Rc<dyn PlatformDisplay>>;
+    fn display(&self, id: DisplayId) -> Option<Rc<dyn PlatformDisplay>>;
+    fn open_window(
+        &self,
+        handle: AnyWindowHandle,
+        options: WindowParams,
+    ) -> Box<dyn PlatformWindow>;
+    fn set_cursor_style(&self, style: CursorStyle);
+    fn write_to_clipboard(&self, item: ClipboardItem);
+    fn read_from_clipboard(&self) -> Option<ClipboardItem>;
+    fn run(&self);
+}
+
+#[derive(Default)]
+pub(crate) struct PlatformHandlers {
+    pub(crate) open_urls: Option<Box<dyn FnMut(Vec<String>)>>,
+    pub(crate) become_active: Option<Box<dyn FnMut()>>,
+    pub(crate) resign_active: Option<Box<dyn FnMut()>>,
+    pub(crate) quit: Option<Box<dyn FnMut()>>,
+    pub(crate) reopen: Option<Box<dyn FnMut()>>,
+    pub(crate) event: Option<Box<dyn FnMut(PlatformInput) -> bool>>,
+    pub(crate) app_menu_action: Option<Box<dyn FnMut(&dyn Action)>>,
+    pub(crate) will_open_app_menu: Option<Box<dyn FnMut()>>,
+    pub(crate) validate_app_menu_command: Option<Box<dyn FnMut(&dyn Action) -> bool>>,
+}
 
+pub(crate) struct LinuxCommon {
+    pub(crate) background_executor: BackgroundExecutor,
+    pub(crate) foreground_executor: ForegroundExecutor,
+    pub(crate) text_system: Arc<LinuxTextSystem>,
+    pub(crate) callbacks: PlatformHandlers,
+    pub(crate) signal: LoopSignal,
+}
+
+impl LinuxCommon {
+    pub fn new(signal: LoopSignal) -> (Self, Channel<Runnable>) {
         let (main_sender, main_receiver) = calloop::channel::channel::<Runnable>();
         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<P: LinuxClient + 'static> 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<dyn PlatformTextSystem> {
-        self.inner.text_system.clone()
+        self.with_common(|common| common.text_system.clone())
     }
 
     fn run(&self, on_finish_launching: Box<dyn FnOnce()>) {
         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<Rc<dyn PlatformDisplay>> {
-        self.client.primary_display()
+        self.primary_display()
     }
 
     fn displays(&self) -> Vec<Rc<dyn PlatformDisplay>> {
-        self.client.displays()
+        self.displays()
     }
 
     fn display(&self, id: DisplayId) -> Option<Rc<dyn PlatformDisplay>> {
-        self.client.display(id)
+        self.display(id)
     }
 
     // todo(linux)
@@ -222,7 +262,7 @@ impl Platform for LinuxPlatform {
         handle: AnyWindowHandle,
         options: WindowParams,
     ) -> Box<dyn PlatformWindow> {
-        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<dyn FnMut(Vec<String>)>) {
-        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<Option<Vec<PathBuf>>> {
         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<Option<PathBuf>> {
         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<dyn FnMut()>) {
-        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<dyn FnMut()>) {
-        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<dyn FnMut()>) {
-        self.inner.callbacks.borrow_mut().quit = Some(callback);
+        self.with_common(|common| {
+            common.callbacks.quit = Some(callback);
+        });
     }
 
     fn on_reopen(&self, callback: Box<dyn FnMut()>) {
-        self.inner.callbacks.borrow_mut().reopen = Some(callback);
+        self.with_common(|common| {
+            common.callbacks.reopen = Some(callback);
+        });
     }
 
     fn on_event(&self, callback: Box<dyn FnMut(PlatformInput) -> 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<dyn FnMut(&dyn Action)>) {
-        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<dyn FnMut()>) {
-        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<dyn FnMut(&dyn Action) -> 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<ClipboardItem> {
-        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<Result<()>> {
         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<anyhow::Result<()>> {
         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<ClipboardItem> {
+        self.read_from_clipboard()
+    }
+}
+
+pub(super) fn is_within_click_distance(a: Point<Pixels>, b: Point<Pixels>) -> 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::*;
-
-    fn build_platform() -> LinuxPlatform {
-        let platform = LinuxPlatform::new();
-        platform
+    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
+        );
     }
 }

crates/gpui/src/platform/linux/util.rs 🔗

@@ -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<Pixels>, b: Point<Pixels>) -> 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
-        );
-    }
-}

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::*;

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<wp_viewporter::WpViewporter>,
-    fractional_scale_manager: Option<wp_fractional_scale_manager_v1::WpFractionalScaleManagerV1>,
-    decoration_manager: Option<zxdg_decoration_manager_v1::ZxdgDecorationManagerV1>,
-    windows: Vec<(xdg_surface::XdgSurface, Rc<WaylandWindowState>)>,
-    outputs: Vec<(wl_output::WlOutput, Rc<RefCell<OutputState>>)>,
-    platform_inner: Rc<LinuxPlatformInner>,
+#[derive(Debug, Clone)]
+pub struct Globals {
+    pub qh: QueueHandle<WaylandClient>,
+    pub compositor: wl_compositor::WlCompositor,
+    pub wm_base: xdg_wm_base::XdgWmBase,
+    pub shm: wl_shm::WlShm,
+    pub viewporter: Option<wp_viewporter::WpViewporter>,
+    pub fractional_scale_manager:
+        Option<wp_fractional_scale_manager_v1::WpFractionalScaleManagerV1>,
+    pub decoration_manager: Option<zxdg_decoration_manager_v1::ZxdgDecorationManagerV1>,
+}
+
+impl Globals {
+    fn new(globals: GlobalList, qh: QueueHandle<WaylandClient>) -> 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<ObjectId, WaylandWindow>,
+    // Output to scale mapping
+    output_scales: HashMap<ObjectId, i32>,
     keymap_state: Option<xkb::State>,
-    click_state: ClickState,
+    click: ClickState,
     repeat: KeyRepeat,
     modifiers: Modifiers,
     scroll_direction: f64,
     axis_source: AxisSource,
-    mouse_location: Point<Pixels>,
+    mouse_location: Option<Point<Pixels>>,
+    enter_token: Option<()>,
     button_pressed: Option<MouseButton>,
-    mouse_focused_window: Option<Rc<WaylandWindowState>>,
-    keyboard_focused_window: Option<Rc<WaylandWindowState>>,
-    loop_handle: Rc<LoopHandle<'static, ()>>,
-}
-
-pub(crate) struct CursorState {
+    mouse_focused_window: Option<WaylandWindow>,
+    keyboard_focused_window: Option<WaylandWindow>,
+    loop_handle: LoopHandle<'static, WaylandClient>,
     cursor_icon_name: String,
-    cursor: Option<Cursor>,
+    cursor: Cursor,
+    clipboard: Clipboard,
+    primary: Primary,
+    event_loop: Option<EventLoop<'static, WaylandClient>>,
+    common: LinuxCommon,
 }
 
-#[derive(Clone)]
-pub(crate) struct WaylandClientState {
-    client_state_inner: Rc<RefCell<WaylandClientStateInner>>,
-    cursor_state: Rc<RefCell<CursorState>>,
-    clipboard: Rc<RefCell<Clipboard>>,
-    primary: Rc<RefCell<Primary>>,
-}
-
-struct ClickState {
+pub struct ClickState {
     last_click: Instant,
     last_location: Point<Pixels>,
     current_count: usize,
@@ -103,11 +124,8 @@ pub(crate) struct KeyRepeat {
     current_keysym: Option<xkb::Keysym>,
 }
 
-pub(crate) struct WaylandClient {
-    platform_inner: Rc<LinuxPlatformInner>,
-    state: WaylandClientState,
-    qh: Arc<QueueHandle<WaylandClientState>>,
-}
+#[derive(Clone)]
+pub struct WaylandClient(Rc<RefCell<WaylandClientState>>);
 
 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<LinuxPlatformInner>) -> Self {
+    pub(crate) fn new() -> Self {
         let conn = Connection::connect_to_env().unwrap();
 
-        let (globals, mut event_queue) = registry_queue_init::<WaylandClientState>(&conn).unwrap();
+        let (globals, mut event_queue) = registry_queue_init::<WaylandClient>(&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::WlOutput, _, _>(
+                    "wl_output" => {
+                        let output = globals.registry().bind::<wl_output::WlOutput, _, _>(
                             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);
-
-        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();
+        WaylandSource::new(conn, event_queue).insert(handle);
 
-        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<Rc<dyn PlatformDisplay>> {
         Vec::new()
     }
@@ -246,61 +256,14 @@ impl Client for WaylandClient {
     fn open_window(
         &self,
         handle: AnyWindowHandle,
-        options: WindowParams,
+        params: WindowParams,
     ) -> Box<dyn PlatformWindow> {
-        let mut state = self.state.client_state_inner.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);
-
-        // 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, ()));
+        let mut state = self.0.borrow_mut();
 
-        wl_surface.frame(&self.qh, wl_surface.clone());
-        wl_surface.commit();
-
-        let window_state: Rc<WaylandWindowState> = 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());
-        }
+        let (window, surface_id) = WaylandWindow::new(state.globals.clone(), params);
+        state.windows.insert(surface_id, window.clone());
 
-        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<RefCell<dyn ClipboardProvider>> {
-        self.state.clipboard.clone()
+    fn with_common<R>(&self, f: impl FnOnce(&mut LinuxCommon) -> R) -> R {
+        f(&mut self.0.borrow_mut().common)
     }
 
-    fn get_primary(&self) -> Rc<RefCell<dyn ClipboardProvider>> {
-        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<crate::ClipboardItem> {
+        self.0
+            .borrow_mut()
+            .clipboard
+            .get_contents()
+            .ok()
+            .map(|s| crate::ClipboardItem {
+                text: s,
+                metadata: None,
+            })
     }
 }
 
-impl Dispatch<wl_registry::WlRegistry, GlobalListContents> for WaylandClientState {
+impl Dispatch<wl_registry::WlRegistry, GlobalListContents> for WaylandClient {
     fn event(
         state: &mut Self,
         registry: &wl_registry::WlRegistry,
@@ -350,7 +336,7 @@ impl Dispatch<wl_registry::WlRegistry, GlobalListContents> for WaylandClientStat
         _: &Connection,
         qh: &QueueHandle<Self>,
     ) {
-        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<wl_registry::WlRegistry, GlobalListContents> for WaylandClientStat
                     registry.bind::<wl_seat::WlSeat, _, _>(name, wl_seat_version(version), qh, ());
                 }
                 "wl_output" => {
-                    state.outputs.push((
-                        registry.bind::<wl_output::WlOutput, _, _>(name, WL_OUTPUT_VERSION, qh, ()),
-                        Rc::new(RefCell::new(OutputState::default())),
-                    ));
+                    let output =
+                        registry.bind::<wl_output::WlOutput, _, _>(name, WL_OUTPUT_VERSION, qh, ());
+
+                    state.output_scales.insert(output.id(), 1);
                 }
                 _ => {}
             },
@@ -374,38 +360,40 @@ impl Dispatch<wl_registry::WlRegistry, GlobalListContents> 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<WlCallback, Arc<WlSurface>> for WaylandClientState {
+impl Dispatch<WlCallback, ObjectId> for WaylandClient {
     fn event(
-        state: &mut Self,
-        _: &WlCallback,
+        this: &mut WaylandClient,
+        _: &wl_callback::WlCallback,
         event: wl_callback::Event,
-        surf: &Arc<WlSurface>,
+        surface_id: &ObjectId,
         _: &Connection,
         qh: &QueueHandle<Self>,
     ) {
-        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<wl_surface::WlSurface, ()> for WaylandClientState {
+impl Dispatch<wl_surface::WlSurface, ()> for WaylandClient {
     fn event(
         state: &mut Self,
         surface: &wl_surface::WlSurface,
@@ -414,85 +402,18 @@ impl Dispatch<wl_surface::WlSurface, ()> for WaylandClientState {
         _: &Connection,
         _: &QueueHandle<Self>,
     ) {
-        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<wl_output::WlOutput, ()> for WaylandClientState {
+impl Dispatch<wl_output::WlOutput, ()> for WaylandClient {
     fn event(
         state: &mut Self,
         output: &wl_output::WlOutput,
@@ -501,90 +422,64 @@ impl Dispatch<wl_output::WlOutput, ()> for WaylandClientState {
         _: &Connection,
         _: &QueueHandle<Self>,
     ) {
-        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<xdg_surface::XdgSurface, ()> for WaylandClientState {
+impl Dispatch<xdg_surface::XdgSurface, ObjectId> for WaylandClient {
     fn event(
         state: &mut Self,
         xdg_surface: &xdg_surface::XdgSurface,
         event: xdg_surface::Event,
-        _: &(),
+        surface_id: &ObjectId,
         _: &Connection,
         _: &QueueHandle<Self>,
     ) {
-        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<xdg_toplevel::XdgToplevel, ()> for WaylandClientState {
+impl Dispatch<xdg_toplevel::XdgToplevel, ObjectId> for WaylandClient {
     fn event(
-        state: &mut Self,
+        this: &mut Self,
         xdg_toplevel: &xdg_toplevel::XdgToplevel,
         event: <xdg_toplevel::XdgToplevel as Proxy>::Event,
-        _: &(),
+        surface_id: &ObjectId,
         _: &Connection,
         _: &QueueHandle<Self>,
     ) {
-        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<xdg_wm_base::XdgWmBase, ()> for WaylandClientState {
+impl Dispatch<xdg_wm_base::XdgWmBase, ()> for WaylandClient {
     fn event(
         _: &mut Self,
         wm_base: &xdg_wm_base::XdgWmBase,
@@ -599,7 +494,7 @@ impl Dispatch<xdg_wm_base::XdgWmBase, ()> for WaylandClientState {
     }
 }
 
-impl Dispatch<wl_seat::WlSeat, ()> for WaylandClientState {
+impl Dispatch<wl_seat::WlSeat, ()> for WaylandClient {
     fn event(
         state: &mut Self,
         seat: &wl_seat::WlSeat,
@@ -622,7 +517,7 @@ impl Dispatch<wl_seat::WlSeat, ()> for WaylandClientState {
     }
 }
 
-impl Dispatch<wl_keyboard::WlKeyboard, ()> for WaylandClientState {
+impl Dispatch<wl_keyboard::WlKeyboard, ()> for WaylandClient {
     fn event(
         this: &mut Self,
         keyboard: &wl_keyboard::WlKeyboard,
@@ -631,7 +526,7 @@ impl Dispatch<wl_keyboard::WlKeyboard, ()> for WaylandClientState {
         conn: &Connection,
         qh: &QueueHandle<Self>,
     ) {
-        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<wl_keyboard::WlKeyboard, ()> 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<wl_keyboard::WlKeyboard, ()> for WaylandClientState {
                 });
 
                 drop(state);
-
                 focused_window.handle_input(input);
             }
             wl_keyboard::Event::Key {
@@ -748,38 +636,33 @@ impl Dispatch<wl_keyboard::WlKeyboard, ()> 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();
-
-                                if !is_repeating {
-                                    return TimeoutAction::Drop;
-                                }
+                            .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();
 
-                                let focused_window =
-                                    state_.keyboard_focused_window.as_ref().unwrap().clone();
+                                    if !is_repeating {
+                                        return TimeoutAction::Drop;
+                                    }
 
-                                drop(state_);
+                                    let focused_window =
+                                        state.keyboard_focused_window.as_ref().unwrap().clone();
 
-                                focused_window.handle_input(input_.clone());
+                                    drop(state);
+                                    focused_window.handle_input(input.clone());
 
-                                TimeoutAction::ToDuration(Duration::from_secs(1) / rate)
+                                    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<wl_keyboard::WlKeyboard, ()> 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<MouseButton> {
     })
 }
 
-impl Dispatch<wl_pointer::WlPointer, ()> for WaylandClientState {
+impl Dispatch<wl_pointer::WlPointer, ()> 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<Self>,
     ) {
-        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<wl_pointer::WlPointer, ()> 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<wl_pointer::WlPointer, ()> 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<wl_pointer::WlPointer, ()> 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);
+                        }
                     }
                     _ => {}
                 }

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<CursorTheme, InvalidId>,
+    theme: Option<CursorTheme>,
     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<WaylandClientState>,
-        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");

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<Box<dyn FnMut()>>,
 }
 
-struct WaylandWindowInner {
-    renderer: BladeRenderer,
-    bounds: Bounds<u32>,
-    scale: f32,
-    input_handler: Option<PlatformInputHandler>,
-    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<wl_surface::WlSurface>, bounds: Bounds<u32>) -> Self {
+pub struct WaylandWindowState {
+    xdg_surface: xdg_surface::XdgSurface,
+    surface: wl_surface::WlSurface,
+    toplevel: xdg_toplevel::XdgToplevel,
+    viewport: Option<wp_viewport::WpViewport>,
+    outputs: HashSet<ObjectId>,
+    globals: Globals,
+    renderer: BladeRenderer,
+    bounds: Bounds<u32>,
+    scale: f32,
+    input_handler: Option<PlatformInputHandler>,
+    decoration_state: WaylandDecorationState,
+    fullscreen: bool,
+    maximized: bool,
+}
+
+impl WaylandWindowState {
+    pub(crate) fn new(
+        surface: wl_surface::WlSurface,
+        xdg_surface: xdg_surface::XdgSurface,
+        viewport: Option<wp_viewport::WpViewport>,
+        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::<c_void>(),
-            display: wl_surf
+            window: surface.id().as_ptr().cast::<c_void>(),
+            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<WaylandWindowInner>,
-    pub(crate) callbacks: RefCell<Callbacks>,
-    pub(crate) surface: Arc<wl_surface::WlSurface>,
-    pub(crate) toplevel: Arc<xdg_toplevel::XdgToplevel>,
-    pub(crate) outputs: RefCell<HashSet<ObjectId>>,
-    viewport: Option<wp_viewport::WpViewport>,
-    fullscreen: RefCell<bool>,
+#[derive(Clone)]
+pub(crate) struct WaylandWindow {
+    pub(crate) state: RcRefCell<WaylandWindowState>,
+    pub(crate) callbacks: Rc<RefCell<Callbacks>>,
 }
 
-impl WaylandWindowState {
-    pub(crate) fn new(
-        wl_surf: Arc<wl_surface::WlSurface>,
-        viewport: Option<wp_viewport::WpViewport>,
-        toplevel: Arc<xdg_toplevel::XdgToplevel>,
-        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,
-            viewport,
-            fullscreen: RefCell::new(false),
+    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,
+            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 update(&self) {
+    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(mut fun) = cb.request_frame.take() {
-            drop(cb);
+        if let Some(fun) = cb.request_frame.as_mut() {
             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<ObjectId, i32>,
+    ) {
+        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<f32>,
     ) {
         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<WaylandWindowState>);
-
 impl HasWindowHandle for WaylandWindow {
     fn window_handle(&self) -> Result<WindowHandle<'_>, HandleError> {
         unimplemented!()
@@ -273,31 +457,29 @@ impl HasDisplayHandle for WaylandWindow {
 }
 
 impl PlatformWindow for WaylandWindow {
-    // todo(linux)
     fn bounds(&self) -> Bounds<DevicePixels> {
-        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<Pixels> {
-        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<PlatformInputHandler> {
-        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<dyn FnMut()>) {
-        self.0.callbacks.borrow_mut().request_frame = Some(callback);
+        self.callbacks.borrow_mut().request_frame = Some(callback);
     }
 
     fn on_input(&self, callback: Box<dyn FnMut(PlatformInput) -> 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<dyn FnMut(bool)>) {
-        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<dyn FnMut(Size<Pixels>, f32)>) {
-        self.0.callbacks.borrow_mut().resize = Some(callback);
+        self.callbacks.borrow_mut().resize = Some(callback);
     }
 
     fn on_fullscreen(&self, callback: Box<dyn FnMut(bool)>) {
-        self.0.callbacks.borrow_mut().fullscreen = Some(callback);
+        self.callbacks.borrow_mut().fullscreen = Some(callback);
     }
 
     fn on_moved(&self, callback: Box<dyn FnMut()>) {
-        self.0.callbacks.borrow_mut().moved = Some(callback);
+        self.callbacks.borrow_mut().moved = Some(callback);
     }
 
     fn on_should_close(&self, callback: Box<dyn FnMut() -> bool>) {
-        self.0.callbacks.borrow_mut().should_close = Some(callback);
+        self.callbacks.borrow_mut().should_close = Some(callback);
     }
 
     fn on_close(&self, callback: Box<dyn FnOnce()>) {
-        self.0.callbacks.borrow_mut().close = Some(callback);
+        self.callbacks.borrow_mut().close = Some(callback);
     }
 
     fn on_appearance_changed(&self, callback: Box<dyn FnMut()>) {
@@ -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<dyn PlatformAtlas> {
-        let inner = self.0.inner.borrow();
-        inner.renderer.sprite_atlas().clone()
+        let state = self.state.borrow();
+        state.renderer.sprite_atlas().clone()
     }
 }
 

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<X11WindowState>,
+pub(crate) struct WindowRef {
+    window: X11Window,
     refresh_event_token: RegistrationToken,
 }
 
-struct X11ClientState {
-    windows: HashMap<xproto::Window, WindowRef>,
-    xkb: xkbc::State,
-    clipboard: Rc<RefCell<X11ClipboardContext<Clipboard>>>,
-    primary: Rc<RefCell<X11ClipboardContext<Primary>>>,
-    click_state: ClickState,
-}
+impl Deref for WindowRef {
+    type Target = X11Window;
 
-struct ClickState {
-    last_click: Instant,
-    last_location: Point<Pixels>,
-    current_count: usize,
+    fn deref(&self) -> &Self::Target {
+        &self.window
+    }
 }
 
-pub(crate) struct X11Client {
-    platform_inner: Rc<LinuxPlatformInner>,
-    xcb_connection: Rc<XCBConnection>,
-    x_root_index: usize,
-    atoms: XcbAtoms,
-    state: RefCell<X11ClientState>,
+pub struct X11ClientState {
+    pub(crate) loop_handle: LoopHandle<'static, X11Client>,
+    pub(crate) event_loop: Option<calloop::EventLoop<'static, X11Client>>,
+
+    pub(crate) last_click: Instant,
+    pub(crate) last_location: Point<Pixels>,
+    pub(crate) current_count: usize,
+
+    pub(crate) xcb_connection: Rc<XCBConnection>,
+    pub(crate) x_root_index: usize,
+    pub(crate) atoms: XcbAtoms,
+    pub(crate) windows: HashMap<xproto::Window, WindowRef>,
+    pub(crate) xkb: xkbc::State,
+
+    pub(crate) common: LinuxCommon,
+    pub(crate) clipboard: X11ClipboardContext<Clipboard>,
+    pub(crate) primary: X11ClipboardContext<Primary>,
 }
 
+#[derive(Clone)]
+pub(crate) struct X11Client(Rc<RefCell<X11ClientState>>);
+
 impl X11Client {
-    pub(crate) fn new(inner: Rc<LinuxPlatformInner>) -> Rc<Self> {
+    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<X11Client> = 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::<ConnectionError>(
                     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<Rc<X11WindowState>> {
-        let state = self.state.borrow();
-        state.windows.get(&win).map(|wr| Rc::clone(&wr.state))
+    fn get_window(&self, win: xproto::Window) -> Option<X11Window> {
+        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<R>(&self, f: impl FnOnce(&mut LinuxCommon) -> R) -> R {
+        f(&mut self.0.borrow_mut().common)
+    }
+
     fn displays(&self) -> Vec<Rc<dyn PlatformDisplay>> {
-        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<dyn PlatformDisplay>)
             })
             .collect()
     }
 
-    fn display(&self, id: DisplayId) -> Option<Rc<dyn PlatformDisplay>> {
-        Some(Rc::new(X11Display::new(
-            &self.xcb_connection,
-            id.0 as usize,
-        )?))
-    }
-
     fn primary_display(&self) -> Option<Rc<dyn PlatformDisplay>> {
+        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<Rc<dyn PlatformDisplay>> {
+        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<dyn PlatformWindow> {
-        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<RefCell<dyn ClipboardProvider>> {
-        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<RefCell<dyn ClipboardProvider>> {
-        self.state.borrow().primary.clone()
+    fn read_from_clipboard(&self) -> Option<crate::ClipboardItem> {
+        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();
     }
 }
 

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<Box<dyn FnMut()>>,
-    input: Option<Box<dyn FnMut(PlatformInput) -> crate::DispatchEventResult>>,
-    active_status_change: Option<Box<dyn FnMut(bool)>>,
-    resize: Option<Box<dyn FnMut(Size<Pixels>, f32)>>,
-    fullscreen: Option<Box<dyn FnMut(bool)>>,
-    moved: Option<Box<dyn FnMut()>>,
-    should_close: Option<Box<dyn FnMut() -> bool>>,
-    close: Option<Box<dyn FnOnce()>>,
-    appearance_changed: Option<Box<dyn FnMut()>>,
-}
-
 x11rb::atom_manager! {
     pub XcbAtoms: AtomsCookie {
         WM_PROTOCOLS,
@@ -51,23 +41,6 @@ x11rb::atom_manager! {
     }
 }
 
-struct LinuxWindowInner {
-    bounds: Bounds<i32>,
-    scale_factor: f32,
-    renderer: BladeRenderer,
-    input_handler: Option<PlatformInputHandler>,
-}
-
-impl LinuxWindowInner {
-    fn content_size(&self) -> Size<Pixels> {
-        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<Box<dyn FnMut()>>,
+    input: Option<Box<dyn FnMut(PlatformInput) -> crate::DispatchEventResult>>,
+    active_status_change: Option<Box<dyn FnMut(bool)>>,
+    resize: Option<Box<dyn FnMut(Size<Pixels>, f32)>>,
+    fullscreen: Option<Box<dyn FnMut(bool)>>,
+    moved: Option<Box<dyn FnMut()>>,
+    should_close: Option<Box<dyn FnMut() -> bool>>,
+    close: Option<Box<dyn FnOnce()>>,
+    appearance_changed: Option<Box<dyn FnMut()>>,
+}
+
 pub(crate) struct X11WindowState {
-    xcb_connection: Rc<XCBConnection>,
-    display: Rc<dyn PlatformDisplay>,
     raw: RawWindow,
-    x_window: xproto::Window,
-    callbacks: RefCell<Callbacks>,
-    inner: RefCell<LinuxWindowInner>,
+
+    bounds: Bounds<i32>,
+    scale_factor: f32,
+    renderer: BladeRenderer,
+    display: Rc<dyn PlatformDisplay>,
+
+    input_handler: Option<PlatformInputHandler>,
 }
 
 #[derive(Clone)]
-pub(crate) struct X11Window(pub(crate) Rc<X11WindowState>);
+pub(crate) struct X11Window {
+    pub(crate) state: Rc<RefCell<X11WindowState>>,
+    pub(crate) callbacks: Rc<RefCell<Callbacks>>,
+    xcb_connection: Rc<XCBConnection>,
+    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<rwh::WindowHandle, rwh::HandleError> {
         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<rwh::DisplayHandle, rwh::HandleError> {
         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<Pixels> {
+        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<XCBConnection>,
+        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<i32>) {
         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<DevicePixels> {
-        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<Pixels> {
-        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<dyn PlatformDisplay> {
-        Rc::clone(&self.0.display)
+        self.state.borrow().display.clone()
     }
 
     fn mouse_position(&self) -> Point<Pixels> {
         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<PlatformInputHandler> {
-        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<dyn FnMut()>) {
-        self.0.callbacks.borrow_mut().request_frame = Some(callback);
+        self.callbacks.borrow_mut().request_frame = Some(callback);
     }
 
     fn on_input(&self, callback: Box<dyn FnMut(PlatformInput) -> 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<dyn FnMut(bool)>) {
-        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<dyn FnMut(Size<Pixels>, f32)>) {
-        self.0.callbacks.borrow_mut().resize = Some(callback);
+        self.callbacks.borrow_mut().resize = Some(callback);
     }
 
     fn on_fullscreen(&self, callback: Box<dyn FnMut(bool)>) {
-        self.0.callbacks.borrow_mut().fullscreen = Some(callback);
+        self.callbacks.borrow_mut().fullscreen = Some(callback);
     }
 
     fn on_moved(&self, callback: Box<dyn FnMut()>) {
-        self.0.callbacks.borrow_mut().moved = Some(callback);
+        self.callbacks.borrow_mut().moved = Some(callback);
     }
 
     fn on_should_close(&self, callback: Box<dyn FnMut() -> bool>) {
-        self.0.callbacks.borrow_mut().should_close = Some(callback);
+        self.callbacks.borrow_mut().should_close = Some(callback);
     }
 
     fn on_close(&self, callback: Box<dyn FnOnce()>) {
-        self.0.callbacks.borrow_mut().close = Some(callback);
+        self.callbacks.borrow_mut().close = Some(callback);
     }
 
     fn on_appearance_changed(&self, callback: Box<dyn FnMut()>) {
-        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<dyn PlatformAtlas> {
-        let inner = self.0.inner.borrow_mut();
+        let inner = self.state.borrow();
         inner.renderer.sprite_atlas().clone()
     }
 }

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]

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 != ?