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
}