ltk/theme/
document.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
// SPDX-License-Identifier: LGPL-2.1-only
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>

//! The top-level theme document: metadata, fonts block and per-mode slots.
//!
//! This is the runtime representation of a fully parsed `theme.json`,
//! and the single source of truth for the active theme — installed via
//! [`super::set_active_document`] and read back via
//! [`super::active_document`].
//!
//! # Modes
//!
//! `fonts` is shared between light and dark and only what actually differs
//! — slot tables, wallpaper paths, optional window-controls overrides — is
//! nested under `modes.light` / `modes.dark`.

use std::collections::HashMap;
use std::path::{ Path, PathBuf };

use super::fonts::FontFamilyDef;
use super::slots::SlotStore;
use super::{ search_paths, LauncherSpec, ThemeError, WallpaperSpec, WindowControlsSpec };

// ─── Mode ────────────────────────────────────────────────────────────────────

/// Per-variant content: the slot table plus the surfaces that don't fit
/// cleanly in slots (wallpaper image, window-controls payload).
#[ derive( Debug, Clone ) ]
pub struct Mode
{
	/// Homescreen / shell wallpaper. Absent means "solid background, no
	/// image" (the renderer falls back to whatever the theme's surface slot
	/// resolves to for the viewport).
	pub wallpaper:       Option<WallpaperSpec>,
	/// Lockscreen / greeter wallpaper. Distinct from `wallpaper` because the
	/// lockscreen is typically quieter (often a darker crop).
	pub lockscreen:      Option<WallpaperSpec>,
	/// Launcher surface styling. Aggregate (background + border_radius)
	/// rather than a slot because the radius is a scalar that widgets treat
	/// as theme-imposed geometry, not a design-system colour token.
	pub launcher:        Option<LauncherSpec>,
	/// Window-decoration controls payload. Kept out of the slot table
	/// because it is a contract with an external app, not a
	/// design-system token.
	pub window_controls: Option<WindowControlsSpec>,
	/// The typed slot table for this mode.
	pub slots:           SlotStore,
}

// ─── ThemeDocument ───────────────────────────────────────────────────────────

/// A fully parsed theme, as loaded from a `theme.json` on disk.
#[ derive( Debug, Clone ) ]
pub struct ThemeDocument
{
	/// Stable identifier used to look up the theme across search paths.
	pub id:    String,
	/// Human-readable display name.
	pub name:  String,
	/// Directory the document was loaded from. `None` for documents built
	/// in-memory (e.g. test fixtures).
	pub root:  Option<PathBuf>,
	/// Font families declared in the document. Indexed by the id used by
	/// [`super::FontRef::Named`]. The same registry is shared between modes.
	pub fonts: HashMap<String, FontFamilyDef>,
	/// Light-mode content.
	pub light: Mode,
	/// Dark-mode content.
	pub dark:  Mode,
}

impl ThemeDocument
{
	/// Return the mode matching `mode`.
	pub fn mode( &self, mode: super::ThemeMode ) -> &Mode
	{
		match mode
		{
			super::ThemeMode::Light => &self.light,
			super::ThemeMode::Dark  => &self.dark,
		}
	}

	/// Load a document from a directory containing a `theme.json`.
	///
	/// Paths inside the document (wallpaper, lockscreen, font sources) are
	/// resolved against `dir`.
	pub fn load_from_dir( dir: &Path ) -> Result<Self, ThemeError>
	{
		super::schema::load_document_from_dir( dir )
	}

	/// Look up a document by id across the standard search paths.
	///
	/// Order, highest priority first:
	/// 1. `$LTK_THEMES_DIR/<id>/` (when the env var is set)
	/// 2. `$XDG_DATA_HOME/ltk/themes/<id>/` (defaults to `~/.local/share/...`)
	/// 3. `/usr/share/ltk/themes/<id>/`
	pub fn find( id: &str ) -> Result<Self, ThemeError>
	{
		for base in search_paths()
		{
			let dir = base.join( id );
			if dir.join( "theme.json" ).is_file()
			{
				return Self::load_from_dir( &dir );
			}
		}
		Err( ThemeError::NotFound( id.to_string() ) )
	}
}