L1: shared LICE drawing kit — theme/palette + component geometry + draw kit; retire GDI text in bank_panel

This commit is contained in:
2026-07-26 20:04:04 -04:00
parent ec05af35ef
commit beca6487c7
10 changed files with 1394 additions and 42 deletions
+108
View File
@@ -0,0 +1,108 @@
#pragma once
// theme — the REAPER-free, LICE-free palette + type-scale core of the shared drawing
// kit (Phase L, L1). This is the "one source of drawing" made testable at its root: a
// ROLE-based color model (bg/base, bg/panel, bg/cell, line/hairline, text/primary,
// text/dim, accent, accent/hot, warn), an INTERACTION-STATE model (rest/hover/active/
// pressed/dragging/focus/disabled), and the WCAG contrast math that lets a unit test
// prove every text-on-surface pair clears its floor ("punch to the floor, not past it").
//
// THE SINGLE POINT OF CHANGE (DS-2): every role color is produced by roleColor() from
// ONE direction constants block (kDirection*, below) carrying the settled B (Neon
// Console) + C spectral values. Switching the visual direction is editing that block and
// nothing else — no shell hardcodes a color; the shell asks the theme by role. The
// spectral (Direction C) hue ramp lives here too (spectralColor) so the signature
// keyboard strip's L3 consumer derives its per-note hue from the same source.
//
// PURE MODULE: NO REAPER types, NO SWELL, NO LICE, NO vendor/ includes. Standard library
// only. Builds and unit-tests without REAPER. Mirror of mode_switch / bank_grid — the
// shell (draw_kit) turns a KitColor into a LICE_pixel at the boundary; the theme never
// names a LICE type.
#include <cstdint>
namespace reasampler {
// A straight 8-bit-per-channel RGBA color, LICE-free. The draw shell converts this to a
// LICE_pixel via LICE_RGBA at the boundary (draw_kit); nothing here depends on LICE's
// packing. Deliberately NOT named "Color"/"RGBA" (both are common collision surfaces);
// "KitColor" scopes it to the kit.
struct KitColor {
std::uint8_t r = 0;
std::uint8_t g = 0;
std::uint8_t b = 0;
std::uint8_t a = 255;
bool operator==(const KitColor& o) const {
return r == o.r && g == o.g && b == o.b && a == o.a;
}
};
// The structural palette roles (direction-independent — §2.1 of the design doc). The
// direction (B/C) sets the concrete hue behind each; the shell always asks by role.
enum class Role {
BgBase, // window canvas
BgPanel, // a raised region (list, waveform pane)
BgCell, // a control / row surface
LineHairline,// separators (used sparingly — elevation carries most separation)
TextPrimary, // labels, values
TextDim, // secondary / units
Accent, // selection / active / focus — where the punch lives
AccentHot, // hover / live / drag feedback (a brighter accent tint)
Warn, // clip / destructive (prune, delete) — reserved for byte-deleting states
};
// The interaction-state model every kit component honors (§3.3). A component draws its
// role surface transformed by its current state; stateShift() below is that transform.
enum class InteractionState {
Rest,
Hover,
Active, // selected / active
Pressed,
Dragging,
Focus,
Disabled,
};
// Text size classes for the WCAG floor. "Large" text (>= ~18.66px, or >= ~14px bold) and
// UI-state indicators clear at 3:1; body text clears at 4.5:1 (WCAG 2.1 AA). The kit's
// four cached fonts map onto these: title -> Large, label/value -> Body, micro -> Body.
enum class TextClass {
Body, // AA 4.5:1
Large, // AA-large 3:1 (also the floor for state indicators)
};
// The concrete color for a role, produced from the ONE direction constants block. This is
// the single choke point the "single point of change" guarantee rests on: the shell has
// no other way to obtain a palette color, so re-picking the direction is editing the
// kDirection* block this reads and nothing else.
KitColor roleColor(Role role);
// The color for a role under an interaction state — roleColor(role) transformed by the
// state (hover lightens toward accent/hot, pressed darkens, disabled desaturates + drops
// alpha, etc.). Surfaces use this so every component gets the whole state model for free.
// Rest returns roleColor(role) unchanged.
KitColor roleColorState(Role role, InteractionState state);
// Direction C's spectral hue ramp: maps a normalized position t in [0, 1] (low note ->
// high note across the keyboard strip) to a color, cool-blue at 0 -> hot-magenta at 1
// (§4 Direction C). The signature keyboard-strip surface (an L3 consumer) derives each
// note/zone's hue from this ONE function so the spectrum is defined in the same place as
// the rest of the palette. t is clamped to [0, 1].
KitColor spectralColor(double t);
// --- WCAG contrast (the "punch" rule, made testable) --------------------------
//
// The relative luminance of a color per WCAG 2.1 (sRGB linearization + the 0.2126/
// 0.7152/0.0722 weighting). Alpha is ignored — contrast is a question about the opaque
// hues; a translucent overlay's effective color is the caller's to compose first.
double relativeLuminance(const KitColor& c);
// The WCAG contrast ratio between two colors, in [1, 21]. Symmetric; order-independent.
double contrastRatio(const KitColor& a, const KitColor& b);
// The contrast floor a text class must clear: 4.5 for Body, 3.0 for Large. The test that
// proves the palette asserts contrastRatio(text, surface) >= textFloor(class) for every
// pair the kit actually draws.
double textFloor(TextClass cls);
} // namespace reasampler