Phase L L3: restyle VST editor + embed strip through the L1 kit (B pastel + spectral 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_editor.cpp
|
||||||
src/vst/reasampler_embed.cpp
|
src/vst/reasampler_embed.cpp
|
||||||
src/vst/reaper_bridge.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
|
# 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).
|
# InitDll/ExitDll dll exports survive the link (see vst3_sdk note above).
|
||||||
${VST3_SDK}/public.sdk/source/main/dllmain.cpp
|
${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
|
# control-panel layout + slider/toggle value<->pixel mapping the editor's parameter surface
|
||||||
# draws + routes against. browser_scroll pulls capture_browser transitively; param_slider +
|
# 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.
|
# 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
|
target_link_libraries(reasampler_vst PRIVATE vst3_sdk editor_geometry bridge_marshal
|
||||||
sample_map capture_paths embed_strip app_version capture_browser keyboard_strip
|
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;
|
# 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.
|
# 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})
|
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 "browser_scroll.h" // S12 scroll-window + thumb + type-to-filter search geometry
|
||||||
#include "capture_browser.h"
|
#include "capture_browser.h"
|
||||||
#include "capture_paths.h" // resolveBankFile (shared M4 path resolution)
|
#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 "editor_geometry.h" // Rect, contains
|
||||||
#include "ext_keys.h"
|
#include "ext_keys.h"
|
||||||
#include "keyboard_strip.h"
|
#include "keyboard_strip.h"
|
||||||
|
#include "theme.h" // Role / InteractionState / KitColor / spectralColor (L3)
|
||||||
#include "note_entry.h" // S12 direct numeric note-entry parse
|
#include "note_entry.h" // S12 direct numeric note-entry parse
|
||||||
#include "param_slider.h" // S12/S15/S16 control-surface layout + value<->pixel mapping
|
#include "param_slider.h" // S12/S15/S16 control-surface layout + value<->pixel mapping
|
||||||
#include "peaks.h" // computeEnvelope
|
#include "peaks.h" // computeEnvelope
|
||||||
@@ -60,42 +63,31 @@ constexpr int kSetupHeight = 176; // the single-capture setup surface (labels +
|
|||||||
constexpr int kStripBandHeight = 40;
|
constexpr int kStripBandHeight = 40;
|
||||||
constexpr int kWaveformHeight = 72; // the S11 waveform band inside the setup surface
|
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
|
// Marker roles (Phase L, L3) — semantic, drawn through the kit's palette. The waveform's
|
||||||
// the same tool. (Phase L's L1 kit replaces these flat fills later; not gated on it.)
|
// start point + the sustain-loop ends are CATEGORICAL kinds (a distinct affordance class,
|
||||||
const LICE_pixel kColBackground = LICE_RGBA(28, 28, 30, 255);
|
// §2.1), not the live/active layer, so they take the categorical accents: start = teal
|
||||||
const LICE_pixel kColTitleBg = LICE_RGBA(20, 20, 22, 255);
|
// (secondary), loop start/end = purple (tertiary). The loop-span fill is a faint purple.
|
||||||
const LICE_pixel kColCardBg = LICE_RGBA(44, 44, 48, 255);
|
constexpr Role kRoleStartMarker = Role::AccentSecondary;
|
||||||
const LICE_pixel kColCardSelBg = LICE_RGBA(48, 72, 64, 255);
|
constexpr Role kRoleLoopMarker = Role::AccentTertiary;
|
||||||
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);
|
|
||||||
|
|
||||||
// ANSI path -- string literals must stay ASCII until the Phase L type kit lands.
|
// --- Rect <-> kit adapters (Phase L, L3) -------------------------------------
|
||||||
void drawText(LICE_IBitmap* bmp, const Rect& r, const char* s, COLORREF col,
|
//
|
||||||
UINT fmt = DT_LEFT | DT_VCENTER | DT_SINGLELINE | DT_NOPREFIX) {
|
// The editor's own sub-rect type is `Rect` (editor_geometry); the kit draws against `KitBox`
|
||||||
HDC dc = bmp->getDC();
|
// (component_geometry). This is the single boundary that bridges them so every draw routes
|
||||||
SetBkMode(dc, TRANSPARENT);
|
// through the L1 kit (theme roles + draw_kit), retiring the shell's raw LICE_RGBA palette +
|
||||||
SetTextColor(dc, col);
|
// GDI DrawTextA path.
|
||||||
RECT gr{r.left, r.top, r.right, r.bottom};
|
KitBox toKitBox(const Rect& r) {
|
||||||
DrawTextA(dc, s, -1, &gr, fmt | DT_END_ELLIPSIS);
|
return KitBox{r.left, r.top, r.width(), r.height()};
|
||||||
}
|
}
|
||||||
|
|
||||||
void drawTextCentered(LICE_IBitmap* bmp, const Rect& r, const char* s, COLORREF col) {
|
// Kit text in a palette ROLE (the common case). Left/Right/Center via Align.
|
||||||
drawText(bmp, r, s, col, DT_CENTER | DT_VCENTER | DT_SINGLELINE | DT_NOPREFIX);
|
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
|
// 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);
|
return std::string(kNames[note % 12]) + std::to_string(octave);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw a mono peak envelope centered vertically in `r` (mirror of bank_panel::drawThumbnail,
|
// Draw a peak envelope in `r` through the kit's shared waveform primitive (Phase L, L3):
|
||||||
// single channel). Each bin is a vertical line from its min to its max about the midline.
|
// 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) {
|
void drawEnvelope(LICE_IBitmap* bmp, const Rect& r, const Envelope& env) {
|
||||||
if (r.width() <= 0 || r.height() <= 0 || env.empty() || env[0].empty()) return;
|
drawWaveform(bmp, toKitBox(r), env);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// A display name for a bank sample id from the snapshotted list ("?" if the id no longer
|
// 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;
|
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();
|
refreshFromBank();
|
||||||
|
|
||||||
const ViewRect& r = getRect();
|
const ViewRect& r = getRect();
|
||||||
@@ -661,6 +652,50 @@ ChannelToggleRects channelToggleRects(const Rect& area) {
|
|||||||
const Rect mono{stereo.left - kChanSegW, top, stereo.left, top + kChanSegH};
|
const Rect mono{stereo.left - kChanSegW, top, stereo.left, top + kChanSegH};
|
||||||
return {mono, stereo};
|
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
|
} // namespace
|
||||||
|
|
||||||
void ReaSamplerEditor::paint(HDC hdc) {
|
void ReaSamplerEditor::paint(HDC hdc) {
|
||||||
@@ -671,13 +706,14 @@ void ReaSamplerEditor::paint(HDC hdc) {
|
|||||||
if (w <= 0 || h <= 0) return;
|
if (w <= 0 || h <= 0) return;
|
||||||
|
|
||||||
LICE_SysBitmap bmp(w, h);
|
LICE_SysBitmap bmp(w, h);
|
||||||
LICE_Clear(&bmp, kColBackground);
|
LICE_Clear(&bmp, toLice(roleColor(Role::BgBase)));
|
||||||
|
|
||||||
const EditorBands bands = computeBands(w, h);
|
const EditorBands bands = computeBands(w, h);
|
||||||
|
|
||||||
// Title band: product name + live readout.
|
// Title band: product name + live readout. Standard B palette — the beta channel gets NO
|
||||||
LICE_FillRect(&bmp, bands.title.left, bands.title.top, bands.title.width(),
|
// distinct accent (settled 2026-07-27); the channel-derived vstPluginName is the only
|
||||||
bands.title.height(), kColTitleBg, 1.0f, 0);
|
// 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)
|
std::string title = reasampler::vstPluginName(); // channel-derived (S18)
|
||||||
if (processor_ && processor_->bridge().isConnected()) {
|
if (processor_ && processor_->bridge().isConnected()) {
|
||||||
if (samples_.empty()) title += " [bank empty]";
|
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,
|
Rect titleText{bands.title.left + 8, bands.title.top, bands.title.right - 8,
|
||||||
bands.title.bottom};
|
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);
|
const bool inZones = (view_ == View::kZones);
|
||||||
LICE_FillRect(&bmp, bands.toggleBrowser.left, bands.toggleBrowser.top,
|
const InteractionState browserState =
|
||||||
bands.toggleBrowser.width(), bands.toggleBrowser.height(),
|
!inZones ? InteractionState::Active
|
||||||
inZones ? kColTabBg : kColTabActiveBg, 1.0f, 0);
|
: (isHovered(HoverKind::kToggleBrowser, -1) ? InteractionState::Hover
|
||||||
LICE_FillRect(&bmp, bands.toggleZones.left, bands.toggleZones.top,
|
: InteractionState::Rest);
|
||||||
bands.toggleZones.width(), bands.toggleZones.height(),
|
const InteractionState zonesState =
|
||||||
inZones ? kColTabActiveBg : kColTabBg, 1.0f, 0);
|
inZones ? InteractionState::Active
|
||||||
drawTextCentered(&bmp, bands.toggleBrowser, "Browser", kRgbText);
|
: (isHovered(HoverKind::kToggleZones, -1) ? InteractionState::Hover
|
||||||
drawTextCentered(&bmp, bands.toggleZones, "Zones", kRgbText);
|
: 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) {
|
if (view_ == View::kZones) {
|
||||||
paintZones(&bmp, w, h);
|
paintZones(&bmp, w, h);
|
||||||
@@ -714,11 +759,12 @@ void ReaSamplerEditor::paint(HDC hdc) {
|
|||||||
if (dropHintTicks_ > 0) {
|
if (dropHintTicks_ > 0) {
|
||||||
const int bannerH = (std::min)(kTitleHeight + 8, h);
|
const int bannerH = (std::min)(kTitleHeight + 8, h);
|
||||||
Rect banner{0, bands.toggleZones.bottom, w, bands.toggleZones.bottom + bannerH};
|
Rect banner{0, bands.toggleZones.bottom, w, bands.toggleZones.bottom + bannerH};
|
||||||
LICE_FillRect(&bmp, banner.left, banner.top, banner.width(), banner.height(),
|
// A transient notice, not the live layer — draw it on the accent-tertiary categorical
|
||||||
kColTabActiveBg, 1.0f, 0);
|
// hue with a dark label so it reads as "attention, not action".
|
||||||
drawTextCentered(&bmp, banner,
|
fillSurface(&bmp, toKitBox(banner), Role::AccentTertiary, InteractionState::Rest);
|
||||||
"Dropped here isn't loaded yet - drop files onto the ReaSampler bank panel to add them.",
|
kitTextCentered(&bmp, banner,
|
||||||
kRgbText);
|
"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);
|
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.
|
// 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 primary{area.left, area.top, area.right, area.top + area.height() / 2};
|
||||||
Rect hint{area.left, primary.bottom, area.right, area.bottom};
|
Rect hint{area.left, primary.bottom, area.right, area.bottom};
|
||||||
drawTextCentered(bmp, primary, msg, kRgbDim);
|
kitTextCentered(bmp, primary, msg, Font::Label, Role::TextDim);
|
||||||
drawTextCentered(bmp,
|
kitTextCentered(bmp, hint,
|
||||||
hint,
|
"To add a sample: drop a file onto the ReaSampler bank panel (the docked window).",
|
||||||
"To add a sample: drop a file onto the ReaSampler bank panel (the docked window).",
|
Font::Micro, Role::TextDim);
|
||||||
kRgbDim);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void ReaSamplerEditor::paintBrowser(LICE_IBitmap* bmp, int w, int h) {
|
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 searchBox = searchBoxRect(fullBrowserArea.width());
|
||||||
const Rect searchAbs{fullBrowserArea.left + searchBox.left, fullBrowserArea.top + searchBox.top,
|
const Rect searchAbs{fullBrowserArea.left + searchBox.left, fullBrowserArea.top + searchBox.top,
|
||||||
fullBrowserArea.left + searchBox.right, fullBrowserArea.top + searchBox.bottom};
|
fullBrowserArea.left + searchBox.right, fullBrowserArea.top + searchBox.bottom};
|
||||||
LICE_FillRect(bmp, searchAbs.left, searchAbs.top, searchAbs.width(), searchAbs.height(),
|
// A focused search box lifts to the focus state (a nudge toward the primary accent + a
|
||||||
searchFocused_ ? kColTabActiveBg : kColTabBg, 1.0f, 0);
|
// 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 sb = searchQuery_.empty()
|
||||||
? std::string("Search captures...")
|
? std::string("Search captures...")
|
||||||
: ("Search: " + searchQuery_ + (searchFocused_ ? "_" : ""));
|
: ("Search: " + searchQuery_ + (searchFocused_ ? "_" : ""));
|
||||||
Rect sbText{searchAbs.left + 6, searchAbs.top, searchAbs.right - 6, searchAbs.bottom};
|
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};
|
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 std::string label = (i == 0) ? "All" : banks_[static_cast<std::size_t>(i - 1)].displayName;
|
||||||
const bool active = (i == 0) ? activeFilterBankId_.empty()
|
const bool active = (i == 0) ? activeFilterBankId_.empty()
|
||||||
: (banks_[static_cast<std::size_t>(i - 1)].id == activeFilterBankId_);
|
: (banks_[static_cast<std::size_t>(i - 1)].id == activeFilterBankId_);
|
||||||
LICE_FillRect(bmp, t.left, t.top, t.width(), t.height(),
|
const InteractionState state =
|
||||||
active ? kColTabActiveBg : kColTabBg, 1.0f, 0);
|
active ? InteractionState::Active
|
||||||
drawTextCentered(bmp, t, label.c_str(), kRgbText);
|
: (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
|
// 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 SampleChoice& s = visible_[static_cast<std::size_t>(i)];
|
||||||
const bool sel = (s.id == selectedId_);
|
const bool sel = (s.id == selectedId_);
|
||||||
LICE_FillRect(bmp, content.left, content.top, content.width(), content.height(),
|
// Card surface: a plain cell (hover lightens), with the SELECTED pick marked by an
|
||||||
sel ? kColCardSelBg : kColCardBg, 1.0f, 0);
|
// 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,
|
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));
|
drawEnvelope(bmp, thumb, thumbnailFor(s.id, bins));
|
||||||
|
|
||||||
// Name + root/key badge under the thumbnail.
|
// Name + root/key badge under the thumbnail.
|
||||||
std::string caption = s.displayName.empty() ? s.id : s.displayName;
|
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 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};
|
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;
|
std::string badge;
|
||||||
if (s.rootNote) badge = "root " + noteLabel(*s.rootNote);
|
if (s.rootNote) badge = "root " + noteLabel(*s.rootNote);
|
||||||
else if (s.key) badge = *s.key;
|
else if (s.key) badge = *s.key;
|
||||||
else badge = "root -";
|
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
|
// 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_);
|
const Rect thumb = scrollThumbRect(bl, cardCount, scrollOffset_);
|
||||||
if (thumb.height() > 0) {
|
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(),
|
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) {
|
void ReaSamplerEditor::paintSetup(LICE_IBitmap* bmp, const Rect& area) {
|
||||||
// The guided single-capture setup: the picked capture's name + root/level, and a
|
// The guided single-capture setup: the picked capture's name + root/level, and a
|
||||||
// keyboard strip with its root marker (drag to set root).
|
// keyboard strip with its root marker (drag to set root). A raised bg/panel region.
|
||||||
LICE_FillRect(bmp, area.left, area.top, area.width(), area.height(),
|
fillSurface(bmp, toKitBox(area), Role::BgPanel, InteractionState::Rest);
|
||||||
kColTitleBg, 1.0f, 0);
|
|
||||||
|
|
||||||
// Effective root: the picked sample's rootNote intrinsic (or middle C when unset).
|
// 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
|
// 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);
|
const ChannelToggleRects chan = channelToggleRects(area);
|
||||||
Rect headerR{area.left + pad, area.top + 4, chan.mono.left - 8, area.top + 22};
|
Rect headerR{area.left + pad, area.top + 4, chan.mono.left - 8, area.top + 22};
|
||||||
std::string header = sampleLabel(samples_, selectedId_) + " root " + noteLabel(root);
|
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),
|
// S7 mono | stereo output-mode toggle. Active segment = accent-primary fill (the live
|
||||||
// the inactive is kColTabBg — the same visual grammar as the Browser/Zones toggle.
|
// mode); inactive lightens on hover — the same visual grammar as the Browser/Zones toggle.
|
||||||
const bool isStereo = (channelMode_ == ChannelMode::Stereo);
|
const bool isStereo = (channelMode_ == ChannelMode::Stereo);
|
||||||
LICE_FillRect(bmp, chan.mono.left, chan.mono.top, chan.mono.width(), chan.mono.height(),
|
const InteractionState monoState =
|
||||||
isStereo ? kColTabBg : kColTabActiveBg, 1.0f, 0);
|
!isStereo ? InteractionState::Active
|
||||||
LICE_FillRect(bmp, chan.stereo.left, chan.stereo.top, chan.stereo.width(),
|
: (isHovered(HoverKind::kChanMono, -1) ? InteractionState::Hover
|
||||||
chan.stereo.height(), isStereo ? kColTabActiveBg : kColTabBg, 1.0f, 0);
|
: InteractionState::Rest);
|
||||||
drawTextCentered(bmp, chan.mono, "Mono", kRgbText);
|
const InteractionState stereoState =
|
||||||
drawTextCentered(bmp, chan.stereo, "Stereo", kRgbText);
|
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};
|
Rect hintR{area.left + pad, headerR.bottom, area.right - pad, headerR.bottom + 16};
|
||||||
drawText(bmp, hintR,
|
kitText(bmp, hintR,
|
||||||
"Drag the waveform markers to set start + loop; drag the keyboard to set root.",
|
"Drag the waveform markers to set start + loop; drag the keyboard to set root.",
|
||||||
kRgbDim);
|
Font::Micro, Role::TextDim);
|
||||||
|
|
||||||
// --- S11 waveform surface: the picked capture's envelope + draggable markers ----------
|
// --- S11 waveform surface: the picked capture's envelope + draggable markers ----------
|
||||||
const std::vector<AudioSample>& pcm = monoPcmFor(selectedId_);
|
const std::vector<AudioSample>& pcm = monoPcmFor(selectedId_);
|
||||||
const std::int64_t frames = static_cast<std::int64_t>(pcm.size());
|
const std::int64_t frames = static_cast<std::int64_t>(pcm.size());
|
||||||
const Rect waveArea = setupWaveformArea(area);
|
const Rect waveArea = setupWaveformArea(area);
|
||||||
LICE_FillRect(bmp, waveArea.left, waveArea.top, waveArea.width(), waveArea.height(),
|
// The waveform pane is a recessed surface (bg/base, one step DOWN from the setup panel)
|
||||||
kColWaveBg, 1.0f, 0);
|
// so the accent-primary envelope reads against it.
|
||||||
|
fillSurface(bmp, toKitBox(waveArea), Role::BgBase, InteractionState::Rest);
|
||||||
if (frames > 0 && waveArea.width() > 0) {
|
if (frames > 0 && waveArea.width() > 0) {
|
||||||
// Envelope at one bin per pixel (full-res view of the decoded PCM, S10 read-only view
|
// 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.
|
// reused). computeEnvelope over the cached mono frames — no new decode.
|
||||||
const int bins = (std::max)(1, waveArea.width());
|
const int bins = (std::max)(1, waveArea.width());
|
||||||
const Envelope env = computeEnvelope(pcm, 1, pcm.size(), static_cast<std::size_t>(bins));
|
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);
|
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) {
|
if (m.hasLoop && m.loopEnd > m.loopStart) {
|
||||||
const int lx = frameToX(waveArea, frames, m.loopStart);
|
const int lx = frameToX(waveArea, frames, m.loopStart);
|
||||||
const int rx = frameToX(waveArea, frames, m.loopEnd);
|
const int rx = frameToX(waveArea, frames, m.loopEnd);
|
||||||
if (rx > lx) {
|
if (rx > lx) {
|
||||||
LICE_FillRect(bmp, lx, waveArea.top, rx - lx, waveArea.height(),
|
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
|
// The three markers: start (teal, secondary) + loop start/end (purple, tertiary) —
|
||||||
// lines the full waveform height. Loop markers dim when no loop is set (the "no loop"
|
// categorical affordance hues (§2.1), 2px vertical lines the full waveform height.
|
||||||
// state — draggable to CREATE one).
|
// 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 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) {
|
for (int i = 0; i < 3; ++i) {
|
||||||
const int mx = frameToX(waveArea, frames, markerFrames[i]);
|
const int mx = frameToX(waveArea, frames, markerFrames[i]);
|
||||||
const bool loopMarker = (i != 0);
|
const bool loopMarker = (i != 0);
|
||||||
const float alpha = (loopMarker && !m.hasLoop) ? 0.4f : 1.0f;
|
const float alpha = (loopMarker && !m.hasLoop) ? 0.4f : 1.0f;
|
||||||
LICE_FillRect(bmp, mx - 1, waveArea.top, 2, waveArea.height(), markerCols[i],
|
LICE_FillRect(bmp, mx - 1, waveArea.top, 2, waveArea.height(),
|
||||||
alpha, 0);
|
toLice(roleColor(markerRoles[i])), alpha, 0);
|
||||||
}
|
}
|
||||||
} else {
|
} 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);
|
const Rect stripArea = setupStripArea(area);
|
||||||
|
drawSpectralStrip(bmp, stripArea);
|
||||||
const StripLayout sl = layoutStrip(stripArea.width(), stripArea.height());
|
const StripLayout sl = layoutStrip(stripArea.width(), stripArea.height());
|
||||||
const int sx = stripArea.left;
|
drawRootMarker(bmp, stripArea, sl, root);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void ReaSamplerEditor::paintZones(LICE_IBitmap* bmp, int w, int h) {
|
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).
|
// 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,
|
Rect addR{bands.content.left + pad, bands.content.top + 4, bands.content.left + pad + 96,
|
||||||
bands.content.top + 4 + 20};
|
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,
|
const KitButtonBox box{toKitBox(addR)};
|
||||||
kColCardSelBorder, 1.0f, 0);
|
const InteractionState state =
|
||||||
drawTextCentered(bmp, addR, "+ Add Zone", kRgbText);
|
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};
|
Rect delR{addR.right + 8, addR.top, addR.right + 8 + 64, addR.bottom};
|
||||||
if (selectedZone_ >= 0) {
|
if (selectedZone_ >= 0) {
|
||||||
LICE_FillRect(bmp, delR.left, delR.top, delR.width(), delR.height(), kColCardBg, 1.0f, 0);
|
const KitButtonBox box{toKitBox(delR)};
|
||||||
LICE_DrawRect(bmp, delR.left, delR.top, delR.width() - 1, delR.height() - 1,
|
const InteractionState state =
|
||||||
kColCardSelBorder, 1.0f, 0);
|
isHovered(HoverKind::kDeleteZone, -1) ? InteractionState::Hover : InteractionState::Rest;
|
||||||
drawTextCentered(bmp, delR, "Delete", kRgbText);
|
// 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);
|
const Rect stripArea = zonesStripArea(bands);
|
||||||
|
drawSpectralStrip(bmp, stripArea);
|
||||||
const StripLayout sl = layoutStrip(stripArea.width(), stripArea.height());
|
const StripLayout sl = layoutStrip(stripArea.width(), stripArea.height());
|
||||||
const int sx = stripArea.left;
|
const int sx = stripArea.left;
|
||||||
const int sy = stripArea.top;
|
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) {
|
for (int i = 0; i < static_cast<int>(map_.zones.size()); ++i) {
|
||||||
const PerformanceZone& z = map_.zones[static_cast<std::size_t>(i)];
|
const PerformanceZone& z = map_.zones[static_cast<std::size_t>(i)];
|
||||||
Rect bar = zoneBarRect(sl, z.lowNote, z.highNote);
|
Rect bar = zoneBarRect(sl, z.lowNote, z.highNote);
|
||||||
|
const int bw = (std::max)(2, bar.width());
|
||||||
const bool sel = (i == selectedZone_);
|
const bool sel = (i == selectedZone_);
|
||||||
LICE_FillRect(bmp, bar.left + sx, sy, (std::max)(2, bar.width()), stripArea.height(),
|
if (sel) {
|
||||||
sel ? kColZoneBarSel : kColZoneBar, sel ? 1.0f : 0.7f, 0);
|
// 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
|
// 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};
|
Rect infoR{stripArea.left, legendTop, stripArea.right, legendTop + 18};
|
||||||
if (selectedZone_ >= 0 && selectedZone_ < static_cast<int>(map_.zones.size())) {
|
if (selectedZone_ >= 0 && selectedZone_ < static_cast<int>(map_.zones.size())) {
|
||||||
const PerformanceZone& z = map_.zones[static_cast<std::size_t>(selectedZone_)];
|
const PerformanceZone& z = map_.zones[static_cast<std::size_t>(selectedZone_)];
|
||||||
drawText(bmp, Rect{infoR.left, infoR.top, infoR.left + 120, infoR.bottom},
|
kitText(bmp, Rect{infoR.left, infoR.top, infoR.left + 120, infoR.bottom},
|
||||||
sampleLabel(samples_, z.sampleId).c_str(), kRgbText);
|
sampleLabel(samples_, z.sampleId).c_str(), Font::Label, Role::TextPrimary);
|
||||||
// Three fields laid out left-to-right after the sample label.
|
// 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 Rect fields = noteEntryFieldsArea(bands);
|
||||||
const char* names[3] = {"Low", "High", "Root"};
|
const char* names[3] = {"Low", "High", "Root"};
|
||||||
const std::string vals[3] = {
|
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) {
|
for (int f = 0; f < 3; ++f) {
|
||||||
const Rect fr = noteEntryFieldRect(fields, f);
|
const Rect fr = noteEntryFieldRect(fields, f);
|
||||||
const bool editing = (entryField_ == f);
|
const bool editing = (entryField_ == f);
|
||||||
LICE_FillRect(bmp, fr.left, fr.top, fr.width(), fr.height(),
|
fillSurface(bmp, toKitBox(fr), Role::BgCell,
|
||||||
editing ? kColTabActiveBg : kColCardBg, 1.0f, 0);
|
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,
|
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]) + ": " +
|
std::string cap = std::string(names[f]) + ": " +
|
||||||
(editing ? (entryText_ + "_") : vals[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()) {
|
} else if (map_.zones.empty()) {
|
||||||
drawText(bmp, infoR,
|
kitText(bmp, infoR,
|
||||||
"No zones. Add Zone maps the picked capture across the keyboard.", kRgbDim);
|
"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 +
|
// 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) {
|
for (const ControlRow& r : rows) {
|
||||||
if (r.row.top >= panel.bottom) break; // clip at the panel bottom
|
if (r.row.top >= panel.bottom) break; // clip at the panel bottom
|
||||||
const ControlLabels lab = labelsFor(static_cast<ParamControl>(r.id));
|
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 double v = controlValue(r.id, play);
|
||||||
|
const bool hov = isHovered(HoverKind::kControl, r.id);
|
||||||
if (r.kind == ControlKind::Toggle) {
|
if (r.kind == ControlKind::Toggle) {
|
||||||
const bool seg1 = (v >= 0.5);
|
const bool seg1 = (v >= 0.5);
|
||||||
const Rect s0 = toggleSegmentRect(r.control, 0);
|
const Rect s0 = toggleSegmentRect(r.control, 0);
|
||||||
const Rect s1 = toggleSegmentRect(r.control, 1);
|
const Rect s1 = toggleSegmentRect(r.control, 1);
|
||||||
LICE_FillRect(bmp, s0.left, s0.top, s0.width(), s0.height(),
|
// The lit segment carries the primary accent (Active); the unlit segment hovers
|
||||||
seg1 ? kColTabBg : kColTabActiveBg, 1.0f, 0);
|
// toward accent/hot when the whole control is under the pointer.
|
||||||
LICE_FillRect(bmp, s1.left, s1.top, s1.width(), s1.height(),
|
const InteractionState s0State =
|
||||||
seg1 ? kColTabActiveBg : kColTabBg, 1.0f, 0);
|
!seg1 ? InteractionState::Active : (hov ? InteractionState::Hover : InteractionState::Rest);
|
||||||
drawTextCentered(bmp, s0, lab.seg0, kRgbText);
|
const InteractionState s1State =
|
||||||
drawTextCentered(bmp, s1, lab.seg1, kRgbText);
|
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 {
|
} 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);
|
const Rect track = sliderTrackRect(r.control);
|
||||||
LICE_FillRect(bmp, track.left, track.top + track.height() / 2 - 1, track.width(), 2,
|
fillSurface(bmp, toKitBox(Rect{track.left, track.top + track.height() / 2 - 1,
|
||||||
kColStripKey, 1.0f, 0);
|
track.right, track.top + track.height() / 2 + 1}),
|
||||||
|
Role::BgCell, InteractionState::Pressed);
|
||||||
const Rect handle = sliderHandleRect(r.control, v);
|
const Rect handle = sliderHandleRect(r.control, v);
|
||||||
LICE_FillRect(bmp, handle.left, handle.top + 2, handle.width(), handle.height() - 4,
|
// Filled portion: track-left to the handle center.
|
||||||
kColRootMarker, 1.0f, 0);
|
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 -------------------------------------------
|
// --- Input: the drag-state machine -------------------------------------------
|
||||||
|
|
||||||
void ReaSamplerEditor::onMouseDown(int x, int y) {
|
void ReaSamplerEditor::onMouseDown(int x, int y) {
|
||||||
@@ -1626,7 +1817,34 @@ LRESULT CALLBACK ReaSamplerEditor::wndProc(HWND hwnd, UINT msg, WPARAM wParam,
|
|||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
case WM_MOUSEMOVE:
|
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;
|
return 0;
|
||||||
case WM_MOUSEWHEEL:
|
case WM_MOUSEWHEEL:
|
||||||
// S12 browser scroll. GET_WHEEL_DELTA_WPARAM is signed; positive == wheel up.
|
// 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.
|
// 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 };
|
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
|
#ifdef _WIN32
|
||||||
void paint(HDC hdc);
|
void paint(HDC hdc);
|
||||||
void paintBrowser(LICE_IBitmap* bmp, int w, int h);
|
void paintBrowser(LICE_IBitmap* bmp, int w, int h);
|
||||||
@@ -111,6 +139,17 @@ private:
|
|||||||
void onMouseDown(int x, int y);
|
void onMouseDown(int x, int y);
|
||||||
void onMouseMove(int x, int y);
|
void onMouseMove(int x, int y);
|
||||||
void onMouseUp(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 onMouseWheel(int delta); // S12 browser scroll (wheel)
|
||||||
void onSearchChar(unsigned int ch); // S12 type-to-filter search keystroke
|
void onSearchChar(unsigned int ch); // S12 type-to-filter search keystroke
|
||||||
|
|
||||||
@@ -242,6 +281,12 @@ private:
|
|||||||
int entryField_ = -1;
|
int entryField_ = -1;
|
||||||
std::string entryText_;
|
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 ------------------------------------------------------
|
// --- Drag-state machine ------------------------------------------------------
|
||||||
DragKind drag_ = DragKind::kNone;
|
DragKind drag_ = DragKind::kNone;
|
||||||
int dragStartX_ = 0; // grab x (px), for the pixel-delta resolver
|
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 "app_version.h" // vstPluginName (channel-derived embed label, S18)
|
||||||
#include "bank_sync.h" // parseBankGeneration (S9 dirty-guard over the per-paint refresh)
|
#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 "editor_geometry.h" // Rect (shared with embed_strip)
|
||||||
#include "embed_strip.h" // the pure strip layout + hit-test
|
#include "embed_strip.h" // the pure strip layout + hit-test
|
||||||
#include "ext_keys.h" // kProjExtBanksKey / kProjExtBankGenKey
|
#include "ext_keys.h" // kProjExtBanksKey / kProjExtBankGenKey
|
||||||
#include "reaper_bridge.h"
|
#include "reaper_bridge.h"
|
||||||
#include "reasampler_processor.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
|
// 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.
|
// reaper_plugin_fx_embed.h's REAPER_FXEMBED_IBitmap::Extended needs as its return type.
|
||||||
@@ -41,15 +44,12 @@ namespace reasampler::vst {
|
|||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
// Palette — mirrored from reasampler_editor.cpp so the inline strip reads as the same tool.
|
// Kit adapter (Phase L, L3): the embed shell's Rect (editor_geometry) -> the kit's KitBox
|
||||||
const LICE_pixel kColBackground = LICE_RGBA(28, 28, 30, 255);
|
// (component_geometry). Every embed surface now draws by palette ROLE via the L1 kit, retiring
|
||||||
const LICE_pixel kColZone = LICE_RGBA(44, 44, 48, 255);
|
// the local pre-L1 forest-green palette + raw GDI DrawTextA.
|
||||||
const LICE_pixel kColZoneSel = LICE_RGBA(58, 96, 84, 255);
|
KitBox toKitBox(const Rect& r) {
|
||||||
const LICE_pixel kColZoneBorder = LICE_RGBA(20, 20, 22, 255);
|
return KitBox{r.left, r.top, r.width(), r.height()};
|
||||||
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);
|
|
||||||
|
|
||||||
// A short display name for a bank sample id, from the snapshotted list (the editor's helper,
|
// 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).
|
// 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
|
return 0; // not a build target off Windows
|
||||||
#endif
|
#endif
|
||||||
case REAPER_FXEMBED_WM_CREATE:
|
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
|
refresh(); // prime the first paint's snapshot
|
||||||
return 0;
|
return 0;
|
||||||
case REAPER_FXEMBED_WM_DESTROY:
|
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
|
// 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).
|
// 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);
|
const EmbedLayout layout = layoutEmbed(w, h);
|
||||||
|
|
||||||
if (map_.zones.empty()) {
|
if (map_.zones.empty()) {
|
||||||
// No opt-in zones authored: show a single faint band spanning the keymap area so the
|
// No opt-in zones authored: a faint bg/cell band spanning the keymap area so the strip
|
||||||
// strip reads as "present, no zones" — the default single-capture face lives in the
|
// reads as "present, no zones" — the default single-capture face lives in the editor.
|
||||||
// editor (this S6 strip mirrors the zones map only).
|
|
||||||
LICE_FillRect(bmp, layout.keymap.left, layout.keymap.top, layout.keymap.width(),
|
LICE_FillRect(bmp, layout.keymap.left, layout.keymap.top, layout.keymap.width(),
|
||||||
layout.keymap.height(), kColEmpty, 0.5f, 0);
|
layout.keymap.height(), toLice(roleColor(Role::BgCell)), 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};
|
|
||||||
const std::string label = reasampler::vstPluginName() + // channel-derived (S18)
|
const std::string label = reasampler::vstPluginName() + // channel-derived (S18)
|
||||||
(samples_.empty() ? " (bank empty)" : " (no zones)");
|
(samples_.empty() ? " (bank empty)" : " (no zones)");
|
||||||
DrawTextA(dc, label.c_str(), -1, &gr,
|
const Rect labelR{layout.keymap.left + 4, layout.keymap.top, layout.keymap.right,
|
||||||
DT_LEFT | DT_VCENTER | DT_SINGLELINE | DT_NOPREFIX);
|
layout.keymap.bottom};
|
||||||
|
text(bmp, toKitBox(labelR), label.c_str(), Font::Label, Role::TextPrimary, Align::Left);
|
||||||
} else {
|
} else {
|
||||||
// Draw each zone as a segment across the keymap span, first-match order (so the
|
// Draw each zone as a segment across the keymap span, first-match order (so the painted
|
||||||
// painted order matches selection + playback). The selected zone is highlighted.
|
// 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) {
|
for (int i = 0; i < static_cast<int>(map_.zones.size()); ++i) {
|
||||||
const PerformanceZone& z = map_.zones[i];
|
const PerformanceZone& z = map_.zones[i];
|
||||||
const Rect r = zoneSegmentRect(layout, z.lowNote, z.highNote);
|
const Rect r = zoneSegmentRect(layout, z.lowNote, z.highNote);
|
||||||
if (r.width() <= 0) continue;
|
if (r.width() <= 0) continue;
|
||||||
const bool sel = (i == selectedZone_);
|
const bool sel = (i == selectedZone_);
|
||||||
LICE_FillRect(bmp, r.left, r.top, r.width(), r.height(),
|
if (sel) {
|
||||||
sel ? kColZoneSel : kColZone, 1.0f, 0);
|
// Static glow halo, then the crisp accent-primary fill.
|
||||||
LICE_DrawRect(bmp, r.left, r.top, r.width() - 1, r.height() - 1, kColZoneBorder,
|
LICE_FillRect(bmp, r.left - 2, r.top, r.width() + 4, r.height(),
|
||||||
1.0f, 0);
|
toLice(roleColor(Role::AccentHot)), 0.30f, 0);
|
||||||
// Label the segment with the sample name when it is wide enough to read.
|
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) {
|
if (r.width() >= 24) {
|
||||||
HDC dc = bmp->getDC();
|
const Rect lr{r.left + 3, r.top, r.right - 2, r.bottom};
|
||||||
SetBkMode(dc, TRANSPARENT);
|
text(bmp, toKitBox(lr), sampleLabel(samples_, z.sampleId).c_str(),
|
||||||
SetTextColor(dc, kRgbText);
|
Font::Label, sel ? Role::BgBase : Role::TextPrimary, Align::Left);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// The level band: a static background here; a live activity level is a later refinement
|
// The level band: a recessed bg/cell channel with an accent-primary fill following the
|
||||||
// (the processor would publish a peak the UI thread reads). Draw the empty band so the
|
// live activity level (a direct level follow — the one permitted "motion", §3.5).
|
||||||
// strip's geometry is complete and the DAW-verify sees the band lifecycle now.
|
|
||||||
if (layout.levelBand.height() > 0) {
|
if (layout.levelBand.height() > 0) {
|
||||||
LICE_FillRect(bmp, layout.levelBand.left, layout.levelBand.top,
|
fillSurface(bmp, toKitBox(layout.levelBand), Role::BgCell, InteractionState::Pressed);
|
||||||
layout.levelBand.width(), layout.levelBand.height(), kColLevelBg, 1.0f,
|
|
||||||
0);
|
|
||||||
const double level = processor_ ? processor_->embedActivityLevel() : 0.0;
|
const double level = processor_ ? processor_->embedActivityLevel() : 0.0;
|
||||||
const Rect fill = levelFillRect(layout, level);
|
const Rect fill = levelFillRect(layout, level);
|
||||||
if (fill.width() > 0) {
|
if (fill.width() > 0) {
|
||||||
LICE_FillRect(bmp, fill.left, fill.top, fill.width(), fill.height(),
|
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));
|
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) ----------------------------
|
// --- Single point of change (structural guarantee) ----------------------------
|
||||||
//
|
//
|
||||||
// roleColor is the ONLY color source; there is no other public accessor that yields a
|
// roleColor is the ONLY color source; there is no other public accessor that yields a
|
||||||
@@ -217,6 +234,7 @@ int main() {
|
|||||||
testTextDimClearsItsFloorOnSurfaces();
|
testTextDimClearsItsFloorOnSurfaces();
|
||||||
testAccentsClearIndicatorFloorOnCell();
|
testAccentsClearIndicatorFloorOnCell();
|
||||||
testTextOnPastelFillClearsBodyFloor();
|
testTextOnPastelFillClearsBodyFloor();
|
||||||
|
testTextOnHoverSurfaceClearsFloor();
|
||||||
testSecondaryTertiaryAreDistinguishable();
|
testSecondaryTertiaryAreDistinguishable();
|
||||||
testWarnClearsStateFloorOnBackground();
|
testWarnClearsStateFloorOnBackground();
|
||||||
testLabelOnActiveSurfaceClearsFloor();
|
testLabelOnActiveSurfaceClearsFloor();
|
||||||
|
|||||||
Reference in New Issue
Block a user