gpui: Add Wayland support (#7664)

Roman , gabydd , and Mikayla Maki created

This PR adds Wayland support to gpui using
[wayland-rs](https://github.com/Smithay/wayland-rs). It is based on
[#7598](https://github.com/zed-industries/zed/pull/7598).

It detects Wayland support at runtime by checking the existence of the
`WAYLAND_DISPLAY` environment variable. If it does not exist or is
empty, the X11 backend will be used. To use the X11 backend in a Wayland
session (for development purposes), you just need to unset
WAYLAND_DISPLAY (`WAYLAND_DISPLAY= cargo run ...`).

At the moment it only creates the window and renders the initial content
provided by `BladeRenderer`, so it can run "Hello world" example.


![image](https://github.com/zed-industries/zed/assets/40907255/1655bc64-4d36-4178-9851-bfe42f03f716)

Todo:
- [x] Add basic Wayland support.
- [x] Add window resizing.
- [x] Add window closing.
- [x] Add window updating.
- [ ] Implement input handling, fractional scaling, and support other
Wayland protocols.
- [ ] Implement all unimplemented todo!(linux).
- [ ] Add window decorations or use custom decorations (like on MacOS).
- [ ] Address other missing functionality.

Release Notes:
- N/A

---------

Co-authored-by: gabydd <gabydinnerdavid@gmail.com>
Co-authored-by: Mikayla Maki <mikayla@zed.dev>

Change summary

Cargo.lock                                                  |  88 +
crates/gpui/Cargo.toml                                      |   3 
crates/gpui/src/app.rs                                      |  53 
crates/gpui/src/platform/linux.rs                           |   4 
crates/gpui/src/platform/linux/platform.rs                  |  98 +
crates/gpui/src/platform/linux/wayland.rs                   |   7 
crates/gpui/src/platform/linux/wayland/client.rs            | 256 +++++
crates/gpui/src/platform/linux/wayland/client_dispatcher.rs |  30 
crates/gpui/src/platform/linux/wayland/display.rs           |  31 
crates/gpui/src/platform/linux/wayland/window.rs            | 350 +++++++
script/linux                                                |   3 
typos.toml                                                  |   3 
12 files changed, 869 insertions(+), 57 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -2489,6 +2489,12 @@ version = "0.15.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
 
+[[package]]
+name = "downcast-rs"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ea835d29036a4087793836fa931b08837ad5e957da9e23886b29586fb9b6650"
+
 [[package]]
 name = "dwrote"
 version = "0.11.0"
@@ -3518,6 +3524,9 @@ dependencies = [
  "util",
  "uuid 1.4.1",
  "waker-fn",
+ "wayland-backend",
+ "wayland-client",
+ "wayland-protocols",
  "xcb",
 ]
 
@@ -5922,7 +5931,7 @@ dependencies = [
  "base64 0.21.4",
  "indexmap 1.9.3",
  "line-wrap",
- "quick-xml",
+ "quick-xml 0.30.0",
  "serde",
  "time",
 ]
@@ -6391,6 +6400,15 @@ dependencies = [
  "memchr",
 ]
 
+[[package]]
+name = "quick-xml"
+version = "0.31.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1004a344b30a54e2ee58d66a71b32d2db2feb0a31f9a2d302bf0536f15de2a33"
+dependencies = [
+ "memchr",
+]
+
 [[package]]
 name = "quick_action_bar"
 version = "0.1.0"
@@ -7245,6 +7263,12 @@ dependencies = [
  "syn 1.0.109",
 ]
 
+[[package]]
+name = "scoped-tls"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294"
+
 [[package]]
 name = "scoped_threadpool"
 version = "0.1.9"
@@ -10320,6 +10344,66 @@ name = "wasmtime-wmemcheck"
 version = "16.0.0"
 source = "git+https://github.com/bytecodealliance/wasmtime?rev=v16.0.0#6613acd1e4817957a4a7745125ef063b43c273a7"
 
+[[package]]
+name = "wayland-backend"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d50fa61ce90d76474c87f5fc002828d81b32677340112b4ef08079a9d459a40"
+dependencies = [
+ "cc",
+ "downcast-rs",
+ "rustix 0.38.30",
+ "scoped-tls",
+ "smallvec",
+ "wayland-sys",
+]
+
+[[package]]
+name = "wayland-client"
+version = "0.31.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "82fb96ee935c2cea6668ccb470fb7771f6215d1691746c2d896b447a00ad3f1f"
+dependencies = [
+ "bitflags 2.4.1",
+ "rustix 0.38.30",
+ "wayland-backend",
+ "wayland-scanner",
+]
+
+[[package]]
+name = "wayland-protocols"
+version = "0.31.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f81f365b8b4a97f422ac0e8737c438024b5951734506b0e1d775c73030561f4"
+dependencies = [
+ "bitflags 2.4.1",
+ "wayland-backend",
+ "wayland-client",
+ "wayland-scanner",
+]
+
+[[package]]
+name = "wayland-scanner"
+version = "0.31.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "63b3a62929287001986fb58c789dce9b67604a397c15c611ad9f747300b6c283"
+dependencies = [
+ "proc-macro2",
+ "quick-xml 0.31.0",
+ "quote",
+]
+
+[[package]]
+name = "wayland-sys"
+version = "0.31.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "15a0c8eaff5216d07f226cb7a549159267f3467b289d9a2e52fd3ef5aae2b7af"
+dependencies = [
+ "dlib",
+ "log",
+ "pkg-config",
+]
+
 [[package]]
 name = "web-sys"
 version = "0.3.64"
@@ -10753,7 +10837,7 @@ dependencies = [
  "as-raw-xcb-connection",
  "bitflags 1.3.2",
  "libc",
- "quick-xml",
+ "quick-xml 0.30.0",
 ]
 
 [[package]]

crates/gpui/Cargo.toml 🔗

@@ -97,6 +97,9 @@ objc = "0.2"
 [target.'cfg(target_os = "linux")'.dependencies]
 flume = "0.11"
 xcb = { version = "1.3", features = ["as-raw-xcb-connection", "present", "randr"] }
+wayland-client= { version = "0.31.2" }
+wayland-protocols = { version = "0.31.2", features = ["client"] }
+wayland-backend = { version = "0.3.3", features = ["client_system"] }
 as-raw-xcb-connection = "1"
 #TODO: use these on all platforms
 blade-graphics = { git = "https://github.com/kvark/blade", rev = "c4f951a88b345724cb952e920ad30e39851f7760" }

crates/gpui/src/app.rs 🔗

@@ -1,18 +1,32 @@
-mod async_context;
-mod entity_map;
-mod model_context;
-#[cfg(any(test, feature = "test-support"))]
-mod test_context;
+use std::{
+    any::{type_name, TypeId},
+    cell::{Ref, RefCell, RefMut},
+    marker::PhantomData,
+    ops::{Deref, DerefMut},
+    path::{Path, PathBuf},
+    rc::{Rc, Weak},
+    sync::{atomic::Ordering::SeqCst, Arc},
+    time::Duration,
+};
 
-pub use async_context::*;
+use anyhow::{anyhow, Result};
 use derive_more::{Deref, DerefMut};
+use futures::{channel::oneshot, future::LocalBoxFuture, Future};
+use slotmap::SlotMap;
+use smol::future::FutureExt;
+use time::UtcOffset;
+
+pub use async_context::*;
+use collections::{FxHashMap, FxHashSet, VecDeque};
 pub use entity_map::*;
 pub use model_context::*;
 use refineable::Refineable;
-use smol::future::FutureExt;
 #[cfg(any(test, feature = "test-support"))]
 pub use test_context::*;
-use time::UtcOffset;
+use util::{
+    http::{self, HttpClient},
+    ResultExt,
+};
 
 use crate::WindowAppearance;
 use crate::{
@@ -23,25 +37,12 @@ use crate::{
     SharedString, SubscriberSet, Subscription, SvgRenderer, Task, TextStyle, TextStyleRefinement,
     TextSystem, View, ViewContext, Window, WindowContext, WindowHandle, WindowId,
 };
-use anyhow::{anyhow, Result};
-use collections::{FxHashMap, FxHashSet, VecDeque};
-use futures::{channel::oneshot, future::LocalBoxFuture, Future};
 
-use slotmap::SlotMap;
-use std::{
-    any::{type_name, TypeId},
-    cell::{Ref, RefCell, RefMut},
-    marker::PhantomData,
-    ops::{Deref, DerefMut},
-    path::{Path, PathBuf},
-    rc::{Rc, Weak},
-    sync::{atomic::Ordering::SeqCst, Arc},
-    time::Duration,
-};
-use util::{
-    http::{self, HttpClient},
-    ResultExt,
-};
+mod async_context;
+mod entity_map;
+mod model_context;
+#[cfg(any(test, feature = "test-support"))]
+mod test_context;
 
 /// The duration for which futures returned from [AppContext::on_app_context] or [ModelContext::on_app_quit] can run before the application fully quits.
 pub const SHUTDOWN_TIMEOUT: Duration = Duration::from_millis(100);

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

@@ -1,3 +1,6 @@
+//todo!(linux): remove this
+#![allow(unused_variables)]
+
 mod blade_atlas;
 mod blade_belt;
 mod blade_renderer;
@@ -6,6 +9,7 @@ mod client_dispatcher;
 mod dispatcher;
 mod platform;
 mod text_system;
+mod wayland;
 mod x11;
 
 pub(crate) use blade_atlas::*;

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

@@ -1,5 +1,6 @@
 #![allow(unused)]
 
+use std::env;
 use std::{
     path::{Path, PathBuf},
     rc::Rc,
@@ -8,21 +9,21 @@ use std::{
 };
 
 use async_task::Runnable;
+use flume::{Receiver, Sender};
 use futures::channel::oneshot;
 use parking_lot::Mutex;
 use time::UtcOffset;
-
-use collections::{HashMap, HashSet};
+use wayland_client::Connection;
 
 use crate::platform::linux::client::Client;
 use crate::platform::linux::client_dispatcher::ClientDispatcher;
+use crate::platform::linux::wayland::{WaylandClient, WaylandClientDispatcher};
 use crate::platform::{X11Client, X11ClientDispatcher, XcbAtoms};
 use crate::{
-    Action, AnyWindowHandle, BackgroundExecutor, Bounds, ClipboardItem, CursorStyle, DisplayId,
+    Action, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DisplayId,
     ForegroundExecutor, Keymap, LinuxDispatcher, LinuxTextSystem, Menu, PathPromptOptions,
-    Platform, PlatformDisplay, PlatformInput, PlatformTextSystem, PlatformWindow, Point, Result,
-    SemanticVersion, Size, Task, WindowAppearance, WindowOptions, X11Display, X11Window,
-    X11WindowState,
+    Platform, PlatformDisplay, PlatformInput, PlatformTextSystem, PlatformWindow, Result,
+    SemanticVersion, Task, WindowOptions,
 };
 
 #[derive(Default)]
@@ -64,35 +65,78 @@ impl Default for LinuxPlatform {
 
 impl LinuxPlatform {
     pub(crate) fn new() -> Self {
-        let (xcb_connection, x_root_index) =
-            xcb::Connection::connect_with_extensions(None, &[xcb::Extension::Present], &[])
-                .unwrap();
-        let atoms = XcbAtoms::intern_all(&xcb_connection).unwrap();
+        let wayland_display = env::var_os("WAYLAND_DISPLAY");
+        let use_wayland = wayland_display.is_some() && !wayland_display.unwrap().is_empty();
 
-        let xcb_connection = Arc::new(xcb_connection);
         let (main_sender, main_receiver) = flume::unbounded::<Runnable>();
-        let client_dispatcher: Arc<dyn ClientDispatcher + Send + Sync> =
-            Arc::new(X11ClientDispatcher::new(&xcb_connection, x_root_index));
-        let dispatcher = LinuxDispatcher::new(main_sender, &client_dispatcher);
-        let dispatcher = Arc::new(dispatcher);
+        let text_system = Arc::new(LinuxTextSystem::new());
+        let callbacks = Mutex::new(Callbacks::default());
+        let state = Mutex::new(LinuxPlatformState {
+            quit_requested: false,
+        });
+
+        if use_wayland {
+            Self::new_wayland(main_sender, main_receiver, text_system, callbacks, state)
+        } else {
+            Self::new_x11(main_sender, main_receiver, text_system, callbacks, state)
+        }
+    }
 
-        let inner = LinuxPlatformInner {
+    fn new_wayland(
+        main_sender: Sender<Runnable>,
+        main_receiver: Receiver<Runnable>,
+        text_system: Arc<LinuxTextSystem>,
+        callbacks: Mutex<Callbacks>,
+        state: Mutex<LinuxPlatformState>,
+    ) -> Self {
+        let conn = Arc::new(Connection::connect_to_env().unwrap());
+        let client_dispatcher: Arc<dyn ClientDispatcher + Send + Sync> =
+            Arc::new(WaylandClientDispatcher::new(&conn));
+        let dispatcher = Arc::new(LinuxDispatcher::new(main_sender, &client_dispatcher));
+        let inner = Arc::new(LinuxPlatformInner {
             background_executor: BackgroundExecutor::new(dispatcher.clone()),
             foreground_executor: ForegroundExecutor::new(dispatcher.clone()),
             main_receiver,
-            text_system: Arc::new(LinuxTextSystem::new()),
-            callbacks: Mutex::new(Callbacks::default()),
-            state: Mutex::new(LinuxPlatformState {
-                quit_requested: false,
-            }),
-        };
-        let inner = Arc::new(inner);
-
-        let x11client = X11Client::new(Arc::clone(&inner), xcb_connection, x_root_index, atoms);
-        let x11client = Arc::new(x11client);
+            text_system,
+            callbacks,
+            state,
+        });
+        let client = Arc::new(WaylandClient::new(Arc::clone(&inner), Arc::clone(&conn)));
+        Self {
+            client,
+            inner: Arc::clone(&inner),
+        }
+    }
 
+    fn new_x11(
+        main_sender: Sender<Runnable>,
+        main_receiver: Receiver<Runnable>,
+        text_system: Arc<LinuxTextSystem>,
+        callbacks: Mutex<Callbacks>,
+        state: Mutex<LinuxPlatformState>,
+    ) -> Self {
+        let (xcb_connection, x_root_index) = xcb::Connection::connect(None).unwrap();
+        let atoms = XcbAtoms::intern_all(&xcb_connection).unwrap();
+        let xcb_connection = Arc::new(xcb_connection);
+        let client_dispatcher: Arc<dyn ClientDispatcher + Send + Sync> =
+            Arc::new(X11ClientDispatcher::new(&xcb_connection, x_root_index));
+        let dispatcher = Arc::new(LinuxDispatcher::new(main_sender, &client_dispatcher));
+        let inner = Arc::new(LinuxPlatformInner {
+            background_executor: BackgroundExecutor::new(dispatcher.clone()),
+            foreground_executor: ForegroundExecutor::new(dispatcher.clone()),
+            main_receiver,
+            text_system,
+            callbacks,
+            state,
+        });
+        let client = Arc::new(X11Client::new(
+            Arc::clone(&inner),
+            xcb_connection,
+            x_root_index,
+            atoms,
+        ));
         Self {
-            client: x11client,
+            client,
             inner: Arc::clone(&inner),
         }
     }

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

@@ -0,0 +1,256 @@
+use std::rc::Rc;
+use std::sync::Arc;
+
+use parking_lot::Mutex;
+use wayland_client::protocol::wl_callback::WlCallback;
+use wayland_client::{
+    delegate_noop,
+    protocol::{
+        wl_buffer, wl_callback, wl_compositor, wl_keyboard, wl_registry, wl_seat, wl_shm,
+        wl_shm_pool,
+        wl_surface::{self, WlSurface},
+    },
+    Connection, Dispatch, EventQueue, Proxy, QueueHandle,
+};
+use wayland_protocols::xdg::shell::client::{xdg_surface, xdg_toplevel, xdg_wm_base};
+
+use crate::platform::linux::client::Client;
+use crate::platform::linux::wayland::window::WaylandWindow;
+use crate::platform::{LinuxPlatformInner, PlatformWindow};
+use crate::{
+    platform::linux::wayland::window::WaylandWindowState, AnyWindowHandle, DisplayId,
+    PlatformDisplay, WindowOptions,
+};
+
+pub(crate) struct WaylandClientState {
+    compositor: Option<wl_compositor::WlCompositor>,
+    buffer: Option<wl_buffer::WlBuffer>,
+    wm_base: Option<xdg_wm_base::XdgWmBase>,
+    windows: Vec<(xdg_surface::XdgSurface, Arc<WaylandWindowState>)>,
+    platform_inner: Arc<LinuxPlatformInner>,
+}
+
+pub(crate) struct WaylandClient {
+    platform_inner: Arc<LinuxPlatformInner>,
+    conn: Arc<Connection>,
+    state: Mutex<WaylandClientState>,
+    event_queue: Mutex<EventQueue<WaylandClientState>>,
+    qh: Arc<QueueHandle<WaylandClientState>>,
+}
+
+impl WaylandClient {
+    pub(crate) fn new(
+        linux_platform_inner: Arc<LinuxPlatformInner>,
+        conn: Arc<Connection>,
+    ) -> Self {
+        let state = WaylandClientState {
+            compositor: None,
+            buffer: None,
+            wm_base: None,
+            windows: Vec::new(),
+            platform_inner: Arc::clone(&linux_platform_inner),
+        };
+        let event_queue: EventQueue<WaylandClientState> = conn.new_event_queue();
+        let qh = event_queue.handle();
+        Self {
+            platform_inner: linux_platform_inner,
+            conn,
+            state: Mutex::new(state),
+            event_queue: Mutex::new(event_queue),
+            qh: Arc::new(qh),
+        }
+    }
+}
+
+impl Client for WaylandClient {
+    fn run(&self, on_finish_launching: Box<dyn FnOnce()>) {
+        let display = self.conn.display();
+        let mut eq = self.event_queue.lock();
+        let _registry = display.get_registry(&self.qh, ());
+
+        eq.roundtrip(&mut self.state.lock()).unwrap();
+
+        on_finish_launching();
+        while !self.platform_inner.state.lock().quit_requested {
+            eq.flush().unwrap();
+            eq.dispatch_pending(&mut self.state.lock()).unwrap();
+            if let Some(guard) = self.conn.prepare_read() {
+                guard.read().unwrap();
+                eq.dispatch_pending(&mut self.state.lock()).unwrap();
+            }
+            if let Ok(runnable) = self.platform_inner.main_receiver.try_recv() {
+                runnable.run();
+            }
+        }
+    }
+
+    fn displays(&self) -> Vec<Rc<dyn PlatformDisplay>> {
+        Vec::new()
+    }
+
+    fn display(&self, id: DisplayId) -> Option<Rc<dyn PlatformDisplay>> {
+        unimplemented!()
+    }
+
+    fn open_window(
+        &self,
+        handle: AnyWindowHandle,
+        options: WindowOptions,
+    ) -> Box<dyn PlatformWindow> {
+        let mut state = self.state.lock();
+
+        let wm_base = state.wm_base.as_ref().unwrap();
+        let compositor = state.compositor.as_ref().unwrap();
+        let wl_surface = compositor.create_surface(&self.qh, ());
+        let xdg_surface = wm_base.get_xdg_surface(&wl_surface, &self.qh, ());
+        let toplevel = xdg_surface.get_toplevel(&self.qh, ());
+        let wl_surface = Arc::new(wl_surface);
+
+        wl_surface.frame(&self.qh, wl_surface.clone());
+        wl_surface.commit();
+
+        let window_state: Arc<WaylandWindowState> = Arc::new(WaylandWindowState::new(
+            &self.conn,
+            wl_surface.clone(),
+            Arc::new(toplevel),
+            options,
+        ));
+
+        state.windows.push((xdg_surface, Arc::clone(&window_state)));
+        Box::new(WaylandWindow(window_state))
+    }
+}
+
+impl Dispatch<wl_registry::WlRegistry, ()> for WaylandClientState {
+    fn event(
+        state: &mut Self,
+        registry: &wl_registry::WlRegistry,
+        event: wl_registry::Event,
+        _: &(),
+        _: &Connection,
+        qh: &QueueHandle<Self>,
+    ) {
+        if let wl_registry::Event::Global {
+            name, interface, ..
+        } = event
+        {
+            match &interface[..] {
+                "wl_compositor" => {
+                    let compositor =
+                        registry.bind::<wl_compositor::WlCompositor, _, _>(name, 1, qh, ());
+                    state.compositor = Some(compositor);
+                }
+                "xdg_wm_base" => {
+                    let wm_base = registry.bind::<xdg_wm_base::XdgWmBase, _, _>(name, 1, qh, ());
+                    state.wm_base = Some(wm_base);
+                }
+                _ => {}
+            };
+        }
+    }
+}
+
+delegate_noop!(WaylandClientState: ignore wl_compositor::WlCompositor);
+delegate_noop!(WaylandClientState: ignore wl_surface::WlSurface);
+delegate_noop!(WaylandClientState: ignore wl_shm::WlShm);
+delegate_noop!(WaylandClientState: ignore wl_shm_pool::WlShmPool);
+delegate_noop!(WaylandClientState: ignore wl_buffer::WlBuffer);
+delegate_noop!(WaylandClientState: ignore wl_seat::WlSeat);
+delegate_noop!(WaylandClientState: ignore wl_keyboard::WlKeyboard);
+
+impl Dispatch<WlCallback, Arc<WlSurface>> for WaylandClientState {
+    fn event(
+        state: &mut Self,
+        _: &WlCallback,
+        event: wl_callback::Event,
+        surf: &Arc<WlSurface>,
+        _: &Connection,
+        qh: &QueueHandle<Self>,
+    ) {
+        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();
+                }
+            }
+        }
+    }
+}
+
+impl Dispatch<xdg_surface::XdgSurface, ()> for WaylandClientState {
+    fn event(
+        state: &mut Self,
+        xdg_surface: &xdg_surface::XdgSurface,
+        event: xdg_surface::Event,
+        _: &(),
+        _: &Connection,
+        _: &QueueHandle<Self>,
+    ) {
+        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 {
+    fn event(
+        state: &mut Self,
+        xdg_toplevel: &xdg_toplevel::XdgToplevel,
+        event: <xdg_toplevel::XdgToplevel as Proxy>::Event,
+        _: &(),
+        _: &Connection,
+        _: &QueueHandle<Self>,
+    ) {
+        if let xdg_toplevel::Event::Configure {
+            width,
+            height,
+            states: _states,
+        } = event
+        {
+            if width == 0 || height == 0 {
+                return;
+            }
+            for window in &state.windows {
+                if window.1.toplevel.id() == xdg_toplevel.id() {
+                    window.1.resize(width, height);
+                    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.state.lock().quit_requested |= state.windows.is_empty();
+        }
+    }
+}
+
+impl Dispatch<xdg_wm_base::XdgWmBase, ()> for WaylandClientState {
+    fn event(
+        _: &mut Self,
+        wm_base: &xdg_wm_base::XdgWmBase,
+        event: <xdg_wm_base::XdgWmBase as Proxy>::Event,
+        _: &(),
+        _: &Connection,
+        _: &QueueHandle<Self>,
+    ) {
+        if let xdg_wm_base::Event::Ping { serial } = event {
+            wm_base.pong(serial);
+        }
+    }
+}

crates/gpui/src/platform/linux/wayland/client_dispatcher.rs 🔗

@@ -0,0 +1,30 @@
+use std::sync::Arc;
+
+use wayland_client::{Connection, EventQueue};
+
+use crate::platform::linux::client_dispatcher::ClientDispatcher;
+
+pub(crate) struct WaylandClientDispatcher {
+    conn: Arc<Connection>,
+    event_queue: Arc<EventQueue<Connection>>,
+}
+
+impl WaylandClientDispatcher {
+    pub(crate) fn new(conn: &Arc<Connection>) -> Self {
+        let event_queue = conn.new_event_queue();
+        Self {
+            conn: Arc::clone(conn),
+            event_queue: Arc::new(event_queue),
+        }
+    }
+}
+
+impl Drop for WaylandClientDispatcher {
+    fn drop(&mut self) {
+        //todo!(linux)
+    }
+}
+
+impl ClientDispatcher for WaylandClientDispatcher {
+    fn dispatch_on_main_thread(&self) {}
+}

crates/gpui/src/platform/linux/wayland/display.rs 🔗

@@ -0,0 +1,31 @@
+use std::fmt::Debug;
+
+use uuid::Uuid;
+
+use crate::{Bounds, DisplayId, GlobalPixels, PlatformDisplay, Size};
+
+#[derive(Debug)]
+pub(crate) struct WaylandDisplay {}
+
+impl PlatformDisplay for WaylandDisplay {
+    // todo!(linux)
+    fn id(&self) -> DisplayId {
+        return DisplayId(123); // return some fake data so it doesn't panic
+    }
+
+    // todo!(linux)
+    fn uuid(&self) -> anyhow::Result<Uuid> {
+        return Ok(Uuid::from_bytes([0; 16])); // return some fake data so it doesn't panic
+    }
+
+    // todo!(linux)
+    fn bounds(&self) -> Bounds<GlobalPixels> {
+        Bounds {
+            origin: Default::default(),
+            size: Size {
+                width: GlobalPixels(1000f32),
+                height: GlobalPixels(500f32),
+            },
+        } // return some fake data so it doesn't panic
+    }
+}

crates/gpui/src/platform/linux/wayland/window.rs 🔗

@@ -0,0 +1,350 @@
+use std::any::Any;
+use std::ffi::c_void;
+use std::rc::Rc;
+use std::sync::Arc;
+
+use blade_graphics as gpu;
+use blade_rwh::{HasRawDisplayHandle, HasRawWindowHandle, RawDisplayHandle, RawWindowHandle};
+use futures::channel::oneshot::Receiver;
+use parking_lot::Mutex;
+use raw_window_handle::{
+    DisplayHandle, HandleError, HasDisplayHandle, HasWindowHandle, WindowHandle,
+};
+use wayland_client::{protocol::wl_surface, Proxy};
+use wayland_protocols::xdg::shell::client::xdg_toplevel;
+
+use crate::platform::linux::blade_renderer::BladeRenderer;
+use crate::platform::linux::wayland::display::WaylandDisplay;
+use crate::platform::{PlatformAtlas, PlatformInputHandler, PlatformWindow};
+use crate::scene::Scene;
+use crate::{
+    px, Bounds, Modifiers, Pixels, PlatformDisplay, PlatformInput, Point, PromptLevel, Size,
+    WindowAppearance, WindowBounds, WindowOptions,
+};
+
+#[derive(Default)]
+pub(crate) struct Callbacks {
+    request_frame: Option<Box<dyn FnMut()>>,
+    input: Option<Box<dyn FnMut(crate::PlatformInput) -> bool>>,
+    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()>>,
+}
+
+struct WaylandWindowInner {
+    renderer: BladeRenderer,
+    bounds: Bounds<i32>,
+}
+
+struct RawWindow {
+    window: *mut c_void,
+    display: *mut c_void,
+}
+
+unsafe impl HasRawWindowHandle for RawWindow {
+    fn raw_window_handle(&self) -> RawWindowHandle {
+        let mut wh = blade_rwh::WaylandWindowHandle::empty();
+        wh.surface = self.window;
+        wh.into()
+    }
+}
+
+unsafe impl HasRawDisplayHandle for RawWindow {
+    fn raw_display_handle(&self) -> RawDisplayHandle {
+        let mut dh = blade_rwh::WaylandDisplayHandle::empty();
+        dh.display = self.display;
+        dh.into()
+    }
+}
+
+impl WaylandWindowInner {
+    fn new(
+        conn: &Arc<wayland_client::Connection>,
+        wl_surf: &Arc<wl_surface::WlSurface>,
+        bounds: Bounds<i32>,
+    ) -> Self {
+        let raw = RawWindow {
+            window: wl_surf.id().as_ptr() as *mut _,
+            display: conn.backend().display_ptr() as *mut _,
+        };
+        let gpu = Arc::new(
+            unsafe {
+                gpu::Context::init_windowed(
+                    &raw,
+                    gpu::ContextDesc {
+                        validation: false,
+                        capture: false,
+                    },
+                )
+            }
+            .unwrap(),
+        );
+        let extent = gpu::Extent {
+            width: bounds.size.width as u32,
+            height: bounds.size.height as u32,
+            depth: 1,
+        };
+        Self {
+            renderer: BladeRenderer::new(gpu, extent),
+            bounds,
+        }
+    }
+}
+
+pub(crate) struct WaylandWindowState {
+    conn: Arc<wayland_client::Connection>,
+    inner: Mutex<WaylandWindowInner>,
+    pub(crate) callbacks: Mutex<Callbacks>,
+    pub(crate) surface: Arc<wl_surface::WlSurface>,
+    pub(crate) toplevel: Arc<xdg_toplevel::XdgToplevel>,
+}
+
+impl WaylandWindowState {
+    pub(crate) fn new(
+        conn: &Arc<wayland_client::Connection>,
+        wl_surf: Arc<wl_surface::WlSurface>,
+        toplevel: Arc<xdg_toplevel::XdgToplevel>,
+        options: WindowOptions,
+    ) -> Self {
+        if options.bounds == WindowBounds::Maximized {
+            toplevel.set_maximized();
+        } else if options.bounds == WindowBounds::Fullscreen {
+            toplevel.set_fullscreen(None);
+        }
+
+        let bounds: Bounds<i32> = match options.bounds {
+            WindowBounds::Fullscreen | WindowBounds::Maximized => Bounds {
+                origin: Point::default(),
+                size: Size {
+                    width: 500,
+                    height: 500,
+                }, //todo!(implement)
+            },
+            WindowBounds::Fixed(bounds) => bounds.map(|p| p.0 as i32),
+        };
+
+        Self {
+            conn: Arc::clone(conn),
+            surface: Arc::clone(&wl_surf),
+            inner: Mutex::new(WaylandWindowInner::new(&Arc::clone(conn), &wl_surf, bounds)),
+            callbacks: Mutex::new(Callbacks::default()),
+            toplevel,
+        }
+    }
+
+    pub fn update(&self) {
+        let mut cb = self.callbacks.lock();
+        if let Some(mut fun) = cb.request_frame.take() {
+            drop(cb);
+            fun();
+            self.callbacks.lock().request_frame = Some(fun);
+        }
+    }
+
+    pub fn resize(&self, width: i32, height: i32) {
+        {
+            let mut inner = self.inner.lock();
+            inner.bounds.size.width = width;
+            inner.bounds.size.height = height;
+            inner.renderer.resize(gpu::Extent {
+                width: width as u32,
+                height: height as u32,
+                depth: 1,
+            });
+        }
+        let mut callbacks = self.callbacks.lock();
+        if let Some(ref mut fun) = callbacks.resize {
+            fun(
+                Size {
+                    width: px(width as f32),
+                    height: px(height as f32),
+                },
+                1.0,
+            );
+        }
+        if let Some(ref mut fun) = callbacks.moved {
+            fun()
+        }
+    }
+
+    pub fn close(&self) {
+        let mut callbacks = self.callbacks.lock();
+        if let Some(fun) = callbacks.close.take() {
+            fun()
+        }
+        self.toplevel.destroy();
+    }
+}
+
+#[derive(Clone)]
+pub(crate) struct WaylandWindow(pub(crate) Arc<WaylandWindowState>);
+
+impl HasWindowHandle for WaylandWindow {
+    fn window_handle(&self) -> Result<WindowHandle<'_>, HandleError> {
+        unimplemented!()
+    }
+}
+
+impl HasDisplayHandle for WaylandWindow {
+    fn display_handle(&self) -> Result<DisplayHandle<'_>, HandleError> {
+        unimplemented!()
+    }
+}
+
+impl PlatformWindow for WaylandWindow {
+    //todo!(linux)
+    fn bounds(&self) -> WindowBounds {
+        WindowBounds::Maximized
+    }
+
+    // todo!(linux)
+    fn content_size(&self) -> Size<Pixels> {
+        let inner = self.0.inner.lock();
+        Size {
+            width: Pixels(inner.bounds.size.width as f32),
+            height: Pixels(inner.bounds.size.height as f32),
+        }
+    }
+
+    // todo!(linux)
+    fn scale_factor(&self) -> f32 {
+        return 1f32;
+    }
+
+    //todo!(linux)
+    fn titlebar_height(&self) -> Pixels {
+        unimplemented!()
+    }
+
+    // todo!(linux)
+    fn appearance(&self) -> WindowAppearance {
+        WindowAppearance::Light
+    }
+
+    // todo!(linux)
+    fn display(&self) -> Rc<dyn PlatformDisplay> {
+        Rc::new(WaylandDisplay {})
+    }
+
+    // todo!(linux)
+    fn mouse_position(&self) -> Point<Pixels> {
+        Point::default()
+    }
+
+    //todo!(linux)
+    fn modifiers(&self) -> Modifiers {
+        crate::Modifiers::default()
+    }
+
+    //todo!(linux)
+    fn as_any_mut(&mut self) -> &mut dyn Any {
+        unimplemented!()
+    }
+
+    fn set_input_handler(&mut self, input_handler: PlatformInputHandler) {
+        //todo!(linux)
+    }
+
+    //todo!(linux)
+    fn take_input_handler(&mut self) -> Option<PlatformInputHandler> {
+        None
+    }
+
+    //todo!(linux)
+    fn prompt(
+        &self,
+        level: PromptLevel,
+        msg: &str,
+        detail: Option<&str>,
+        answers: &[&str],
+    ) -> Receiver<usize> {
+        unimplemented!()
+    }
+
+    fn activate(&self) {
+        //todo!(linux)
+    }
+
+    fn set_title(&mut self, title: &str) {
+        self.0.toplevel.set_title(title.to_string());
+    }
+
+    fn set_edited(&mut self, edited: bool) {
+        //todo!(linux)
+    }
+
+    fn show_character_palette(&self) {
+        //todo!(linux)
+    }
+
+    fn minimize(&self) {
+        //todo!(linux)
+    }
+
+    fn zoom(&self) {
+        //todo!(linux)
+    }
+
+    fn toggle_full_screen(&self) {
+        //todo!(linux)
+    }
+
+    fn on_request_frame(&self, callback: Box<dyn FnMut()>) {
+        self.0.callbacks.lock().request_frame = Some(callback);
+    }
+
+    fn on_input(&self, callback: Box<dyn FnMut(PlatformInput) -> bool>) {
+        //todo!(linux)
+    }
+
+    fn on_active_status_change(&self, callback: Box<dyn FnMut(bool)>) {
+        //todo!(linux)
+    }
+
+    fn on_resize(&self, callback: Box<dyn FnMut(Size<Pixels>, f32)>) {
+        self.0.callbacks.lock().resize = Some(callback);
+    }
+
+    fn on_fullscreen(&self, callback: Box<dyn FnMut(bool)>) {
+        //todo!(linux)
+    }
+
+    fn on_moved(&self, callback: Box<dyn FnMut()>) {
+        self.0.callbacks.lock().moved = Some(callback);
+    }
+
+    fn on_should_close(&self, callback: Box<dyn FnMut() -> bool>) {
+        self.0.callbacks.lock().should_close = Some(callback);
+    }
+
+    fn on_close(&self, callback: Box<dyn FnOnce()>) {
+        self.0.callbacks.lock().close = Some(callback);
+    }
+
+    fn on_appearance_changed(&self, callback: Box<dyn FnMut()>) {
+        //todo!(linux)
+    }
+
+    // todo!(linux)
+    fn is_topmost_for_position(&self, position: Point<Pixels>) -> bool {
+        false
+    }
+
+    fn draw(&self, scene: &Scene) {
+        let mut inner = self.0.inner.lock();
+        inner.renderer.draw(scene);
+    }
+
+    fn sprite_atlas(&self) -> Arc<dyn PlatformAtlas> {
+        let inner = self.0.inner.lock();
+        inner.renderer.atlas().clone()
+    }
+
+    fn set_graphics_profiler_enabled(&self, enabled: bool) {
+        //todo!(linux)
+    }
+}

script/linux 🔗

@@ -11,6 +11,7 @@ if [[ -n $apt ]]; then
     libasound2-dev
     libfontconfig-dev
     vulkan-validationlayers*
+    libwayland-dev
   )
   $maysudo "$apt" install -y "${deps[@]}"
   exit 0
@@ -24,6 +25,7 @@ if [[ -n $dnf ]]; then
     alsa-lib-devel
     fontconfig-devel
     vulkan-validation-layers
+    wayland-devel
   )
   $maysudo "$dnf" install -y "${deps[@]}"
   exit 0
@@ -37,6 +39,7 @@ if [[ -n $pacman ]]; then
     alsa-lib
     fontconfig
     vulkan-validation-layers
+    wayland
   )
   $maysudo "$pacman" -S --needed --noconfirm "${deps[@]}"
   exit 0

typos.toml 🔗

@@ -21,7 +21,6 @@ extend-ignore-re = [
     '"ba"',
     ":ba\\|z",
     # :/ crates/collab/migrations/20231009181554_add_release_channel_to_rooms.sql
-    "ADD COLUMN enviroment TEXT",
-    "ALTER TABLE rooms DROP COLUMN enviroment;",
+    "COLUMN enviroment",
 ]
 check-filename = true