diff --git a/crates/gpui/src/platform/linux.rs b/crates/gpui/src/platform/linux.rs index 1628e22f37..3f6804b4a3 100644 --- a/crates/gpui/src/platform/linux.rs +++ b/crates/gpui/src/platform/linux.rs @@ -6,6 +6,7 @@ mod headless; mod platform; mod wayland; mod x11; +mod xdg_desktop_portal; pub(crate) use dispatcher::*; pub(crate) use headless::*; diff --git a/crates/gpui/src/platform/linux/platform.rs b/crates/gpui/src/platform/linux/platform.rs index 7850a1fd9d..71c069a9f4 100644 --- a/crates/gpui/src/platform/linux/platform.rs +++ b/crates/gpui/src/platform/linux/platform.rs @@ -8,6 +8,7 @@ use std::io::Read; use std::ops::{Deref, DerefMut}; use std::os::fd::{AsRawFd, FromRawFd}; use std::panic::Location; +use std::rc::Weak; use std::{ path::{Path, PathBuf}, process::Command, @@ -27,11 +28,13 @@ use flume::{Receiver, Sender}; use futures::channel::oneshot; use parking_lot::Mutex; use time::UtcOffset; +use util::ResultExt; use wayland_client::Connection; use wayland_protocols::wp::cursor_shape::v1::client::wp_cursor_shape_device_v1::Shape; use xkbcommon::xkb::{self, Keycode, Keysym, State}; use crate::platform::linux::wayland::WaylandClient; +use crate::platform::linux::xdg_desktop_portal::window_appearance; use crate::{ px, Action, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CosmicTextSystem, CursorStyle, DisplayId, ForegroundExecutor, Keymap, Keystroke, LinuxDispatcher, Menu, MenuItem, Modifiers, @@ -86,6 +89,7 @@ pub(crate) struct LinuxCommon { pub(crate) background_executor: BackgroundExecutor, pub(crate) foreground_executor: ForegroundExecutor, pub(crate) text_system: Arc, + pub(crate) appearance: WindowAppearance, pub(crate) callbacks: PlatformHandlers, pub(crate) signal: LoopSignal, } @@ -96,12 +100,18 @@ impl LinuxCommon { let text_system = Arc::new(CosmicTextSystem::new()); let callbacks = PlatformHandlers::default(); - let dispatcher = Arc::new(LinuxDispatcher::new(main_sender)); + let dispatcher = Arc::new(LinuxDispatcher::new(main_sender.clone())); + + let background_executor = BackgroundExecutor::new(dispatcher.clone()); + let appearance = window_appearance(&background_executor) + .log_err() + .unwrap_or(WindowAppearance::Light); let common = LinuxCommon { - background_executor: BackgroundExecutor::new(dispatcher.clone()), + background_executor, foreground_executor: ForegroundExecutor::new(dispatcher.clone()), text_system, + appearance, callbacks, signal, }; @@ -462,8 +472,8 @@ impl Platform for P { }) } - fn window_appearance(&self) -> crate::WindowAppearance { - crate::WindowAppearance::Light + fn window_appearance(&self) -> WindowAppearance { + self.with_common(|common| common.appearance) } fn register_url_scheme(&self, _: &str) -> Task> { diff --git a/crates/gpui/src/platform/linux/wayland/client.rs b/crates/gpui/src/platform/linux/wayland/client.rs index aa2fb1eebd..efe96e552b 100644 --- a/crates/gpui/src/platform/linux/wayland/client.rs +++ b/crates/gpui/src/platform/linux/wayland/client.rs @@ -1,6 +1,7 @@ use core::hash; use std::cell::{RefCell, RefMut}; use std::ffi::OsString; +use std::ops::{Deref, DerefMut}; use std::os::fd::{AsRawFd, BorrowedFd}; use std::path::PathBuf; use std::rc::{Rc, Weak}; @@ -15,6 +16,7 @@ use collections::HashMap; use copypasta::wayland_clipboard::{create_clipboards_from_external, Clipboard, Primary}; use copypasta::ClipboardProvider; use filedescriptor::Pipe; +use parking_lot::Mutex; use smallvec::SmallVec; use util::ResultExt; use wayland_backend::client::ObjectId; @@ -65,9 +67,12 @@ use crate::platform::linux::is_within_click_distance; use crate::platform::linux::wayland::cursor::Cursor; use crate::platform::linux::wayland::serial::{SerialKind, SerialTracker}; use crate::platform::linux::wayland::window::WaylandWindow; +use crate::platform::linux::xdg_desktop_portal::{Event as XDPEvent, XDPEventSource}; use crate::platform::linux::LinuxClient; use crate::platform::PlatformWindow; -use crate::{point, px, FileDropEvent, ForegroundExecutor, MouseExitEvent, SCROLL_LINES}; +use crate::{ + point, px, FileDropEvent, ForegroundExecutor, MouseExitEvent, WindowAppearance, SCROLL_LINES, +}; use crate::{ AnyWindowHandle, CursorStyle, DisplayId, KeyDownEvent, KeyUpEvent, Keystroke, Modifiers, ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, @@ -342,6 +347,22 @@ impl WaylandClient { let cursor = Cursor::new(&conn, &globals, 24); + handle.insert_source(XDPEventSource::new(&common.background_executor), { + move |event, _, client| match event { + XDPEvent::WindowAppearance(appearance) => { + if let Some(client) = client.0.upgrade() { + let mut client = client.borrow_mut(); + + client.common.appearance = appearance; + + for (_, window) in &mut client.windows { + window.set_appearance(appearance); + } + } + } + } + }); + let mut state = Rc::new(RefCell::new(WaylandClientState { serial_tracker: SerialTracker::new(), globals, @@ -430,6 +451,7 @@ impl LinuxClient for WaylandClient { state.globals.clone(), WaylandClientStatePtr(Rc::downgrade(&self.0)), params, + state.common.appearance, ); state.windows.insert(surface_id, window.0.clone()); diff --git a/crates/gpui/src/platform/linux/wayland/window.rs b/crates/gpui/src/platform/linux/wayland/window.rs index 3593bd537a..cf6a016e8f 100644 --- a/crates/gpui/src/platform/linux/wayland/window.rs +++ b/crates/gpui/src/platform/linux/wayland/window.rs @@ -2,7 +2,7 @@ use std::any::Any; use std::cell::{Ref, RefCell, RefMut}; use std::ffi::c_void; use std::num::NonZeroU32; -use std::ops::Range; +use std::ops::{Deref, Range}; use std::ptr::NonNull; use std::rc::{Rc, Weak}; use std::sync::Arc; @@ -10,6 +10,7 @@ use std::sync::Arc; use blade_graphics as gpu; use collections::{HashMap, HashSet}; use futures::channel::oneshot::Receiver; +use parking_lot::Mutex; use raw_window_handle as rwh; use wayland_backend::client::ObjectId; use wayland_client::protocol::wl_region::WlRegion; @@ -70,6 +71,7 @@ pub struct WaylandWindowState { acknowledged_first_configure: bool, pub surface: wl_surface::WlSurface, decoration: Option, + appearance: WindowAppearance, blur: Option, toplevel: xdg_toplevel::XdgToplevel, viewport: Option, @@ -100,6 +102,7 @@ impl WaylandWindowState { xdg_surface: xdg_surface::XdgSurface, toplevel: xdg_toplevel::XdgToplevel, decoration: Option, + appearance: WindowAppearance, viewport: Option, client: WaylandClientStatePtr, globals: Globals, @@ -158,6 +161,7 @@ impl WaylandWindowState { maximized: false, callbacks: Callbacks::default(), client, + appearance, } } } @@ -215,6 +219,7 @@ impl WaylandWindow { globals: Globals, client: WaylandClientStatePtr, params: WindowParams, + appearance: WindowAppearance, ) -> (Self, ObjectId) { let surface = globals.compositor.create_surface(&globals.qh, ()); let xdg_surface = globals @@ -251,6 +256,7 @@ impl WaylandWindow { xdg_surface, toplevel, decoration, + appearance, viewport, client, globals, @@ -571,6 +577,15 @@ impl WaylandWindowStatePtr { fun(focus); } } + + pub fn set_appearance(&mut self, appearance: WindowAppearance) { + self.state.borrow_mut().appearance = appearance; + + let mut callbacks = self.callbacks.borrow_mut(); + if let Some(ref mut fun) = callbacks.appearance_changed { + (fun)() + } + } } impl rwh::HasWindowHandle for WaylandWindow { @@ -618,9 +633,8 @@ impl PlatformWindow for WaylandWindow { self.borrow().scale } - // todo(linux) fn appearance(&self) -> WindowAppearance { - WindowAppearance::Light + self.borrow().appearance } // todo(linux) @@ -777,7 +791,7 @@ impl PlatformWindow for WaylandWindow { } fn on_appearance_changed(&self, callback: Box) { - // todo(linux) + self.0.callbacks.borrow_mut().appearance_changed = Some(callback); } fn draw(&self, scene: &Scene) { diff --git a/crates/gpui/src/platform/linux/x11/client.rs b/crates/gpui/src/platform/linux/x11/client.rs index ec803a13df..7b335bba3e 100644 --- a/crates/gpui/src/platform/linux/x11/client.rs +++ b/crates/gpui/src/platform/linux/x11/client.rs @@ -2,6 +2,7 @@ use std::cell::RefCell; use std::ffi::OsString; use std::ops::Deref; use std::rc::{Rc, Weak}; +use std::sync::OnceLock; use std::time::{Duration, Instant}; use calloop::generic::{FdWrapper, Generic}; @@ -10,6 +11,7 @@ use calloop::{channel, EventLoop, LoopHandle, RegistrationToken}; use collections::HashMap; use copypasta::x11_clipboard::{Clipboard, Primary, X11ClipboardContext}; use copypasta::ClipboardProvider; +use parking_lot::Mutex; use util::ResultExt; use x11rb::connection::{Connection, RequestConnection}; @@ -27,11 +29,11 @@ use xkbc::x11::ffi::{XKB_X11_MIN_MAJOR_XKB_VERSION, XKB_X11_MIN_MINOR_XKB_VERSIO use xkbcommon::xkb as xkbc; use crate::platform::linux::LinuxClient; -use crate::platform::{LinuxCommon, PlatformWindow}; +use crate::platform::{LinuxCommon, PlatformWindow, WaylandClientState}; use crate::{ modifiers_from_xinput_info, point, px, AnyWindowHandle, Bounds, CursorStyle, DisplayId, - Keystroke, Modifiers, ModifiersChangedEvent, Pixels, PlatformDisplay, PlatformInput, Point, - ScrollDelta, Size, TouchPhase, WindowParams, X11Window, + ForegroundExecutor, Keystroke, Modifiers, ModifiersChangedEvent, Pixels, PlatformDisplay, + PlatformInput, Point, ScrollDelta, Size, TouchPhase, WindowAppearance, WindowParams, X11Window, }; use super::{ @@ -42,6 +44,7 @@ use super::{button_of_key, modifiers_from_state, pressed_button_from_mask}; use super::{XimCallbackEvent, XimHandler}; use crate::platform::linux::is_within_click_distance; use crate::platform::linux::platform::DOUBLE_CLICK_INTERVAL; +use crate::platform::linux::xdg_desktop_portal::{Event as XDPEvent, XDPEventSource}; pub(super) const XINPUT_MASTER_DEVICE: u16 = 1; @@ -358,6 +361,17 @@ impl X11Client { } }) .expect("Failed to initialize XIM 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); + } + } + } + }); + X11Client(Rc::new(RefCell::new(X11ClientState { event_loop: Some(event_loop), loop_handle: handle, @@ -824,6 +838,7 @@ impl LinuxClient for X11Client { x_window, &state.atoms, state.scale_factor, + state.common.appearance, ); let screen_resources = state diff --git a/crates/gpui/src/platform/linux/x11/window.rs b/crates/gpui/src/platform/linux/x11/window.rs index 9eb63f4377..268b3b7980 100644 --- a/crates/gpui/src/platform/linux/x11/window.rs +++ b/crates/gpui/src/platform/linux/x11/window.rs @@ -28,6 +28,8 @@ use x11rb::{ xcb_ffi::XCBConnection, }; +use std::ops::Deref; +use std::rc::Weak; use std::{ cell::{Ref, RefCell, RefMut}, collections::HashMap, @@ -174,6 +176,7 @@ pub(crate) struct X11WindowState { renderer: BladeRenderer, display: Rc, input_handler: Option, + appearance: WindowAppearance, } #[derive(Clone)] @@ -223,6 +226,7 @@ impl X11WindowState { x_window: xproto::Window, atoms: &XcbAtoms, scale_factor: f32, + appearance: WindowAppearance, ) -> Self { let x_screen_index = params .display_id @@ -375,6 +379,7 @@ impl X11WindowState { renderer: BladeRenderer::new(gpu, config), atoms: *atoms, input_handler: None, + appearance, } } @@ -431,6 +436,7 @@ impl X11Window { x_window: xproto::Window, atoms: &XcbAtoms, scale_factor: f32, + appearance: WindowAppearance, ) -> Self { Self(X11WindowStatePtr { state: Rc::new(RefCell::new(X11WindowState::new( @@ -442,6 +448,7 @@ impl X11Window { x_window, atoms, scale_factor, + appearance, ))), callbacks: Rc::new(RefCell::new(Callbacks::default())), xcb_connection: xcb_connection.clone(), @@ -622,6 +629,15 @@ impl X11WindowStatePtr { fun(focus); } } + + pub fn set_appearance(&mut self, appearance: WindowAppearance) { + self.state.borrow_mut().appearance = appearance; + + let mut callbacks = self.callbacks.borrow_mut(); + if let Some(ref mut fun) = callbacks.appearance_changed { + (fun)() + } + } } impl PlatformWindow for X11Window { @@ -656,9 +672,8 @@ impl PlatformWindow for X11Window { self.0.state.borrow().scale_factor } - // todo(linux) fn appearance(&self) -> WindowAppearance { - WindowAppearance::Light + self.0.state.borrow().appearance } fn display(&self) -> Rc { diff --git a/crates/gpui/src/platform/linux/xdg_desktop_portal.rs b/crates/gpui/src/platform/linux/xdg_desktop_portal.rs new file mode 100644 index 0000000000..e23af679b5 --- /dev/null +++ b/crates/gpui/src/platform/linux/xdg_desktop_portal.rs @@ -0,0 +1,133 @@ +//! Provides a [calloop] event source from [XDG Desktop Portal] events +//! +//! This module uses the [ashpd] crate and handles many async loop +use std::future::Future; + +use ashpd::desktop::settings::{ColorScheme, Settings}; +use calloop::channel::{Channel, Sender}; +use calloop::{EventSource, Poll, PostAction, Readiness, Token, TokenFactory}; +use parking_lot::Mutex; +use smol::stream::StreamExt; +use util::ResultExt; + +use crate::{BackgroundExecutor, WindowAppearance}; + +pub enum Event { + WindowAppearance(WindowAppearance), +} + +pub struct XDPEventSource { + channel: Channel, +} + +impl XDPEventSource { + pub fn new(executor: &BackgroundExecutor) -> Self { + let (sender, channel) = calloop::channel::channel(); + + Self::spawn_observer(executor, Self::appearance_observer(sender.clone())); + + Self { channel } + } + + fn spawn_observer( + executor: &BackgroundExecutor, + to_spawn: impl Future> + Send + 'static, + ) { + executor + .spawn(async move { + to_spawn.await.log_err(); + }) + .detach() + } + + async fn appearance_observer(sender: Sender) -> Result<(), anyhow::Error> { + let settings = Settings::new().await?; + + // We observe the color change during the execution of the application + let mut stream = settings.receive_color_scheme_changed().await?; + while let Some(scheme) = stream.next().await { + sender.send(Event::WindowAppearance(WindowAppearance::from_native( + scheme, + )))?; + } + + Ok(()) + } +} + +impl EventSource for XDPEventSource { + type Event = Event; + type Metadata = (); + type Ret = (); + type Error = anyhow::Error; + + fn process_events( + &mut self, + readiness: Readiness, + token: Token, + mut callback: F, + ) -> Result + where + F: FnMut(Self::Event, &mut Self::Metadata) -> Self::Ret, + { + self.channel.process_events(readiness, token, |evt, _| { + if let calloop::channel::Event::Msg(msg) = evt { + (callback)(msg, &mut ()) + } + })?; + + Ok(PostAction::Continue) + } + + fn register( + &mut self, + poll: &mut Poll, + token_factory: &mut TokenFactory, + ) -> calloop::Result<()> { + self.channel.register(poll, token_factory)?; + + Ok(()) + } + + fn reregister( + &mut self, + poll: &mut Poll, + token_factory: &mut TokenFactory, + ) -> calloop::Result<()> { + self.channel.reregister(poll, token_factory)?; + + Ok(()) + } + + fn unregister(&mut self, poll: &mut Poll) -> calloop::Result<()> { + self.channel.unregister(poll)?; + + Ok(()) + } +} + +impl WindowAppearance { + fn from_native(cs: ColorScheme) -> WindowAppearance { + match cs { + ColorScheme::PreferDark => WindowAppearance::Dark, + ColorScheme::PreferLight => WindowAppearance::Light, + ColorScheme::NoPreference => WindowAppearance::Light, + } + } + + fn set_native(&mut self, cs: ColorScheme) { + *self = Self::from_native(cs); + } +} + +pub fn window_appearance(executor: &BackgroundExecutor) -> Result { + executor.block(async { + let settings = Settings::new().await?; + + let scheme = settings.color_scheme().await?; + + let appearance = WindowAppearance::from_native(scheme); + + Ok(appearance) + }) +}