linux: Get the color scheme through xdg-desktop-portal (#11926)

citorva created

The method has been tested on:
- Gnome 46 (Working)
- Gnome 40 (Not supported)

Tasks

- [x] Implements a draft which get and provides the user theme to
components which needs it
- [x] Implements a way to call the callback function when the theme is
updated
- [X] Cleans the code

Release notes:
- N/A

Change summary

crates/gpui/src/platform/linux.rs                    |   1 
crates/gpui/src/platform/linux/platform.rs           |  18 +
crates/gpui/src/platform/linux/wayland/client.rs     |  24 ++
crates/gpui/src/platform/linux/wayland/window.rs     |  22 +
crates/gpui/src/platform/linux/x11/client.rs         |  21 +
crates/gpui/src/platform/linux/x11/window.rs         |  19 +
crates/gpui/src/platform/linux/xdg_desktop_portal.rs | 133 ++++++++++++++
7 files changed, 224 insertions(+), 14 deletions(-)

Detailed changes

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

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<CosmicTextSystem>,
+    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<P: LinuxClient + 'static> 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<anyhow::Result<()>> {

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());
 

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<zxdg_toplevel_decoration_v1::ZxdgToplevelDecorationV1>,
+    appearance: WindowAppearance,
     blur: Option<org_kde_kwin_blur::OrgKdeKwinBlur>,
     toplevel: xdg_toplevel::XdgToplevel,
     viewport: Option<wp_viewport::WpViewport>,
@@ -100,6 +102,7 @@ impl WaylandWindowState {
         xdg_surface: xdg_surface::XdgSurface,
         toplevel: xdg_toplevel::XdgToplevel,
         decoration: Option<zxdg_toplevel_decoration_v1::ZxdgToplevelDecorationV1>,
+        appearance: WindowAppearance,
         viewport: Option<wp_viewport::WpViewport>,
         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<dyn FnMut()>) {
-        // todo(linux)
+        self.0.callbacks.borrow_mut().appearance_changed = Some(callback);
     }
 
     fn draw(&self, scene: &Scene) {

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

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<dyn PlatformDisplay>,
     input_handler: Option<PlatformInputHandler>,
+    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<dyn PlatformDisplay> {

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<Event>,
+}
+
+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<Output = Result<(), anyhow::Error>> + Send + 'static,
+    ) {
+        executor
+            .spawn(async move {
+                to_spawn.await.log_err();
+            })
+            .detach()
+    }
+
+    async fn appearance_observer(sender: Sender<Event>) -> 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<F>(
+        &mut self,
+        readiness: Readiness,
+        token: Token,
+        mut callback: F,
+    ) -> Result<PostAction, Self::Error>
+    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<WindowAppearance, anyhow::Error> {
+    executor.block(async {
+        let settings = Settings::new().await?;
+
+        let scheme = settings.color_scheme().await?;
+
+        let appearance = WindowAppearance::from_native(scheme);
+
+        Ok(appearance)
+    })
+}