Q-W2: split bank_panel.cpp (3459 LOC) into eight shell/panel TUs — reasampler::panel internals, per-seam public headers, shim retired; zero behavior change, 60/60 green

This commit is contained in:
2026-07-29 10:55:58 -04:00
parent b5788c82f6
commit 30a4ffd01b
20 changed files with 4084 additions and 3596 deletions
+503
View File
@@ -0,0 +1,503 @@
// panel_drag.cpp — the card-drag / hover state machine seam of the docked bank panel
// (Q-W2 split of bank_panel.cpp; the T4-01 NEW seam; M11/L7/S17). Owns WM_MOUSEMOVE
// (hover resolution + tooltip timing + the live drag), the drop-target/gesture
// classification, the cursor cues, button-up drop dispatch (reorder / replace /
// move / copy / instrument-drop / OS drag-out), and right-click menu routing. Its
// PURE mirror is core/ui/card_drag (gesture precedence + slot hit-test) with
// core/ui/drag_out owning the OS-drag boundary decision — this shell supplies only
// the live rects, modifier state, and side effects.
//
// PER-MOUSE-MOVE GUARDRAIL (T4-28): everything on the move path stays plain
// free-function calls — no interface, no virtual dispatch.
//
// Compiled into the reaper_reasampler MODULE. No REAPER API functions are called
// directly here (the FX-hotspot / OS-drag / instrument-drop shells own theirs);
// REAPER SDK types arrive via panel_state.h.
#include <cstdlib> // std::abs (drag threshold)
#include <string>
#include <vector>
#include "shell/panel/panel_state.h"
#include "actions.h" // persistBankOp — shared undo-block wrapper (R-B)
#include "shell/actions/drag_out_win.h" // OLE / SWELL drag-out initiation seam (M11)
#include "shell/actions/instrument_drop_win.h" // resolveFxDropTarget / performInstrumentDrop (S17)
namespace reasampler::panel {
// --- Drag (move between regions/onto a tab) -----------------------------------
constexpr int kDragThreshold = 5; // px the pointer must move to begin a drag
// Resolves the drop target under client (x, y) during a drag, updating dropKind /
// dropBankId. A drop onto the pool region -> the pool; onto a named tab -> that bank;
// anywhere else -> none.
void updateDropTarget(int x, int y) {
RECT cr{};
GetClientRect(g_panel.hwnd, &cr);
const int w = cr.right - cr.left, h = cr.bottom - cr.top;
g_panel.dropKind = DropKind::None;
g_panel.dropBankId.clear();
if (banksShown()) {
const RECT br = banksRegionRect(w, h);
const TabStripRect strip = banksTabStripRect(br);
const std::vector<const Bank*> tabs = namedBanks();
const TabHit hit = hitTestTabStrip(x, y, strip,
static_cast<int>(tabs.size()), kTabSpec,
g_panel.tabScroll);
if (hit.kind == TabHitKind::Tab) {
g_panel.dropKind = DropKind::Tab;
g_panel.dropBankId = tabs[static_cast<std::size_t>(hit.index)]->id;
return;
}
// Tab takes precedence over the region; if the point is in the banks region but
// not on a specific tab, treat the whole grid as a drop zone for the shown bank.
// No valid target when there are no named banks or no shown bank.
if (!g_panel.shownBankId.empty() && book() && book()->bank(g_panel.shownBankId)) {
if (x >= br.left && x < br.right && y >= br.top && y < br.bottom) {
g_panel.dropKind = DropKind::BanksRegion;
g_panel.dropBankId = g_panel.shownBankId;
return;
}
}
}
if (poolShown()) {
const RECT pr = poolRegionRect(w, h);
const RECT grid = regionGridRect(pr, false);
if (x >= grid.left && x < grid.right && y >= grid.top && y < grid.bottom) {
g_panel.dropKind = DropKind::PoolRegion;
return;
}
}
}
// The destination bank id under the current drop target (pool id for PoolRegion; the tab/
// shown-bank id for Tab/BanksRegion; "" for no target). Derived from updateDropTarget's
// dropKind/dropBankId — the single source of "what bank is under the pointer".
std::string dropTargetBankId() {
switch (g_panel.dropKind) {
case DropKind::PoolRegion: return std::string(kPoolBankId);
case DropKind::Tab:
case DropKind::BanksRegion: return g_panel.dropBankId;
case DropKind::None: return {};
}
return {};
}
// L7: classifies the live in-grid drag into a pure CardGesture and (for a same-bank drop)
// the target slot, updating g_panel.cardGesture / dragTargetSlot. Call AFTER updateDropTarget
// so dropKind/dropBankId are current. The pure card_drag::decideCardGesture owns the
// precedence (leave-client -> OS; other-bank -> move/copy; same-bank grid -> reorder/replace);
// the shell only supplies the region verdict, the same-bank target slot + occupancy, and the
// live modifier state. The OS-drag-out boundary is handled by the existing decideGesture path
// in onMouseMove BEFORE this runs, so here the pointer is always inside the client.
void classifyCardDrag(int x, int y) {
g_panel.cardGesture = CardGesture::None;
g_panel.dragTargetSlot = -1;
RECT cr{};
GetClientRect(g_panel.hwnd, &cr);
const int w = cr.right - cr.left, h = cr.bottom - cr.top;
const PanelClientRect client{cr.left, cr.top, w, h};
const std::string destBank = dropTargetBankId();
DragModifiers mods;
mods.ctrl = ctrlDown();
mods.alt = altDown();
if (!destBank.empty() && destBank == g_panel.dragSourceBankId) {
// Same-bank grid: a reorder/replace target. Resolve the slot the pointer sits over
// in the SOURCE bank's own region display + whether it is occupied.
// Uses computeSlotRectsForDrop (one trailing row past maxSlot) so a drop beyond
// the last occupied card resolves to a valid trailing slot, not a -1 miss.
mods.region = DropRegion::SameBankGrid;
const bool isBanks = g_panel.dragSourceRegion == Region::Banks;
const RECT region = isBanks ? banksRegionRect(w, h) : poolRegionRect(w, h);
const RegionDisplay disp = regionDisplay(region, isBanks, g_panel.dragSourceRegion);
const RECT grid = regionGridRect(region, isBanks);
const int gridW = grid.right - grid.left;
const std::vector<SlotCellRect> dropRects =
computeSlotRectsForDrop(disp.bank ? disp.bank->slots.maxSlot() : -1,
gridW, kGrid);
// Translate the drop rects to client space (matching regionDisplay's translation).
std::vector<SlotCellRect> dropRectsClient = dropRects;
for (SlotCellRect& r : dropRectsClient) { r.x += grid.left; r.y += grid.top; }
const int slot = hitTestSlot(x, y, dropRectsClient);
mods.targetSlot = slot;
mods.slotOccupied = slot >= 0 && !disp.idAtSlot(slot).empty();
g_panel.dragTargetSlot = slot;
} else if (!destBank.empty()) {
mods.region = DropRegion::OtherBankOrTab; // move/copy to a different bank/tab
} else {
mods.region = DropRegion::DeadSpace; // header/footer/gap — a no-op drop
}
const DragState st{/*dragging=*/true, /*hasArmedSamples=*/!g_panel.dragSampleIds.empty()};
g_panel.cardGesture = decideCardGesture(x, y, client, st, mods);
}
// Maps the pure L7 cursor cue to a SWELL stock cursor and sets it. The cue DECISION is pure
// (card_drag::cursorForGesture); the shell owns only this SetCursor call + the resource choice.
// Stock SWELL cursors (vendor/WDL/WDL/swell/swell-types.h:1320-1329, mirroring the Win32 OCR_*
// set): Reorder -> IDC_SIZEALL (four-way move, the file-manager reorder idiom); Move ->
// IDC_HAND (grab-and-place to another bank/tab); Copy -> IDC_UPARROW (no stock copy cursor
// exists cross-platform — this is the closest distinct stock cue; a bespoke copy cursor would
// need a resource file, deliberately NOT added); Replace -> IDC_SIZEWE (a distinct "swap
// occupant" cue, shown ONLY when the pure result is Replace, i.e. Alt over an occupied slot);
// OsDragOut -> the OS drag loop owns the cursor once handed off, so leave it (arrow here is
// never seen — the handoff happens before this runs); Default/None -> IDC_ARROW.
void applyDragCursor(CardGesture g) {
const char* idc = IDC_ARROW;
switch (cursorForGesture(g)) {
case CursorCue::Reorder: idc = IDC_SIZEALL; break;
case CursorCue::Move: idc = IDC_HAND; break;
case CursorCue::Copy: idc = IDC_UPARROW; break;
case CursorCue::Replace: idc = IDC_SIZEWE; break;
case CursorCue::OsDragOut: return; // OS drag owns the cursor; do not fight it
case CursorCue::Default: idc = IDC_ARROW; break;
}
SetCursor(LoadCursor(nullptr, idc));
}
// Resolves the topmost INTERACTIVE element under client (x, y) for hover feedback, mirroring
// handleClick's precedence exactly (so the element that lights on hover is the one a click
// would hit). Returns HoverKind::None for the grid / dead space / a point outside the client
// (the grid cells carry their own selection/focus chrome, not a kit hover surface). Pure
// resolution over the same pure geometry the click path uses.
Hover resolveHover(int x, int y) {
if (!g_panel.hwnd) return Hover{};
RECT cr{};
GetClientRect(g_panel.hwnd, &cr);
const int w = cr.right - cr.left, h = cr.bottom - cr.top;
// TOP toolbar: the far-right More button, then the frequent buttons (matching the click
// order — first zone top-to-bottom).
{
const MenuButtonRect mb = topMenuButtonRect(w);
if (hitTestMenuButton(x, y, mb)) return Hover{HoverKind::MoreButton, -1};
const int hit = toolbarHit(x, y, topToolbarActionRect(w), topBarRows());
if (hit >= 0) return Hover{HoverKind::TopBarButton, hit};
}
// Footer: mode-toggle segments, Tail button, then Prune (matching the click order).
{
const int seg = footerToggleSegmentHit(x, y, w, h);
if (seg >= 0) return Hover{HoverKind::ModeSegment, seg};
const FooterBarLayout fb = footerBarLayoutFor(w, h);
if (hitTestFooterBar(x, y, fb) == FooterHit::Tail) return Hover{HoverKind::TailButton, -1};
const ButtonRect pb = pruneButtonRectFor(w, h);
if (hitTestPruneButton(x, y, pb)) return Hover{HoverKind::PruneButton, -1};
}
// BOTTOM toolbar buttons.
{
const int hit = toolbarHit(x, y, bottomToolbarRect(w, h), bottomBarRows());
if (hit >= 0) return Hover{HoverKind::BottomBarButton, hit};
}
// Region chrome: full-height toggles, create button, tabs.
if (poolShown()) {
const RECT pr = poolRegionRect(w, h);
const RECT ftb = fullHtBtnRect(pr);
if (x >= ftb.left && x < ftb.right && y >= ftb.top && y < ftb.bottom)
return Hover{HoverKind::FullHtPool, -1};
}
if (banksShown()) {
const RECT br = banksRegionRect(w, h);
const RECT ftb = fullHtBtnRect(br);
if (x >= ftb.left && x < ftb.right && y >= ftb.top && y < ftb.bottom)
return Hover{HoverKind::FullHtBanks, -1};
const RECT cb = createBtnRect(br);
if (x >= cb.left && x < cb.right && y >= cb.top && y < cb.bottom)
return Hover{HoverKind::CreateBank, -1};
const TabStripRect strip = banksTabStripRect(br);
const std::vector<const Bank*> tabs = namedBanks();
const TabHit hit = hitTestTabStrip(x, y, strip, static_cast<int>(tabs.size()),
kTabSpec, g_panel.tabScroll);
if (hit.kind == TabHitKind::Tab) return Hover{HoverKind::Tab, hit.index};
}
return Hover{};
}
// Updates the live hover element and repaints ONLY on a change (sub-frame feedback, no
// per-move jank — the "speed is the selling point" repaint discipline). L5: a hover CHANGE also
// resets the tooltip timer (hoverSinceTick) and hides any shown tooltip, so the tooltip only
// appears after the pointer rests kTooltipDelayMs on ONE element (the delay is applied by the
// poll tick in maybeShowTooltip). A move within the SAME element leaves the timer running.
void updateHover(int x, int y) {
const Hover next = resolveHover(x, y);
if (next != g_panel.hovered) {
g_panel.hovered = next;
g_panel.hoverSinceTick = GetTickCount();
if (g_panel.tooltipShown) { g_panel.tooltipShown = false; }
invalidatePanel();
}
}
// Applies the tooltip hover-delay: if a tooltip-bearing element has been hovered past
// kTooltipDelayMs and the tooltip is not yet shown, latch it and repaint once. Driven from the
// OnTimer poll (bankPanelRefresh) so the tooltip appears after a rest with no dedicated timer;
// WM_MOUSEMOVE's updateHover resets the timer, so a moving pointer never trips it. No-op when the
// current hover has no tooltip (grid / chrome / the More button).
void maybeShowTooltip() {
if (g_panel.tooltipShown) return;
const HoverKind k = g_panel.hovered.kind;
if (k != HoverKind::TopBarButton && k != HoverKind::BottomBarButton) return;
const unsigned int now = GetTickCount();
if (now - g_panel.hoverSinceTick >= kTooltipDelayMs) {
g_panel.tooltipShown = true;
invalidatePanel();
}
}
void onMouseMove(int x, int y) {
// Hover feedback (L2): resolve + repaint-on-change, but NOT during a drag (the drag owns
// the visual feedback then — a drop-target highlight, not a hover). Cleared to None when
// the pointer is over the grid / dead space.
if (!g_panel.dragging && !g_panel.dragArmed) updateHover(x, y);
if (g_panel.dragArmed && !g_panel.dragging) {
if (std::abs(x - g_panel.dragStartX) > kDragThreshold ||
std::abs(y - g_panel.dragStartY) > kDragThreshold) {
// Threshold crossed — begin the drag. Snapshot the payload NOW.
g_panel.dragging = true;
g_panel.dragSourceBankId = bankIdForRegion(g_panel.dragSourceRegion);
g_panel.dragSampleIds = focusedSelectionIds();
// The single card actually grabbed = the focus ordinal's id. This is the L7
// in-grid reorder/replace subject (see onLBtnUp) — "drag a card" is a single-card
// gesture, distinct from the multi-select move/copy payload in dragSampleIds.
{
const RegionDisplay disp = focusedDisplay();
const int f = g_panel.selection.focus;
g_panel.dragPrimaryId =
(f >= 0 && f < disp.occupiedCount())
? disp.orderedIds[static_cast<std::size_t>(f)] : std::string{};
}
g_panel.hovered = Hover{}; // clear hover — the drag owns the visual feedback now
g_panel.tooltipShown = false; // a drag never shows a tooltip
// SetCapture was already called at drag-arm time (handleClick); no re-capture needed.
}
}
if (g_panel.dragging) {
// M11 gesture boundary, REFINED by S17. While a drag with samples is under way and the
// pointer is INSIDE the client rect it stays the internal bank-to-bank drag (invariant
// #4, byte-identical). Once it LEAVES the client rect the pure drag_out::decideGesture
// splits the outside case three ways: a single-capture drag over REAPER's OWN UI is an
// InstrumentDrop (hover-track the FX button, drop on release); a multi-capture drag OR a
// pointer that has left REAPER entirely is the unchanged M11 OsDrag; inside stays
// Internal. The shell supplies the "over REAPER's UI" predicate via GetThingFromPoint.
RECT cr{};
GetClientRect(g_panel.hwnd, &cr);
const PanelClientRect client{cr.left, cr.top, cr.right - cr.left, cr.bottom - cr.top};
const bool inside = (x >= cr.left && x < cr.right && y >= cr.top && y < cr.bottom);
DragState st{/*dragging=*/true, /*hasArmedSamples=*/!g_panel.dragSampleIds.empty()};
st.singleCapture = (g_panel.dragSampleIds.size() == 1);
// Resolve the FX drop target only when OUTSIDE the client rect (the S17 middle case can
// only arise there) and only for a single-capture payload — the SDK hit-test is skipped
// on the common internal-drag path so it costs nothing there. The screen conversion is
// Windows-only (D5); resolveFxDropTarget owns the REAPER hit query.
FxDropTarget fx;
if (!inside && st.singleCapture) {
POINT sp{x, y};
ClientToScreen(g_panel.hwnd, &sp);
fx = resolveFxDropTarget(sp.x, sp.y);
st.overReaperUi = fx.overReaperUi;
}
const DragGesture gesture = decideGesture(x, y, client, st);
if (gesture == DragGesture::InstrumentDrop) {
// Track the FX hotspot for the release; the highlight is REAPER's own FX-button
// hover feedback under the pointer (the drop is driven on button-up). We keep the
// internal-drag capture alive so we keep receiving moves (unlike OsDrag, this does
// NOT hand off to a modal OS loop). Clear any internal drop-target highlight so the
// panel does not also paint a bank-drop cue while the drag is out over a track.
g_panel.instrumentDropTrack = fx.valid() ? fx.track : nullptr;
g_panel.dropKind = DropKind::None;
g_panel.dropBankId.clear();
invalidatePanel();
return;
}
// Left InstrumentDrop territory (back inside, or over a non-FX area): drop the FX target.
g_panel.instrumentDropTrack = nullptr;
if (gesture == DragGesture::OsDrag) {
// Resolve the payload to existing on-disk paths BEFORE tearing down internal
// drag state (the resolver reads dragSourceBankId / dragSampleIds).
const std::vector<std::string> paths = resolveDragPathsForOs();
// Reset internal drag state and release capture NOW: DoDragDrop runs its own
// modal loop and takes over mouse capture, so the internal drag must be fully
// wound down first (no stale dragging/dropKind, no lingering SetCapture). A
// cancelled/empty OS drag therefore leaves the panel in a clean, no-op state
// (invariant #2 — nothing mutated).
if (GetCapture() == g_panel.hwnd) ReleaseCapture();
g_panel.dragArmed = false;
g_panel.dragging = false;
g_panel.dropKind = DropKind::None;
g_panel.dropBankId.clear();
g_panel.cardGesture = CardGesture::None;
g_panel.dragTargetSlot = -1;
g_panel.dragPrimaryId.clear();
invalidatePanel();
// Empty path list -> nothing draggable (all stale/missing); do not start a drag.
if (!paths.empty())
initiateDragOut(g_panel.hwnd, paths); // COPY-ONLY; blocking on Windows
return;
}
// Inside the client: classify the in-grid gesture (L7 reorder/replace vs the existing
// move/copy) and reflect it as a cursor cue. updateDropTarget first so dropKind/
// dropBankId are current for classifyCardDrag's same-vs-other-bank decision.
updateDropTarget(x, y);
classifyCardDrag(x, y);
applyDragCursor(g_panel.cardGesture);
invalidatePanel();
}
}
// L7 in-grid REORDER drop: move the grabbed card to targetSlot within its bank (gap-
// preserving; onto a gap = place there, onto an occupant = insert-before-and-shift — the pure
// BankBook::reorderSample owns the semantics). One drop = one Ctrl-Z (persistBankOp opens the
// batched undo point + saves). A no-op reorder (already at the target, model returns false)
// opens no undo point. Selection reasons over slot order, so it is cleared after — the
// fingerprint pass rebuilds it against the new order.
void doReorderDrop(const std::string& id, const std::string& bankId, int targetSlot) {
if (!book() || id.empty() || bankId.empty() || targetSlot < 0) return;
if (!book()->reorderSample(id, bankId, targetSlot)) return; // rejected/no-op: no undo point
persistBankOp("ReaSampler: reorder sample");
g_panel.selection = Selection{};
invalidatePanel();
}
// L7 Alt-REPLACE drop: the grabbed card `newId` takes the occupant `oldId`'s slot; `oldId` is
// removed from the bank's index (index-only, file untouched — pool guard enforced in the pure
// BankBook::replaceSample). Rejected (pool guard / absent) = a true NO-OP: no fallback insert,
// no undo point (per spec). One drop = one Ctrl-Z on success.
void doReplaceDrop(const std::string& newId, const std::string& oldId,
const std::string& bankId) {
if (!book() || newId.empty() || oldId.empty() || bankId.empty()) return;
if (!book()->replaceSample(newId, oldId, bankId)) return; // pool-guard reject: NO-OP
persistBankOp("ReaSampler: replace sample");
g_panel.selection = Selection{};
invalidatePanel();
}
// Clears all drag-state fields to their resting values. Called from every exit path
// (button-up, WM_CAPTURECHANGED, WM_DESTROY, closePanel) so the set of cleared fields
// stays consistent across all four sites.
void resetDragState() {
g_panel.dragArmed = false;
g_panel.dragging = false;
g_panel.dropKind = DropKind::None;
g_panel.dropBankId.clear();
g_panel.cardGesture = CardGesture::None;
g_panel.dragTargetSlot = -1;
g_panel.dragPrimaryId.clear();
g_panel.instrumentDropTrack = nullptr;
}
// Commits (or abandons) a drag on button-up. The resolved pure CardGesture decides:
// * Reorder / Replace -> in-grid, within the source bank (L7); one Ctrl-Z each.
// * Move / Copy -> the EXISTING cross-bank transfer (unchanged; Ctrl = copy).
// * None -> a drop over dead space / the source-bank gap = no-op.
// OsDragOut is never seen here: the pointer-left-client handoff happens live in onMouseMove.
void onLBtnUp(int x, int y) {
if (g_panel.dragging) {
// S17 drop-and-load: a release while hover-tracking a valid FX hotspot instantiates a
// ReaSampler 9000 on that track preloaded with the dragged capture — NOT a bank move,
// NOT an OS drag, NEVER a timeline insert. Takes priority over the L7 in-grid / cross-bank
// drop (the pointer is out over a track, not over a bank region). Single-capture only (the
// gesture never armed for a multi payload), so dragSampleIds.front() is the capture.
if (g_panel.instrumentDropTrack && g_panel.dragSampleIds.size() == 1) {
const std::string sampleId = g_panel.dragSampleIds.front();
performInstrumentDrop(g_panel.instrumentDropTrack,
buildInstrumentDropPreset(sampleId));
// Read-only over the bank + arrange: the ONLY mutations are the new FX instance +
// its state (both undoable in performInstrumentDrop). No book change, no ext-state,
// no dirty-mark here.
} else {
updateDropTarget(x, y);
classifyCardDrag(x, y); // re-resolve at the drop point (modifiers may have changed)
const CardGesture g = g_panel.cardGesture;
if (g == CardGesture::Reorder) {
doReorderDrop(g_panel.dragPrimaryId, g_panel.dragSourceBankId,
g_panel.dragTargetSlot);
} else if (g == CardGesture::Replace) {
// Replace targets the OCCUPANT of the target slot with the single grabbed card.
const bool isBanks = g_panel.dragSourceRegion == Region::Banks;
RECT cr{};
GetClientRect(g_panel.hwnd, &cr);
const int w = cr.right - cr.left, h = cr.bottom - cr.top;
const RECT region = isBanks ? banksRegionRect(w, h) : poolRegionRect(w, h);
const RegionDisplay disp = regionDisplay(region, isBanks, g_panel.dragSourceRegion);
const std::string occupant = disp.idAtSlot(g_panel.dragTargetSlot);
// Replace only makes sense for a single grabbed card over a DIFFERENT occupant.
if (!occupant.empty() && occupant != g_panel.dragPrimaryId)
doReplaceDrop(g_panel.dragPrimaryId, occupant, g_panel.dragSourceBankId);
} else if (g == CardGesture::Move || g == CardGesture::Copy) {
const std::string destId = dropTargetBankId();
if (!destId.empty() && destId != g_panel.dragSourceBankId &&
!g_panel.dragSampleIds.empty()) {
transferSamples(g_panel.dragSampleIds, g_panel.dragSourceBankId, destId,
/*copy=*/g == CardGesture::Copy);
}
}
}
// CardGesture::None -> no-op drop (dead space, or same-bank gap resolved to None).
SetCursor(LoadCursor(nullptr, IDC_ARROW)); // restore the arrow on drop
if (GetCapture() == g_panel.hwnd) ReleaseCapture();
} else if (g_panel.dragArmed) {
// Press-release on a selected cell with no drag: treat as a plain click that
// collapses the multi-selection to the pressed cell (standard behavior).
// Release capture acquired at arm time (handleClick) — drag never started.
if (GetCapture() == g_panel.hwnd) ReleaseCapture();
const BankModel* idx = indexForRegion(g_panel.focusedRegion);
const int count = idx ? static_cast<int>(idx->size()) : 0;
const int focus = g_panel.selection.focus;
if (focus >= 0)
g_panel.selection = applyClick(g_panel.selection, focus, false, false, count);
}
resetDragState();
invalidatePanel();
}
// A right-click: on a named tab -> the tab management menu; on a grid cell of the
// focused region with a selection -> the move/copy menu.
void handleRightClick(int x, int y) {
RECT cr{};
GetClientRect(g_panel.hwnd, &cr);
const int w = cr.right - cr.left, h = cr.bottom - cr.top;
// Tab management menu.
if (banksShown()) {
const RECT br = banksRegionRect(w, h);
const TabStripRect strip = banksTabStripRect(br);
const std::vector<const Bank*> tabs = namedBanks();
const TabHit hit = hitTestTabStrip(x, y, strip,
static_cast<int>(tabs.size()), kTabSpec,
g_panel.tabScroll);
if (hit.kind == TabHitKind::Tab) {
POINT pt{x, y};
ClientToScreen(g_panel.hwnd, &pt);
showTabMenu(pt.x, pt.y, tabs[static_cast<std::size_t>(hit.index)]->id);
return;
}
}
// Grid selection menu (move/copy). Only when the right-click lands in the focused
// region's grid and there is a selection.
Region reg = Region::Pool;
if (regionAt(x, y, reg) && reg == g_panel.focusedRegion &&
!g_panel.selection.empty()) {
POINT pt{x, y};
ClientToScreen(g_panel.hwnd, &pt);
showSelectionMenu(pt.x, pt.y);
}
}
} // namespace reasampler::panel