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:
parent
21d7746aec
commit
9163de61b8
|
@ -3,6 +3,7 @@ mod controller;
|
||||||
mod dialogs;
|
mod dialogs;
|
||||||
mod menu_bar;
|
mod menu_bar;
|
||||||
mod movie;
|
mod movie;
|
||||||
|
mod theme;
|
||||||
mod widgets;
|
mod widgets;
|
||||||
|
|
||||||
pub use controller::GuiController;
|
pub use controller::GuiController;
|
||||||
|
|
|
@ -1,18 +1,17 @@
|
||||||
use crate::backends::DesktopUiBackend;
|
use crate::backends::DesktopUiBackend;
|
||||||
use crate::custom_event::RuffleEvent;
|
use crate::custom_event::RuffleEvent;
|
||||||
use crate::gui::movie::{MovieView, MovieViewRenderer};
|
use crate::gui::movie::{MovieView, MovieViewRenderer};
|
||||||
|
use crate::gui::theme::ThemeController;
|
||||||
use crate::gui::{RuffleGui, MENU_HEIGHT};
|
use crate::gui::{RuffleGui, MENU_HEIGHT};
|
||||||
use crate::player::{LaunchOptions, PlayerController};
|
use crate::player::{LaunchOptions, PlayerController};
|
||||||
use crate::preferences::GlobalPreferences;
|
use crate::preferences::GlobalPreferences;
|
||||||
use anyhow::anyhow;
|
use anyhow::anyhow;
|
||||||
use egui::{Context, ViewportId};
|
use egui::{Context, ViewportId};
|
||||||
use fontdb::{Database, Family, Query, Source};
|
use fontdb::{Database, Family, Query, Source};
|
||||||
use futures::StreamExt;
|
|
||||||
use ruffle_core::{Player, PlayerEvent};
|
use ruffle_core::{Player, PlayerEvent};
|
||||||
use ruffle_render_wgpu::backend::{request_adapter_and_device, WgpuRenderBackend};
|
use ruffle_render_wgpu::backend::{request_adapter_and_device, WgpuRenderBackend};
|
||||||
use ruffle_render_wgpu::descriptors::Descriptors;
|
use ruffle_render_wgpu::descriptors::Descriptors;
|
||||||
use ruffle_render_wgpu::utils::{format_list, get_backend_names};
|
use ruffle_render_wgpu::utils::{format_list, get_backend_names};
|
||||||
use std::error::Error;
|
|
||||||
use std::sync::{Arc, MutexGuard};
|
use std::sync::{Arc, MutexGuard};
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
use unic_langid::LanguageIdentifier;
|
use unic_langid::LanguageIdentifier;
|
||||||
|
@ -20,7 +19,7 @@ use url::Url;
|
||||||
use wgpu::SurfaceError;
|
use wgpu::SurfaceError;
|
||||||
use winit::dpi::PhysicalSize;
|
use winit::dpi::PhysicalSize;
|
||||||
use winit::event::WindowEvent;
|
use winit::event::WindowEvent;
|
||||||
use winit::event_loop::{EventLoop, EventLoopProxy};
|
use winit::event_loop::EventLoop;
|
||||||
use winit::keyboard::{Key, NamedKey};
|
use winit::keyboard::{Key, NamedKey};
|
||||||
use winit::window::{Theme, Window};
|
use winit::window::{Theme, Window};
|
||||||
|
|
||||||
|
@ -41,6 +40,7 @@ pub struct GuiController {
|
||||||
size: PhysicalSize<u32>,
|
size: PhysicalSize<u32>,
|
||||||
/// If this is set, we should not render the main menu.
|
/// If this is set, we should not render the main menu.
|
||||||
no_gui: bool,
|
no_gui: bool,
|
||||||
|
theme_controller: ThemeController,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GuiController {
|
impl GuiController {
|
||||||
|
@ -95,13 +95,7 @@ impl GuiController {
|
||||||
let descriptors = Descriptors::new(instance, adapter, device, queue);
|
let descriptors = Descriptors::new(instance, adapter, device, queue);
|
||||||
let egui_ctx = Context::default();
|
let egui_ctx = Context::default();
|
||||||
|
|
||||||
let theme = start_theme_watcher(event_loop.clone())
|
let theme_controller = ThemeController::new(window.clone(), egui_ctx.clone()).await;
|
||||||
.await
|
|
||||||
.or_else(|| window.theme());
|
|
||||||
if let Some(Theme::Light) = theme {
|
|
||||||
egui_ctx.set_visuals(egui::Visuals::light());
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut egui_winit =
|
let mut egui_winit =
|
||||||
egui_winit::State::new(egui_ctx, ViewportId::ROOT, window.as_ref(), None, None);
|
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);
|
egui_winit.set_max_texture_side(descriptors.limits.max_texture_dimension_2d as usize);
|
||||||
|
@ -140,15 +134,12 @@ impl GuiController {
|
||||||
movie_view_renderer,
|
movie_view_renderer,
|
||||||
size,
|
size,
|
||||||
no_gui,
|
no_gui,
|
||||||
|
theme_controller,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_theme(&self, theme: Theme) {
|
pub fn set_theme(&self, theme: Theme) {
|
||||||
self.egui_winit.egui_ctx().set_visuals(match theme {
|
self.theme_controller.set_theme(theme);
|
||||||
Theme::Light => egui::Visuals::light(),
|
|
||||||
Theme::Dark => egui::Visuals::dark(),
|
|
||||||
});
|
|
||||||
self.window.request_redraw();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn descriptors(&self) -> &Arc<Descriptors> {
|
pub fn descriptors(&self) -> &Arc<Descriptors> {
|
||||||
|
@ -515,53 +506,3 @@ fn load_system_fonts(
|
||||||
|
|
||||||
Ok(fd)
|
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))
|
|
||||||
}
|
|
||||||
|
|
|
@ -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,
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue