From 9163de61b87f7d50d04fc46b4c983a8be91bb968 Mon Sep 17 00:00:00 2001 From: Kamil Jarosz Date: Mon, 29 Jul 2024 19:41:59 +0200 Subject: [PATCH] desktop: Add ThemeController ThemeController is responsible for managing Ruffle's theme. It takes into account the user preference, integrates with D-Bus and handles platform-specific differences. --- desktop/src/gui.rs | 1 + desktop/src/gui/controller.rs | 71 ++----------------- desktop/src/gui/theme.rs | 130 ++++++++++++++++++++++++++++++++++ 3 files changed, 137 insertions(+), 65 deletions(-) create mode 100644 desktop/src/gui/theme.rs diff --git a/desktop/src/gui.rs b/desktop/src/gui.rs index b7167e363..6e06a44d2 100644 --- a/desktop/src/gui.rs +++ b/desktop/src/gui.rs @@ -3,6 +3,7 @@ mod controller; mod dialogs; mod menu_bar; mod movie; +mod theme; mod widgets; pub use controller::GuiController; diff --git a/desktop/src/gui/controller.rs b/desktop/src/gui/controller.rs index 19bf086be..9ac9c7dc9 100644 --- a/desktop/src/gui/controller.rs +++ b/desktop/src/gui/controller.rs @@ -1,18 +1,17 @@ use crate::backends::DesktopUiBackend; use crate::custom_event::RuffleEvent; use crate::gui::movie::{MovieView, MovieViewRenderer}; +use crate::gui::theme::ThemeController; use crate::gui::{RuffleGui, MENU_HEIGHT}; use crate::player::{LaunchOptions, PlayerController}; use crate::preferences::GlobalPreferences; use anyhow::anyhow; use egui::{Context, ViewportId}; use fontdb::{Database, Family, Query, Source}; -use futures::StreamExt; use ruffle_core::{Player, PlayerEvent}; use ruffle_render_wgpu::backend::{request_adapter_and_device, WgpuRenderBackend}; use ruffle_render_wgpu::descriptors::Descriptors; use ruffle_render_wgpu::utils::{format_list, get_backend_names}; -use std::error::Error; use std::sync::{Arc, MutexGuard}; use std::time::{Duration, Instant}; use unic_langid::LanguageIdentifier; @@ -20,7 +19,7 @@ use url::Url; use wgpu::SurfaceError; use winit::dpi::PhysicalSize; use winit::event::WindowEvent; -use winit::event_loop::{EventLoop, EventLoopProxy}; +use winit::event_loop::EventLoop; use winit::keyboard::{Key, NamedKey}; use winit::window::{Theme, Window}; @@ -41,6 +40,7 @@ pub struct GuiController { size: PhysicalSize, /// If this is set, we should not render the main menu. no_gui: bool, + theme_controller: ThemeController, } impl GuiController { @@ -95,13 +95,7 @@ impl GuiController { let descriptors = Descriptors::new(instance, adapter, device, queue); let egui_ctx = Context::default(); - let theme = start_theme_watcher(event_loop.clone()) - .await - .or_else(|| window.theme()); - if let Some(Theme::Light) = theme { - egui_ctx.set_visuals(egui::Visuals::light()); - } - + let theme_controller = ThemeController::new(window.clone(), egui_ctx.clone()).await; let mut egui_winit = egui_winit::State::new(egui_ctx, ViewportId::ROOT, window.as_ref(), None, None); egui_winit.set_max_texture_side(descriptors.limits.max_texture_dimension_2d as usize); @@ -140,15 +134,12 @@ impl GuiController { movie_view_renderer, size, no_gui, + theme_controller, }) } pub fn set_theme(&self, theme: Theme) { - self.egui_winit.egui_ctx().set_visuals(match theme { - Theme::Light => egui::Visuals::light(), - Theme::Dark => egui::Visuals::dark(), - }); - self.window.request_redraw(); + self.theme_controller.set_theme(theme); } pub fn descriptors(&self) -> &Arc { @@ -515,53 +506,3 @@ fn load_system_fonts( Ok(fd) } - -#[cfg(target_os = "linux")] -async fn start_theme_watcher(event_loop: EventLoopProxy) -> Option { - start_dbus_theme_watcher_linux(event_loop) - .await - .inspect_err(|err| { - tracing::warn!("Error registering theme watcher: {}", err); - }) - .ok() -} - -#[cfg(not(target_os = "linux"))] -async fn start_theme_watcher(_event_loop: EventLoopProxy) -> Option { - None -} - -#[cfg(target_os = "linux")] -async fn start_dbus_theme_watcher_linux( - event_loop: EventLoopProxy, -) -> Result> { - use crate::dbus::{ColorScheme, FreedesktopSettings}; - - fn to_theme(color_scheme: ColorScheme) -> Theme { - match color_scheme { - ColorScheme::Default => Theme::Light, - ColorScheme::PreferLight => Theme::Light, - ColorScheme::PreferDark => Theme::Dark, - } - } - - let connection = zbus::Connection::session().await?; - let settings = FreedesktopSettings::new(&connection).await?; - let scheme = settings.color_scheme().await?; - - let mut stream = Box::pin(settings.watch_color_scheme().await?); - tokio::spawn(Box::pin(async move { - while let Some(scheme) = stream.next().await { - match scheme { - Ok(scheme) => { - let _ = event_loop.send_event(RuffleEvent::ThemeChanged(to_theme(scheme))); - } - Err(err) => { - tracing::warn!("Error while watching for color scheme changes: {}", err); - } - } - } - })); - - Ok(to_theme(scheme)) -} diff --git a/desktop/src/gui/theme.rs b/desktop/src/gui/theme.rs new file mode 100644 index 000000000..d6ad15a4c --- /dev/null +++ b/desktop/src/gui/theme.rs @@ -0,0 +1,130 @@ +#[cfg(target_os = "linux")] +use crate::dbus::{ColorScheme, FreedesktopSettings}; +use egui::Context; +use futures::StreamExt; +use std::error::Error; +use std::sync::{Arc, Weak}; +use tokio::sync::{Mutex, MutexGuard}; +use winit::window::{Theme, Window}; + +struct ThemeControllerData { + window: Weak, + egui_ctx: Context, + + #[cfg(target_os = "linux")] + zbus_connection: Option, +} + +#[derive(Clone)] +pub struct ThemeController(Arc>); + +impl ThemeController { + pub async fn new(window: Arc, egui_ctx: Context) -> Self { + let this = Self(Arc::new(Mutex::new(ThemeControllerData { + window: Arc::downgrade(&window), + egui_ctx, + #[cfg(target_os = "linux")] + zbus_connection: zbus::Connection::session() + .await + .inspect_err(|err| tracing::warn!("Failed to connect to D-Bus: {err}")) + .ok(), + }))); + + #[cfg(target_os = "linux")] + this.start_dbus_theme_watcher_linux().await; + + if let Ok(theme) = this.get_system_theme().await { + this.set_theme(theme); + } + + this + } + + #[cfg(target_os = "linux")] + async fn start_dbus_theme_watcher_linux(&self) { + async fn start_inner(this: &ThemeController) -> Result<(), Box> { + let Some(ref connection) = this.data().zbus_connection else { + return Ok(()); + }; + + let settings = FreedesktopSettings::new(connection).await?; + + let mut stream = Box::pin(settings.watch_color_scheme().await?); + + let this2 = this.clone(); + tokio::spawn(Box::pin(async move { + while let Some(scheme) = stream.next().await { + match scheme { + Ok(scheme) => { + this2.set_theme(scheme_to_theme(scheme)); + } + Err(err) => { + tracing::warn!( + "Error while watching for color scheme changes: {}", + err + ); + } + } + } + })); + + Ok(()) + } + + if let Err(err) = start_inner(self).await { + tracing::warn!("Error registering theme watcher: {}", err); + } + } + + fn data(&self) -> MutexGuard<'_, ThemeControllerData> { + self.0.try_lock().expect("Non-reentrant data mutex") + } + + pub fn set_theme(&self, theme: Theme) { + let data = self.data(); + self.set_theme_internal(data, theme); + } + + fn set_theme_internal(&self, data: MutexGuard<'_, ThemeControllerData>, theme: Theme) { + data.egui_ctx.set_visuals(match theme { + Theme::Light => egui::Visuals::light(), + Theme::Dark => egui::Visuals::dark(), + }); + if let Some(window) = data.window.upgrade() { + window.request_redraw(); + } + } + + #[cfg(target_os = "linux")] + async fn get_system_theme(&self) -> Result> { + let Some(ref connection) = self.data().zbus_connection else { + return Ok(Theme::Dark); + }; + let settings = FreedesktopSettings::new(connection).await?; + let scheme = settings.color_scheme().await?; + + Ok(scheme_to_theme(scheme)) + } + + #[cfg(not(target_os = "linux"))] + pub async fn get_system_theme(&self) -> Result> { + #[derive(thiserror::Error, Debug)] + #[error("Unsupported operation")] + struct UnsupportedOperationError; + self.data() + .window + .upgrade() + .and_then(|w| w.theme()) + .ok_or(Box::new(UnsupportedOperationError)) + } +} + +#[cfg(target_os = "linux")] +fn scheme_to_theme(color_scheme: ColorScheme) -> Theme { + use crate::dbus::ColorScheme; + match color_scheme { + ColorScheme::Default => Theme::Light, + ColorScheme::PreferLight => Theme::Light, + ColorScheme::PreferDark => Theme::Dark, + } +}