refactor: split every monolithic module into focused submodules
Each source file that had grown beyond a single concern is replaced by an identically-named directory containing focused submodules. `src/event_loop/mod.rs` (878 lines) becomes a directory with clipboard, context_menu, cursor_shape, drag, focus, handlers, invalidation, overlays_reconcile, repeat, run, surface, text_editing, and tooltip. Every widget, input handler, and theme component follows the same split. Public interfaces are unchanged — only the internal file layout moves. image bumped from 0.25.2 to 0.25.9.
This commit is contained in:
150
src/theme/active.rs
Normal file
150
src/theme/active.rs
Normal file
@@ -0,0 +1,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
|
||||
}
|
||||
Reference in New Issue
Block a user