diff --git a/Cargo.lock b/Cargo.lock index 7c75f9ff86..14c6edd672 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4889,6 +4889,7 @@ dependencies = [ "log", "media", "metal", + "mio 1.0.0", "num_cpus", "objc", "oo7", @@ -5143,9 +5144,9 @@ dependencies = [ [[package]] name = "hermit-abi" -version = "0.3.3" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" [[package]] name = "hex" @@ -5659,7 +5660,7 @@ version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" dependencies = [ - "hermit-abi 0.3.3", + "hermit-abi 0.3.9", "libc", "windows-sys 0.48.0", ] @@ -5690,7 +5691,7 @@ dependencies = [ "fnv", "lazy_static", "libc", - "mio", + "mio 0.8.11", "rand 0.8.5", "serde", "tempfile", @@ -6639,6 +6640,19 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "mio" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4929e1f84c5e54c3ec6141cd5d8b5a5c055f031f80cf78f2072920173cb4d880" +dependencies = [ + "hermit-abi 0.3.9", + "libc", + "log", + "wasi 0.11.0+wasi-snapshot-preview1", + "windows-sys 0.52.0", +] + [[package]] name = "miow" version = "0.6.0" @@ -6877,7 +6891,7 @@ dependencies = [ "kqueue", "libc", "log", - "mio", + "mio 0.8.11", "walkdir", "windows-sys 0.48.0", ] @@ -7057,7 +7071,7 @@ version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" dependencies = [ - "hermit-abi 0.3.3", + "hermit-abi 0.3.9", "libc", ] @@ -11098,7 +11112,7 @@ dependencies = [ "backtrace", "bytes 1.5.0", "libc", - "mio", + "mio 0.8.11", "num_cpus", "parking_lot", "pin-project-lite", diff --git a/crates/gpui/Cargo.toml b/crates/gpui/Cargo.toml index 0781c7cdd6..c7c3a92a95 100644 --- a/crates/gpui/Cargo.toml +++ b/crates/gpui/Cargo.toml @@ -141,6 +141,7 @@ xim = { git = "https://github.com/npmania/xim-rs", rev = "27132caffc5b9bc9c432ca ] } font-kit = { git = "https://github.com/zed-industries/font-kit", rev = "5a5c4d4", features = ["source-fontconfig-dlopen"] } x11-clipboard = "0.9.2" +mio = { version = "1.0.0", features = ["os-poll", "os-ext"] } [target.'cfg(windows)'.dependencies] windows.workspace = true diff --git a/crates/gpui/src/platform/linux/dispatcher.rs b/crates/gpui/src/platform/linux/dispatcher.rs index 0047b113a8..ad4628ed5d 100644 --- a/crates/gpui/src/platform/linux/dispatcher.rs +++ b/crates/gpui/src/platform/linux/dispatcher.rs @@ -5,9 +5,10 @@ use calloop::{ timer::TimeoutAction, EventLoop, }; +use mio::Waker; use parking::{Parker, Unparker}; use parking_lot::Mutex; -use std::{thread, time::Duration}; +use std::{sync::Arc, thread, time::Duration}; use util::ResultExt; struct TimerAfter { @@ -18,6 +19,7 @@ struct TimerAfter { pub(crate) struct LinuxDispatcher { parker: Mutex, main_sender: Sender, + main_waker: Option>, timer_sender: Sender, background_sender: flume::Sender, _background_threads: Vec>, @@ -25,7 +27,7 @@ pub(crate) struct LinuxDispatcher { } impl LinuxDispatcher { - pub fn new(main_sender: Sender) -> Self { + pub fn new(main_sender: Sender, main_waker: Option>) -> Self { let (background_sender, background_receiver) = flume::unbounded::(); let thread_count = std::thread::available_parallelism() .map(|i| i.get()) @@ -77,6 +79,7 @@ impl LinuxDispatcher { Self { parker: Mutex::new(Parker::new()), main_sender, + main_waker, timer_sender, background_sender, _background_threads: background_threads, @@ -96,6 +99,9 @@ impl PlatformDispatcher for LinuxDispatcher { fn dispatch_on_main_thread(&self, runnable: Runnable) { self.main_sender.send(runnable).ok(); + if let Some(main_waker) = self.main_waker.as_ref() { + main_waker.wake().ok(); + } } fn dispatch_after(&self, duration: Duration, runnable: Runnable) { diff --git a/crates/gpui/src/platform/linux/headless/client.rs b/crates/gpui/src/platform/linux/headless/client.rs index bb066684f8..7f9a623a6e 100644 --- a/crates/gpui/src/platform/linux/headless/client.rs +++ b/crates/gpui/src/platform/linux/headless/client.rs @@ -22,7 +22,7 @@ impl HeadlessClient { pub(crate) fn new() -> Self { let event_loop = EventLoop::try_new().unwrap(); - let (common, main_receiver) = LinuxCommon::new(event_loop.get_signal()); + let (common, main_receiver) = LinuxCommon::new(Box::new(event_loop.get_signal()), None); let handle = event_loop.handle(); diff --git a/crates/gpui/src/platform/linux/platform.rs b/crates/gpui/src/platform/linux/platform.rs index 3c3be3f6cd..54fc4aa17d 100644 --- a/crates/gpui/src/platform/linux/platform.rs +++ b/crates/gpui/src/platform/linux/platform.rs @@ -26,6 +26,7 @@ use calloop::{EventLoop, LoopHandle, LoopSignal}; use filedescriptor::FileDescriptor; use flume::{Receiver, Sender}; use futures::channel::oneshot; +use mio::Waker; use parking_lot::Mutex; use time::UtcOffset; use util::ResultExt; @@ -84,6 +85,16 @@ pub(crate) struct PlatformHandlers { pub(crate) validate_app_menu_command: Option bool>>, } +pub trait QuitSignal { + fn quit(&mut self); +} + +impl QuitSignal for LoopSignal { + fn quit(&mut self) { + self.stop(); + } +} + pub(crate) struct LinuxCommon { pub(crate) background_executor: BackgroundExecutor, pub(crate) foreground_executor: ForegroundExecutor, @@ -91,17 +102,20 @@ pub(crate) struct LinuxCommon { pub(crate) appearance: WindowAppearance, pub(crate) auto_hide_scrollbars: bool, pub(crate) callbacks: PlatformHandlers, - pub(crate) signal: LoopSignal, + pub(crate) quit_signal: Box, pub(crate) menus: Vec, } impl LinuxCommon { - pub fn new(signal: LoopSignal) -> (Self, Channel) { + pub fn new( + quit_signal: Box, + main_waker: Option>, + ) -> (Self, Channel) { let (main_sender, main_receiver) = calloop::channel::channel::(); let text_system = Arc::new(CosmicTextSystem::new()); let callbacks = PlatformHandlers::default(); - let dispatcher = Arc::new(LinuxDispatcher::new(main_sender.clone())); + let dispatcher = Arc::new(LinuxDispatcher::new(main_sender.clone(), main_waker)); let background_executor = BackgroundExecutor::new(dispatcher.clone()); @@ -112,7 +126,7 @@ impl LinuxCommon { appearance: WindowAppearance::Light, auto_hide_scrollbars: false, callbacks, - signal, + quit_signal, menus: Vec::new(), }; @@ -146,7 +160,7 @@ impl Platform for P { } fn quit(&self) { - self.with_common(|common| common.signal.stop()); + self.with_common(|common| common.quit_signal.quit()); } fn compositor_name(&self) -> &'static str { diff --git a/crates/gpui/src/platform/linux/wayland/client.rs b/crates/gpui/src/platform/linux/wayland/client.rs index 250df50875..22750d1814 100644 --- a/crates/gpui/src/platform/linux/wayland/client.rs +++ b/crates/gpui/src/platform/linux/wayland/client.rs @@ -310,7 +310,7 @@ impl WaylandClientStatePtr { } } if state.windows.is_empty() { - state.common.signal.stop(); + state.common.quit_signal.quit(); } } } @@ -406,7 +406,7 @@ impl WaylandClient { let event_loop = EventLoop::::try_new().unwrap(); - let (common, main_receiver) = LinuxCommon::new(event_loop.get_signal()); + let (common, main_receiver) = LinuxCommon::new(Box::new(event_loop.get_signal()), None); let handle = event_loop.handle(); handle @@ -443,7 +443,7 @@ impl WaylandClient { let mut cursor = Cursor::new(&conn, &globals, 24); handle - .insert_source(XDPEventSource::new(&common.background_executor), { + .insert_source(XDPEventSource::new(&common.background_executor, None), { move |event, _, client| match event { XDPEvent::WindowAppearance(appearance) => { if let Some(client) = client.0.upgrade() { diff --git a/crates/gpui/src/platform/linux/x11/client.rs b/crates/gpui/src/platform/linux/x11/client.rs index ab0b9a79b1..ba42bc3fa4 100644 --- a/crates/gpui/src/platform/linux/x11/client.rs +++ b/crates/gpui/src/platform/linux/x11/client.rs @@ -1,19 +1,23 @@ use std::cell::RefCell; use std::collections::HashSet; use std::ops::Deref; +use std::os::fd::AsRawFd; use std::rc::{Rc, Weak}; +use std::sync::Arc; use std::time::{Duration, Instant}; -use calloop::generic::{FdWrapper, Generic}; -use calloop::{EventLoop, LoopHandle, RegistrationToken}; +use anyhow::Context; +use async_task::Runnable; +use calloop::channel::Channel; use collections::HashMap; -use util::ResultExt; +use futures::channel::oneshot; +use mio::{Interest, Token, Waker}; +use util::ResultExt; use x11rb::connection::{Connection, RequestConnection}; use x11rb::cursor; use x11rb::errors::ConnectionError; -use x11rb::protocol::randr::ConnectionExt as _; use x11rb::protocol::xinput::ConnectionExt; use x11rb::protocol::xkb::ConnectionExt as _; use x11rb::protocol::xproto::{ChangeWindowAttributesAux, ConnectionExt as _}; @@ -30,7 +34,7 @@ use crate::platform::{LinuxCommon, PlatformWindow}; use crate::{ modifiers_from_xinput_info, point, px, AnyWindowHandle, Bounds, ClipboardItem, CursorStyle, DisplayId, Keystroke, Modifiers, ModifiersChangedEvent, Pixels, PlatformDisplay, PlatformInput, - Point, ScrollDelta, Size, TouchPhase, WindowParams, X11Window, + Point, QuitSignal, ScrollDelta, Size, TouchPhase, WindowParams, X11Window, }; use super::{ @@ -47,7 +51,6 @@ pub(super) const XINPUT_MASTER_DEVICE: u16 = 1; pub(crate) struct WindowRef { window: X11WindowStatePtr, - refresh_event_token: RegistrationToken, } impl WindowRef { @@ -95,15 +98,18 @@ impl From for EventHandlerError { } pub struct X11ClientState { - pub(crate) loop_handle: LoopHandle<'static, X11Client>, - pub(crate) event_loop: Option>, + /// poll is in an Option so we can take it out in `run()` without + /// mutating self. + poll: Option, + quit_signal_rx: oneshot::Receiver<()>, + runnables: Channel, + xdp_event_source: XDPEventSource, pub(crate) last_click: Instant, pub(crate) last_location: Point, pub(crate) current_count: usize, pub(crate) scale_factor: f32, - pub(crate) xcb_connection: Rc, pub(crate) x_root_index: usize, pub(crate) _resource_database: Database, @@ -139,14 +145,46 @@ impl X11ClientStatePtr { let client = X11Client(self.0.upgrade().expect("client already dropped")); let mut state = client.0.borrow_mut(); - if let Some(window_ref) = state.windows.remove(&x_window) { - state.loop_handle.remove(window_ref.refresh_event_token); + if state.windows.remove(&x_window).is_none() { + log::warn!( + "failed to remove X window {} from client state, does not exist", + x_window + ); } state.cursor_styles.remove(&x_window); if state.windows.is_empty() { - state.common.signal.stop(); + state.common.quit_signal.quit(); + } + } +} + +struct ChannelQuitSignal { + tx: Option>, + waker: Option>, +} + +impl ChannelQuitSignal { + fn new(waker: Option>) -> (Self, oneshot::Receiver<()>) { + let (tx, rx) = oneshot::channel::<()>(); + + let quit_signal = ChannelQuitSignal { + tx: Some(tx), + waker, + }; + + (quit_signal, rx) + } +} + +impl QuitSignal for ChannelQuitSignal { + fn quit(&mut self) { + if let Some(tx) = self.tx.take() { + tx.send(()).log_err(); + if let Some(waker) = self.waker.as_ref() { + waker.wake().ok(); + } } } } @@ -156,27 +194,12 @@ pub(crate) struct X11Client(Rc>); impl X11Client { pub(crate) fn new() -> Self { - let event_loop = EventLoop::try_new().unwrap(); + let mut poll = mio::Poll::new().unwrap(); - let (common, main_receiver) = LinuxCommon::new(event_loop.get_signal()); + let waker = Arc::new(Waker::new(poll.registry(), WAKER_TOKEN).unwrap()); - let handle = event_loop.handle(); - - handle - .insert_source(main_receiver, { - let handle = handle.clone(); - move |event, _, _: &mut X11Client| { - if let calloop::channel::Event::Msg(runnable) = event { - // Insert the runnables as idle callbacks, so we make sure that user-input and X11 - // events have higher priority and runnables are only worked off after the event - // callbacks. - handle.insert_idle(|_| { - runnable.run(); - }); - } - } - }) - .unwrap(); + let (quit_signal, quit_signal_rx) = ChannelQuitSignal::new(Some(waker.clone())); + let (common, runnables) = LinuxCommon::new(Box::new(quit_signal), Some(waker.clone())); let (xcb_connection, x_root_index) = XCBConnection::connect(None).unwrap(); xcb_connection @@ -275,105 +298,18 @@ impl X11Client { None }; - // Safety: Safe if xcb::Connection always returns a valid fd - let fd = unsafe { FdWrapper::new(Rc::clone(&xcb_connection)) }; - - handle - .insert_source( - Generic::new_with_error::( - fd, - calloop::Interest::READ, - calloop::Mode::Level, - ), - { - let xcb_connection = xcb_connection.clone(); - move |_readiness, _, client| { - let mut events = Vec::new(); - let mut windows_to_refresh = HashSet::new(); - - while let Some(event) = xcb_connection.poll_for_event()? { - if let Event::Expose(event) = event { - windows_to_refresh.insert(event.window); - } else { - events.push(event); - } - } - - for window in windows_to_refresh.into_iter() { - if let Some(window) = client.get_window(window) { - window.refresh(); - } - } - - for event in events.into_iter() { - let mut state = client.0.borrow_mut(); - if state.ximc.is_none() || state.xim_handler.is_none() { - drop(state); - client.handle_event(event); - continue; - } - - let mut ximc = state.ximc.take().unwrap(); - let mut xim_handler = state.xim_handler.take().unwrap(); - let xim_connected = xim_handler.connected; - drop(state); - - let xim_filtered = match ximc.filter_event(&event, &mut xim_handler) { - Ok(handled) => handled, - Err(err) => { - log::error!("XIMClientError: {}", err); - false - } - }; - let xim_callback_event = xim_handler.last_callback_event.take(); - - let mut state = client.0.borrow_mut(); - state.ximc = Some(ximc); - state.xim_handler = Some(xim_handler); - drop(state); - - if let Some(event) = xim_callback_event { - client.handle_xim_callback_event(event); - } - - if xim_filtered { - continue; - } - - if xim_connected { - client.xim_handle_event(event); - } else { - client.handle_event(event); - } - } - - Ok(calloop::PostAction::Continue) - } - }, - ) - .expect("Failed to initialize x11 event source"); - - handle - .insert_source(XDPEventSource::new(&common.background_executor), { - move |event, _, client| match event { - XDPEvent::WindowAppearance(appearance) => { - client.with_common(|common| common.appearance = appearance); - for (_, window) in &mut client.0.borrow_mut().windows { - window.window.set_appearance(appearance); - } - } - XDPEvent::CursorTheme(_) | XDPEvent::CursorSize(_) => { - // noop, X11 manages this for us. - } - } - }) - .unwrap(); + let xdp_event_source = + XDPEventSource::new(&common.background_executor, Some(waker.clone())); X11Client(Rc::new(RefCell::new(X11ClientState { - modifiers: Modifiers::default(), - event_loop: Some(event_loop), - loop_handle: handle, + poll: Some(poll), + runnables, + + xdp_event_source, + quit_signal_rx, common, + + modifiers: Modifiers::default(), last_click: Instant::now(), last_location: Point::new(px(0.0), px(0.0)), current_count: 0, @@ -468,6 +404,110 @@ impl X11Client { .map(|window_reference| window_reference.window.clone()) } + fn read_x11_events(&self) -> (HashSet, Vec) { + let mut events = Vec::new(); + let mut windows_to_refresh = HashSet::new(); + let mut state = self.0.borrow_mut(); + + let mut last_key_release: Option = None; + + loop { + match state.xcb_connection.poll_for_event() { + Ok(Some(event)) => { + if let Event::Expose(expose_event) = event { + windows_to_refresh.insert(expose_event.window); + } else { + match event { + Event::KeyRelease(_) => { + last_key_release = Some(event); + } + Event::KeyPress(key_press) => { + if let Some(Event::KeyRelease(key_release)) = + last_key_release.take() + { + // We ignore that last KeyRelease if it's too close to this KeyPress, + // suggesting that it's auto-generated by X11 as a key-repeat event. + if key_release.detail != key_press.detail + || key_press.time.wrapping_sub(key_release.time) > 20 + { + events.push(Event::KeyRelease(key_release)); + } + } + events.push(Event::KeyPress(key_press)); + } + _ => { + if let Some(release_event) = last_key_release.take() { + events.push(release_event); + } + events.push(event); + } + } + } + } + Ok(None) => { + // Add any remaining stored KeyRelease event + if let Some(release_event) = last_key_release.take() { + events.push(release_event); + } + break; + } + Err(e) => { + log::warn!("error polling for X11 events: {e:?}"); + break; + } + } + } + + (windows_to_refresh, events) + } + + fn process_x11_events(&self, events: Vec) { + for event in events.into_iter() { + let mut state = self.0.borrow_mut(); + if state.ximc.is_none() || state.xim_handler.is_none() { + drop(state); + self.handle_event(event); + continue; + } + + let mut ximc = state.ximc.take().unwrap(); + let mut xim_handler = state.xim_handler.take().unwrap(); + let xim_connected = xim_handler.connected; + drop(state); + + // let xim_filtered = false; + let xim_filtered = match ximc.filter_event(&event, &mut xim_handler) { + Ok(handled) => handled, + Err(err) => { + log::error!("XIMClientError: {}", err); + false + } + }; + let xim_callback_event = xim_handler.last_callback_event.take(); + + let mut state = self.0.borrow_mut(); + state.ximc = Some(ximc); + state.xim_handler = Some(xim_handler); + + if let Some(event) = xim_callback_event { + drop(state); + self.handle_xim_callback_event(event); + } else { + drop(state); + } + + if xim_filtered { + continue; + } + + if xim_connected { + self.xim_handle_event(event); + } else { + self.handle_event(event); + } + } + } + fn handle_event(&self, event: Event) -> Option<()> { match event { Event::ClientMessage(event) => { @@ -902,11 +942,13 @@ impl X11Client { } } +const XCB_CONNECTION_TOKEN: Token = Token(0); +const WAKER_TOKEN: Token = Token(1); + impl LinuxClient for X11Client { fn compositor_name(&self) -> &'static str { "X11" } - fn with_common(&self, f: impl FnOnce(&mut LinuxCommon) -> R) -> R { f(&mut self.0.borrow_mut().common) } @@ -972,69 +1014,8 @@ impl LinuxClient for X11Client { state.common.appearance, )?; - let screen_resources = state - .xcb_connection - .randr_get_screen_resources(x_window) - .unwrap() - .reply() - .expect("Could not find available screens"); - - let mode = screen_resources - .crtcs - .iter() - .find_map(|crtc| { - let crtc_info = state - .xcb_connection - .randr_get_crtc_info(*crtc, x11rb::CURRENT_TIME) - .ok()? - .reply() - .ok()?; - - screen_resources - .modes - .iter() - .find(|m| m.id == crtc_info.mode) - }) - .expect("Unable to find screen refresh rate"); - - let refresh_event_token = state - .loop_handle - .insert_source(calloop::timer::Timer::immediate(), { - let refresh_duration = mode_refresh_rate(mode); - move |mut instant, (), client| { - let state = client.0.borrow_mut(); - state - .xcb_connection - .send_event( - false, - x_window, - xproto::EventMask::EXPOSURE, - xproto::ExposeEvent { - response_type: xproto::EXPOSE_EVENT, - sequence: 0, - window: x_window, - x: 0, - y: 0, - width: 0, - height: 0, - count: 1, - }, - ) - .unwrap(); - let _ = state.xcb_connection.flush().unwrap(); - // Take into account that some frames have been skipped - let now = Instant::now(); - while instant < now { - instant += refresh_duration; - } - calloop::timer::TimeoutAction::ToInstant(instant) - } - }) - .expect("Failed to initialize refresh timer"); - let window_ref = WindowRef { window: window.0.clone(), - refresh_event_token, }; state.windows.insert(x_window, window_ref); @@ -1157,14 +1138,123 @@ impl LinuxClient for X11Client { } fn run(&self) { - let mut event_loop = self + let mut poll = self .0 .borrow_mut() - .event_loop + .poll .take() - .expect("App is already running"); + .context("no poll set on X11Client. calling run more than once is not possible") + .unwrap(); - event_loop.run(None, &mut self.clone(), |_| {}).log_err(); + let xcb_fd = self.0.borrow().xcb_connection.as_raw_fd(); + let mut xcb_source = mio::unix::SourceFd(&xcb_fd); + poll.registry() + .register(&mut xcb_source, XCB_CONNECTION_TOKEN, Interest::READABLE) + .unwrap(); + + let mut events = mio::Events::with_capacity(1024); + let mut next_refresh_needed = Instant::now(); + + 'run_loop: loop { + let poll_timeout = next_refresh_needed - Instant::now(); + // We rounding the poll_timeout down so `mio` doesn't round it up to the next higher milliseconds + let poll_timeout = Duration::from_millis(poll_timeout.as_millis() as u64); + + if poll_timeout >= Duration::from_millis(1) { + let _ = poll.poll(&mut events, Some(poll_timeout)); + }; + + let mut state = self.0.borrow_mut(); + + // Check if we need to quit + if let Ok(Some(())) = state.quit_signal_rx.try_recv() { + return; + } + + // Redraw windows + let now = Instant::now(); + if now > next_refresh_needed { + // This will be pulled down to 16ms (or less) if a window is open + let mut frame_length = Duration::from_millis(100); + + let mut windows = vec![]; + for (_, window_ref) in state.windows.iter() { + if !window_ref.window.state.borrow().destroyed { + frame_length = frame_length.min(window_ref.window.refresh_rate()); + windows.push(window_ref.window.clone()); + } + } + + drop(state); + + for window in windows { + window.refresh(); + } + + state = self.0.borrow_mut(); + + // In the case that we're looping a bit too fast, slow down + next_refresh_needed = now.max(next_refresh_needed) + frame_length; + } + + // X11 events + drop(state); + + loop { + let (x_windows, events) = self.read_x11_events(); + for x_window in x_windows { + if let Some(window) = self.get_window(x_window) { + window.refresh(); + } + } + + if events.len() == 0 { + break; + } + self.process_x11_events(events); + + // When X11 is sending us events faster than we can handle we'll + // let the frame rate drop to 10fps to try and avoid getting too behind. + if Instant::now() > next_refresh_needed + Duration::from_millis(80) { + continue 'run_loop; + } + } + + state = self.0.borrow_mut(); + + // Runnables + while let Ok(runnable) = state.runnables.try_recv() { + drop(state); + runnable.run(); + state = self.0.borrow_mut(); + + if Instant::now() + Duration::from_millis(1) >= next_refresh_needed { + continue 'run_loop; + } + } + + // XDG events + if let Ok(event) = state.xdp_event_source.try_recv() { + match event { + XDPEvent::WindowAppearance(appearance) => { + let mut windows = state + .windows + .values() + .map(|window| window.window.clone()) + .collect::>(); + drop(state); + + self.with_common(|common| common.appearance = appearance); + for mut window in windows { + window.set_appearance(appearance); + } + } + XDPEvent::CursorTheme(_) | XDPEvent::CursorSize(_) => { + // noop, X11 manages this for us. + } + }; + }; + } } fn active_window(&self) -> Option { @@ -1178,19 +1268,6 @@ impl LinuxClient for X11Client { } } -// Adatpted from: -// https://docs.rs/winit/0.29.11/src/winit/platform_impl/linux/x11/monitor.rs.html#103-111 -pub fn mode_refresh_rate(mode: &randr::ModeInfo) -> Duration { - if mode.dot_clock == 0 || mode.htotal == 0 || mode.vtotal == 0 { - return Duration::from_millis(16); - } - - let millihertz = mode.dot_clock as u64 * 1_000 / (mode.htotal as u64 * mode.vtotal as u64); - let micros = 1_000_000_000 / millihertz; - log::info!("Refreshing at {} micros", micros); - Duration::from_micros(micros) -} - fn fp3232_to_f32(value: xinput::Fp3232) -> f32 { value.integral as f32 + value.frac as f32 / u32::MAX as f32 } diff --git a/crates/gpui/src/platform/linux/x11/window.rs b/crates/gpui/src/platform/linux/x11/window.rs index d77be6f34c..b15de3df73 100644 --- a/crates/gpui/src/platform/linux/x11/window.rs +++ b/crates/gpui/src/platform/linux/x11/window.rs @@ -14,6 +14,7 @@ use util::{maybe, ResultExt}; use x11rb::{ connection::Connection, protocol::{ + randr::{self, ConnectionExt as _}, xinput::{self, ConnectionExt as _}, xproto::{ self, ClientMessageEvent, ConnectionExt as _, EventMask, TranslateCoordinatesReply, @@ -31,6 +32,7 @@ use std::{ ptr::NonNull, rc::Rc, sync::{self, Arc}, + time::Duration, }; use super::{X11Display, XINPUT_MASTER_DEVICE}; @@ -159,6 +161,7 @@ pub struct Callbacks { pub struct X11WindowState { pub destroyed: bool, + refresh_rate: Duration, client: X11ClientStatePtr, executor: ForegroundExecutor, atoms: XcbAtoms, @@ -178,7 +181,7 @@ pub(crate) struct X11WindowStatePtr { pub state: Rc>, pub(crate) callbacks: Rc>, xcb_connection: Rc, - x_window: xproto::Window, + pub x_window: xproto::Window, } impl rwh::HasWindowHandle for RawWindow { @@ -397,6 +400,31 @@ impl X11WindowState { }; xcb_connection.map_window(x_window).unwrap(); + let screen_resources = xcb_connection + .randr_get_screen_resources(x_window) + .unwrap() + .reply() + .expect("Could not find available screens"); + + let mode = screen_resources + .crtcs + .iter() + .find_map(|crtc| { + let crtc_info = xcb_connection + .randr_get_crtc_info(*crtc, x11rb::CURRENT_TIME) + .ok()? + .reply() + .ok()?; + + screen_resources + .modes + .iter() + .find(|m| m.id == crtc_info.mode) + }) + .expect("Unable to find screen refresh rate"); + + let refresh_rate = mode_refresh_rate(&mode); + Ok(Self { client, executor, @@ -413,6 +441,7 @@ impl X11WindowState { appearance, handle, destroyed: false, + refresh_rate, }) } @@ -715,6 +744,10 @@ impl X11WindowStatePtr { (fun)() } } + + pub fn refresh_rate(&self) -> Duration { + self.state.borrow().refresh_rate + } } impl PlatformWindow for X11Window { @@ -1039,3 +1072,16 @@ impl PlatformWindow for X11Window { false } } + +// Adapted from: +// https://docs.rs/winit/0.29.11/src/winit/platform_impl/linux/x11/monitor.rs.html#103-111 +pub fn mode_refresh_rate(mode: &randr::ModeInfo) -> Duration { + if mode.dot_clock == 0 || mode.htotal == 0 || mode.vtotal == 0 { + return Duration::from_millis(16); + } + + let millihertz = mode.dot_clock as u64 * 1_000 / (mode.htotal as u64 * mode.vtotal as u64); + let micros = 1_000_000_000 / millihertz; + log::info!("Refreshing at {} micros", micros); + Duration::from_micros(micros) +} diff --git a/crates/gpui/src/platform/linux/xdg_desktop_portal.rs b/crates/gpui/src/platform/linux/xdg_desktop_portal.rs index b36e482639..4252f76583 100644 --- a/crates/gpui/src/platform/linux/xdg_desktop_portal.rs +++ b/crates/gpui/src/platform/linux/xdg_desktop_portal.rs @@ -2,9 +2,13 @@ //! //! This module uses the [ashpd] crate +use std::sync::Arc; + +use anyhow::anyhow; use ashpd::desktop::settings::{ColorScheme, Settings}; -use calloop::channel::Channel; +use calloop::channel::{Channel, Sender}; use calloop::{EventSource, Poll, PostAction, Readiness, Token, TokenFactory}; +use mio::Waker; use smol::stream::StreamExt; use crate::{BackgroundExecutor, WindowAppearance}; @@ -20,31 +24,45 @@ pub struct XDPEventSource { } impl XDPEventSource { - pub fn new(executor: &BackgroundExecutor) -> Self { + pub fn new(executor: &BackgroundExecutor, waker: Option>) -> Self { let (sender, channel) = calloop::channel::channel(); let background = executor.clone(); executor .spawn(async move { + fn send_event( + sender: &Sender, + waker: &Option>, + event: T, + ) -> Result<(), std::sync::mpsc::SendError> { + sender.send(event)?; + if let Some(waker) = waker { + waker.wake().ok(); + }; + Ok(()) + } + let settings = Settings::new().await?; if let Ok(initial_appearance) = settings.color_scheme().await { - sender.send(Event::WindowAppearance(WindowAppearance::from_native( - initial_appearance, - )))?; + send_event( + &sender, + &waker, + Event::WindowAppearance(WindowAppearance::from_native(initial_appearance)), + )?; } if let Ok(initial_theme) = settings .read::("org.gnome.desktop.interface", "cursor-theme") .await { - sender.send(Event::CursorTheme(initial_theme))?; + send_event(&sender, &waker, Event::CursorTheme(initial_theme))?; } if let Ok(initial_size) = settings .read::("org.gnome.desktop.interface", "cursor-size") .await { - sender.send(Event::CursorSize(initial_size))?; + send_event(&sender, &waker, Event::CursorSize(initial_size))?; } if let Ok(mut cursor_theme_changed) = settings @@ -55,11 +73,12 @@ impl XDPEventSource { .await { let sender = sender.clone(); + let waker = waker.clone(); background .spawn(async move { while let Some(theme) = cursor_theme_changed.next().await { let theme = theme?; - sender.send(Event::CursorTheme(theme))?; + send_event(&sender, &waker, Event::CursorTheme(theme))?; } anyhow::Ok(()) }) @@ -74,11 +93,12 @@ impl XDPEventSource { .await { let sender = sender.clone(); + let waker = waker.clone(); background .spawn(async move { while let Some(size) = cursor_size_changed.next().await { let size = size?; - sender.send(Event::CursorSize(size))?; + send_event(&sender, &waker, Event::CursorSize(size))?; } anyhow::Ok(()) }) @@ -87,9 +107,11 @@ impl XDPEventSource { let mut appearance_changed = settings.receive_color_scheme_changed().await?; while let Some(scheme) = appearance_changed.next().await { - sender.send(Event::WindowAppearance(WindowAppearance::from_native( - scheme, - )))?; + send_event( + &sender, + &waker, + Event::WindowAppearance(WindowAppearance::from_native(scheme)), + )?; } anyhow::Ok(()) @@ -98,6 +120,12 @@ impl XDPEventSource { Self { channel } } + + pub fn try_recv(&self) -> anyhow::Result { + self.channel + .try_recv() + .map_err(|error| anyhow!("{}", error)) + } } impl EventSource for XDPEventSource {