palette: enlarge the region title into WCAG large class; repoint the grabbed envelope handle off hue

Corrects theme.h's large-text thresholds, names accent/secondary's real
binding limiter, and adds compositeOver so the loop-span fill's 2.25:1
under-floor pair is asserted rather than assumed.
This commit is contained in:
2026-07-31 14:07:01 -04:00
parent 91f71f92bd
commit a19d645a49
10 changed files with 221 additions and 82 deletions
+34 -12
View File
@@ -25,11 +25,21 @@ using audio::computeEnvelope;
namespace {
// Marker roles — semantic, drawn through the kit's palette: start AND loop start/end both
// = teal (secondary). Markers are 2px bars and a 20%-alpha span fill, not the 1px trace, so
// = teal (secondary). Markers are 2px bars and a translucent span fill, not the 1px trace, so
// they live with 1.92:1 against the waveform; the trace, which cannot, has its own role.
// Do not collapse the two back onto one role — they overlap in this rect.
// Do not collapse the two back onto one role — they overlap in this rect. The trace crossing
// the loop-span fill is a KNOWN, ACCEPTED under-floor pair (2.25:1 against a 3:1 floor), and no
// trace value fixes it — see the two-neighbour rule in core/ui/CLAUDE.md. If it is ever
// resolved, the FILL is what changes; do not nudge a color to chase it.
constexpr Role kRoleStartMarker = Role::AccentSecondary;
constexpr Role kRoleLoopMarker = Role::AccentSecondary;
// Envelope-handle half-extents. Grabbed grows and hollows out; kNodeGrabRadius (envelope_edit)
// is the PICK radius and is unrelated — a handle may draw larger than it without widening any
// hit region.
constexpr int kEnvHandleRadius = 3;
constexpr int kEnvHandleGrabbedRadius = 5;
constexpr int kEnvHandleRingPx = 2;
} // namespace
void ReaSamplerEditor::paintWaveform(LICE_IBitmap* bmp, const Rect& band) {
@@ -84,7 +94,8 @@ void ReaSamplerEditor::paintWaveform(LICE_IBitmap* bmp, const Rect& band) {
const int rx = frameToX(overlay, frames, m.loopEnd);
if (rx > lx) {
LICE_FillRect(bmp, lx, overlayRect.y, rx - lx, overlayRect.height,
toLice(roleColor(kRoleLoopMarker)), 0.20f, 0);
toLice(roleColor(kRoleLoopMarker)),
static_cast<float>(kLoopSpanFillAlpha), 0);
}
}
const std::int64_t markerFrames[3] = {m.start, m.loopStart, m.loopEnd};
@@ -124,24 +135,35 @@ void ReaSamplerEditor::paintEnvelopeOverlay(LICE_IBitmap* bmp, const OverlayArea
}
prev = &v;
}
// Handles: a square per draggable stage node, a ROUND knot per curvable segment. Lit
// accent-hot when this node is the grabbed one. Every vertex is guaranteed in-bounds; the
// handle is additionally clamped inside the band so one on an edge node never overhangs
// into the neighbouring bands.
// Handles: a square per draggable stage node, a ROUND knot per curvable segment. Every
// vertex is guaranteed in-bounds; the handle is additionally clamped inside the band so one
// on an edge node never overhangs into the neighbouring bands.
//
// GRAB is signalled by SIZE + a punched-out core, NOT by a hotter hue — the one place the
// kit's "brighter = hotter" convention is deliberately inverted, because a brighter tint is
// a LOWER-contrast tint here: accent/hot sits at 1.15:1 against the lime it is drawn over.
// The two-neighbour ceiling (core/ui/CLAUDE.md) leaves at most 1.05:1 between ANY two values
// that both clear the floor over lime and bg/base, so no color can carry this state. The
// grabbed mark stays overlay/trace and reads by its 3.07:1 ring against the lime plus a
// 3.06:1 bg/base core inside it.
const LICE_pixel handle = toLice(roleColor(Role::OverlayTrace));
const LICE_pixel handleHot = toLice(roleColor(Role::AccentHot));
const LICE_pixel core = toLice(roleColor(Role::BgBase));
for (const EnvVertex& v : poly) {
if (v.node == EnvNode::Origin || v.node == EnvNode::ReleaseEnd) continue;
const bool grabbed = (drag_ == DragKind::kEnvNode && envNode_ == v.node);
const int r = 3;
const int r = grabbed ? kEnvHandleGrabbedRadius : kEnvHandleRadius;
const int ir = r - kEnvHandleRingPx; // core radius; > 0 only when grabbed
const int hx = (std::max)(area.x + r, (std::min)(area.right() - 1 - r, v.x));
const int hy = (std::max)(area.y + r, (std::min)(area.bottom() - 1 - r, v.y));
if (v.knot) {
LICE_FillCircle(bmp, static_cast<float>(hx), static_cast<float>(hy),
static_cast<float>(r), grabbed ? handleHot : handle, 1.0f, 0, true);
static_cast<float>(r), handle, 1.0f, 0, true);
if (grabbed)
LICE_FillCircle(bmp, static_cast<float>(hx), static_cast<float>(hy),
static_cast<float>(ir), core, 1.0f, 0, true);
} else {
LICE_FillRect(bmp, hx - r, hy - r, 2 * r, 2 * r, grabbed ? handleHot : handle, 1.0f,
0);
LICE_FillRect(bmp, hx - r, hy - r, 2 * r, 2 * r, handle, 1.0f, 0);
if (grabbed) LICE_FillRect(bmp, hx - ir, hy - ir, 2 * ir, 2 * ir, core, 1.0f, 0);
}
}
}
+13
View File
@@ -40,7 +40,17 @@ namespace {
// this converts a KitColor's 8-bit alpha so a disabled surface composites at the right opacity.
float drawAlpha(const KitColor& c) { return c.a / 255.0f; }
// Font::RegionTitle exists so an ACCENT-colored title can answer to the 3:1 indicator floor
// instead of the 4.5:1 body floor. That entitlement is the font's, not the color's, so the
// metrics are pinned here against theme's thresholds: shrink either one and the build stops
// rather than silently reclassifying every pair drawn in it.
constexpr int kRegionTitlePx = 19;
constexpr int kRegionTitleWeight = FW_BOLD;
static_assert(kRegionTitlePx >= ui::kLargeTextMinBoldPx, "region title must clear WCAG large");
static_assert(kRegionTitleWeight >= FW_BOLD, "the large-bold threshold requires bold, not semi");
struct KitFonts {
LICE_CachedFont regionTitle;
LICE_CachedFont title;
LICE_CachedFont label;
LICE_CachedFont valueMono;
@@ -65,6 +75,7 @@ void loadFont(LICE_CachedFont& dst, int pxHeight, int weight, const char* face)
LICE_CachedFont* fontFor(Font f) {
if (!g_fonts.ready) return nullptr;
switch (f) {
case Font::RegionTitle: return &g_fonts.regionTitle;
case Font::Title: return &g_fonts.title;
case Font::Label: return &g_fonts.label;
case Font::ValueMono: return &g_fonts.valueMono;
@@ -136,6 +147,7 @@ RECT toRect(const KitBox& b) {
void kitFontsInit() {
if (g_fonts.ready) return; // idempotent
loadFont(g_fonts.regionTitle, kRegionTitlePx, kRegionTitleWeight, "Segoe UI");
loadFont(g_fonts.title, 15, FW_SEMIBOLD, "Segoe UI");
loadFont(g_fonts.label, 12, FW_NORMAL, "Segoe UI");
loadFont(g_fonts.valueMono, 12, FW_NORMAL, "Consolas");
@@ -147,6 +159,7 @@ void kitFontsShutdown() {
if (!g_fonts.ready) return; // idempotent
// g_fonts is a static instance, never re-created, so free the HFONTs explicitly:
// handing each a null font with OWNS_HFONT cleans up the prior HFONT (lice_text.h).
g_fonts.regionTitle.SetFromHFont(nullptr, LICE_FONT_FLAG_OWNS_HFONT);
g_fonts.title.SetFromHFont(nullptr, LICE_FONT_FLAG_OWNS_HFONT);
g_fonts.label.SetFromHFont(nullptr, LICE_FONT_FLAG_OWNS_HFONT);
g_fonts.valueMono.SetFromHFont(nullptr, LICE_FONT_FLAG_OWNS_HFONT);
+8 -7
View File
@@ -42,13 +42,14 @@ using ui::ListRowBox;
using ui::Role;
using ui::SliderGeometry;
// The kit's four cached fonts. Consumers pass a Font to text() to pick the size/weight;
// the kit maps it to the matching LICE_CachedFont.
// The kit's cached fonts. Consumers pass a Font to text() to pick the size/weight; the kit
// maps it to the matching LICE_CachedFont.
enum class Font {
Title, // ~15px semibold — region titles, headings
Label, // ~12px regular — labels, body
ValueMono, // ~12px tabular/mono — numbers (dB/ms/notes) that must not jitter
Micro, // ~10px dim — units, counts, keybinding sub-labels
RegionTitle, // 19px BOLD — text that must qualify as WCAG large-scale (3:1 floor)
Title, // ~15px semibold — headings (BODY class: 4.5:1 floor)
Label, // ~12px regular — labels, body
ValueMono, // ~12px tabular/mono — numbers (dB/ms/notes) that must not jitter
Micro, // ~10px dim — units, counts, keybinding sub-labels
};
// Horizontal text alignment for text(). Vertical is always centered in the rect (the kit's
@@ -58,7 +59,7 @@ enum class Align { Left, Center, Right };
// The one place a pure KitColor becomes a LICE_pixel. Defined in draw_kit.cpp.
LICE_pixel toLice(const KitColor& c);
// Creates the four cached fonts once; idempotent. Segoe UI for title/label/micro,
// Creates the cached fonts once; idempotent. Segoe UI for region-title/title/label/micro,
// Consolas (tabular) for value-mono. No-op-safe: if font creation fails, text()
// draws nothing rather than crashing.
void kitFontsInit();
+4 -2
View File
@@ -327,12 +327,14 @@ void drawRegionHeader(LICE_IBitmap* bmp, const RECT& region, const char* title,
// Title, left. The two regions are distinct KINDS of container, so the title carries a
// CATEGORICAL accent (secondary/tertiary mark kinds, never intensity) — Pool = secondary
// teal, Banks = tertiary purple. This is a category mark, NOT the "what's live" signal
// (that stays the primary-lime "Active:" readout beside it).
// (that stays the primary-lime "Active:" readout beside it). Font::RegionTitle, not
// Font::Title: an accent this deep only clears the 3:1 indicator floor, so the type has to
// be large enough to be ENTITLED to it — see draw_kit.cpp's metrics static_assert.
RECT titleRc = hdr;
titleRc.left += 8;
titleRc.right = titleRc.left + 120;
const Role titleRole = poolBtnIsPool ? Role::AccentSecondary : Role::AccentTertiary;
kitText(bmp, toKitBox(titleRc), title, Font::Title, titleRole, Align::Left);
kitText(bmp, toKitBox(titleRc), title, Font::RegionTitle, titleRole, Align::Left);
// Active-bank readout — the UNMISTAKABLE indicator, in the PRIMARY accent role in BOTH
// region headers so the active/capture-target bank is legible even when it is not the
+3 -1
View File
@@ -198,7 +198,9 @@ inline constexpr int kTooltipCharPx = 7;
inline constexpr int kTooltipTextH = 14;
// Client area top to bottom: top toolbar | split body | bottom toolbar | footer.
inline constexpr int kRegionHeaderHeight = 24;
// 26, not 24: Font::RegionTitle's line box (19px em + Segoe UI's leading) is ~25px, and
// DT_VCENTER clips to the rect.
inline constexpr int kRegionHeaderHeight = 26;
inline constexpr int kTabStripHeight = 26;
inline constexpr int kSplitDividerHeight = 3;
inline constexpr int kFullHtBtnWidth = 22;