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

//! The eight-slot semantic [`Palette`] every widget speaks in terms of, plus
//! the derived [`WindowControlsSpec`] fallback used when a theme document
//! omits the explicit `window_controls` block.

use crate::types::Color;

use super::prefs::WindowControlsSpec;
use super::slots::SlotStore;

// ─── Palette ─────────────────────────────────────────────────────────────────

/// Semantic colour tokens shared by every widget.
#[ derive( Debug, Clone, Copy ) ]
pub struct Palette
{
	/// Page / wallpaper background when no image is present.
	pub bg:              Color,
	/// Floating surface over the wallpaper (cards, panels, dock) — usually translucent.
	pub surface:         Color,
	/// Elevated surface (hovered card, pressed toggle, inner pill).
	pub surface_alt:     Color,
	/// Primary foreground text.
	pub text_primary:    Color,
	/// Subdued text (date strip, helper text, disabled labels).
	pub text_secondary:  Color,
	/// Brand accent used for active toggles, sliders, focus rings.
	pub accent:          Color,
	/// Thin separators and low-contrast borders.
	pub divider:         Color,
	/// Tint for symbolic icons (wifi / battery / search / etc).
	pub icon:            Color,
	/// Foreground colour for destructive / error states — text in
	/// "delete" buttons, error helper rows under invalid inputs,
	/// the border of an errored pill.
	pub danger:          Color,
	/// Soft fill behind error states. Pairs with `danger` as
	/// foreground; light enough that body text remains legible on
	/// top.
	pub danger_bg:       Color,
}

// ─── Palette projection ──────────────────────────────────────────────────────

impl Palette
{
	/// Project a [`SlotStore`] onto the eight canonical palette fields.
	/// Slot ids are the ones declared in the default theme JSON
	/// (`bg-page`, `surface`, `surface-alt`, `text-primary`,
	/// `text-secondary`, `accent`, `divider`, `icon`). Missing slots
	/// fall back to a documented sensible default so downstream widgets
	/// never see uninitialised colours. Used by [`crate::theme::palette()`] and
	/// [`crate::theme::window_controls`].
	pub fn from_slots( slots: &SlotStore ) -> Self
	{
		Self
		{
			bg:             slots.color( "bg-page"        ).unwrap_or( Color::WHITE ),
			surface:        slots.color( "surface"        ).unwrap_or( Color::WHITE ),
			surface_alt:    slots.color( "surface-alt"    ).unwrap_or( Color::WHITE ),
			text_primary:   slots.color( "text-primary"   ).unwrap_or( Color::BLACK ),
			text_secondary: slots.color( "text-secondary" ).unwrap_or( Color::rgba( 0.0, 0.0, 0.0, 0.6 ) ),
			accent:         slots.color( "accent"         ).unwrap_or( Color::hex( 0x00, 0xCE, 0xB1 ) ),
			divider:        slots.color( "divider"        ).unwrap_or( Color::rgba( 0.0, 0.0, 0.0, 0.08 ) ),
			icon:           slots.color( "icon"           ).unwrap_or( Color::BLACK ),
			// Conservative defaults: a rich red for fg, a tinted
			// pink wash for bg. Themes can override via the
			// `danger` / `danger-bg` slot ids.
			danger:         slots.color( "danger"         ).unwrap_or( Color::hex( 0xA8, 0x00, 0x10 ) ),
			danger_bg:      slots.color( "danger-bg"      ).unwrap_or( Color::rgba( 1.0, 0.85, 0.88, 1.0 ) ),
		}
	}
}

// ─── Shared helpers ──────────────────────────────────────────────────────────

/// Derive a sensible [`WindowControlsSpec`] from a [`Palette`] when the
/// theme document omits the `window_controls` block. Also used by the
/// JSON loader in `schema.rs` when individual overrides are absent.
pub ( crate ) fn default_window_controls( palette: Palette ) -> WindowControlsSpec
{
	WindowControlsSpec
	{
		bar_bg:         Color { a: 1.0, ..palette.surface },
		icon:           palette.icon,
		hover_bg:       palette.surface_alt,
		pressed_bg:     palette.divider,
		close_hover_bg: Color::rgba( 0.92, 0.18, 0.18, 0.90 ),
		close_icon:     Color::WHITE,
		focus_ring:     palette.accent,
	}
}

#[ cfg( test ) ]
mod tests
{
	use super::*;
	use super::super::slots::{ self, Metadata, Slot };

	#[ test ]
	fn palette_from_slots_uses_canonical_ids()
	{
		let mut store = slots::SlotStore::new();
		store.insert
		(
			"bg-page",
			Slot::Color { value: Color::hex( 0x01, 0x02, 0x03 ), meta: Metadata::default() },
		);
		store.insert
		(
			"text-primary",
			Slot::Color { value: Color::hex( 0xF0, 0xF1, 0xF2 ), meta: Metadata::default() },
		);
		let p = Palette::from_slots( &store );
		assert_eq!( p.bg,           Color::hex( 0x01, 0x02, 0x03 ) );
		assert_eq!( p.text_primary, Color::hex( 0xF0, 0xF1, 0xF2 ) );
		// Missing slots fall back to documented sensible defaults.
		assert_eq!( p.icon, Color::BLACK );
	}
}