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 );
}
}