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 menu_bar;
|
||||
mod movie;
|
||||
mod theme;
|
||||
mod widgets;
|
||||
|
||||
pub use controller::GuiController;
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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