// SPDX-License-Identifier: LGPL-2.1-only // Copyright (C) 2026 Liberux Labs, S. L. //! The ten-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 ten 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`, `danger`, /// `danger-bg`). 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 ); } }