dd067a192e
Fixes the critical finding — Browse/curve-popup no longer trigger 60 Hz whole-client repaints when the meter is invisible. Pushes the fast-path and self-containment geometry into master_meter with tests; batches the four minor findings.
144 lines
6.8 KiB
C++
144 lines
6.8 KiB
C++
// 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"
|
|
|
|
#ifdef _WIN32
|
|
|
|
#include <algorithm>
|
|
#include <string>
|
|
|
|
#include "shell/instrument/editor_internal.h" // kit adapters
|
|
#include "shell/instrument/reasampler_processor.h"
|
|
|
|
namespace reasampler::vst {
|
|
|
|
using namespace reasampler::ui; // kit vocabulary (Role / InteractionState / KitBox / …)
|
|
using namespace reasampler::instrument::ui; // pure geometry (bands / chrome)
|
|
|
|
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_IBitmap* bmp = ensureBackBuffer(w, h);
|
|
if (!bmp) return;
|
|
|
|
// 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 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 — see
|
|
// meterFastPathEligible (master_meter) for why.
|
|
if (meterFastPathEligible(meterRects_, Rect::ltrb(dx, dy, dr, db))) {
|
|
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 even if something else invalidates a rect that happens to match the
|
|
// stale field bounds — invalidateMeter itself already skips while covered (see there).
|
|
if (view_ == View::kBrowse || curvePopup_ != CurveTarget::kNone) meterRects_ = {};
|
|
}
|
|
|
|
BitBlt(hdc, dx, dy, dr - dx, db - dy, bmp->getDC(), dx, dy, SRCCOPY);
|
|
}
|
|
|
|
void ReaSamplerEditor::paintSample(LICE_IBitmap* bmp, int w, int h) {
|
|
const FaceLayout fl = faceLayout(w, h);
|
|
const bool empty = selectedId_.empty();
|
|
|
|
// Resolved ONCE per paint and handed to both bands that show it. The chrome enable and the
|
|
// waveform marks are two views of the same `hasLoop`, so they must not resolve it
|
|
// separately — and with no loop override set the resolve costs a bridge read plus a bank
|
|
// parse, which is not a cost to pay twice a frame.
|
|
SetupMarkers marks;
|
|
if (!empty) marks = pickedMarkers(static_cast<std::int64_t>(monoPcmFor(selectedId_).size()));
|
|
|
|
paintChrome(bmp, fl, empty, marks);
|
|
|
|
// Nothing loaded: the lower bands carry the "pick a capture" prompt pointing at Browse
|
|
// (which the chrome lit above), and there is nothing to deck.
|
|
if (empty) {
|
|
Rect body = Rect::ltrb(fl.bands.waveform.x, fl.bands.waveform.y,
|
|
fl.bands.waveform.right(), fl.bands.decks.bottom());
|
|
paintEmptyState(bmp, body);
|
|
return;
|
|
}
|
|
|
|
paintWaveform(bmp, fl.bands.waveform, marks);
|
|
paintDeck(bmp, fl);
|
|
|
|
// The curve popup: a centered sheet over the whole face, drawn last.
|
|
if (curvePopup_ != CurveTarget::kNone) paintCurvePopup(bmp, w, h);
|
|
|
|
// The piano strip's note-name chip overhangs its band, so it goes on top of everything.
|
|
paintChromeTooltip(bmp, fl, w, h);
|
|
}
|
|
|
|
void ReaSamplerEditor::paintEmptyState(LICE_IBitmap* bmp, const Rect& area) {
|
|
// Shown when no card is drawn (nothing to pick): distinguish a genuinely empty bank from
|
|
// a bank filter that hides everything. Either way it is the "pick a capture" empty state.
|
|
const char* msg = samples_.empty()
|
|
? "No captures in this project yet - capture audio into the bank to play it here."
|
|
: "No captures in this bank filter. Choose another bank tab above.";
|
|
// Split the area so the primary line sits centered and the ingest affordance sits just
|
|
// below it. The affordance is the shipped ingest gesture (drop onto the docked panel) —
|
|
// kept discoverable here regardless of whether a drop ever lands on this window.
|
|
Rect primary = Rect::ltrb(area.x, area.y, area.right(), area.y + area.height / 2);
|
|
Rect hint = Rect::ltrb(area.x, primary.bottom(), area.right(), area.bottom());
|
|
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);
|
|
}
|
|
|
|
} // namespace reasampler::vst
|
|
|
|
#endif // _WIN32
|