Merge pL-w3-vst-restyle: Phase L L3 — VST editor + embed strip restyled through the L1 kit (B pastel-on-grey + pastel spectral keyboard strip)
This commit is contained in:
+12
-1
@@ -978,6 +978,11 @@ if(WIN32 AND EXISTS "${VST3_SDK}/public.sdk/source/main/pluginfactory.cpp")
|
||||
src/vst/reasampler_editor.cpp
|
||||
src/vst/reasampler_embed.cpp
|
||||
src/vst/reaper_bridge.cpp
|
||||
# The Phase L (L1) draw kit — the ONE source of drawing the editor + embed shells
|
||||
# now consume (L3). Compiled into the MODULE (not a static lib) for the same reason
|
||||
# bank_panel does: it is the only kit TU touching LICE, and its cached-font engine
|
||||
# (LICE_CachedFont) needs the LICE_SRC TUs below linked into this artifact.
|
||||
src/draw_kit.cpp
|
||||
# SDK module entry — compiled into the module (not the static lib) so the
|
||||
# InitDll/ExitDll dll exports survive the link (see vst3_sdk note above).
|
||||
${VST3_SDK}/public.sdk/source/main/dllmain.cpp
|
||||
@@ -1009,9 +1014,15 @@ if(WIN32 AND EXISTS "${VST3_SDK}/public.sdk/source/main/pluginfactory.cpp")
|
||||
# control-panel layout + slider/toggle value<->pixel mapping the editor's parameter surface
|
||||
# draws + routes against. browser_scroll pulls capture_browser transitively; param_slider +
|
||||
# note_entry link editor_geometry / the stdlib only. All engine-free, DAW-verified in the shell.
|
||||
# theme + component_geometry + bank_grid: the Phase L (L1) draw-kit's PURE deps (L3). The
|
||||
# kit draws every editor/embed surface by palette ROLE via draw_kit.cpp (compiled into the
|
||||
# module above): theme supplies role->KitColor + spectralColor, component_geometry the
|
||||
# KitBox/button/slider geometry, bank_grid the shared dB display-compression the waveform
|
||||
# primitive uses. All REAPER/SWELL-free.
|
||||
target_link_libraries(reasampler_vst PRIVATE vst3_sdk editor_geometry bridge_marshal
|
||||
sample_map capture_paths embed_strip app_version capture_browser keyboard_strip
|
||||
waveform_view bank_sync browser_scroll note_entry param_slider)
|
||||
waveform_view bank_sync browser_scroll note_entry param_slider
|
||||
theme component_geometry bank_grid)
|
||||
# SDK_INC gives reaper_vst3_interfaces.h + reaper_plugin_functions.h for the bridge;
|
||||
# WDL_INC gives LICE for the editor. The VST3 SDK headers come from vst3_sdk PUBLIC.
|
||||
target_include_directories(reasampler_vst PRIVATE ${SDK_INC} ${WDL_INC})
|
||||
|
||||
+380
-162
@@ -13,9 +13,12 @@
|
||||
#include "browser_scroll.h" // S12 scroll-window + thumb + type-to-filter search geometry
|
||||
#include "capture_browser.h"
|
||||
#include "capture_paths.h" // resolveBankFile (shared M4 path resolution)
|
||||
#include "component_geometry.h" // KitBox — the kit text/fill draw box (Phase L, L3)
|
||||
#include "draw_kit.h" // the L1 draw kit: fillSurface/drawButton/text/drawWaveform (L3)
|
||||
#include "editor_geometry.h" // Rect, contains
|
||||
#include "ext_keys.h"
|
||||
#include "keyboard_strip.h"
|
||||
#include "theme.h" // Role / InteractionState / KitColor / spectralColor (L3)
|
||||
#include "note_entry.h" // S12 direct numeric note-entry parse
|
||||
#include "param_slider.h" // S12/S15/S16 control-surface layout + value<->pixel mapping
|
||||
#include "peaks.h" // computeEnvelope
|
||||
@@ -60,42 +63,31 @@ constexpr int kSetupHeight = 176; // the single-capture setup surface (labels +
|
||||
constexpr int kStripBandHeight = 40;
|
||||
constexpr int kWaveformHeight = 72; // the S11 waveform band inside the setup surface
|
||||
|
||||
// Palette — house style, mirrored from bank_panel's dark theme so the instrument reads as
|
||||
// the same tool. (Phase L's L1 kit replaces these flat fills later; not gated on it.)
|
||||
const LICE_pixel kColBackground = LICE_RGBA(28, 28, 30, 255);
|
||||
const LICE_pixel kColTitleBg = LICE_RGBA(20, 20, 22, 255);
|
||||
const LICE_pixel kColCardBg = LICE_RGBA(44, 44, 48, 255);
|
||||
const LICE_pixel kColCardSelBg = LICE_RGBA(48, 72, 64, 255);
|
||||
const LICE_pixel kColCardBorder = LICE_RGBA(70, 70, 76, 255);
|
||||
const LICE_pixel kColCardSelBorder = LICE_RGBA(120, 200, 160, 255);
|
||||
const LICE_pixel kColTabBg = LICE_RGBA(36, 36, 40, 255);
|
||||
const LICE_pixel kColTabActiveBg = LICE_RGBA(58, 96, 84, 255);
|
||||
const LICE_pixel kColThumb = LICE_RGBA(120, 200, 160, 255);
|
||||
const LICE_pixel kColStripBg = LICE_RGBA(36, 36, 40, 255);
|
||||
const LICE_pixel kColStripKey = LICE_RGBA(52, 52, 58, 255);
|
||||
const LICE_pixel kColRootMarker = LICE_RGBA(120, 200, 160, 255);
|
||||
const LICE_pixel kColWaveBg = LICE_RGBA(24, 24, 26, 255);
|
||||
const LICE_pixel kColWaveform = LICE_RGBA(120, 200, 160, 255);
|
||||
const LICE_pixel kColStartMarker = LICE_RGBA(230, 200, 120, 255); // start point (amber)
|
||||
const LICE_pixel kColLoopMarker = LICE_RGBA(120, 170, 230, 255); // loop start/end (blue)
|
||||
const LICE_pixel kColLoopRegion = LICE_RGBA(120, 170, 230, 60); // loop span fill (faint)
|
||||
const LICE_pixel kColZoneBar = LICE_RGBA(58, 96, 84, 255);
|
||||
const LICE_pixel kColZoneBarSel = LICE_RGBA(120, 200, 160, 255);
|
||||
const COLORREF kRgbText = RGB(210, 230, 220);
|
||||
const COLORREF kRgbDim = RGB(140, 150, 146);
|
||||
// Marker roles (Phase L, L3) — semantic, drawn through the kit's palette. The waveform's
|
||||
// start point + the sustain-loop ends are CATEGORICAL kinds (a distinct affordance class,
|
||||
// §2.1), not the live/active layer, so they take the categorical accents: start = teal
|
||||
// (secondary), loop start/end = purple (tertiary). The loop-span fill is a faint purple.
|
||||
constexpr Role kRoleStartMarker = Role::AccentSecondary;
|
||||
constexpr Role kRoleLoopMarker = Role::AccentTertiary;
|
||||
|
||||
// ANSI path -- string literals must stay ASCII until the Phase L type kit lands.
|
||||
void drawText(LICE_IBitmap* bmp, const Rect& r, const char* s, COLORREF col,
|
||||
UINT fmt = DT_LEFT | DT_VCENTER | DT_SINGLELINE | DT_NOPREFIX) {
|
||||
HDC dc = bmp->getDC();
|
||||
SetBkMode(dc, TRANSPARENT);
|
||||
SetTextColor(dc, col);
|
||||
RECT gr{r.left, r.top, r.right, r.bottom};
|
||||
DrawTextA(dc, s, -1, &gr, fmt | DT_END_ELLIPSIS);
|
||||
// --- Rect <-> kit adapters (Phase L, L3) -------------------------------------
|
||||
//
|
||||
// The editor's own sub-rect type is `Rect` (editor_geometry); the kit draws against `KitBox`
|
||||
// (component_geometry). This is the single boundary that bridges them so every draw routes
|
||||
// through the L1 kit (theme roles + draw_kit), retiring the shell's raw LICE_RGBA palette +
|
||||
// GDI DrawTextA path.
|
||||
KitBox toKitBox(const Rect& r) {
|
||||
return KitBox{r.left, r.top, r.width(), r.height()};
|
||||
}
|
||||
|
||||
void drawTextCentered(LICE_IBitmap* bmp, const Rect& r, const char* s, COLORREF col) {
|
||||
drawText(bmp, r, s, col, DT_CENTER | DT_VCENTER | DT_SINGLELINE | DT_NOPREFIX);
|
||||
// Kit text in a palette ROLE (the common case). Left/Right/Center via Align.
|
||||
void kitText(LICE_IBitmap* bmp, const Rect& r, const char* s, Font font, Role role,
|
||||
Align align = Align::Left) {
|
||||
text(bmp, toKitBox(r), s, font, role, align);
|
||||
}
|
||||
|
||||
void kitTextCentered(LICE_IBitmap* bmp, const Rect& r, const char* s, Font font, Role role) {
|
||||
text(bmp, toKitBox(r), s, font, role, Align::Center);
|
||||
}
|
||||
|
||||
// A short MIDI-note label ("C4", "F#3") for the root badge. Middle C (60) is C4 (the
|
||||
@@ -109,20 +101,12 @@ std::string noteLabel(int note) {
|
||||
return std::string(kNames[note % 12]) + std::to_string(octave);
|
||||
}
|
||||
|
||||
// Draw a mono peak envelope centered vertically in `r` (mirror of bank_panel::drawThumbnail,
|
||||
// single channel). Each bin is a vertical line from its min to its max about the midline.
|
||||
// Draw a peak envelope in `r` through the kit's shared waveform primitive (Phase L, L3):
|
||||
// midline + accent-primary min/max columns with the same dB display compression the dock
|
||||
// panel thumbnail uses, so a waveform reads identically wherever it is drawn. The caller has
|
||||
// already filled the surface behind it (bg/panel), matching drawWaveform's contract.
|
||||
void drawEnvelope(LICE_IBitmap* bmp, const Rect& r, const Envelope& env) {
|
||||
if (r.width() <= 0 || r.height() <= 0 || env.empty() || env[0].empty()) return;
|
||||
const ChannelEnvelope& ch = env[0];
|
||||
const int mid = r.top + r.height() / 2;
|
||||
const int halfH = r.height() / 2;
|
||||
const int bins = static_cast<int>(ch.size());
|
||||
for (int x = 0; x < r.width() && x < bins; ++x) {
|
||||
const MinMax& mm = ch[static_cast<std::size_t>(x)];
|
||||
const int yTop = mid - static_cast<int>(mm.max * halfH);
|
||||
const int yBot = mid - static_cast<int>(mm.min * halfH);
|
||||
LICE_Line(bmp, r.left + x, yTop, r.left + x, yBot, kColThumb, 1.0f, 0, false);
|
||||
}
|
||||
drawWaveform(bmp, toKitBox(r), env);
|
||||
}
|
||||
|
||||
// A display name for a bank sample id from the snapshotted list ("?" if the id no longer
|
||||
@@ -522,6 +506,13 @@ void ReaSamplerEditor::attachedToParent() {
|
||||
classRegistered = true;
|
||||
}
|
||||
|
||||
// Create the kit's cached AA fonts before the first paint (Phase L, L3). Idempotent, so a
|
||||
// reopen (or a co-resident embed strip that also inits) is a cheap no-op. NOT torn down on
|
||||
// editor close: the embed strip in the SAME binary shares the kit's process-global font
|
||||
// set, so a per-view shutdown could free fonts still in use by the other view. The tiny
|
||||
// static HFONT set is reclaimed by the OS at module unload. See the L3 handoff note.
|
||||
kitFontsInit();
|
||||
|
||||
refreshFromBank();
|
||||
|
||||
const ViewRect& r = getRect();
|
||||
@@ -661,6 +652,50 @@ ChannelToggleRects channelToggleRects(const Rect& area) {
|
||||
const Rect mono{stereo.left - kChanSegW, top, stereo.left, top + kChanSegH};
|
||||
return {mono, stereo};
|
||||
}
|
||||
|
||||
// Draw the pastel spectral keyboard-strip background (Phase L, L3) — the signature surface.
|
||||
// Fills each MIDI key column with its spectral hue (spectralColor over note/127), then draws
|
||||
// faint per-octave hairline ticks for orientation. Shared by the setup face + the Zones strip
|
||||
// so both read as the same spectrum. `stripArea` is the absolute strip rect.
|
||||
void drawSpectralStrip(LICE_IBitmap* bmp, const Rect& stripArea) {
|
||||
if (stripArea.width() <= 0 || stripArea.height() <= 0) return;
|
||||
const StripLayout sl = layoutStrip(stripArea.width(), stripArea.height());
|
||||
const int sx = stripArea.left;
|
||||
const int sy = stripArea.top;
|
||||
const int h = stripArea.height();
|
||||
// A pastel spectral column per key. Each key's local x from keyRect; fill from this key's
|
||||
// left to the next key's left so the sweep tiles with no gaps. Low alpha keeps it a quiet
|
||||
// backdrop the root/zone marks sit over.
|
||||
for (int n = 0; n <= 127; ++n) {
|
||||
const Rect k = keyRect(sl, n);
|
||||
const int x0 = k.left + sx;
|
||||
const int x1 = (n < 127) ? keyRect(sl, n + 1).left + sx : stripArea.right;
|
||||
const int cw = (std::max)(1, x1 - x0);
|
||||
const KitColor hue = spectralColor(static_cast<double>(n) / 127.0);
|
||||
LICE_FillRect(bmp, x0, sy, cw, h, toLice(hue), 0.55f, 0);
|
||||
}
|
||||
// Faint per-octave key ticks (hairline role) for orientation.
|
||||
const LICE_pixel tick = toLice(roleColor(Role::LineHairline));
|
||||
for (int n = 0; n <= 127; n += 12) {
|
||||
const Rect k = keyRect(sl, n);
|
||||
LICE_Line(bmp, k.left + sx, sy, k.left + sx, sy + h, tick, 1.0f, 0, false);
|
||||
}
|
||||
}
|
||||
|
||||
// Draw the single-capture root marker on the strip: an accent-primary bar with a soft STATIC
|
||||
// glow (a wider, lower-alpha accent bar behind it) — the "this is live" mark. Never animated.
|
||||
void drawRootMarker(LICE_IBitmap* bmp, const Rect& stripArea, const StripLayout& sl, int root) {
|
||||
const int sx = stripArea.left;
|
||||
const int sy = stripArea.top;
|
||||
const int h = stripArea.height();
|
||||
const Rect marker = rootMarkerRect(sl, root);
|
||||
const int mw = (std::max)(2, marker.width());
|
||||
const LICE_pixel accent = toLice(roleColor(Role::AccentPrimary));
|
||||
const LICE_pixel glow = toLice(roleColor(Role::AccentHot));
|
||||
// Static glow: a wider low-alpha halo behind the crisp bar (a drawn state, not a pulse).
|
||||
LICE_FillRect(bmp, marker.left + sx - 3, sy, mw + 6, h, glow, 0.30f, 0);
|
||||
LICE_FillRect(bmp, marker.left + sx, sy, mw, h, accent, 1.0f, 0);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
void ReaSamplerEditor::paint(HDC hdc) {
|
||||
@@ -671,13 +706,14 @@ void ReaSamplerEditor::paint(HDC hdc) {
|
||||
if (w <= 0 || h <= 0) return;
|
||||
|
||||
LICE_SysBitmap bmp(w, h);
|
||||
LICE_Clear(&bmp, kColBackground);
|
||||
LICE_Clear(&bmp, toLice(roleColor(Role::BgBase)));
|
||||
|
||||
const EditorBands bands = computeBands(w, h);
|
||||
|
||||
// Title band: product name + live readout.
|
||||
LICE_FillRect(&bmp, bands.title.left, bands.title.top, bands.title.width(),
|
||||
bands.title.height(), kColTitleBg, 1.0f, 0);
|
||||
// Title band: product name + live readout. Standard B palette — the beta channel gets NO
|
||||
// distinct accent (settled 2026-07-27); the channel-derived vstPluginName is the only
|
||||
// beta-vs-stable signal. bg/panel one step up from the canvas, primary-role title text.
|
||||
fillSurface(&bmp, toKitBox(bands.title), Role::BgPanel, InteractionState::Rest);
|
||||
std::string title = reasampler::vstPluginName(); // channel-derived (S18)
|
||||
if (processor_ && processor_->bridge().isConnected()) {
|
||||
if (samples_.empty()) title += " [bank empty]";
|
||||
@@ -689,18 +725,27 @@ void ReaSamplerEditor::paint(HDC hdc) {
|
||||
}
|
||||
Rect titleText{bands.title.left + 8, bands.title.top, bands.title.right - 8,
|
||||
bands.title.bottom};
|
||||
drawText(&bmp, titleText, title.c_str(), kRgbText);
|
||||
kitText(&bmp, titleText, title.c_str(), Font::Title, Role::TextPrimary);
|
||||
|
||||
// Toggle band: Browser | Zones.
|
||||
// Toggle band: Browser | Zones — two segmented switches. Active = accent-primary fill
|
||||
// ("this is live"); hover lightens the inactive segment toward accent/hot.
|
||||
const bool inZones = (view_ == View::kZones);
|
||||
LICE_FillRect(&bmp, bands.toggleBrowser.left, bands.toggleBrowser.top,
|
||||
bands.toggleBrowser.width(), bands.toggleBrowser.height(),
|
||||
inZones ? kColTabBg : kColTabActiveBg, 1.0f, 0);
|
||||
LICE_FillRect(&bmp, bands.toggleZones.left, bands.toggleZones.top,
|
||||
bands.toggleZones.width(), bands.toggleZones.height(),
|
||||
inZones ? kColTabActiveBg : kColTabBg, 1.0f, 0);
|
||||
drawTextCentered(&bmp, bands.toggleBrowser, "Browser", kRgbText);
|
||||
drawTextCentered(&bmp, bands.toggleZones, "Zones", kRgbText);
|
||||
const InteractionState browserState =
|
||||
!inZones ? InteractionState::Active
|
||||
: (isHovered(HoverKind::kToggleBrowser, -1) ? InteractionState::Hover
|
||||
: InteractionState::Rest);
|
||||
const InteractionState zonesState =
|
||||
inZones ? InteractionState::Active
|
||||
: (isHovered(HoverKind::kToggleZones, -1) ? InteractionState::Hover
|
||||
: InteractionState::Rest);
|
||||
fillSurface(&bmp, toKitBox(bands.toggleBrowser), Role::BgCell, browserState);
|
||||
fillSurface(&bmp, toKitBox(bands.toggleZones), Role::BgCell, zonesState);
|
||||
// Active segment's label sits on the accent fill — draw it in bg/base for contrast
|
||||
// (the tight text-on-pastel-fill pair, §4); the inactive label stays text/primary.
|
||||
kitTextCentered(&bmp, bands.toggleBrowser, "Browser", Font::Label,
|
||||
!inZones ? Role::BgBase : Role::TextPrimary);
|
||||
kitTextCentered(&bmp, bands.toggleZones, "Zones", Font::Label,
|
||||
inZones ? Role::BgBase : Role::TextPrimary);
|
||||
|
||||
if (view_ == View::kZones) {
|
||||
paintZones(&bmp, w, h);
|
||||
@@ -714,11 +759,12 @@ void ReaSamplerEditor::paint(HDC hdc) {
|
||||
if (dropHintTicks_ > 0) {
|
||||
const int bannerH = (std::min)(kTitleHeight + 8, h);
|
||||
Rect banner{0, bands.toggleZones.bottom, w, bands.toggleZones.bottom + bannerH};
|
||||
LICE_FillRect(&bmp, banner.left, banner.top, banner.width(), banner.height(),
|
||||
kColTabActiveBg, 1.0f, 0);
|
||||
drawTextCentered(&bmp, banner,
|
||||
"Dropped here isn't loaded yet - drop files onto the ReaSampler bank panel to add them.",
|
||||
kRgbText);
|
||||
// A transient notice, not the live layer — draw it on the accent-tertiary categorical
|
||||
// hue with a dark label so it reads as "attention, not action".
|
||||
fillSurface(&bmp, toKitBox(banner), Role::AccentTertiary, InteractionState::Rest);
|
||||
kitTextCentered(&bmp, banner,
|
||||
"Dropped here isn't loaded yet - drop files onto the ReaSampler bank panel to add them.",
|
||||
Font::Label, Role::BgBase);
|
||||
}
|
||||
|
||||
BitBlt(hdc, 0, 0, w, h, bmp.getDC(), 0, 0, SRCCOPY);
|
||||
@@ -735,11 +781,10 @@ void ReaSamplerEditor::paintEmptyState(LICE_IBitmap* bmp, const Rect& area) {
|
||||
// discoverable here regardless of whether a drop ever lands on THIS window.
|
||||
Rect primary{area.left, area.top, area.right, area.top + area.height() / 2};
|
||||
Rect hint{area.left, primary.bottom, area.right, area.bottom};
|
||||
drawTextCentered(bmp, primary, msg, kRgbDim);
|
||||
drawTextCentered(bmp,
|
||||
hint,
|
||||
"To add a sample: drop a file onto the ReaSampler bank panel (the docked window).",
|
||||
kRgbDim);
|
||||
kitTextCentered(bmp, primary, msg, Font::Label, Role::TextDim);
|
||||
kitTextCentered(bmp, hint,
|
||||
"To add a sample: drop a file onto the ReaSampler bank panel (the docked window).",
|
||||
Font::Micro, Role::TextDim);
|
||||
}
|
||||
|
||||
void ReaSamplerEditor::paintBrowser(LICE_IBitmap* bmp, int w, int h) {
|
||||
@@ -755,14 +800,25 @@ void ReaSamplerEditor::paintBrowser(LICE_IBitmap* bmp, int w, int h) {
|
||||
const Rect searchBox = searchBoxRect(fullBrowserArea.width());
|
||||
const Rect searchAbs{fullBrowserArea.left + searchBox.left, fullBrowserArea.top + searchBox.top,
|
||||
fullBrowserArea.left + searchBox.right, fullBrowserArea.top + searchBox.bottom};
|
||||
LICE_FillRect(bmp, searchAbs.left, searchAbs.top, searchAbs.width(), searchAbs.height(),
|
||||
searchFocused_ ? kColTabActiveBg : kColTabBg, 1.0f, 0);
|
||||
// A focused search box lifts to the focus state (a nudge toward the primary accent + a
|
||||
// text/primary ring drawn below); hover lightens; else the resting cell surface.
|
||||
const InteractionState searchState =
|
||||
searchFocused_ ? InteractionState::Focus
|
||||
: (isHovered(HoverKind::kSearchBox, -1) ? InteractionState::Hover
|
||||
: InteractionState::Rest);
|
||||
fillSurface(bmp, toKitBox(searchAbs), Role::BgCell, searchState);
|
||||
if (searchFocused_) {
|
||||
const KitColor ring = roleColor(Role::TextPrimary);
|
||||
LICE_DrawRect(bmp, searchAbs.left, searchAbs.top, searchAbs.width() - 1,
|
||||
searchAbs.height() - 1, toLice(ring), 1.0f, 0);
|
||||
}
|
||||
{
|
||||
std::string sb = searchQuery_.empty()
|
||||
? std::string("Search captures...")
|
||||
: ("Search: " + searchQuery_ + (searchFocused_ ? "_" : ""));
|
||||
Rect sbText{searchAbs.left + 6, searchAbs.top, searchAbs.right - 6, searchAbs.bottom};
|
||||
drawText(bmp, sbText, sb.c_str(), searchQuery_.empty() ? kRgbDim : kRgbText);
|
||||
kitText(bmp, sbText, sb.c_str(), Font::Label,
|
||||
searchQuery_.empty() ? Role::TextDim : Role::TextPrimary);
|
||||
}
|
||||
|
||||
const Rect browserArea{fullBrowserArea.left, searchAbs.bottom, fullBrowserArea.right, setupTop};
|
||||
@@ -782,9 +838,13 @@ void ReaSamplerEditor::paintBrowser(LICE_IBitmap* bmp, int w, int h) {
|
||||
const std::string label = (i == 0) ? "All" : banks_[static_cast<std::size_t>(i - 1)].displayName;
|
||||
const bool active = (i == 0) ? activeFilterBankId_.empty()
|
||||
: (banks_[static_cast<std::size_t>(i - 1)].id == activeFilterBankId_);
|
||||
LICE_FillRect(bmp, t.left, t.top, t.width(), t.height(),
|
||||
active ? kColTabActiveBg : kColTabBg, 1.0f, 0);
|
||||
drawTextCentered(bmp, t, label.c_str(), kRgbText);
|
||||
const InteractionState state =
|
||||
active ? InteractionState::Active
|
||||
: (isHovered(HoverKind::kFilterTab, i) ? InteractionState::Hover
|
||||
: InteractionState::Rest);
|
||||
fillSurface(bmp, toKitBox(t), Role::BgCell, state);
|
||||
kitTextCentered(bmp, t, label.c_str(), Font::Label,
|
||||
active ? Role::BgBase : Role::TextPrimary);
|
||||
}
|
||||
|
||||
// Cards: only the S12 visible window at the current scroll offset (a bank longer than the
|
||||
@@ -808,22 +868,29 @@ void ReaSamplerEditor::paintBrowser(LICE_IBitmap* bmp, int w, int h) {
|
||||
|
||||
const SampleChoice& s = visible_[static_cast<std::size_t>(i)];
|
||||
const bool sel = (s.id == selectedId_);
|
||||
LICE_FillRect(bmp, content.left, content.top, content.width(), content.height(),
|
||||
sel ? kColCardSelBg : kColCardBg, 1.0f, 0);
|
||||
// Card surface: a plain cell (hover lightens), with the SELECTED pick marked by an
|
||||
// accent-primary border (the "this is live" signal, §2.1) — the same convention the
|
||||
// dock panel's L7 selection uses (normal cell + accent border, no inversion). The
|
||||
// waveform thumbnail draws over bg/panel so its accent columns read against the cell.
|
||||
const InteractionState cardState =
|
||||
isHovered(HoverKind::kCard, i) ? InteractionState::Hover : InteractionState::Rest;
|
||||
fillSurface(bmp, toKitBox(content), Role::BgCell, cardState);
|
||||
const KitColor cardBorder =
|
||||
sel ? roleColor(Role::AccentPrimary) : roleColor(Role::LineHairline);
|
||||
LICE_DrawRect(bmp, content.left, content.top, content.width() - 1, content.height() - 1,
|
||||
sel ? kColCardSelBorder : kColCardBorder, 1.0f, 0);
|
||||
toLice(cardBorder), 1.0f, 0);
|
||||
drawEnvelope(bmp, thumb, thumbnailFor(s.id, bins));
|
||||
|
||||
// Name + root/key badge under the thumbnail.
|
||||
std::string caption = s.displayName.empty() ? s.id : s.displayName;
|
||||
Rect nameR{labelR.left + 3, labelR.top, labelR.right - 3, labelR.top + labelR.height() / 2};
|
||||
Rect badgeR{labelR.left + 3, nameR.bottom, labelR.right - 3, labelR.bottom};
|
||||
drawText(bmp, nameR, caption.c_str(), kRgbText);
|
||||
kitText(bmp, nameR, caption.c_str(), Font::Label, Role::TextPrimary);
|
||||
std::string badge;
|
||||
if (s.rootNote) badge = "root " + noteLabel(*s.rootNote);
|
||||
else if (s.key) badge = *s.key;
|
||||
else badge = "root -";
|
||||
drawText(bmp, badgeR, badge.c_str(), kRgbDim);
|
||||
kitText(bmp, badgeR, badge.c_str(), Font::Micro, Role::TextDim);
|
||||
}
|
||||
|
||||
// S12 scrollbar: a thumb in the grid's right-edge gutter, sized/positioned by the pure
|
||||
@@ -832,8 +899,10 @@ void ReaSamplerEditor::paintBrowser(LICE_IBitmap* bmp, int w, int h) {
|
||||
{
|
||||
const Rect thumb = scrollThumbRect(bl, cardCount, scrollOffset_);
|
||||
if (thumb.height() > 0) {
|
||||
const bool dragging = (drag_ == DragKind::kScrollThumb);
|
||||
const KitColor tc = roleColor(dragging ? Role::AccentHot : Role::AccentPrimary);
|
||||
LICE_FillRect(bmp, thumb.left + ox, thumb.top + oy, thumb.width(), thumb.height(),
|
||||
kColRootMarker, 0.8f, 0);
|
||||
toLice(tc), 0.8f, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -846,9 +915,8 @@ void ReaSamplerEditor::paintBrowser(LICE_IBitmap* bmp, int w, int h) {
|
||||
|
||||
void ReaSamplerEditor::paintSetup(LICE_IBitmap* bmp, const Rect& area) {
|
||||
// The guided single-capture setup: the picked capture's name + root/level, and a
|
||||
// keyboard strip with its root marker (drag to set root).
|
||||
LICE_FillRect(bmp, area.left, area.top, area.width(), area.height(),
|
||||
kColTitleBg, 1.0f, 0);
|
||||
// keyboard strip with its root marker (drag to set root). A raised bg/panel region.
|
||||
fillSurface(bmp, toKitBox(area), Role::BgPanel, InteractionState::Rest);
|
||||
|
||||
// Effective root: the picked sample's rootNote intrinsic (or middle C when unset).
|
||||
// Read from samples_ (the full unfiltered list) so a bank-filter that hides the
|
||||
@@ -867,78 +935,80 @@ void ReaSamplerEditor::paintSetup(LICE_IBitmap* bmp, const Rect& area) {
|
||||
const ChannelToggleRects chan = channelToggleRects(area);
|
||||
Rect headerR{area.left + pad, area.top + 4, chan.mono.left - 8, area.top + 22};
|
||||
std::string header = sampleLabel(samples_, selectedId_) + " root " + noteLabel(root);
|
||||
drawText(bmp, headerR, header.c_str(), kRgbText);
|
||||
kitText(bmp, headerR, header.c_str(), Font::Label, Role::TextPrimary);
|
||||
|
||||
// S7 mono | stereo output-mode toggle. The active segment highlights (kColTabActiveBg),
|
||||
// the inactive is kColTabBg — the same visual grammar as the Browser/Zones toggle.
|
||||
// S7 mono | stereo output-mode toggle. Active segment = accent-primary fill (the live
|
||||
// mode); inactive lightens on hover — the same visual grammar as the Browser/Zones toggle.
|
||||
const bool isStereo = (channelMode_ == ChannelMode::Stereo);
|
||||
LICE_FillRect(bmp, chan.mono.left, chan.mono.top, chan.mono.width(), chan.mono.height(),
|
||||
isStereo ? kColTabBg : kColTabActiveBg, 1.0f, 0);
|
||||
LICE_FillRect(bmp, chan.stereo.left, chan.stereo.top, chan.stereo.width(),
|
||||
chan.stereo.height(), isStereo ? kColTabActiveBg : kColTabBg, 1.0f, 0);
|
||||
drawTextCentered(bmp, chan.mono, "Mono", kRgbText);
|
||||
drawTextCentered(bmp, chan.stereo, "Stereo", kRgbText);
|
||||
const InteractionState monoState =
|
||||
!isStereo ? InteractionState::Active
|
||||
: (isHovered(HoverKind::kChanMono, -1) ? InteractionState::Hover
|
||||
: InteractionState::Rest);
|
||||
const InteractionState stereoState =
|
||||
isStereo ? InteractionState::Active
|
||||
: (isHovered(HoverKind::kChanStereo, -1) ? InteractionState::Hover
|
||||
: InteractionState::Rest);
|
||||
fillSurface(bmp, toKitBox(chan.mono), Role::BgCell, monoState);
|
||||
fillSurface(bmp, toKitBox(chan.stereo), Role::BgCell, stereoState);
|
||||
kitTextCentered(bmp, chan.mono, "Mono", Font::Label,
|
||||
!isStereo ? Role::BgBase : Role::TextPrimary);
|
||||
kitTextCentered(bmp, chan.stereo, "Stereo", Font::Label,
|
||||
isStereo ? Role::BgBase : Role::TextPrimary);
|
||||
|
||||
Rect hintR{area.left + pad, headerR.bottom, area.right - pad, headerR.bottom + 16};
|
||||
drawText(bmp, hintR,
|
||||
"Drag the waveform markers to set start + loop; drag the keyboard to set root.",
|
||||
kRgbDim);
|
||||
kitText(bmp, hintR,
|
||||
"Drag the waveform markers to set start + loop; drag the keyboard to set root.",
|
||||
Font::Micro, Role::TextDim);
|
||||
|
||||
// --- S11 waveform surface: the picked capture's envelope + draggable markers ----------
|
||||
const std::vector<AudioSample>& pcm = monoPcmFor(selectedId_);
|
||||
const std::int64_t frames = static_cast<std::int64_t>(pcm.size());
|
||||
const Rect waveArea = setupWaveformArea(area);
|
||||
LICE_FillRect(bmp, waveArea.left, waveArea.top, waveArea.width(), waveArea.height(),
|
||||
kColWaveBg, 1.0f, 0);
|
||||
// The waveform pane is a recessed surface (bg/base, one step DOWN from the setup panel)
|
||||
// so the accent-primary envelope reads against it.
|
||||
fillSurface(bmp, toKitBox(waveArea), Role::BgBase, InteractionState::Rest);
|
||||
if (frames > 0 && waveArea.width() > 0) {
|
||||
// Envelope at one bin per pixel (full-res view of the decoded PCM, S10 read-only view
|
||||
// reused). computeEnvelope over the cached mono frames — no new decode.
|
||||
const int bins = (std::max)(1, waveArea.width());
|
||||
const Envelope env = computeEnvelope(pcm, 1, pcm.size(), static_cast<std::size_t>(bins));
|
||||
drawEnvelope(bmp, waveArea, env); // reuses the thumbnail envelope draw (kColThumb)
|
||||
drawEnvelope(bmp, waveArea, env); // kit drawWaveform — accent-primary columns
|
||||
|
||||
const SetupMarkers m = pickedMarkers(frames);
|
||||
// Faint loop-region fill between the loop markers (only when a loop is set).
|
||||
// Faint loop-region fill between the loop markers (only when a loop is set) — the
|
||||
// categorical loop-marker hue (tertiary purple) at low alpha.
|
||||
if (m.hasLoop && m.loopEnd > m.loopStart) {
|
||||
const int lx = frameToX(waveArea, frames, m.loopStart);
|
||||
const int rx = frameToX(waveArea, frames, m.loopEnd);
|
||||
if (rx > lx) {
|
||||
LICE_FillRect(bmp, lx, waveArea.top, rx - lx, waveArea.height(),
|
||||
kColLoopRegion, 1.0f, 0);
|
||||
toLice(roleColor(kRoleLoopMarker)), 0.20f, 0);
|
||||
}
|
||||
}
|
||||
// The three markers: start (amber), loop start + loop end (blue). Drawn as 2px vertical
|
||||
// lines the full waveform height. Loop markers dim when no loop is set (the "no loop"
|
||||
// state — draggable to CREATE one).
|
||||
// The three markers: start (teal, secondary) + loop start/end (purple, tertiary) —
|
||||
// categorical affordance hues (§2.1), 2px vertical lines the full waveform height.
|
||||
// Loop markers dim when no loop is set (the "no loop" state — draggable to CREATE one).
|
||||
const std::int64_t markerFrames[3] = {m.start, m.loopStart, m.loopEnd};
|
||||
const LICE_pixel markerCols[3] = {kColStartMarker, kColLoopMarker, kColLoopMarker};
|
||||
const Role markerRoles[3] = {kRoleStartMarker, kRoleLoopMarker, kRoleLoopMarker};
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
const int mx = frameToX(waveArea, frames, markerFrames[i]);
|
||||
const bool loopMarker = (i != 0);
|
||||
const float alpha = (loopMarker && !m.hasLoop) ? 0.4f : 1.0f;
|
||||
LICE_FillRect(bmp, mx - 1, waveArea.top, 2, waveArea.height(), markerCols[i],
|
||||
alpha, 0);
|
||||
LICE_FillRect(bmp, mx - 1, waveArea.top, 2, waveArea.height(),
|
||||
toLice(roleColor(markerRoles[i])), alpha, 0);
|
||||
}
|
||||
} else {
|
||||
drawTextCentered(bmp, waveArea, "(decoding...)", kRgbDim);
|
||||
kitTextCentered(bmp, waveArea, "(decoding...)", Font::Label, Role::TextDim);
|
||||
}
|
||||
|
||||
// Keyboard strip with the root marker.
|
||||
// Keyboard strip with the root marker — the signature Direction-C PASTEL SPECTRAL surface
|
||||
// (§4). Each key column is hue-mapped low->high across the accent trio (spectralColor:
|
||||
// lime -> teal -> purple), so the strip reads as an extension of the accent system. The
|
||||
// root marker lifts to accent-primary with a STATIC glow (never a pulse — §3.5).
|
||||
const Rect stripArea = setupStripArea(area);
|
||||
drawSpectralStrip(bmp, stripArea);
|
||||
const StripLayout sl = layoutStrip(stripArea.width(), stripArea.height());
|
||||
const int sx = stripArea.left;
|
||||
const int sy = stripArea.top;
|
||||
LICE_FillRect(bmp, stripArea.left, stripArea.top, stripArea.width(), stripArea.height(),
|
||||
kColStripBg, 1.0f, 0);
|
||||
// Faint per-octave key ticks for orientation.
|
||||
for (int n = 0; n <= 127; n += 12) {
|
||||
Rect k = keyRect(sl, n);
|
||||
LICE_Line(bmp, k.left + sx, sy, k.left + sx, sy + stripArea.height(), kColStripKey,
|
||||
1.0f, 0, false);
|
||||
}
|
||||
Rect marker = rootMarkerRect(sl, root);
|
||||
LICE_FillRect(bmp, marker.left + sx, sy, (std::max)(2, marker.width()), stripArea.height(),
|
||||
kColRootMarker, 1.0f, 0);
|
||||
drawRootMarker(bmp, stripArea, sl, root);
|
||||
}
|
||||
|
||||
void ReaSamplerEditor::paintZones(LICE_IBitmap* bmp, int w, int h) {
|
||||
@@ -949,37 +1019,46 @@ void ReaSamplerEditor::paintZones(LICE_IBitmap* bmp, int w, int h) {
|
||||
// with one bar per zone. Delete is a small × on the selected zone (keystroke also).
|
||||
Rect addR{bands.content.left + pad, bands.content.top + 4, bands.content.left + pad + 96,
|
||||
bands.content.top + 4 + 20};
|
||||
LICE_FillRect(bmp, addR.left, addR.top, addR.width(), addR.height(), kColCardBg, 1.0f, 0);
|
||||
LICE_DrawRect(bmp, addR.left, addR.top, addR.width() - 1, addR.height() - 1,
|
||||
kColCardSelBorder, 1.0f, 0);
|
||||
drawTextCentered(bmp, addR, "+ Add Zone", kRgbText);
|
||||
{
|
||||
const KitButtonBox box{toKitBox(addR)};
|
||||
const InteractionState state =
|
||||
isHovered(HoverKind::kAddZone, -1) ? InteractionState::Hover : InteractionState::Rest;
|
||||
drawButton(bmp, box, "+ Add Zone", state, /*warn=*/false);
|
||||
}
|
||||
|
||||
Rect delR{addR.right + 8, addR.top, addR.right + 8 + 64, addR.bottom};
|
||||
if (selectedZone_ >= 0) {
|
||||
LICE_FillRect(bmp, delR.left, delR.top, delR.width(), delR.height(), kColCardBg, 1.0f, 0);
|
||||
LICE_DrawRect(bmp, delR.left, delR.top, delR.width() - 1, delR.height() - 1,
|
||||
kColCardSelBorder, 1.0f, 0);
|
||||
drawTextCentered(bmp, delR, "Delete", kRgbText);
|
||||
const KitButtonBox box{toKitBox(delR)};
|
||||
const InteractionState state =
|
||||
isHovered(HoverKind::kDeleteZone, -1) ? InteractionState::Hover : InteractionState::Rest;
|
||||
// Deleting a zone is not a byte-destroying act (no file removed — the bank is
|
||||
// read-only here), so it is a normal button, not `warn`.
|
||||
drawButton(bmp, box, "Delete", state, /*warn=*/false);
|
||||
}
|
||||
|
||||
// The zones strip.
|
||||
// The zones strip — the same PASTEL SPECTRAL surface as the setup face (§4), with one
|
||||
// bar per zone over the spectrum. The SELECTED zone lifts to accent-primary + a static
|
||||
// glow ("which zone is live"); the rest take the categorical secondary hue at low alpha.
|
||||
const Rect stripArea = zonesStripArea(bands);
|
||||
drawSpectralStrip(bmp, stripArea);
|
||||
const StripLayout sl = layoutStrip(stripArea.width(), stripArea.height());
|
||||
const int sx = stripArea.left;
|
||||
const int sy = stripArea.top;
|
||||
LICE_FillRect(bmp, stripArea.left, stripArea.top, stripArea.width(), stripArea.height(),
|
||||
kColStripBg, 1.0f, 0);
|
||||
for (int n = 0; n <= 127; n += 12) {
|
||||
Rect k = keyRect(sl, n);
|
||||
LICE_Line(bmp, k.left + sx, sy, k.left + sx, sy + stripArea.height(), kColStripKey,
|
||||
1.0f, 0, false);
|
||||
}
|
||||
for (int i = 0; i < static_cast<int>(map_.zones.size()); ++i) {
|
||||
const PerformanceZone& z = map_.zones[static_cast<std::size_t>(i)];
|
||||
Rect bar = zoneBarRect(sl, z.lowNote, z.highNote);
|
||||
const int bw = (std::max)(2, bar.width());
|
||||
const bool sel = (i == selectedZone_);
|
||||
LICE_FillRect(bmp, bar.left + sx, sy, (std::max)(2, bar.width()), stripArea.height(),
|
||||
sel ? kColZoneBarSel : kColZoneBar, sel ? 1.0f : 0.7f, 0);
|
||||
if (sel) {
|
||||
// Static glow halo behind the live zone, then the crisp accent-primary bar.
|
||||
LICE_FillRect(bmp, bar.left + sx - 2, sy, bw + 4, stripArea.height(),
|
||||
toLice(roleColor(Role::AccentHot)), 0.30f, 0);
|
||||
LICE_FillRect(bmp, bar.left + sx, sy, bw, stripArea.height(),
|
||||
toLice(roleColor(Role::AccentPrimary)), 1.0f, 0);
|
||||
} else {
|
||||
LICE_FillRect(bmp, bar.left + sx, sy, bw, stripArea.height(),
|
||||
toLice(roleColor(Role::AccentSecondary)), 0.55f, 0);
|
||||
}
|
||||
}
|
||||
|
||||
// A one-line legend of the selected zone below the strip, with three click-to-type numeric
|
||||
@@ -989,9 +1068,10 @@ void ReaSamplerEditor::paintZones(LICE_IBitmap* bmp, int w, int h) {
|
||||
Rect infoR{stripArea.left, legendTop, stripArea.right, legendTop + 18};
|
||||
if (selectedZone_ >= 0 && selectedZone_ < static_cast<int>(map_.zones.size())) {
|
||||
const PerformanceZone& z = map_.zones[static_cast<std::size_t>(selectedZone_)];
|
||||
drawText(bmp, Rect{infoR.left, infoR.top, infoR.left + 120, infoR.bottom},
|
||||
sampleLabel(samples_, z.sampleId).c_str(), kRgbText);
|
||||
// Three fields laid out left-to-right after the sample label.
|
||||
kitText(bmp, Rect{infoR.left, infoR.top, infoR.left + 120, infoR.bottom},
|
||||
sampleLabel(samples_, z.sampleId).c_str(), Font::Label, Role::TextPrimary);
|
||||
// Three fields laid out left-to-right after the sample label. A focused field lifts to
|
||||
// the Focus state (accent nudge + ring); values in tabular mono so digits don't jitter.
|
||||
const Rect fields = noteEntryFieldsArea(bands);
|
||||
const char* names[3] = {"Low", "High", "Root"};
|
||||
const std::string vals[3] = {
|
||||
@@ -1000,17 +1080,21 @@ void ReaSamplerEditor::paintZones(LICE_IBitmap* bmp, int w, int h) {
|
||||
for (int f = 0; f < 3; ++f) {
|
||||
const Rect fr = noteEntryFieldRect(fields, f);
|
||||
const bool editing = (entryField_ == f);
|
||||
LICE_FillRect(bmp, fr.left, fr.top, fr.width(), fr.height(),
|
||||
editing ? kColTabActiveBg : kColCardBg, 1.0f, 0);
|
||||
fillSurface(bmp, toKitBox(fr), Role::BgCell,
|
||||
editing ? InteractionState::Focus : InteractionState::Rest);
|
||||
const KitColor border =
|
||||
editing ? roleColor(Role::TextPrimary) : roleColor(Role::LineHairline);
|
||||
LICE_DrawRect(bmp, fr.left, fr.top, fr.width() - 1, fr.height() - 1,
|
||||
kColCardBorder, 1.0f, 0);
|
||||
toLice(border), 1.0f, 0);
|
||||
std::string cap = std::string(names[f]) + ": " +
|
||||
(editing ? (entryText_ + "_") : vals[f]);
|
||||
drawText(bmp, Rect{fr.left + 4, fr.top, fr.right - 2, fr.bottom}, cap.c_str(), kRgbText);
|
||||
kitText(bmp, Rect{fr.left + 4, fr.top, fr.right - 2, fr.bottom}, cap.c_str(),
|
||||
Font::ValueMono, Role::TextPrimary);
|
||||
}
|
||||
} else if (map_.zones.empty()) {
|
||||
drawText(bmp, infoR,
|
||||
"No zones. Add Zone maps the picked capture across the keyboard.", kRgbDim);
|
||||
kitText(bmp, infoR,
|
||||
"No zones. Add Zone maps the picked capture across the keyboard.",
|
||||
Font::Label, Role::TextDim);
|
||||
}
|
||||
|
||||
// The S12/S15/S16 parameter surface for the selected zone (play mode + AHDSR / Trigger +
|
||||
@@ -1070,29 +1154,136 @@ void ReaSamplerEditor::paintControls(LICE_IBitmap* bmp, const Rect& panel) {
|
||||
for (const ControlRow& r : rows) {
|
||||
if (r.row.top >= panel.bottom) break; // clip at the panel bottom
|
||||
const ControlLabels lab = labelsFor(static_cast<ParamControl>(r.id));
|
||||
drawText(bmp, r.label, lab.label, kRgbDim);
|
||||
kitText(bmp, r.label, lab.label, Font::Micro, Role::TextDim);
|
||||
const double v = controlValue(r.id, play);
|
||||
const bool hov = isHovered(HoverKind::kControl, r.id);
|
||||
if (r.kind == ControlKind::Toggle) {
|
||||
const bool seg1 = (v >= 0.5);
|
||||
const Rect s0 = toggleSegmentRect(r.control, 0);
|
||||
const Rect s1 = toggleSegmentRect(r.control, 1);
|
||||
LICE_FillRect(bmp, s0.left, s0.top, s0.width(), s0.height(),
|
||||
seg1 ? kColTabBg : kColTabActiveBg, 1.0f, 0);
|
||||
LICE_FillRect(bmp, s1.left, s1.top, s1.width(), s1.height(),
|
||||
seg1 ? kColTabActiveBg : kColTabBg, 1.0f, 0);
|
||||
drawTextCentered(bmp, s0, lab.seg0, kRgbText);
|
||||
drawTextCentered(bmp, s1, lab.seg1, kRgbText);
|
||||
// The lit segment carries the primary accent (Active); the unlit segment hovers
|
||||
// toward accent/hot when the whole control is under the pointer.
|
||||
const InteractionState s0State =
|
||||
!seg1 ? InteractionState::Active : (hov ? InteractionState::Hover : InteractionState::Rest);
|
||||
const InteractionState s1State =
|
||||
seg1 ? InteractionState::Active : (hov ? InteractionState::Hover : InteractionState::Rest);
|
||||
fillSurface(bmp, toKitBox(s0), Role::BgCell, s0State);
|
||||
fillSurface(bmp, toKitBox(s1), Role::BgCell, s1State);
|
||||
kitTextCentered(bmp, s0, lab.seg0, Font::Micro, !seg1 ? Role::BgBase : Role::TextPrimary);
|
||||
kitTextCentered(bmp, s1, lab.seg1, Font::Micro, seg1 ? Role::BgBase : Role::TextPrimary);
|
||||
} else {
|
||||
// Track groove (recessed cell) + accent filled portion up to the handle + a raised
|
||||
// handle. Dragging THIS control brightens the fill/handle (accent/hot).
|
||||
const bool dragging = (drag_ == DragKind::kParamSlider && dragParamId_ == r.id);
|
||||
const Rect track = sliderTrackRect(r.control);
|
||||
LICE_FillRect(bmp, track.left, track.top + track.height() / 2 - 1, track.width(), 2,
|
||||
kColStripKey, 1.0f, 0);
|
||||
fillSurface(bmp, toKitBox(Rect{track.left, track.top + track.height() / 2 - 1,
|
||||
track.right, track.top + track.height() / 2 + 1}),
|
||||
Role::BgCell, InteractionState::Pressed);
|
||||
const Rect handle = sliderHandleRect(r.control, v);
|
||||
LICE_FillRect(bmp, handle.left, handle.top + 2, handle.width(), handle.height() - 4,
|
||||
kColRootMarker, 1.0f, 0);
|
||||
// Filled portion: track-left to the handle center.
|
||||
const int fillW = (std::max)(0, (handle.left + handle.width() / 2) - track.left);
|
||||
if (fillW > 0) {
|
||||
LICE_FillRect(bmp, track.left, track.top + track.height() / 2 - 1, fillW, 2,
|
||||
toLice(roleColor(dragging ? Role::AccentHot : Role::AccentPrimary)),
|
||||
1.0f, 0);
|
||||
}
|
||||
const KitButtonBox knob{toKitBox(Rect{handle.left, handle.top + 2, handle.right,
|
||||
handle.bottom - 2})};
|
||||
drawButton(bmp, knob, nullptr,
|
||||
dragging ? InteractionState::Dragging
|
||||
: (hov ? InteractionState::Hover : InteractionState::Rest),
|
||||
/*warn=*/false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Hover resolution (Phase L, L3) ------------------------------------------
|
||||
//
|
||||
// Resolve the interactive element under (x, y) into hover_ and repaint only on change (an
|
||||
// idle move is free — the "sub-frame feedback, zero cost when nothing changed" discipline).
|
||||
// Mirrors onMouseDown's hit-test order, but read-only: it never mutates selection/map. Only
|
||||
// the frequently-touched interactive surfaces light on hover; a purely decorative region
|
||||
// resolves to kNone (clearing any prior hover). Windows-only.
|
||||
void ReaSamplerEditor::resolveHover(int x, int y) {
|
||||
HoverTarget h; // kNone by default
|
||||
RECT cr{};
|
||||
GetClientRect(childHwnd_, &cr);
|
||||
const int w = cr.right - cr.left;
|
||||
const int hgt = cr.bottom - cr.top;
|
||||
const EditorBands bands = computeBands(w, hgt);
|
||||
|
||||
if (contains(bands.toggleBrowser, x, y)) {
|
||||
h = {HoverKind::kToggleBrowser, -1};
|
||||
} else if (contains(bands.toggleZones, x, y)) {
|
||||
h = {HoverKind::kToggleZones, -1};
|
||||
} else if (view_ == View::kBrowser) {
|
||||
const bool havePick = !selectedId_.empty();
|
||||
const int setupTop = havePick
|
||||
? (std::max)(bands.content.top, bands.content.bottom - kSetupHeight)
|
||||
: bands.content.bottom;
|
||||
const Rect fullBrowserArea{bands.content.left, bands.content.top, bands.content.right, setupTop};
|
||||
const Rect searchBox = searchBoxRect(fullBrowserArea.width());
|
||||
const Rect searchAbs{fullBrowserArea.left + searchBox.left, fullBrowserArea.top + searchBox.top,
|
||||
fullBrowserArea.left + searchBox.right, fullBrowserArea.top + searchBox.bottom};
|
||||
if (contains(searchAbs, x, y)) {
|
||||
h = {HoverKind::kSearchBox, -1};
|
||||
} else {
|
||||
const Rect browserArea{fullBrowserArea.left, searchAbs.bottom, fullBrowserArea.right, setupTop};
|
||||
const BrowserLayout bl = layoutBrowser(browserArea.width(), browserArea.height());
|
||||
const int bx = x - browserArea.left;
|
||||
const int by = y - browserArea.top;
|
||||
const int tabCount = static_cast<int>(banks_.size()) + 1;
|
||||
const int tab = filterTabHitTest(bl, tabCount, bx, by);
|
||||
const int card = (tab >= 0)
|
||||
? -1
|
||||
: cardHitTest(bl, static_cast<int>(visible_.size()), bx, by + scrollOffset_);
|
||||
if (tab >= 0) {
|
||||
h = {HoverKind::kFilterTab, tab};
|
||||
} else if (card >= 0) {
|
||||
h = {HoverKind::kCard, card};
|
||||
} else if (havePick) {
|
||||
const Rect area{bands.content.left, setupTop, bands.content.right, bands.content.bottom};
|
||||
const ChannelToggleRects chan = channelToggleRects(area);
|
||||
if (contains(chan.mono, x, y)) h = {HoverKind::kChanMono, -1};
|
||||
else if (contains(chan.stereo, x, y)) h = {HoverKind::kChanStereo, -1};
|
||||
}
|
||||
}
|
||||
} else { // Zones view
|
||||
const int pad = 8;
|
||||
Rect addR{bands.content.left + pad, bands.content.top + 4, bands.content.left + pad + 96,
|
||||
bands.content.top + 4 + 20};
|
||||
Rect delR{addR.right + 8, addR.top, addR.right + 8 + 64, addR.bottom};
|
||||
if (contains(addR, x, y)) {
|
||||
h = {HoverKind::kAddZone, -1};
|
||||
} else if (selectedZone_ >= 0 && contains(delR, x, y)) {
|
||||
h = {HoverKind::kDeleteZone, -1};
|
||||
} else {
|
||||
// The param control panel (a selected zone, or the single-capture face's defaults).
|
||||
ZonePlaySeconds play;
|
||||
bool haveTarget = false;
|
||||
if (selectedZone_ >= 0 && selectedZone_ < static_cast<int>(map_.zones.size())) {
|
||||
play = map_.zones[static_cast<std::size_t>(selectedZone_)].play;
|
||||
haveTarget = true;
|
||||
} else if (map_.zones.empty() && !selectedId_.empty()) {
|
||||
play = PerformanceZone{}.play;
|
||||
haveTarget = true;
|
||||
}
|
||||
if (haveTarget) {
|
||||
const Rect panel = zonesControlPanel(bands);
|
||||
const std::vector<ControlDesc> descs = controlDescs(play);
|
||||
const std::vector<ControlRow> rows = layoutControls(panel, descs);
|
||||
const int id = controlAtPoint(rows, x, y);
|
||||
if (id >= 0) h = {HoverKind::kControl, id};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (h != hover_) {
|
||||
hover_ = h;
|
||||
invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
// --- Input: the drag-state machine -------------------------------------------
|
||||
|
||||
void ReaSamplerEditor::onMouseDown(int x, int y) {
|
||||
@@ -1626,7 +1817,34 @@ LRESULT CALLBACK ReaSamplerEditor::wndProc(HWND hwnd, UINT msg, WPARAM wParam,
|
||||
}
|
||||
return 0;
|
||||
case WM_MOUSEMOVE:
|
||||
if (self) self->onMouseMove(GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam));
|
||||
if (self) {
|
||||
const int mx = GET_X_LPARAM(lParam);
|
||||
const int my = GET_Y_LPARAM(lParam);
|
||||
// Hover feedback (Phase L, L3): resolve the element under the pointer and
|
||||
// repaint on change. Arm WM_MOUSELEAVE once per "over" cycle so the hover
|
||||
// clears when the pointer leaves the child (TrackMouseEvent is one-shot).
|
||||
if (!self->mouseTracking_) {
|
||||
TRACKMOUSEEVENT tme{};
|
||||
tme.cbSize = sizeof(tme);
|
||||
tme.dwFlags = TME_LEAVE;
|
||||
tme.hwndTrack = hwnd;
|
||||
TrackMouseEvent(&tme);
|
||||
self->mouseTracking_ = true;
|
||||
}
|
||||
// While a drag is in flight the drag owns the surface; skip hover resolution
|
||||
// (a hover repaint mid-drag would fight the live drag feedback).
|
||||
if (self->drag_ == DragKind::kNone) self->resolveHover(mx, my);
|
||||
self->onMouseMove(mx, my);
|
||||
}
|
||||
return 0;
|
||||
case WM_MOUSELEAVE:
|
||||
if (self) {
|
||||
self->mouseTracking_ = false;
|
||||
if (self->hover_.kind != HoverKind::kNone) {
|
||||
self->hover_ = HoverTarget{};
|
||||
self->invalidate();
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
case WM_MOUSEWHEEL:
|
||||
// S12 browser scroll. GET_WHEEL_DELTA_WPARAM is signed; positive == wheel up.
|
||||
|
||||
@@ -100,6 +100,34 @@ private:
|
||||
// a different marker set; here it is start-point + the sustain loop's two ends.
|
||||
enum class WaveMarker { kStart = 0, kLoopStart = 1, kLoopEnd = 2, kCount = 3 };
|
||||
|
||||
// --- Hover model (Phase L, L3) ------------------------------------------------
|
||||
//
|
||||
// The interactive element under the pointer, resolved live in WM_MOUSEMOVE so the kit
|
||||
// draws its hover state on that element only ("hover on every interactive element" +
|
||||
// "sub-frame feedback = the perception of speed", §3.3/§3.5). Cleared to kNone on
|
||||
// WM_MOUSELEAVE (tracked via TrackMouseEvent). `index` disambiguates within a kind
|
||||
// (tab ordinal, visible-card index, control-row id); -1 when not applicable. Mirror of
|
||||
// bank_panel's L2 hover model.
|
||||
enum class HoverKind {
|
||||
kNone,
|
||||
kToggleBrowser, // the Browser toggle segment
|
||||
kToggleZones, // the Zones toggle segment
|
||||
kSearchBox, // the browser search box
|
||||
kFilterTab, // a bank-filter tab (index = tab ordinal, 0 = All)
|
||||
kCard, // a capture card (index = visible_ index)
|
||||
kChanMono, // the mono channel-mode segment
|
||||
kChanStereo, // the stereo channel-mode segment
|
||||
kAddZone, // the "+ Add Zone" button
|
||||
kDeleteZone, // the "Delete" zone button
|
||||
kControl, // a param-panel control row (index = ControlDesc id)
|
||||
};
|
||||
struct HoverTarget {
|
||||
HoverKind kind = HoverKind::kNone;
|
||||
int index = -1;
|
||||
bool operator==(const HoverTarget& o) const { return kind == o.kind && index == o.index; }
|
||||
bool operator!=(const HoverTarget& o) const { return !(*this == o); }
|
||||
};
|
||||
|
||||
#ifdef _WIN32
|
||||
void paint(HDC hdc);
|
||||
void paintBrowser(LICE_IBitmap* bmp, int w, int h);
|
||||
@@ -111,6 +139,17 @@ private:
|
||||
void onMouseDown(int x, int y);
|
||||
void onMouseMove(int x, int y);
|
||||
void onMouseUp(int x, int y);
|
||||
|
||||
// Resolve the interactive element under (x, y) into hover_ (Phase L, L3). Called from
|
||||
// WM_MOUSEMOVE (also while a drag is in flight — the resolved element just isn't used
|
||||
// for a hover repaint mid-drag). Repaints only when the hovered element changed, so an
|
||||
// idle mouse-move is free. Windows-only (the hit-tests use the shell's Win32 client rect).
|
||||
void resolveHover(int x, int y);
|
||||
// True iff element (kind, index) is the live hover_ target — the shell maps this to the
|
||||
// kit's Hover interaction state when the element has no more-specific state (Active, etc.).
|
||||
bool isHovered(HoverKind kind, int index) const {
|
||||
return hover_.kind == kind && hover_.index == index;
|
||||
}
|
||||
void onMouseWheel(int delta); // S12 browser scroll (wheel)
|
||||
void onSearchChar(unsigned int ch); // S12 type-to-filter search keystroke
|
||||
|
||||
@@ -242,6 +281,12 @@ private:
|
||||
int entryField_ = -1;
|
||||
std::string entryText_;
|
||||
|
||||
// --- Hover state (Phase L, L3; transient, never persisted) --------------------
|
||||
HoverTarget hover_; // the interactive element under the pointer
|
||||
#ifdef _WIN32
|
||||
bool mouseTracking_ = false; // TrackMouseEvent armed for WM_MOUSELEAVE this "over" cycle
|
||||
#endif
|
||||
|
||||
// --- Drag-state machine ------------------------------------------------------
|
||||
DragKind drag_ = DragKind::kNone;
|
||||
int dragStartX_ = 0; // grab x (px), for the pixel-delta resolver
|
||||
|
||||
@@ -9,11 +9,14 @@
|
||||
|
||||
#include "app_version.h" // vstPluginName (channel-derived embed label, S18)
|
||||
#include "bank_sync.h" // parseBankGeneration (S9 dirty-guard over the per-paint refresh)
|
||||
#include "component_geometry.h" // KitBox — the kit text/fill draw box (Phase L, L3)
|
||||
#include "draw_kit.h" // the L1 draw kit: fillSurface/text (L3)
|
||||
#include "editor_geometry.h" // Rect (shared with embed_strip)
|
||||
#include "embed_strip.h" // the pure strip layout + hit-test
|
||||
#include "ext_keys.h" // kProjExtBanksKey / kProjExtBankGenKey
|
||||
#include "reaper_bridge.h"
|
||||
#include "reasampler_processor.h"
|
||||
#include "theme.h" // Role / InteractionState / spectralColor (L3)
|
||||
|
||||
// wdltypes.h first: it defines INT_PTR portably (and pulls <windows.h> on Windows), which
|
||||
// reaper_plugin_fx_embed.h's REAPER_FXEMBED_IBitmap::Extended needs as its return type.
|
||||
@@ -41,15 +44,12 @@ namespace reasampler::vst {
|
||||
|
||||
namespace {
|
||||
#ifdef _WIN32
|
||||
// Palette — mirrored from reasampler_editor.cpp so the inline strip reads as the same tool.
|
||||
const LICE_pixel kColBackground = LICE_RGBA(28, 28, 30, 255);
|
||||
const LICE_pixel kColZone = LICE_RGBA(44, 44, 48, 255);
|
||||
const LICE_pixel kColZoneSel = LICE_RGBA(58, 96, 84, 255);
|
||||
const LICE_pixel kColZoneBorder = LICE_RGBA(20, 20, 22, 255);
|
||||
const LICE_pixel kColLevelBg = LICE_RGBA(20, 20, 22, 255);
|
||||
const LICE_pixel kColLevelFill = LICE_RGBA(120, 200, 160, 255);
|
||||
const LICE_pixel kColEmpty = LICE_RGBA(70, 70, 74, 255);
|
||||
const COLORREF kRgbText = RGB(210, 230, 220);
|
||||
// Kit adapter (Phase L, L3): the embed shell's Rect (editor_geometry) -> the kit's KitBox
|
||||
// (component_geometry). Every embed surface now draws by palette ROLE via the L1 kit, retiring
|
||||
// the local pre-L1 forest-green palette + raw GDI DrawTextA.
|
||||
KitBox toKitBox(const Rect& r) {
|
||||
return KitBox{r.left, r.top, r.width(), r.height()};
|
||||
}
|
||||
|
||||
// A short display name for a bank sample id, from the snapshotted list (the editor's helper,
|
||||
// duplicated small rather than shared across the shell/pure boundary).
|
||||
@@ -136,6 +136,12 @@ TPtrInt ReaSamplerEmbed::embed_message(int msg, TPtrInt parm2, TPtrInt parm3) {
|
||||
return 0; // not a build target off Windows
|
||||
#endif
|
||||
case REAPER_FXEMBED_WM_CREATE:
|
||||
#ifdef _WIN32
|
||||
// Create the kit's cached AA fonts before the first paint (Phase L, L3).
|
||||
// Idempotent + process-global (shared with the editor in this binary); NOT torn
|
||||
// down per-view — the OS reclaims the tiny static HFONT set at module unload.
|
||||
kitFontsInit();
|
||||
#endif
|
||||
refresh(); // prime the first paint's snapshot
|
||||
return 0;
|
||||
case REAPER_FXEMBED_WM_DESTROY:
|
||||
@@ -184,62 +190,65 @@ bool ReaSamplerEmbed::paint(TPtrInt bitmap, TPtrInt drawInfo) {
|
||||
|
||||
// REAPER hands us its own bitmap sized to the embed area; draw directly into it (unlike
|
||||
// the editor, which owns a LICE_SysBitmap and BitBlt's). Origin is the bitmap's (0,0).
|
||||
LICE_FillRect(bmp, 0, 0, w, h, kColBackground, 1.0f, 0);
|
||||
// Base canvas through the kit (bg/base + micro-gradient), Phase L L3.
|
||||
fillSurface(bmp, KitBox{0, 0, w, h}, Role::BgBase, InteractionState::Rest);
|
||||
|
||||
const EmbedLayout layout = layoutEmbed(w, h);
|
||||
|
||||
if (map_.zones.empty()) {
|
||||
// No opt-in zones authored: show a single faint band spanning the keymap area so the
|
||||
// strip reads as "present, no zones" — the default single-capture face lives in the
|
||||
// editor (this S6 strip mirrors the zones map only).
|
||||
// No opt-in zones authored: a faint bg/cell band spanning the keymap area so the strip
|
||||
// reads as "present, no zones" — the default single-capture face lives in the editor.
|
||||
LICE_FillRect(bmp, layout.keymap.left, layout.keymap.top, layout.keymap.width(),
|
||||
layout.keymap.height(), kColEmpty, 0.5f, 0);
|
||||
HDC dc = bmp->getDC();
|
||||
SetBkMode(dc, TRANSPARENT);
|
||||
SetTextColor(dc, kRgbText);
|
||||
RECT gr{layout.keymap.left + 4, layout.keymap.top, layout.keymap.right,
|
||||
layout.keymap.bottom};
|
||||
layout.keymap.height(), toLice(roleColor(Role::BgCell)), 0.5f, 0);
|
||||
const std::string label = reasampler::vstPluginName() + // channel-derived (S18)
|
||||
(samples_.empty() ? " (bank empty)" : " (no zones)");
|
||||
DrawTextA(dc, label.c_str(), -1, &gr,
|
||||
DT_LEFT | DT_VCENTER | DT_SINGLELINE | DT_NOPREFIX);
|
||||
const Rect labelR{layout.keymap.left + 4, layout.keymap.top, layout.keymap.right,
|
||||
layout.keymap.bottom};
|
||||
text(bmp, toKitBox(labelR), label.c_str(), Font::Label, Role::TextPrimary, Align::Left);
|
||||
} else {
|
||||
// Draw each zone as a segment across the keymap span, first-match order (so the
|
||||
// painted order matches selection + playback). The selected zone is highlighted.
|
||||
// Draw each zone as a segment across the keymap span, first-match order (so the painted
|
||||
// order matches selection + playback). Each segment takes its PASTEL SPECTRAL hue from
|
||||
// the center of its key span (spectralColor — §4), so the strip reads as the same
|
||||
// spectrum as the editor's keyboard strip. The SELECTED zone lifts to accent-primary
|
||||
// + a static glow ("which zone is live", never a pulse — §3.5).
|
||||
for (int i = 0; i < static_cast<int>(map_.zones.size()); ++i) {
|
||||
const PerformanceZone& z = map_.zones[i];
|
||||
const Rect r = zoneSegmentRect(layout, z.lowNote, z.highNote);
|
||||
if (r.width() <= 0) continue;
|
||||
const bool sel = (i == selectedZone_);
|
||||
LICE_FillRect(bmp, r.left, r.top, r.width(), r.height(),
|
||||
sel ? kColZoneSel : kColZone, 1.0f, 0);
|
||||
LICE_DrawRect(bmp, r.left, r.top, r.width() - 1, r.height() - 1, kColZoneBorder,
|
||||
1.0f, 0);
|
||||
// Label the segment with the sample name when it is wide enough to read.
|
||||
if (sel) {
|
||||
// Static glow halo, then the crisp accent-primary fill.
|
||||
LICE_FillRect(bmp, r.left - 2, r.top, r.width() + 4, r.height(),
|
||||
toLice(roleColor(Role::AccentHot)), 0.30f, 0);
|
||||
LICE_FillRect(bmp, r.left, r.top, r.width(), r.height(),
|
||||
toLice(roleColor(Role::AccentPrimary)), 1.0f, 0);
|
||||
} else {
|
||||
const double t = ((z.lowNote + z.highNote) * 0.5) / 127.0;
|
||||
LICE_FillRect(bmp, r.left, r.top, r.width(), r.height(),
|
||||
toLice(spectralColor(t)), 0.65f, 0);
|
||||
}
|
||||
LICE_DrawRect(bmp, r.left, r.top, r.width() - 1, r.height() - 1,
|
||||
toLice(roleColor(Role::LineHairline)), 1.0f, 0);
|
||||
// Label the segment with the sample name when it is wide enough to read. The
|
||||
// selected (accent-fill) segment draws its label in bg/base for contrast (the
|
||||
// tight text-on-pastel pair, §4); the rest in text/primary.
|
||||
if (r.width() >= 24) {
|
||||
HDC dc = bmp->getDC();
|
||||
SetBkMode(dc, TRANSPARENT);
|
||||
SetTextColor(dc, kRgbText);
|
||||
RECT gr{r.left + 3, r.top, r.right - 2, r.bottom};
|
||||
const std::string label = sampleLabel(samples_, z.sampleId);
|
||||
DrawTextA(dc, label.c_str(), -1, &gr,
|
||||
DT_LEFT | DT_VCENTER | DT_SINGLELINE | DT_NOPREFIX | DT_END_ELLIPSIS);
|
||||
const Rect lr{r.left + 3, r.top, r.right - 2, r.bottom};
|
||||
text(bmp, toKitBox(lr), sampleLabel(samples_, z.sampleId).c_str(),
|
||||
Font::Label, sel ? Role::BgBase : Role::TextPrimary, Align::Left);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The level band: a static background here; a live activity level is a later refinement
|
||||
// (the processor would publish a peak the UI thread reads). Draw the empty band so the
|
||||
// strip's geometry is complete and the DAW-verify sees the band lifecycle now.
|
||||
// The level band: a recessed bg/cell channel with an accent-primary fill following the
|
||||
// live activity level (a direct level follow — the one permitted "motion", §3.5).
|
||||
if (layout.levelBand.height() > 0) {
|
||||
LICE_FillRect(bmp, layout.levelBand.left, layout.levelBand.top,
|
||||
layout.levelBand.width(), layout.levelBand.height(), kColLevelBg, 1.0f,
|
||||
0);
|
||||
fillSurface(bmp, toKitBox(layout.levelBand), Role::BgCell, InteractionState::Pressed);
|
||||
const double level = processor_ ? processor_->embedActivityLevel() : 0.0;
|
||||
const Rect fill = levelFillRect(layout, level);
|
||||
if (fill.width() > 0) {
|
||||
LICE_FillRect(bmp, fill.left, fill.top, fill.width(), fill.height(),
|
||||
kColLevelFill, 1.0f, 0);
|
||||
toLice(roleColor(Role::AccentPrimary)), 1.0f, 0);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -134,6 +134,23 @@ static void testLabelOnActiveSurfaceClearsFloor() {
|
||||
CHECK(contrastRatio(labelOnActive, activeFill) >= textFloor(TextClass::Body));
|
||||
}
|
||||
|
||||
// Text drawn on a HOVER-state surface (Phase L, L3): the VST editor + embed restyle lights
|
||||
// hover on inactive segments/tabs/cards by lightening bg/cell toward accent/hot (~10%).
|
||||
// Lightening the surface REDUCES contrast against light text, so re-verify the pairs on the
|
||||
// hover surface, not just on rest bg/cell. (A distinct drawn pair the L3 shells introduce,
|
||||
// not covered by the rest-surface tests above.)
|
||||
// - text/PRIMARY (card name, segment/tab labels) is BODY -> must clear AA 4.5:1 even hovered.
|
||||
// - text/DIM is used ONLY as a compact UI LABEL on a hoverable surface (the card's root/key
|
||||
// BADGE — the one dim-on-hover pair the restyle draws), so it clears at the 3:1 large/
|
||||
// UI-label floor, per §2.1's "confine dim to large/UI-label use where the 3:1 floor
|
||||
// applies." A lightened surface pulls a mid-grey dim toward the body floor; the badge is
|
||||
// deliberately a label, not body, so 3:1 is the correct floor and this locks it there.
|
||||
static void testTextOnHoverSurfaceClearsFloor() {
|
||||
const KitColor hover = roleColorState(Role::BgCell, InteractionState::Hover);
|
||||
CHECK(contrastRatio(roleColor(Role::TextPrimary), hover) >= textFloor(TextClass::Body));
|
||||
CHECK(contrastRatio(roleColor(Role::TextDim), hover) >= textFloor(TextClass::Large));
|
||||
}
|
||||
|
||||
// --- Single point of change (structural guarantee) ----------------------------
|
||||
//
|
||||
// roleColor is the ONLY color source; there is no other public accessor that yields a
|
||||
@@ -217,6 +234,7 @@ int main() {
|
||||
testTextDimClearsItsFloorOnSurfaces();
|
||||
testAccentsClearIndicatorFloorOnCell();
|
||||
testTextOnPastelFillClearsBodyFloor();
|
||||
testTextOnHoverSurfaceClearsFloor();
|
||||
testSecondaryTertiaryAreDistinguishable();
|
||||
testWarnClearsStateFloorOnBackground();
|
||||
testLabelOnActiveSurfaceClearsFloor();
|
||||
|
||||
Reference in New Issue
Block a user