Files
lumbridge-code/crates/lumbridge-theme/src/palette.rs
T
Metal AgentandClaude Opus 5 1556b87f37 Derive the interface palette instead of hardcoding eleven colours
main.rs held eleven `const … : u32` colours, and spikes/floem-shell held a
byte-identical copy of the same eleven. Every one was a judgement call made once,
and no user could change any of them without recompiling.

lumbridge-theme takes a syntax theme's five anchors — background, foreground,
comment, and the git added/deleted/modified colours where the theme has them —
and derives the whole role set. The frame is the editor background pushed one
logarithmic contrast step away from the content, so the work surface is the
brightest thing on screen; a theme already at black lifts its surface instead of
sinking its frame, which is why a pitch-black theme still shows a seam.

Adapted from Buzz's adaptive-theme.ts (block/buzz, Apache-2.0) as a
specification, not as copied code. The golden vectors were taken by running the
original under Node — a research pass had supplied Python-derived vectors and
claimed they reproduced it byte-exactly, and they did not: Python rounds
half-to-even, JavaScript rounds half-up, they disagree on exactly one channel
value of 22.5, and that decides whether the luminance bisection converges a step
early. github-dark's chrome is #171a1d, not #191c20.

Provenance colours are separate roles from state colours, with a test holding
them pairwise distinct in every theme, because decision 0012 colours a usage
reading by where its number came from and never by how alarming it is.

This changed no pixels, and that was verified rather than asserted: the only
difference between before-and-after screenshots is the digits of a process ID.
The check earned its keep — the mechanical rename had rewritten three user-facing
strings, turning the sidebar's "ATTENTION · 0" into "theme.attention · 0" and
"+ ADD PANEL" into "+ ADD theme.surface". A literal-by-literal diff now confirms
zero strings changed.

The default theme pins its roles to the previous constants to make that true;
the anchors underneath are real, and a test bounds how far the pure derivation
sits from them. The terminal ANSI palette keeps its own table, so 29 colour
literals remain in main.rs, all terminal. The catalog, its attribution, and the
picker are separate work.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-31 23:23:30 -07:00

101 lines
3.6 KiB
Rust

//! The role set every surface paints from.
//!
//! Roles are named for what they do, not for a container ladder. `surface_active`
//! is the selected panel and the hover fill; it is not "surface container
//! highest", because nothing in Lumbridge has to reason about how many
//! containers deep it is.
//!
//! Two rules the rest of the application depends on:
//!
//! - **Provenance roles are distinct from state roles.** Decision 0012 says a
//! usage value is coloured by where it came from, never by how alarming it is.
//! They are separate fields here so a theme cannot quietly collapse the two,
//! and a test asserts the five stay pairwise distinguishable.
//! - **Nothing is derived lazily.** A `Palette` is computed once when the theme
//! changes and then only read, so a render pass never runs the bisection.
use crate::color::Srgb;
/// Every colour the interface is allowed to use.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct Palette {
/// The application frame: window background, sidebar, footer, tab gutter.
pub chrome: Srgb,
/// The work surface: panel bodies, terminals, editors.
pub surface: Srgb,
/// One step off the surface: unselected tabs, inline cards.
pub surface_raised: Srgb,
/// Two steps: the selected panel, hover fills, chip backgrounds.
pub surface_active: Srgb,
/// Floating above everything: the command palette, popovers, menus.
pub surface_overlay: Srgb,
/// The seam where the frame meets the content.
pub surface_between: Srgb,
pub border: Srgb,
/// A divider that should be felt rather than seen.
pub border_quiet: Srgb,
pub text: Srgb,
/// Secondary text. The theme's own comment colour, so it is legible against
/// the surface by construction rather than by our guess.
pub muted: Srgb,
/// Focus rings, selection, the active tab underline.
pub accent: Srgb,
/// Black or white, whichever reads on `accent`.
pub on_accent: Srgb,
pub success: Srgb,
pub danger: Srgb,
/// A danger fill quiet enough to sit behind text.
pub danger_container: Srgb,
/// Needs a human. Paired with a glyph, never used alone.
pub attention: Srgb,
pub attention_wash: Srgb,
/// Tooltips and inverted chips.
pub inverse_surface: Srgb,
pub on_inverse: Srgb,
pub scrim: Srgb,
pub is_dark: bool,
}
impl Palette {
/// The colour a usage reading is drawn in, by where the number came from.
///
/// Decision 0012's rule made explicit: the argument is a provenance, and
/// there is no way to pass a value in, so this cannot accidentally become
/// "red when low".
#[must_use]
pub const fn provenance(&self, provenance: Provenance) -> Srgb {
match provenance {
Provenance::Provider => self.success,
Provenance::Harness => self.accent,
Provenance::Local => self.text,
Provenance::Estimated => self.attention,
Provenance::Unavailable => self.muted,
}
}
/// The five provenance colours, for the distinctness test.
#[must_use]
pub const fn provenance_roles(&self) -> [Srgb; 5] {
[
self.success,
self.accent,
self.text,
self.attention,
self.muted,
]
}
}
/// Where a usage number came from.
///
/// A structural mirror of `lumbridge_core::UsageProvenance`, kept here so this
/// crate stays free of a dependency on the ledger. The UI maps between them.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Provenance {
Provider,
Harness,
Local,
Estimated,
Unavailable,
}