// draw_kit — the LICE/SWELL shell half of the drawing kit. See draw_kit.h. // SHELL: the only kit file that touches LICE + SWELL. Colors come from `theme`; // geometry from `component_geometry`. DAW-verified, not unit-tested. #include "shell/panel/draw_kit.h" #include #include "core/ui/bank_grid.h" // compressAmplitudeForDisplay — the shared dB display curve // On Windows use native Win32 (windows.h first); on mac/linux SWELL is provided by the host. #ifdef _WIN32 #include #else #include "swell/swell.h" #endif #include "wdltypes.h" #include "lice/lice.h" #include "lice/lice_text.h" namespace reasampler { using audio::ChannelEnvelope; using audio::columnMinMax; using audio::MinMax; using ui::compressAmplitudeForDisplay; using ui::roleColor; using ui::roleColorState; using ui::spectralColor; using ui::WaveformBand; using ui::waveformBand; using ui::WaveformColumnSpan; using ui::waveformColumnSpan; // The one place a pure KitColor becomes a LICE_pixel (LICE_RGBA(r,g,b,a), verified against // lice.h). The theme owns the color; the shell owns the packing. LICE_pixel toLice(const KitColor& c) { return LICE_RGBA(c.r, c.g, c.b, c.a); } namespace { // LICE_FillRect etc. take a float alpha (0..1) separate from the pixel's own alpha byte — // 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; 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::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; 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 gives a flat fill // dimension without a border. 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 (lice.h) takes initial R/G/B/A plus per-axis deltas: ir..ia are the // top-left color, drdy..dady ramp DOWN the height so the bottom row reaches `bottom`. 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(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(lighten(base.r)); top.g = static_cast(lighten(base.g)); top.b = static_cast(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 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"); loadFont(g_fonts.micro, 10, FW_NORMAL, "Segoe UI"); g_fonts.ready = true; } 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); g_fonts.micro.SetFromHFont(nullptr, LICE_FONT_FLAG_OWNS_HFONT); g_fonts.ready = false; } 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); } 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); fillGradient(bmp, b, top, bottom); // Corner radius scales with height, clamped so tiny buttons stay legible. 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(b.x), static_cast(b.y), static_cast(b.width - 1), static_cast(b.height - 1), radius, toLice(borderCol), drawAlpha(borderCol), 0, true); if (label && *label) { text(bmp, b, label, Font::Label, buttonLabelRole(state), Align::Center); } } Role buttonLabelRole(InteractionState state) { // Active fill is the accent — the mark goes in bg/base for contrast; else text/primary. return state == InteractionState::Active ? Role::BgBase : Role::TextPrimary; } void drawSlider(LICE_IBitmap* bmp, const SliderGeometry& geom, InteractionState state) { if (!bmp || geom.track.empty()) return; fillSurface(bmp, geom.track, Role::BgCell, InteractionState::Pressed); 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); } 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; fillSurface(bmp, b, Role::BgCell, state); // Focus ring is text/primary, 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); } // Active rows draw the label in bg/base for contrast against the accent fill. 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(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(ch)]; const WaveformBand band = waveformBand(box.y + ch * bandH, bandH); LICE_Line(bmp, box.x + 2, band.midY, box.x + box.width - 2, band.midY, midCol, 1.0f, 0, false); if (bins.empty() || innerW <= 0) continue; // One filled span per pixel column (see draw_kit.h — gap-free via columnMinMax), then // an antialiased stroke joining each column's extremes to its neighbour's. The fill // alone leaves the outline stepped — a vertical span has no aa to apply — and where two // adjacent columns differ sharply it reads as a comb rather than one envelope. The // stroke is the SAME ink as the fill it edges, so it can only soften the boundary. WaveformColumnSpan prev; for (int col = 0; col < innerW; ++col) { const MinMax mm = columnMinMax(bins, innerW, col); const int x = box.x + 2 + col; const WaveformColumnSpan s = waveformColumnSpan( band, compressAmplitudeForDisplay(mm.max), compressAmplitudeForDisplay(mm.min)); LICE_Line(bmp, x, s.bottom, x, s.top, waveCol, 1.0f, 0, false); if (col > 0) { const float xPrev = static_cast(x - 1); const float xF = static_cast(x); LICE_FLine(bmp, xPrev, static_cast(prev.topF), xF, static_cast(s.topF), waveCol, 1.0f, 0, true); LICE_FLine(bmp, xPrev, static_cast(prev.bottomF), xF, static_cast(s.bottomF), waveCol, 1.0f, 0, true); } prev = s; } } } } // namespace reasampler