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.
This commit is contained in:
Kamil Jarosz 2024-07-29 19:41:59 +02:00 committed by Nathan Adams
parent 21d7746aec
commit 9163de61b8
3 changed files with 137 additions and 65 deletions

View File

@ -3,6 +3,7 @@ mod controller;
mod dialogs;
mod menu_bar;
mod movie;
mod theme;
mod widgets;
pub use controller::GuiController;

View File

@ -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<u32>,
/// 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<Descriptors> {
@ -515,53 +506,3 @@ fn load_system_fonts(
Ok(fd)
}
#[cfg(target_os = "linux")]
async fn start_theme_watcher(event_loop: EventLoopProxy<RuffleEvent>) -> Option<Theme> {
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<RuffleEvent>) -> Option<Theme> {
None
}
#[cfg(target_os = "linux")]
async fn start_dbus_theme_watcher_linux(
event_loop: EventLoopProxy<RuffleEvent>,
) -> Result<Theme, Box<dyn Error>> {
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))
}

130
desktop/src/gui/theme.rs Normal file
View File

@ -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<Window>,
egui_ctx: Context,
#[cfg(target_os = "linux")]
zbus_connection: Option<zbus::Connection>,
}
#[derive(Clone)]
pub struct ThemeController(Arc<Mutex<ThemeControllerData>>);
impl ThemeController {
pub async fn new(window: Arc<Window>, 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<dyn Error>> {
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<Theme, Box<dyn Error>> {
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<Theme, Box<dyn Error>> {
#[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,
}
}