Q-W1 pt2: core/shell/app relocation + sub-namespaces; one concrete ui::Rect (LTRB fork retired); slot_map split from bank_book; BankIndex→BankModel; 59/59 green

This commit is contained in:
2026-07-28 20:48:56 -04:00
parent 67a41728f3
commit 847936f813
222 changed files with 2247 additions and 2079 deletions
+330
View File
@@ -0,0 +1,330 @@
#include "core/namespaces.h"
// draw_kit — the LICE/SWELL shell half of the drawing kit. See draw_kit.h.
//
// Compiled into the reaper_reasampler MODULE. SHELL layer: it is the only kit file that
// touches LICE + SWELL. All colors come from the pure `theme` module; all geometry from
// the pure `component_geometry` module. DAW-verified, not unit-tested.
#include "shell/panel/draw_kit.h"
#include <cstddef>
#include "core/ui/bank_grid.h" // compressAmplitudeForDisplay — the shared dB display curve (pure)
// SWELL / LICE. On Windows use native Win32 (windows.h first); on mac/linux SWELL is
// provided by the host. Mirrors bank_panel.cpp's include discipline.
#ifdef _WIN32
#include <windows.h>
#else
#include "swell/swell.h"
#endif
#include "wdltypes.h"
#include "lice/lice.h"
#include "lice/lice_text.h"
namespace reasampler {
// --- KitColor <-> LICE boundary ----------------------------------------------
// The one place a pure KitColor becomes a LICE_pixel. Verified packing: LICE_RGBA(r,g,b,a)
// (lice.h:57). The theme owns the color; the shell owns the packing. Declared in draw_kit.h
// so shell translation units (bank_panel) can use it without duplicating the LICE_RGBA pack.
LICE_pixel toLice(const KitColor& c) {
return LICE_RGBA(c.r, c.g, c.b, c.a);
}
namespace {
// The draw alpha the LICE primitives take (0..1), from the KitColor's 8-bit alpha. Used so
// a disabled surface (alpha 0.4) composites at the right opacity — LICE_FillRect etc. take
// a float alpha argument separate from the pixel's own alpha byte.
float drawAlpha(const KitColor& c) { return c.a / 255.0f; }
// --- Font set (owned by the kit) ---------------------------------------------
struct KitFonts {
LICE_CachedFont title;
LICE_CachedFont label;
LICE_CachedFont valueMono;
LICE_CachedFont micro;
bool ready = false;
};
KitFonts g_fonts;
// Creates one HFONT and hands it to a cached font with OWNS_HFONT so the cached font frees
// it (lice_text.h:41). Negative lfHeight = point-ish pixel height (Win32 convention). The
// face is chosen here so a change is one line.
void loadFont(LICE_CachedFont& dst, int pxHeight, int weight, const char* face) {
HFONT hf = CreateFont(-pxHeight, 0, 0, 0, weight, FALSE, FALSE, FALSE,
DEFAULT_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS,
DEFAULT_QUALITY, DEFAULT_PITCH | FF_DONTCARE, face);
if (!hf) return; // dst stays with no HFONT; DrawText on it renders nothing (safe)
dst.SetFromHFont(hf, LICE_FONT_FLAG_OWNS_HFONT);
dst.SetBkMode(TRANSPARENT);
}
LICE_CachedFont* fontFor(Font f) {
if (!g_fonts.ready) return nullptr;
switch (f) {
case Font::Title: return &g_fonts.title;
case Font::Label: return &g_fonts.label;
case Font::ValueMono: return &g_fonts.valueMono;
case Font::Micro: return &g_fonts.micro;
}
return nullptr;
}
UINT alignFlag(Align a) {
switch (a) {
case Align::Left: return DT_LEFT;
case Align::Center: return DT_CENTER;
case Align::Right: return DT_RIGHT;
}
return DT_LEFT;
}
// A 1px inner highlight on the top edge and shadow on the bottom edge — the vwnd trick
// that gives a flat fill dimension (§2.2). Lightens the top row, darkens the bottom row.
void innerEdges(LICE_IBitmap* bmp, const KitBox& b, float alpha) {
if (b.width < 2 || b.height < 2) return;
const LICE_pixel hi = LICE_RGBA(255, 255, 255, 255);
const LICE_pixel lo = LICE_RGBA(0, 0, 0, 255);
// Top inner highlight (subtle) and bottom inner shadow (subtle), inset 1px from the
// vertical edges so corners read clean.
LICE_Line(bmp, b.x + 1, b.y, b.x + b.width - 2, b.y, hi, 0.10f * alpha, 0, false);
LICE_Line(bmp, b.x + 1, b.y + b.height - 1, b.x + b.width - 2, b.y + b.height - 1,
lo, 0.22f * alpha, 0, false);
}
// The kit's core surface fill: a top-down micro-gradient (a few percent lighter at the
// top) + the inner highlight/shadow. Used by fillSurface and the component draws.
void fillGradient(LICE_IBitmap* bmp, const KitBox& b, const KitColor& top,
const KitColor& bottom) {
if (b.empty()) return;
const float a = drawAlpha(top);
// LICE_GradRect wants initial R/G/B/A (0..1) and per-axis deltas. Verified signature
// lice.h:466 — ir..ia are the top-left color; drdy..dady ramp DOWN the height so the
// bottom row reaches `bottom`. No horizontal ramp (drdx.. = 0).
const float ir = top.r / 255.0f, ig = top.g / 255.0f, ib = top.b / 255.0f;
const float dr = (bottom.r - top.r) / 255.0f;
const float dg = (bottom.g - top.g) / 255.0f;
const float db = (bottom.b - top.b) / 255.0f;
const float h = static_cast<float>(b.height);
LICE_GradRect(bmp, b.x, b.y, b.width, b.height,
ir, ig, ib, a,
0.0f, 0.0f, 0.0f, 0.0f, // no per-x ramp
dr / h, dg / h, db / h, 0.0f, // per-y ramp: top -> bottom
LICE_BLIT_MODE_COPY);
innerEdges(bmp, b, a);
}
// A surface color and its gradient partner (a few percent lighter at the top). Elevation
// reads as a subtle top-lightening of the same hue.
void gradientPair(const KitColor& base, KitColor& top, KitColor& bottom) {
top = base;
// Lighten the top ~7% (clamped by the theme's own values staying < 255 in practice).
auto lighten = [](int v) { int r = v + (v * 7) / 100 + 4; return r > 255 ? 255 : r; };
top.r = static_cast<unsigned char>(lighten(base.r));
top.g = static_cast<unsigned char>(lighten(base.g));
top.b = static_cast<unsigned char>(lighten(base.b));
bottom = base;
}
RECT toRect(const KitBox& b) {
return RECT{b.x, b.y, b.x + b.width, b.y + b.height};
}
} // namespace
// --- Font lifecycle ----------------------------------------------------------
void kitFontsInit() {
if (g_fonts.ready) return; // idempotent
// §3.1 type scale: title ~15px semibold, label ~12px, value-mono ~12px tabular,
// micro ~10px. Segoe UI (universal on the Windows target); Consolas for numerics.
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");
loadFont(g_fonts.micro, 10, FW_NORMAL, "Segoe UI");
g_fonts.ready = true;
}
void kitFontsShutdown() {
if (!g_fonts.ready) return; // idempotent
// LICE_CachedFont's destructor frees its OWNS_HFONT HFONT. Re-assigning an empty font
// via SetFromHFont(nullptr) would leak nothing but also do nothing useful; instead we
// mark not-ready and let the fonts release their HFONTs when g_fonts is reset. Because
// g_fonts is a static instance (not re-created), free the HFONTs explicitly by handing
// each a null font, which OWNS semantics clean up the prior HFONT (lice_text.h:41).
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);
g_fonts.micro.SetFromHFont(nullptr, LICE_FONT_FLAG_OWNS_HFONT);
g_fonts.ready = false;
}
// --- Text --------------------------------------------------------------------
void text(LICE_IBitmap* bmp, const KitBox& box, const char* str,
Font font, const KitColor& color, Align align) {
if (!bmp || !str || box.empty()) return;
LICE_CachedFont* f = fontFor(font);
if (!f) return; // before init or font-create failed: draw nothing (safe)
f->SetTextColor(toLice(color));
f->SetBkMode(TRANSPARENT);
RECT rc = toRect(box);
f->DrawText(bmp, str, -1, &rc,
alignFlag(align) | DT_VCENTER | DT_SINGLELINE | DT_END_ELLIPSIS);
}
void text(LICE_IBitmap* bmp, const KitBox& box, const char* str,
Font font, Role role, Align align) {
text(bmp, box, str, font, roleColor(role), align);
}
// --- Surfaces + components ----------------------------------------------------
void fillSurface(LICE_IBitmap* bmp, const KitBox& box, Role role, InteractionState state) {
if (!bmp || box.empty()) return;
const KitColor base = roleColorState(role, state);
KitColor top, bottom;
gradientPair(base, top, bottom);
fillGradient(bmp, box, top, bottom);
}
void drawButton(LICE_IBitmap* bmp, const KitButtonBox& button, const char* label,
InteractionState state, bool warn) {
const KitBox& b = button.box;
if (!bmp || b.empty()) return;
const Role surfaceRole = warn ? Role::Warn : Role::BgCell;
const KitColor base = roleColorState(surfaceRole, state);
KitColor top, bottom;
gradientPair(base, top, bottom);
// Rounded surface: fill the interior gradient, then an AA rounded border. Corner
// radius scales gently with height, clamped so tiny buttons stay legible.
fillGradient(bmp, b, top, bottom);
const int radius = b.height >= 20 ? 5 : (b.height >= 12 ? 3 : 2);
const KitColor borderCol =
(state == InteractionState::Active || state == InteractionState::Focus)
? roleColor(Role::AccentPrimary)
: roleColor(Role::LineHairline);
LICE_RoundRect(bmp, static_cast<float>(b.x), static_cast<float>(b.y),
static_cast<float>(b.width - 1), static_cast<float>(b.height - 1),
radius, toLice(borderCol), drawAlpha(borderCol), 0, true);
if (label && *label) {
// Active fill is the accent — draw its label in the base bg for contrast; else
// text/primary (disabled dims via the state on the surface, label stays primary
// but the whole control reads recessed).
const Role textRole = (state == InteractionState::Active)
? Role::BgBase
: Role::TextPrimary;
text(bmp, b, label, Font::Label, textRole, Align::Center);
}
}
void drawSlider(LICE_IBitmap* bmp, const SliderGeometry& geom, InteractionState state) {
if (!bmp || geom.track.empty()) return;
// Track groove: the cell surface, recessed (pressed-ish) so it reads as a channel.
fillSurface(bmp, geom.track, Role::BgCell, InteractionState::Pressed);
// Filled portion up to the handle: the accent (hover/dragging brighten it).
if (!geom.filled.empty()) {
const InteractionState fillState =
(state == InteractionState::Hover || state == InteractionState::Dragging)
? InteractionState::Hover
: InteractionState::Active;
KitColor top, bottom;
gradientPair(roleColorState(Role::AccentPrimary, fillState), top, bottom);
fillGradient(bmp, geom.filled, top, bottom);
}
// Handle: a raised knob honoring state.
if (!geom.handle.empty()) {
const KitButtonBox knob{geom.handle};
drawButton(bmp, knob, nullptr, state, /*warn=*/false);
}
}
void drawListRow(LICE_IBitmap* bmp, const ListRowBox& row, const char* label,
int thumbWidth, InteractionState state) {
const KitBox& b = row.box;
if (!bmp || b.empty()) return;
// Row surface: bg/cell transformed by state (hover lightens, active = accent).
fillSurface(bmp, b, Role::BgCell, state);
// Focus ring: a 1px text/primary rectangle, distinct from the accent selection fill.
if (state == InteractionState::Focus) {
const KitColor ring = roleColor(Role::TextPrimary);
LICE_DrawRect(bmp, b.x, b.y, b.width - 1, b.height - 1,
toLice(ring), drawAlpha(ring), 0);
}
// Label in the width after the reserved thumbnail inset. Active rows draw the label in
// bg/base for contrast against the accent fill; else text/primary.
if (label && *label) {
const int inset = thumbWidth > 0 ? thumbWidth + 6 : 6;
KitBox labelBox{b.x + inset, b.y, b.width - inset - 6, b.height};
if (!labelBox.empty()) {
const Role tr = (state == InteractionState::Active) ? Role::BgBase
: Role::TextPrimary;
text(bmp, labelBox, label, Font::Label, tr, Align::Left);
}
}
}
void drawWaveform(LICE_IBitmap* bmp, const KitBox& box, const Envelope& env) {
if (!bmp || box.empty()) return;
const LICE_pixel midCol = toLice(roleColor(Role::LineHairline));
const LICE_pixel waveCol = toLice(roleColor(Role::AccentPrimary));
if (env.empty()) {
const int midY = box.y + box.height / 2;
LICE_Line(bmp, box.x + 2, midY, box.x + box.width - 2, midY,
midCol, 1.0f, 0, false);
return;
}
const int channels = static_cast<int>(env.size());
const int bandH = box.height / channels;
const int innerW = waveformColumnCount(box); // columns: box.x+2 .. box.x+2+innerW-1
for (int ch = 0; ch < channels; ++ch) {
const ChannelEnvelope& bins = env[static_cast<std::size_t>(ch)];
const int bandTop = box.y + ch * bandH;
const int midY = bandTop + bandH / 2;
const double halfSpan = (bandH / 2) - 2;
LICE_Line(bmp, box.x + 2, midY, box.x + box.width - 2, midY,
midCol, 1.0f, 0, false);
if (bins.empty() || innerW <= 0) continue;
// Render one filled vertical span per pixel column. peaks::columnMinMax merges
// all bins that project to column `col` under the exact same partition as
// computeEnvelope used to build the envelope, so every pixel column is covered
// with no gaps regardless of the bins-to-pixels ratio. With one bin per column
// (kWaveformOversample == 1) each span covers the true min/max of exactly the
// frames that fall in that column. Same dB display compression everywhere
// (bank_grid, pure).
for (int col = 0; col < innerW; ++col) {
const MinMax mm = columnMinMax(bins, innerW, col);
const int x = box.x + 2 + col;
int yMax = midY - static_cast<int>(
compressAmplitudeForDisplay(mm.max) * halfSpan);
int yMin = midY - static_cast<int>(
compressAmplitudeForDisplay(mm.min) * halfSpan);
if (yMax < bandTop) yMax = bandTop;
if (yMin > bandTop + bandH - 1) yMin = bandTop + bandH - 1;
LICE_Line(bmp, x, yMin, x, yMax, waveCol, 1.0f, 0, false);
}
}
}
} // namespace reasampler
+143
View File
@@ -0,0 +1,143 @@
#include "core/namespaces.h"
#pragma once
// draw_kit — the LICE-facing SHELL half of the shared drawing kit (Phase L, L1). This is
// the ONE source of drawing for the whole system: every surface (bank_panel now; the VST
// editor + embed strip at L3) fills, buttons, rows, sliders, waveforms, and — above all —
// draws TEXT through this kit, so a control looks identical everywhere because it is the
// same kit function. It replaces the flat LICE_FillRect blocks and raw-GDI DrawTextA with
// gradient/AA surfaces (the vwnd micro-gradient + inner highlight/shadow trick) and cached
// anti-aliased text (LICE_CachedFont), honoring the interaction-state model.
//
// PURE/SHELL SPLIT (CLAUDE.md §load-bearing): this file is SHELL — it touches LICE and
// SWELL (HFONT). All palette decisions come from the pure `theme` module (role -> KitColor);
// all layout/hit-test from the pure `component_geometry` / mode_switch / etc. modules. This
// file only turns those pure answers into LICE calls. It is DAW-verified, not unit-tested.
//
// FONT LIFECYCLE (owned here): the kit holds a small set of LICE_CachedFonts (title / label
// / value-mono / micro). kitFontsInit() creates them once (from HFONTs handed off with
// LICE_FONT_FLAG_OWNS_HFONT, so the cached font frees the HFONT itself — verified in
// lice_text.h §SetFromHFont doc: "OWNS means LICE_IFont will clean up hfont on font change
// or exit"). kitFontsShutdown() deletes the cached fonts. The consumer calls init on panel
// open and shutdown on close/teardown. text() no-ops safely before init (defensive), so a
// draw that races construction never crashes.
//
// DOUBLE-BUFFER DISCIPLINE (§3.5 "zero-jank"): every function here draws into the caller's
// offscreen LICE_IBitmap; the caller BitBlt's once. Nothing here draws direct-to-DC.
#include "core/ui/component_geometry.h" // KitBox / SliderGeometry — the pure geometry the shell draws
#include "core/audio/peaks.h" // Envelope — the waveform primitive's input
#include "core/ui/theme.h" // Role / InteractionState / KitColor / TextClass
// LICE types at the boundary (this is the shell half). LICE_IBitmap is forward-declared
// to keep the header light. LICE_pixel is a typedef (unsigned int) — not forward-declarable
// — so the full lice.h is included only for the toLice() declaration; on Windows lice.h
// pulls in <windows.h>, which is fine since draw_kit.h is shell-only and never included by
// a pure module.
#ifdef _WIN32
#include <windows.h>
#endif
#include "lice/lice.h"
class LICE_IBitmap;
namespace reasampler {
// The kit's four cached fonts (§3.1 type scale). 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
};
// Horizontal text alignment for text(). Vertical is always centered in the rect (the kit's
// single-line convention); a caller wanting multi-line composes rows itself.
enum class Align { Left, Center, Right };
// --- KitColor → LICE_pixel conversion ----------------------------------------
// The one place a pure KitColor becomes a LICE_pixel. Declared here so any shell
// translation unit that already includes draw_kit.h can use it without duplicating
// the LICE_RGBA packing. Defined in draw_kit.cpp.
LICE_pixel toLice(const KitColor& c);
// --- Font lifecycle (owned by the kit) ---------------------------------------
// Creates the four cached fonts once. Idempotent: a second call before shutdown is a no-op
// (the kit already holds live fonts). Safe to call on every panel open. Uses the platform
// UI sans (Segoe UI) for title/label/micro and a tabular mono (Consolas) for value-mono;
// the exact HFONT is created here, so a face change is a one-line edit. NO-OP-SAFE: if font
// creation fails, text() degrades to drawing nothing rather than crashing.
void kitFontsInit();
// Deletes the cached fonts (which free their owned HFONTs — LICE_FONT_FLAG_OWNS_HFONT).
// Idempotent. The consumer calls this on panel close / extension shutdown.
void kitFontsShutdown();
// --- Text (the single biggest "temple os -> modern" lever) -------------------
// Draws a single line of AA cached-font text in `color` inside `box`, horizontally aligned
// per `align` and vertically centered, clipped with an end-ellipsis. This REPLACES the
// GDI SetTextColor + DrawText path. No-op (safe) before kitFontsInit() or on a null bitmap.
void text(LICE_IBitmap* bmp, const KitBox& box, const char* str,
Font font, const KitColor& color, Align align);
// Convenience overload: text in a palette ROLE's color (the common case — the shell almost
// always wants text/primary or text/dim, not a raw color).
void text(LICE_IBitmap* bmp, const KitBox& box, const char* str,
Font font, Role role, Align align);
// --- Surfaces + components ----------------------------------------------------
// The kit's foundational fill: a micro-gradient (a few percent lighter at the top, via
// LICE_GradRect) plus a 1px inner top-highlight and bottom-shadow — the vwnd trick that
// kills the flat look (§2.2). Every button/row/cell fills through this so elevation reads
// without a border. `role` picks the surface color; `state` transforms it per the
// interaction model (hover lightens, pressed darkens, disabled desaturates, etc.).
void fillSurface(LICE_IBitmap* bmp, const KitBox& box, Role role, InteractionState state);
// A rounded, gradient-filled button with the inner highlight/shadow and a centered label,
// honoring the interaction state. `warn == true` swaps the surface to the warn role (for
// byte-deleting verbs like prune/delete) — the only place warn is drawn. A degenerate box
// is a no-op.
void drawButton(LICE_IBitmap* bmp, const KitButtonBox& button, const char* label,
InteractionState state, bool warn);
// A horizontal slider: the track groove, the accent-filled portion up to the handle, and
// the handle (a raised knob honoring state — hover/dragging brighten it). `geom` is the
// pure SliderGeometry the caller computed; the kit only draws it. Degenerate geom is a no-op.
void drawSlider(LICE_IBitmap* bmp, const SliderGeometry& geom, InteractionState state);
// A selectable list row: the row surface (rest/hover/active/focus via state), an optional
// leading thumbnail area reserved at `thumbWidth` px (0 for none — the caller draws the
// thumbnail into the returned-by-convention left inset), and a left-aligned label in the
// remaining width. Focus draws a 1px text/primary ring distinct from the accent selection
// fill. A degenerate row is a no-op.
void drawListRow(LICE_IBitmap* bmp, const ListRowBox& row, const char* label,
int thumbWidth, InteractionState state);
// waveformColumnCount — declared in component_geometry.h (already included above). Returns
// the drawable column count inside `box` (box.width minus the fixed 2px insets each side).
// Callers pass this value directly as the `binCount` argument to peaks::computeEnvelope;
// overbinning (more bins than columns) costs memory and CPU without changing a rendered
// pixel — peaks::columnMinMax's exact partition already makes the draw gap-free.
// Multiplier kept at 1 (no oversampling). kWaveformOversample is present only so existing
// call sites `kWaveformOversample * waveformColumnCount(box)` compile unchanged; a value of
// 1 means they request exactly one bin per column, which is correct. The gap-free render
// comes from peaks::columnMinMax's exact partition, NOT from extra bins.
inline constexpr int kWaveformOversample = 1;
// A waveform envelope drawn as a min/max plot over the bg/panel surface: a midline per
// channel and one accent vertical span PER PIXEL COLUMN, each column covering the true
// extremes of every bin that projects to it (peaks::columnMinMax — gap-free at any
// bins-to-pixels ratio because columnMinMax partitions bins exactly as computeEnvelope
// does, so every pixel column is always covered). The ONE waveform shape in the system:
// the dock-panel thumbnail, the browser cards, and the editor hero all render through
// this. `box` is the draw region; `env` is the per-channel min/max envelope from
// peaks::computeEnvelope, sized to waveformColumnCount(box) bins (clamped to frame count).
// An empty env draws just the midline. The caller fills the surface first (or passes a
// box already filled); this draws only the wave + midline.
void drawWaveform(LICE_IBitmap* bmp, const KitBox& box, const Envelope& env);
} // namespace reasampler