arama_theme/lib.rs
1//! # arama-theme
2//!
3//! Token-driven styling for arama, backed by the Snora Design system
4//! (RFC 010) with a runtime-selectable theme preset (RFC 011).
5//!
6//! ## Three styling layers
7//!
8//! A theme switch must move three layers together:
9//!
10//! * **A — Snora button tokens.** The four button style functions below
11//! ([`primary`], [`ghost`], [`secondary`], [`danger`]) resolve the active
12//! [`snora::design::Tokens`] from the current preset.
13//! * **B — Snora container tokens.** Reserved for future card surfaces;
14//! driven by the same [`tokens`].
15//! * **C — Base iced theme.** [`iced_theme`] returns the matching iced
16//! [`Theme`] for the application's `.theme()` callback, so the window
17//! background and all stock iced widgets track the preset.
18//!
19//! ## Global state
20//!
21//! The active preset is stored in a global `AtomicU8` — the same lock-free
22//! pattern arama uses for the i18n locale — so [`set_theme`] and the lookup
23//! functions are safe to call from any thread without lifetime friction in
24//! `view()`.
25
26use std::sync::atomic::{AtomicU8, Ordering};
27
28use arama_env::ThemePreset;
29use iced::{Theme, widget::button};
30use snora::design::Tokens;
31
32// ---------------------------------------------------------------------------
33// Global preset state
34// ---------------------------------------------------------------------------
35
36static THEME_ID: AtomicU8 = AtomicU8::new(0 /* ThemePreset::Light */);
37
38/// Set the active theme preset. Safe to call from any thread.
39pub fn set_theme(preset: ThemePreset) {
40 THEME_ID.store(preset as u8, Ordering::Relaxed);
41}
42
43/// Return the currently active theme preset.
44pub fn current_theme() -> ThemePreset {
45 match THEME_ID.load(Ordering::Relaxed) {
46 1 => ThemePreset::Dark,
47 2 => ThemePreset::HighContrastLight,
48 3 => ThemePreset::HighContrastDark,
49 _ => ThemePreset::Light,
50 }
51}
52
53// ---------------------------------------------------------------------------
54// Preset → tokens (layers A / B) and → iced Theme (layer C)
55// ---------------------------------------------------------------------------
56
57/// The Snora Design tokens for the active preset.
58///
59/// Returns an owned `Tokens`; snora's style helpers clone tokens into their
60/// style closures anyway, so this avoids any `'static` lifetime constraint.
61/// `Tokens` is small and `Clone`; the per-button clone cost in `view()` is
62/// negligible.
63fn tokens() -> Tokens {
64 match current_theme() {
65 ThemePreset::Light => Tokens::light(),
66 ThemePreset::Dark => Tokens::dark(),
67 ThemePreset::HighContrastLight => Tokens::high_contrast_light(),
68 ThemePreset::HighContrastDark => Tokens::high_contrast_dark(),
69 }
70}
71
72/// The base iced [`Theme`] for the active preset (layer C).
73///
74/// iced 0.14 has no built-in high-contrast theme, so the high-contrast
75/// presets map to the matching `Light` / `Dark` base. arama's own controls
76/// (buttons, and future cards) still get the full high-contrast tokens via
77/// layers A / B; only stock iced widgets fall back to the base theme.
78pub fn iced_theme() -> Theme {
79 match current_theme() {
80 ThemePreset::Light | ThemePreset::HighContrastLight => Theme::Light,
81 ThemePreset::Dark | ThemePreset::HighContrastDark => Theme::Dark,
82 }
83}
84
85// ---------------------------------------------------------------------------
86// Button style functions (layer A) — drop-in shape for iced's `.style(...)`
87// ---------------------------------------------------------------------------
88
89/// Primary (accent) button style — active navigation item, confirmations.
90pub fn primary(_theme: &Theme, status: button::Status) -> button::Style {
91 snora::design::style::button::primary(&tokens(), status)
92}
93
94/// Ghost (transparent) button style — token-driven equivalent of iced's
95/// `button::text`, used for inactive navigation items.
96pub fn ghost(_theme: &Theme, status: button::Status) -> button::Style {
97 snora::design::style::button::ghost(&tokens(), status)
98}
99
100/// Secondary button style — non-primary actions such as "Skip".
101pub fn secondary(_theme: &Theme, status: button::Status) -> button::Style {
102 snora::design::style::button::secondary(&tokens(), status)
103}
104
105/// Danger button style — destructive actions such as "Stop".
106pub fn danger(_theme: &Theme, status: button::Status) -> button::Style {
107 snora::design::style::button::danger(&tokens(), status)
108}