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

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
This commit is contained in:
citorva 2024-05-27 02:00:10 +02:00 committed by GitHub
parent b1cfd46d37
commit a84344a82e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 224 additions and 14 deletions

View File

@ -6,6 +6,7 @@ mod headless;
mod platform;
mod wayland;
mod x11;
mod xdg_desktop_portal;
pub(crate) use dispatcher::*;
pub(crate) use headless::*;

View File

@ -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<()>> {

View File

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

View File

@ -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) {

View File

@ -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

View File

@ -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> {

View File

@ -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)
})
}