Ω-W1-T3: run the master meter on its own 60 FPS timer, and make a meter frame cost one rect

This commit is contained in:
2026-08-03 12:39:36 -04:00
parent 0eb2c67875
commit 4320bfdb08
7 changed files with 248 additions and 88 deletions
+64 -25
View File
@@ -1,7 +1,8 @@
// editor_paint.cpp — the ReaSamplerEditor's paint dispatch: the WM_PAINT entry, the Sample
// face's band composition (chrome / waveform / decks, each drawn by its own TU), the empty
// state, and the drop-affordance banner. Windows-only; draws through the shared kit by
// palette role. All layout math is pure (sample_bands) — this TU only sequences.
// editor_paint.cpp — the ReaSamplerEditor's paint dispatch: the WM_PAINT entry over a retained
// back buffer (with the meter's dirty-rect fast path), the Sample face's band composition
// (chrome / waveform / decks, each drawn by its own TU), the empty state, and the
// drop-affordance banner. Windows-only; draws through the shared kit by palette role. All
// layout math is pure (sample_bands) — this TU only sequences.
#include "shell/instrument/reasampler_editor.h"
@@ -18,37 +19,75 @@ namespace reasampler::vst {
using namespace reasampler::ui; // kit vocabulary (Role / InteractionState / KitBox / …)
using namespace reasampler::instrument::ui; // pure geometry (bands / chrome)
void ReaSamplerEditor::paint(HDC hdc) {
LICE_IBitmap* ReaSamplerEditor::ensureBackBuffer(int w, int h) {
if (w <= 0 || h <= 0) return nullptr;
// resize() is a no-op at the current size, so the steady-state paint allocates nothing.
if (!backBuffer_) backBuffer_ = new LICE_SysBitmap(w, h);
else backBuffer_->resize(w, h);
return backBuffer_;
}
void ReaSamplerEditor::releaseBackBuffer() {
delete backBuffer_;
backBuffer_ = nullptr;
}
void ReaSamplerEditor::paint(HDC hdc, const RECT& dirty) {
RECT cr{};
GetClientRect(childHwnd_, &cr);
const int w = cr.right - cr.left;
const int h = cr.bottom - cr.top;
if (w <= 0 || h <= 0) return;
LICE_SysBitmap bmp(w, h);
LICE_Clear(&bmp, toLice(roleColor(Role::BgBase)));
LICE_IBitmap* bmp = ensureBackBuffer(w, h);
if (!bmp) return;
// Sample is home; Browse is a full-window modal overlay drawn over it, so the Sample
// face draws first and the modal reads as a sheet layered on top.
paintSample(&bmp, w, h);
if (view_ == View::kBrowse) paintBrowse(&bmp, w, h);
// The host's update rect, clipped to the client area.
const int dx = (std::max)(0, static_cast<int>(dirty.left));
const int dy = (std::max)(0, static_cast<int>(dirty.top));
const int dr = (std::min)(w, static_cast<int>(dirty.right));
const int db = (std::min)(h, static_cast<int>(dirty.bottom));
if (dr <= dx || db <= dy) return;
// A transient banner flashed after a file was dropped on this window. It reiterates the
// shipped ingest gesture rather than swallowing the drop silently. Drawn last so it
// overlays whatever view is up; decays via onSyncTimer (dropHintTicks_).
if (dropHintTicks_ > 0) {
const int bannerTop = (std::min)(kTitleHeight, h);
const int bannerH = (std::min)(kTitleHeight + 8, (std::max)(0, h - bannerTop));
Rect banner = Rect::ltrb(0, bannerTop, w, bannerTop + bannerH);
// 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);
// A meter frame's dirty rect lies wholly inside the bar field, and nothing else is drawn
// there — so the field is all that needs redrawing, and the rest of the face is still in the
// back buffer from the last full compose. Decided GEOMETRICALLY rather than by a flag,
// because Windows coalesces a meter invalidate with any other pending one into a single
// union rect: a flag would then paint a meter frame over a face that had really changed.
const Rect& mf = meterRects_.field;
if (!mf.empty() && dx >= mf.x && dy >= mf.y && dr <= mf.right() && db <= mf.bottom()) {
paintMeterField(bmp);
} else {
// Repopulated by paintDeck when the meter is on screen; left empty by the empty state.
meterRects_ = {};
LICE_Clear(bmp, toLice(roleColor(Role::BgBase)));
// Sample is home; Browse is a full-window modal overlay drawn over it, so the Sample
// face draws first and the modal reads as a sheet layered on top.
paintSample(bmp, w, h);
if (view_ == View::kBrowse) paintBrowse(bmp, w, h);
// A transient banner flashed after a file was dropped on this window. It reiterates the
// shipped ingest gesture rather than swallowing the drop silently. Drawn last so it
// overlays whatever view is up; decays via onSyncTimer (dropHintTicks_).
if (dropHintTicks_ > 0) {
const int bannerTop = (std::min)(kTitleHeight, h);
const int bannerH = (std::min)(kTitleHeight + 8, (std::max)(0, h - bannerTop));
Rect banner = Rect::ltrb(0, bannerTop, w, bannerTop + bannerH);
// 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);
}
// A sheet layered over the face covers the meter, so the fast path must not paint
// through it — dropping the rect routes the next meter tick down this path instead.
if (view_ == View::kBrowse || curvePopup_ != CurveTarget::kNone) meterRects_ = {};
}
BitBlt(hdc, 0, 0, w, h, bmp.getDC(), 0, 0, SRCCOPY);
BitBlt(hdc, dx, dy, dr - dx, db - dy, bmp->getDC(), dx, dy, SRCCOPY);
}
void ReaSamplerEditor::paintSample(LICE_IBitmap* bmp, int w, int h) {