// SPDX-License-Identifier: LGPL-2.1-only // Copyright (C) 2026 Liberux Labs, S. L. //! Process-wide active theme state. //! //! The active document is published once and read by every per-slot //! accessor in [`crate::theme`]. First access triggers a disk load of the //! `default` theme; if that fails, an embedded B/W document takes over //! and the [`is_fallback_active`] flag flips on so the draw layer can paint //! a warning banner. use std::sync::{ Arc, RwLock }; use super::assets; use super::document::ThemeDocument; use super::fallback; use super::prefs::ThemeMode; #[ derive( Clone ) ] pub ( crate ) struct ActiveState { /// Currently loaded theme document. The single source of truth — /// every consumer-facing accessor in this module reads through it /// against the active [`ThemeMode`]. pub ( crate ) document: Arc, pub ( crate ) mode: ThemeMode, /// `true` when the active document was produced by /// [`fallback::document`] because `ThemeDocument::find("default")` /// failed. The draw path stamps every frame with a red banner in /// this state so the user sees the missing-theme signal without /// the process having to abort. pub ( crate ) is_fallback: bool, } // `RwLock::new` and `Option::None` are both const, so this needs no lazy init. static ACTIVE: RwLock> = RwLock::new( None ); /// Read the active state, loading the `default` theme from disk on /// first access. If the `default` theme cannot be found in any search /// path, the crate falls back to an in-memory B/W /// [`fallback::document`], marks the state with `is_fallback = true` /// (so the draw path can paint the warning banner), and logs a line /// to stderr pointing at the install instructions. The process never /// panics on first-access — making ltk embeddable inside programs /// that want to handle missing theme gracefully. pub ( crate ) fn ensure_active() -> ActiveState { { let guard = ACTIVE.read().expect( "theme: ACTIVE poisoned" ); if let Some( s ) = guard.as_ref() { return s.clone(); } } let mut guard = ACTIVE.write().expect( "theme: ACTIVE poisoned" ); if guard.is_none() { let ( doc, is_fallback ) = match ThemeDocument::find( "default" ) { Ok( d ) => ( d, false ), Err( e ) => { eprintln! ( "[ltk] default theme not found ({e}); using embedded B/W \ fallback. Install the `ltk-theme-default` Debian package \ (Provides: ltk-theme) or set `LTK_THEMES_DIR` to a \ directory containing `default/theme.json` to get the \ real theme back." ); ( fallback::document(), true ) } }; *guard = Some( ActiveState { document: Arc::new( doc ), mode: ThemeMode::Light, is_fallback, }); } guard.as_ref().expect( "just installed" ).clone() } /// Install `doc` as the active theme. The current mode is preserved /// (defaulting to [`ThemeMode::Light`] if nothing was set yet). Also /// clears the fallback flag — an explicit install supersedes the /// embedded B/W document and the warning banner stops painting from /// the next frame on. pub fn set_active_document( doc: ThemeDocument ) { let mut guard = ACTIVE.write().expect( "theme: ACTIVE poisoned" ); let mode = guard.as_ref().map( |s| s.mode ).unwrap_or( ThemeMode::Light ); *guard = Some( ActiveState { document: Arc::new( doc ), mode, is_fallback: false, }); // Drop any rasterised icons cached against the previous theme's // paths. Cache keys embed absolute paths so old entries are never // *wrong*, just dead memory; clearing keeps the working set small. assets::clear_svg_cache(); } /// Switch the active variant. The document is left untouched — if nothing /// has been loaded yet, the default theme is loaded first. pub fn set_active_mode( mode: ThemeMode ) { // Ensure the default theme is loaded before mutating the mode so we // never publish an `ActiveState` with a missing document. let _ = ensure_active(); let mut guard = ACTIVE.write().expect( "theme: ACTIVE poisoned" ); if let Some( s ) = guard.as_mut() { s.mode = mode; } } /// The id of the active theme. pub fn active_theme_id() -> String { ensure_active().document.id.clone() } /// The active variant (light or dark). pub fn active_mode() -> ThemeMode { ensure_active().mode } /// The currently loaded theme document. Use this for slot-typed lookups /// when the per-slot helpers ([`crate::theme::color`], [`crate::theme::surface()`], …) are not /// expressive enough — e.g. iterating `mode.slots.entries`. pub fn active_document() -> Arc { ensure_active().document } /// `true` when the active theme was produced by the embedded B/W /// fallback because `ThemeDocument::find("default")` failed at /// first-access. Flipped back to `false` the moment a consumer calls /// [`set_active_document`] with any document (even another call to /// the same fallback — the point is "this state came from an /// explicit install, not from the missing-theme code path"). /// /// The draw layer reads this to decide whether to stamp each surface /// with the red "install `ltk-theme-default`" banner; apps can read /// it too, e.g. to log a diagnostic or surface a first-run /// installation helper. pub fn is_fallback_active() -> bool { ensure_active().is_fallback }