ltk/theme/active.rs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150
// SPDX-License-Identifier: LGPL-2.1-only
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>
//! 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<ThemeDocument>,
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<Option<ActiveState>> = 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<ThemeDocument>
{
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
}