use dioxus::prelude::*;
use std::{error::Error, fmt::Display};
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub enum Theme {
#[default]
Light,
Dark,
}
impl Display for Theme {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
Self::Light => write!(f, "light"),
Self::Dark => write!(f, "dark"),
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum ThemeError {
Unsupported,
CheckFailed,
UnknownTheme,
}
impl Error for ThemeError {}
impl Display for ThemeError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
Self::Unsupported => write!(f, "the current platform is not supported"),
Self::CheckFailed => write!(
f,
"the system returned an error while checking the color theme"
),
Self::UnknownTheme => write!(
f,
"the system provided a theme other than `light` or `dark`"
),
}
}
}
type ThemeResult = Result<Theme, ThemeError>;
pub fn use_system_theme() -> ReadOnlySignal<ThemeResult> {
let mut system_theme = match try_use_context::<Signal<ThemeResult>>() {
Some(s) => s,
None => {
let signal = Signal::new_in_scope(Err(ThemeError::Unsupported), ScopeId::ROOT);
provide_root_context(signal)
}
};
use_effect(move || {
system_theme.set(get_theme());
listen(system_theme);
});
use_hook(|| ReadOnlySignal::new(system_theme))
}
#[cfg(target_family = "wasm")]
fn listen(mut theme: Signal<ThemeResult>) {
use wasm_bindgen::{JsCast, closure::Closure};
use web_sys::MediaQueryList;
let Some(window) = web_sys::window() else {
theme.set(Err(ThemeError::Unsupported));
return;
};
let Ok(query) = window.match_media("(prefers-color-scheme: dark)") else {
theme.set(Err(ThemeError::CheckFailed));
return;
};
let Some(query) = query else {
theme.set(Err(ThemeError::UnknownTheme));
return;
};
let listener = Closure::wrap(Box::new(move |query: MediaQueryList| {
match query.matches() {
true => theme.set(Ok(Theme::Dark)),
false => theme.set(Ok(Theme::Light)),
};
}) as Box<dyn FnMut(MediaQueryList)>);
let cb = listener.as_ref().clone();
listener.forget();
query.set_onchange(Some(cb.unchecked_ref()));
}
#[cfg(not(target_family = "wasm"))]
fn listen(mut theme: Signal<ThemeResult>) {
use dioxus_desktop::{
WindowEvent,
tao::{event::Event, window::Theme as TaoTheme},
window,
};
let window = window();
window.create_wry_event_handler(move |event, _| {
if let Event::WindowEvent {
event: WindowEvent::ThemeChanged(new_theme),
..
} = event
{
match new_theme {
TaoTheme::Dark => theme.set(Ok(Theme::Dark)),
TaoTheme::Light => theme.set(Ok(Theme::Light)),
_ => theme.set(Err(ThemeError::UnknownTheme)),
};
}
});
}
#[cfg(target_os = "linux")]
fn listen(mut theme: Signal<ThemeResult>) {
theme.set(Err(ThemeError::Unsupported));
}
pub fn get_theme() -> ThemeResult {
get_theme_platform()
}
#[cfg(target_family = "wasm")]
fn get_theme_platform() -> ThemeResult {
let Some(window) = web_sys::window() else {
return Err(ThemeError::Unsupported);
};
let Some(query) = window
.match_media("(prefers-color-scheme: dark)")
.or(Err(ThemeError::CheckFailed))?
else {
return Err(ThemeError::UnknownTheme);
};
match query.matches() {
true => Ok(Theme::Dark),
false => Ok(Theme::Light),
}
}
#[cfg(not(target_family = "wasm"))]
fn get_theme_platform() -> ThemeResult {
use dioxus_desktop::DesktopContext;
use dioxus_desktop::tao::window::Theme as TaoTheme;
let Some(window) = try_consume_context::<DesktopContext>() else {
return Err(ThemeError::Unsupported);
};
let theme = window.theme();
match theme {
TaoTheme::Light => Ok(Theme::Light),
TaoTheme::Dark => Ok(Theme::Dark),
_ => Err(ThemeError::UnknownTheme),
}
}
#[cfg(not(any(target_family = "wasm", target_os = "windows", target_os = "macos")))]
fn get_theme_platform() -> ThemeResult {
Err(ThemeError::Unsupported)
}