use crate::core::Color;
use crate::style::WidgetStyle;
#[cfg(device_profile)]
pub fn resolved_theme_style(widget_name: &str) -> Option<WidgetStyle> {
crate::theme::resolved_theme_style(widget_name)
}
#[cfg(not(device_profile))]
pub fn resolved_theme_style(_widget_name: &str) -> Option<WidgetStyle> {
None
}
#[cfg(device_profile)]
pub fn resolved_theme_style_for(kind_name: &str, class_name: Option<&str>) -> Option<WidgetStyle> {
crate::theme::resolved_theme_style_for(kind_name, class_name)
}
#[cfg(not(device_profile))]
pub fn resolved_theme_style_for(
_kind_name: &str,
_class_name: Option<&str>,
) -> Option<WidgetStyle> {
None
}
#[cfg(device_profile)]
pub fn theme_test_guard() -> crate::compat::MutexGuard<'static, ()> {
crate::theme::theme_test_guard()
}
#[cfg(not(device_profile))]
pub fn theme_test_guard() -> crate::compat::MutexGuard<'static, ()> {
static GUARD: crate::compat::LazyMutex = crate::compat::LazyMutex::new();
GUARD.lock()
}
#[cfg(device_profile)]
pub use crate::theme::{AppearanceMode, Colors, SemanticColor, Theme};
#[cfg(not(device_profile))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum SemanticColor {
Info,
Success,
Warning,
Error,
}
#[cfg(not(device_profile))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Hash)]
pub enum AppearanceMode {
#[default]
Light,
Dark,
}
#[cfg(not(device_profile))]
#[derive(Debug, Clone, Copy, Default)]
pub struct Theme {
pub colors: Colors,
}
#[cfg(not(device_profile))]
#[derive(Debug, Clone, Copy, Default)]
pub struct Colors {
pub background: Color,
pub foreground: Color,
pub primary: Color,
pub secondary: Color,
pub accent: Color,
pub error: Color,
pub warning: Color,
pub success: Color,
pub disabled: Color,
pub info: Color,
pub outline: Color,
pub outline_variant: Color,
pub scrim: Color,
pub surface_container: Color,
pub surface_container_high: Color,
pub inverse_surface: Color,
pub on_inverse_surface: Color,
}
#[cfg(device_profile)]
pub fn semantic_color(token: SemanticColor) -> Option<Color> {
crate::theme::semantic_color(token)
}
#[cfg(not(device_profile))]
pub fn semantic_color(_token: SemanticColor) -> Option<Color> {
None
}
pub enum LayerColor {
Scrim,
SurfaceContainer,
SurfaceContainerHigh,
InverseSurface,
OnInverseSurface,
}
#[cfg(device_profile)]
pub fn layer_color(layer: LayerColor) -> Option<Color> {
let manager = theme_manager();
let theme = manager.current_theme()?;
Some(match layer {
LayerColor::Scrim => theme.colors.scrim,
LayerColor::SurfaceContainer => theme.colors.surface_container,
LayerColor::SurfaceContainerHigh => theme.colors.surface_container_high,
LayerColor::InverseSurface => theme.colors.inverse_surface,
LayerColor::OnInverseSurface => theme.colors.on_inverse_surface,
})
}
#[cfg(not(device_profile))]
pub fn layer_color(_layer: LayerColor) -> Option<Color> {
None
}
#[cfg(device_profile)]
pub fn motion_tokens() -> (u32, u32, u32) {
let (fast, normal, slow) = crate::style::theme_manager()
.current_theme()
.map(|theme| (theme.motion.fast, theme.motion.normal, theme.motion.slow))
.unwrap_or((100, 200, 300));
reduce_motion(fast, normal, slow)
}
fn reduce_motion(fast: u32, normal: u32, slow: u32) -> (u32, u32, u32) {
if crate::style::environment::environment().prefers_reduced_motion() {
(0, 0, 0)
} else {
(fast, normal, slow)
}
}
#[cfg(not(device_profile))]
pub fn motion_tokens() -> (u32, u32, u32) {
reduce_motion(100, 200, 300)
}
#[cfg(device_profile)]
pub fn motion_easing() -> crate::style::animation::EasingFunction {
crate::style::theme_manager()
.current_theme()
.map(|theme| theme.motion.easing)
.unwrap_or_default()
}
#[cfg(not(device_profile))]
pub fn motion_easing() -> crate::style::animation::EasingFunction {
crate::style::animation::EasingFunction::default()
}
#[cfg(device_profile)]
pub fn theme_manager() -> crate::compat::MutexGuard<'static, crate::theme::ThemeManager> {
crate::theme::global_theme_manager()
}
#[cfg(not(device_profile))]
pub fn theme_manager() -> ThemeManagerPlaceholder {
ThemeManagerPlaceholder
}
#[cfg(not(device_profile))]
#[derive(Debug, Clone, Copy, Default)]
pub struct ThemeManagerPlaceholder;
#[cfg(not(device_profile))]
impl ThemeManagerPlaceholder {
pub fn current_theme(&self) -> Option<Theme> {
None
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum HighContrastMode {
#[default]
None,
BlackOnWhite,
WhiteOnBlack,
Custom {
fg: Color,
bg: Color,
},
}
impl HighContrastMode {
pub fn forced_pair(self) -> Option<(Color, Color)> {
match self {
Self::None => None,
Self::BlackOnWhite => Some((Color::WHITE, Color::BLACK)),
Self::WhiteOnBlack => Some((Color::BLACK, Color::WHITE)),
Self::Custom { fg, bg } => Some((bg, fg)),
}
}
pub fn contrast_ratio(self) -> Option<f32> {
self.forced_pair().map(|(bg, fg)| bg.contrast_ratio(fg))
}
pub fn is_active(self) -> bool {
self != Self::None
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn none_forces_no_pair() {
assert_eq!(HighContrastMode::None.forced_pair(), None);
assert!(!HighContrastMode::None.is_active());
assert_eq!(HighContrastMode::None.contrast_ratio(), None);
}
#[test]
fn the_named_modes_force_the_documented_pairs() {
assert_eq!(
HighContrastMode::BlackOnWhite.forced_pair(),
Some((Color::WHITE, Color::BLACK))
);
assert_eq!(
HighContrastMode::WhiteOnBlack.forced_pair(),
Some((Color::BLACK, Color::WHITE))
);
assert_eq!(
HighContrastMode::Custom { fg: Color::RED, bg: Color::BLUE }.forced_pair(),
Some((Color::BLUE, Color::RED))
);
}
#[test]
fn the_named_modes_meet_wcag_aaa() {
let ratio = HighContrastMode::BlackOnWhite.contrast_ratio().expect("a forced pair");
assert!((ratio - 21.0).abs() < 1e-3, "expected 21:1, got {ratio}");
assert_eq!(HighContrastMode::WhiteOnBlack.contrast_ratio(), Some(ratio));
}
#[test]
fn a_custom_pair_reports_its_actual_ratio() {
let poor = HighContrastMode::Custom {
fg: Color::rgb(128, 128, 128),
bg: Color::rgb(120, 120, 120),
};
let ratio = poor.contrast_ratio().expect("a forced pair");
assert!(ratio < 1.5, "expected a poor ratio, got {ratio}");
let good = HighContrastMode::Custom { fg: Color::BLACK, bg: Color::WHITE };
assert!(good.contrast_ratio().expect("a forced pair") >= 4.5, "WCAG AA for normal text");
}
#[test]
fn is_active_distinguishes_none_from_the_rest() {
assert!(!HighContrastMode::None.is_active());
assert!(HighContrastMode::BlackOnWhite.is_active());
assert!(HighContrastMode::WhiteOnBlack.is_active());
assert!(HighContrastMode::Custom { fg: Color::BLACK, bg: Color::WHITE }.is_active());
}
}