linux: create a hidden window inside the platform

Dzmitry Malyshau created

It allows us to receive messages from the dispatcher,
which breaks us out of waiting and lets us execute
main thread runnables as a part of the main loop.

Change summary

crates/gpui/src/platform/linux/dispatcher.rs  | 82 ++++++++++++++------
crates/gpui/src/platform/linux/platform.rs    | 34 +++++--
crates/gpui/src/platform/linux/text_system.rs |  2 
crates/gpui/src/platform/linux/window.rs      |  1 
4 files changed, 80 insertions(+), 39 deletions(-)

Detailed changes

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

@@ -7,29 +7,50 @@ use async_task::Runnable;
 use parking::{Parker, Unparker};
 use parking_lot::Mutex;
 use std::{
-    panic, thread,
+    panic,
+    sync::Arc,
+    thread,
     time::{Duration, Instant},
 };
+use xcb::x;
 
 pub(crate) struct LinuxDispatcher {
+    xcb_connection: Arc<xcb::Connection>,
+    x_listener_window: x::Window,
     parker: Mutex<Parker>,
     timed_tasks: Mutex<Vec<(Instant, Runnable)>>,
     main_sender: flume::Sender<Runnable>,
-    main_receiver: flume::Receiver<Runnable>,
     background_sender: flume::Sender<Runnable>,
-    background_thread: thread::JoinHandle<()>,
+    _background_thread: thread::JoinHandle<()>,
     main_thread_id: thread::ThreadId,
 }
 
-impl Default for LinuxDispatcher {
-    fn default() -> Self {
-        Self::new()
-    }
-}
-
 impl LinuxDispatcher {
-    pub fn new() -> Self {
-        let (main_sender, main_receiver) = flume::unbounded::<Runnable>();
+    pub fn new(
+        main_sender: flume::Sender<Runnable>,
+        xcb_connection: &Arc<xcb::Connection>,
+        x_root_index: i32,
+    ) -> Self {
+        let x_listener_window = xcb_connection.generate_id();
+        let screen = xcb_connection
+            .get_setup()
+            .roots()
+            .nth(x_root_index as usize)
+            .unwrap();
+        xcb_connection.send_request(&x::CreateWindow {
+            depth: 0,
+            wid: x_listener_window,
+            parent: screen.root(),
+            x: 0,
+            y: 0,
+            width: 1,
+            height: 1,
+            border_width: 0,
+            class: x::WindowClass::InputOnly,
+            visual: screen.root_visual(),
+            value_list: &[],
+        });
+
         let (background_sender, background_receiver) = flume::unbounded::<Runnable>();
         let background_thread = thread::spawn(move || {
             for runnable in background_receiver {
@@ -37,21 +58,23 @@ impl LinuxDispatcher {
             }
         });
         LinuxDispatcher {
+            xcb_connection: Arc::clone(xcb_connection),
+            x_listener_window,
             parker: Mutex::new(Parker::new()),
             timed_tasks: Mutex::new(Vec::new()),
             main_sender,
-            main_receiver,
             background_sender,
-            background_thread,
+            _background_thread: background_thread,
             main_thread_id: thread::current().id(),
         }
     }
+}
 
-    pub fn tick_main(&self) {
-        assert!(self.is_main_thread());
-        if let Ok(runnable) = self.main_receiver.try_recv() {
-            runnable.run();
-        }
+impl Drop for LinuxDispatcher {
+    fn drop(&mut self) {
+        self.xcb_connection.send_request(&x::DestroyWindow {
+            window: self.x_listener_window,
+        });
     }
 }
 
@@ -66,6 +89,18 @@ impl PlatformDispatcher for LinuxDispatcher {
 
     fn dispatch_on_main_thread(&self, runnable: Runnable) {
         self.main_sender.send(runnable).unwrap();
+        // Send a message to the invisible window, forcing
+        // tha main loop to wake up and dispatch the runnable.
+        self.xcb_connection.send_request(&x::SendEvent {
+            propagate: false,
+            destination: x::SendEventDest::Window(self.x_listener_window),
+            event_mask: x::EventMask::NO_EVENT,
+            event: &x::VisibilityNotifyEvent::new(
+                self.x_listener_window,
+                x::Visibility::Unobscured,
+            ),
+        });
+        self.xcb_connection.flush().unwrap();
     }
 
     fn dispatch_after(&self, duration: Duration, runnable: Runnable) {
@@ -76,24 +111,17 @@ impl PlatformDispatcher for LinuxDispatcher {
     }
 
     fn tick(&self, background_only: bool) -> bool {
-        let mut ran = false;
-        if self.is_main_thread() && !background_only {
-            for runnable in self.main_receiver.try_iter() {
-                runnable.run();
-                ran = true;
-            }
-        }
         let mut timed_tasks = self.timed_tasks.lock();
+        let old_count = timed_tasks.len();
         while let Some(&(moment, _)) = timed_tasks.last() {
             if moment <= Instant::now() {
                 let (_, runnable) = timed_tasks.pop().unwrap();
                 runnable.run();
-                ran = true;
             } else {
                 break;
             }
         }
-        ran
+        timed_tasks.len() != old_count
     }
 
     fn park(&self) {

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

@@ -7,6 +7,7 @@ use crate::{
     PlatformTextSystem, PlatformWindow, Point, Result, SemanticVersion, Size, Task, WindowOptions,
 };
 
+use async_task::Runnable;
 use collections::{HashMap, HashSet};
 use futures::channel::oneshot;
 use parking_lot::Mutex;
@@ -37,12 +38,13 @@ pub(crate) struct LinuxPlatform {
     atoms: XcbAtoms,
     background_executor: BackgroundExecutor,
     foreground_executor: ForegroundExecutor,
-    dispatcher: Arc<LinuxDispatcher>,
+    main_receiver: flume::Receiver<Runnable>,
     text_system: Arc<LinuxTextSystem>,
     state: Mutex<LinuxPlatformState>,
 }
 
 pub(crate) struct LinuxPlatformState {
+    quit_requested: bool,
     windows: HashMap<x::Window, Arc<LinuxWindowState>>,
 }
 
@@ -57,17 +59,24 @@ impl LinuxPlatform {
         let (xcb_connection, x_root_index) = xcb::Connection::connect(None).unwrap();
         let atoms = XcbAtoms::intern_all(&xcb_connection).unwrap();
 
-        let dispatcher = Arc::new(LinuxDispatcher::new());
+        let xcb_connection = Arc::new(xcb_connection);
+        let (main_sender, main_receiver) = flume::unbounded::<Runnable>();
+        let dispatcher = Arc::new(LinuxDispatcher::new(
+            main_sender,
+            &xcb_connection,
+            x_root_index,
+        ));
 
         Self {
-            xcb_connection: Arc::new(xcb_connection),
+            xcb_connection,
             x_root_index,
             atoms,
             background_executor: BackgroundExecutor::new(dispatcher.clone()),
             foreground_executor: ForegroundExecutor::new(dispatcher.clone()),
-            dispatcher,
+            main_receiver,
             text_system: Arc::new(LinuxTextSystem::new()),
             state: Mutex::new(LinuxPlatformState {
+                quit_requested: false,
                 windows: HashMap::default(),
             }),
         }
@@ -92,8 +101,7 @@ impl Platform for LinuxPlatform {
         //Note: here and below, don't keep the lock() open when calling
         // into window functions as they may invoke callbacks that need
         // to immediately access the platform (self).
-
-        while !self.state.lock().windows.is_empty() {
+        while !self.state.lock().quit_requested {
             let event = self.xcb_connection.wait_for_event().unwrap();
             match event {
                 xcb::Event::X(x::Event::ClientMessage(ev)) => {
@@ -129,15 +137,18 @@ impl Platform for LinuxPlatform {
                     };
                     window.configure(bounds)
                 }
-                ref other => {
-                    println!("Other event {:?}", other);
-                }
+                _ => {}
+            }
+
+            if let Ok(runnable) = self.main_receiver.try_recv() {
+                runnable.run();
             }
-            self.dispatcher.tick_main();
         }
     }
 
-    fn quit(&self) {}
+    fn quit(&self) {
+        self.state.lock().quit_requested = true;
+    }
 
     fn restart(&self) {}
 
@@ -186,6 +197,7 @@ impl Platform for LinuxPlatform {
             x_window,
             &self.atoms,
         ));
+
         self.state
             .lock()
             .windows

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

@@ -76,7 +76,7 @@ impl PlatformTextSystem for LinuxTextSystem {
         unimplemented!()
     }
     fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option<GlyphId> {
-        unimplemented!()
+        None
     }
     fn glyph_raster_bounds(&self, params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>> {
         unimplemented!()

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

@@ -237,6 +237,7 @@ impl LinuxWindowState {
         if let Some(fun) = self.callbacks.lock().close.take() {
             fun();
         }
+        self.xcb_connection.flush().unwrap();
     }
 
     pub fn expose(&self) {