Files
reasampler/src/shell/panel/panel_state.h
T

507 lines
19 KiB
C++

#pragma once
// panel_state — INTERNAL shared state + cross-seam contract of the docked bank panel.
// Included ONLY by the panel's own TUs; outside consumers use the per-seam public
// headers (panel_window.h / panel_input.h / panel_bank_ops.h / panel_layout.h).
// Cross-seam calls are plain free functions — direct call-through, no virtual dispatch
// (audition and per-mouse-move paths must stay direct calls).
//
// REFERENCE-INVALIDATION GUARDRAIL: a bank-structural mutation (create/delete/
// evacuate/activate/move) can reallocate the book's vector, so a BankModel& / Bank*
// must NEVER be cached across one. Every seam resolves fresh after any mutation and
// passes bank IDS (not references) into the model ops.
#include <cstdint>
#include <string>
#include <unordered_map>
#include <vector>
// On macOS/Linux SWELL is provided by the host (SWELL_PROVIDED_BY_APP); on Windows
// we use native Win32 (windows.h first, then swell.h no-ops on _WIN32).
#ifdef _WIN32
#include <windows.h>
#else
#include <pthread.h>
#endif
#include "wdltypes.h"
#include "swell/swell.h"
// REAPER API function pointers are declared per-TU; main.cpp owns the definitions.
#include "reaper_plugin.h"
#include "core/audio/peaks.h"
#include "core/capture/capture_paths.h"
#include "core/capture/render_settings.h"
#include "core/capture/tail_control.h"
#include "core/model/bank_book.h"
#include "core/model/bank_model.h"
#include "core/ui/action_bar.h"
#include "core/ui/bank_grid.h"
#include "core/ui/card_drag.h"
#include "core/ui/card_meta.h"
#include "core/ui/component_geometry.h"
#include "core/ui/drag_out.h"
#include "core/ui/footer_bar.h"
#include "core/ui/mode_enable.h"
#include "core/ui/overflow_menu.h"
#include "core/ui/prune_button.h"
#include "core/ui/tab_strip.h"
#include "core/ui/theme.h"
#include "core/ui/tooltip.h"
#include "core/version/app_version.h"
#include "core/view/guid_diff.h"
#include "core/view/lane_keys.h"
#include "core/view/mode_switch.h"
#include "core/wire/instrument_drop.h"
#include "shell/panel/panel_layout.h"
namespace reasampler {
class ReaSamplerSession;
}
namespace reasampler::panel {
// core/ui
using ui::ActionBarRect;
using ui::ActionBarSlot;
using ui::ActionBarSpec;
using ui::ActionCluster;
using ui::ButtonRect;
using ui::CardGesture;
using ui::CellRect;
using ui::ClusterSpec;
using ui::CursorCue;
using ui::DragModifiers;
using ui::DragState;
using ui::DropClass;
using ui::DropContext;
using ui::DropCue;
using ui::DropRegion;
using ui::FooterBarLayout;
using ui::FooterBarSpec;
using ui::FooterHit;
using ui::FooterRect;
using ui::GridSpec;
using ui::InteractionState;
using ui::KitBox;
using ui::KitButtonBox;
using ui::KitColor;
using ui::MenuBarRect;
using ui::MenuButtonRect;
using ui::MenuButtonSpec;
using ui::MusicalLength;
using ui::NavKey;
using ui::PanelClientRect;
using ui::PruneButtonSpec;
using ui::ResolvedSample;
using ui::Role;
using ui::Selection;
using ui::SlotCellRect;
using ui::TabHit;
using ui::TabHitKind;
using ui::TabRect;
using ui::TabStripLayout;
using ui::TabStripRect;
using ui::TabStripSpec;
using ui::TagTarget;
using ui::ThumbnailKey;
using ui::TooltipBox;
using ui::TooltipSpec;
using ui::applyClick;
using ui::assemblePathList;
using ui::cardNameStrip;
using ui::clampTabScroll;
using ui::columnsForWidth;
using ui::computeBarSlots;
using ui::computeFooterBar;
using ui::computeMenuButton;
using ui::computePruneButton;
using ui::computeSlotRects;
using ui::computeSlotRectsForDrop;
using ui::computeTabRects;
using ui::computeTabStripLayout;
using ui::computeTooltip;
using ui::cueForDropClass;
using ui::cursorForGesture;
using ui::decideCardGesture;
using ui::decideDropClass;
using ui::formatBarsBeats;
using ui::formatSecondsMs;
using ui::hitTestActionBar;
using ui::hitTestFooterBar;
using ui::hitTestMenuButton;
using ui::hitTestPruneButton;
using ui::hitTestSlot;
using ui::hitTestTabStrip;
using ui::kCardStripHeight;
using ui::kCardStripPad;
using ui::menuButtonReserve;
using ui::modeSegmentEnabled;
using ui::navigate;
using ui::needsSurfaceProbe;
using ui::roleColor;
using ui::stripActionPrefix;
using ui::tagButtonEnabled;
using ui::thumbnailKeyString;
using ui::waveformColumnCount;
// core/view
using view::GuidBaseline;
using view::HeaderRect;
using view::SegmentRect;
using view::computeSegmentRects;
using view::hitTestSegment;
using view::isOnManualLane;
// core/capture
using capture::CaptureActionDef;
using capture::TailMode;
using capture::TailSetting;
using capture::adjustManualMs;
using capture::captureActionTable;
using capture::clampManualMs;
using capture::cycleTailMode;
using capture::kManualStepMs;
using capture::normalizeSlashes;
using capture::resolveBankFile;
using capture::tailToggleLabel;
// core/audio
using audio::Envelope;
using audio::computeEnvelope;
// core/model
using model::BankModel;
using model::Sample;
// core/version
using version::actionDisplayPrefix;
using version::appVersion;
using version::channelCommandId;
using version::dockIdent;
using version::dockTitle;
// core/wire
using wire::buildInstrumentDropPreset;
// All color comes from `theme` by role, drawn through the kit. Only pixel layout
// metrics live here, shared by layout/render/input/drag so draw and hit-test can
// never drift.
inline const GridSpec kGrid{/*cellWidth=*/140, /*cellHeight=*/84, /*gap=*/10};
// Footer: [Arrange|Design] toggle, per-mode count, Tail button, Prune button
// (footer_bar left group + prune_button right).
inline constexpr int kFooterHeight = 30;
// Two task-grouped toolbars drawn through action_bar: top = capture + placement,
// bottom = Design-View tag/switch verbs, directly above the footer.
inline constexpr int kTopToolbarHeight = 28;
inline constexpr int kBottomToolbarHeight = 28;
// Hover-delay tooltip timing, driven off the OnTimer poll (no dedicated timer). Kit
// font is proportional, so char width is a generous estimate (pads, never clips).
inline constexpr unsigned int kTooltipDelayMs = 500;
inline constexpr int kTooltipCharPx = 7;
inline constexpr int kTooltipTextH = 14;
// Client area top to bottom: top toolbar | split body | bottom toolbar | footer.
// 26, not 24: Font::RegionTitle's line box (19px em + Segoe UI's leading) is ~25px, and
// DT_VCENTER clips to the rect.
inline constexpr int kRegionHeaderHeight = 26;
inline constexpr int kTabStripHeight = 26;
inline constexpr int kSplitDividerHeight = 3;
inline constexpr int kFullHtBtnWidth = 22;
inline constexpr int kCreateBtnWidth = 22;
inline const TabStripSpec kTabSpec{/*tabWidth=*/96, /*chevronWidth=*/20};
// Far-right More ("...") overflow-menu button: one source of truth for its
// geometry + the reserve action_bar leaves for it.
inline const MenuButtonSpec kMenuBtnSpec{/*buttonWidth=*/28, /*rightInset=*/6,
/*verticalInset=*/3, /*minLeftInset=*/40};
// Shared by both toolbars' draw and hit-test (identical button shape top and
// bottom). clusterGap is wider than buttonGap so semantic groups read as groups.
inline const ActionBarSpec kBarSpec{/*buttonWidth=*/108, /*buttonGap=*/4, /*clusterGap=*/24,
/*sidePad=*/8, /*verticalInset=*/3};
struct CachedThumbnail {
Envelope envelope;
int width = 0;
};
// Which of the two split regions currently owns the selection / receives keyboard
// input. The move/copy source is the focused region's displayed bank.
enum class Region { Pool, Banks };
// What a drag is dropping onto, resolved live under the pointer. BanksRegion fires
// when the pointer is anywhere in the named-banks grid that is NOT on a specific tab
// (tab takes precedence); the resolved bank is always shownBankId.
enum class DropKind { None, PoolRegion, Tab, BanksRegion };
// The hovered interactive element, resolved live in WM_MOUSEMOVE so the kit draws its
// hover state on that element only. SWELL exposes no WM_MOUSELEAVE (confirmed: no hit in
// vendor/WDL/WDL/swell), so hover is cleared by a move that resolves to None rather than a
// leave message; the panel is Windows-only but this stays portable-safe.
// `index` disambiguates within a kind (action-bar button index, tab index); -1 when N/A.
enum class HoverKind {
None,
TopBarButton, // index = flat action index into topBarRows
BottomBarButton, // index = flat action index into bottomBarRows
MoreButton,
PruneButton,
FullHtPool,
FullHtBanks,
CreateBank,
Tab, // index = tab ordinal
TailButton,
ModeSegment, // index = segment ordinal
};
struct Hover {
HoverKind kind = HoverKind::None;
int index = -1;
bool operator==(const Hover& o) const { return kind == o.kind && index == o.index; }
bool operator!=(const Hover& o) const { return !(*this == o); }
};
// Base rest/hover resolver; Active/Pressed are decided per-element by the caller
// (e.g. an active tab draws Active regardless of hover).
inline InteractionState hoverState(const Hover& hovered, HoverKind kind, int index) {
return (hovered.kind == kind && hovered.index == index) ? InteractionState::Hover
: InteractionState::Rest;
}
struct PanelState {
ReaSamplerSession* session = nullptr;
HWND hwnd = nullptr;
bool open = false;
std::string bankFingerprint;
std::uint64_t generation = 0;
std::unordered_map<std::string, CachedThumbnail> cache;
// One live selection, scoped to `focusedRegion`. Switching regions moves the
// selection with the focus.
Selection selection;
int selItemCount = 0;
Region focusedRegion = Region::Pool;
// Resolved on WM_MOUSEMOVE; repaint fires only when this changes.
Hover hovered;
// Custom hover-delay tooltip. hoverSinceTick resets on every hover change;
// tooltipShown latches once the delay elapses so the OnTimer poll repaints once.
unsigned int hoverSinceTick = 0;
bool tooltipShown = false;
// Polled on the OnTimer tick (REAPER exposes no transport-change callback). Feeds
// modeSegmentEnabled (core/ui/footer_bar.h). Cached rather than read per paint AND
// per click so the pixel the user saw and the click they made cannot disagree
// within a tick.
bool modeSwitchBlocked = false;
BankPanelFullHeight fullHeight = BankPanelFullHeight::Split;
// The named bank the banks region shows — distinct from the active/capture-target
// bank. Reconciled each fingerprint pass so it always names a live bank (or empty).
std::string shownBankId;
int tabScroll = 0;
// A drag begins only after the pointer moves past a threshold from a press
// that landed on a selected grid cell — disambiguates from the multi-select
// drag (which begins immediately on any grid press).
bool dragArmed = false;
bool dragging = false;
int dragStartX = 0, dragStartY = 0;
Region dragSourceRegion = Region::Pool;
std::string dragSourceBankId;
std::vector<std::string> dragSampleIds;
std::string dragPrimaryId; // the single card grabbed — the reorder/replace subject
DropKind dropKind = DropKind::None;
std::string dropBankId; // destination bank id when dropKind==Tab
// Resolved each mouse-move by card_drag::decideCardGesture. dragTargetSlot >= 0
// only for a Reorder/Replace over the source bank's own grid.
CardGesture cardGesture = CardGesture::None;
int dragTargetSlot = -1;
// NO out-of-client drop-target state is kept here on purpose (core/ui/CLAUDE.md: "the
// drag-out law is per-move and stateless"). Do not reintroduce a remembered target or a
// "blocked" latch.
// Authoritative tail setting lives in ReaSamplerSession, not here; panel reads it for
// drawing and mutates via footer click / scroll-wheel. bankPanelTailSetting is the
// capture actions' read seam.
preview_register_t preview{};
PCM_source* previewSrc = nullptr;
bool previewActive = false;
bool previewInited = false; // guards double init / deinit
// Each timer tick diffs the live track+item GUID set against the previous tick to
// auto-tag new content. GuidBaseline self-arms on first observe() so pre-existing
// content is never mass-tagged. Project-load re-arm is driven by persist's load
// signal, not a ReaProject* compare — a recycled address previously mis-tagged tracks.
GuidBaseline contentBaseline;
bool reloadPending = false; // set by bankPanelNotifyProjectLoaded; drained next detect tick
};
// Defined in panel_window.cpp (the lifecycle owner).
extern PanelState g_panel;
// The grid draws in the bank's persisted SlotMap order (sparse, gap-preserving),
// NOT BankModel insertion order. regionDisplay (panel_layout.cpp) is the single
// place that resolves a region's display.
//
// TWO INDEX SPACES the whole panel must keep straight:
// * SLOT — display position 0..maxSlot; gaps are empty, valid drop targets.
// What pixels/hit-tests speak.
// * SELECTION — the dense occupied-ordinal [0, occupied) space the pure
// Selection/applyClick/navigate reason in. Selection index i <->
// orderedIds[i]. Keyboard nav therefore skips gaps.
// RegionDisplay carries both plus the translation, resolved fresh each call (never
// cached across a mutation, per the reference-invalidation guardrail).
struct RegionDisplay {
std::vector<std::string> orderedIds; // occupied ids in slot order (selection space)
std::vector<SlotCellRect> slotRects; // one rect per slot 0..maxSlot, viewport coords
const Bank* bank = nullptr;
// The id occupying `slot`, or "" for an empty slot / out of range.
std::string idAtSlot(int slot) const {
return bank ? bank->slots.idAt(slot) : std::string{};
}
// The slot a selection ordinal `sel` maps to, or -1. orderedIds[sel] -> its slot.
int slotForSelection(int sel) const {
if (sel < 0 || sel >= static_cast<int>(orderedIds.size()) || !bank) return -1;
return bank->slots.slotOf(orderedIds[static_cast<std::size_t>(sel)]);
}
// The selection ordinal for `slot` (index of its occupant in orderedIds), or -1 when
// the slot is empty. Inverse of slotForSelection.
int selectionForSlot(int slot) const {
const std::string id = idAtSlot(slot);
if (id.empty()) return -1;
for (std::size_t i = 0; i < orderedIds.size(); ++i)
if (orderedIds[i] == id) return static_cast<int>(i);
return -1;
}
int occupiedCount() const { return static_cast<int>(orderedIds.size()); }
};
// One action button. Row order IS the flat action index the pure action_bar slots
// carry. Built by panel_layout; consumed by render, input, drag.
struct ActionBarRow {
std::string suffix;
std::string shortLabel;
std::string fullName;
ActionCluster cluster = ActionCluster::Capture;
bool enabled = true; // opposite-mode gate for bottom-bar tag buttons; always true
// for the top bar (unconditional triggers)
};
// Modifier state at event time. Alt = the replace modifier.
inline bool ctrlDown() { return (GetAsyncKeyState(VK_CONTROL) & 0x8000) != 0; }
inline bool shiftDown() { return (GetAsyncKeyState(VK_SHIFT) & 0x8000) != 0; }
inline bool altDown() { return (GetAsyncKeyState(VK_MENU) & 0x8000) != 0; }
inline void invalidatePanel() {
if (g_panel.hwnd) InvalidateRect(g_panel.hwnd, nullptr, FALSE);
}
// Cross-seam contract, grouped by owning TU; all plain free functions.
// panel_bank_ops.cpp — book/bank accessors + the bank-CRUD verbs + menus.
BankBook* book();
const BankModel* indexForRegion(Region r);
std::string bankIdForRegion(Region r);
std::vector<const Bank*> namedBanks();
std::string currentProjectDir();
void doCreateBank();
void transferSamples(const std::vector<std::string>& sampleIds,
const std::string& srcBankId, const std::string& destBankId,
bool copy);
void removeSamples(const std::vector<std::string>& sampleIds,
const std::string& srcBankId);
std::vector<std::string> focusedSelectionIds();
std::vector<std::string> resolveDragPathsForOs();
void showTabMenu(int screenX, int screenY, const std::string& bankId);
void showSelectionMenu(int screenX, int screenY);
void showMoreMenu();
// panel_layout.cpp — toolbar/footer/menu rects, row/cluster builders, split geometry,
// region rects, the display bridge. Draw and hit-test both call these so they never drift.
int modeCount();
MenuButtonRect topMenuButtonRect(int w);
ActionBarRect topToolbarActionRect(int w);
RECT panelFooter(int w, int h);
FooterBarLayout footerBarLayoutFor(int w, int h);
ButtonRect pruneButtonRectFor(int w, int h);
bool pointInFooter(int x, int y);
ActionBarRect bottomToolbarRect(int w, int h);
RECT splitBody(int w, int h);
bool poolShown();
bool banksShown();
RECT poolRegionRect(int w, int h);
RECT banksRegionRect(int w, int h);
RECT regionHeaderRect(const RECT& region);
TabStripRect banksTabStripRect(const RECT& region);
RECT regionGridRect(const RECT& region, bool isBanks);
RECT fullHtBtnRect(const RECT& region);
RECT createBtnRect(const RECT& region);
RegionDisplay regionDisplay(const RECT& region, bool isBanks, Region reg);
RegionDisplay focusedDisplay();
bool regionAt(int x, int y, Region& out);
int footerToggleSegmentHit(int x, int y, int w, int h);
int columnsForRegion(Region reg);
std::vector<ActionBarRow> topBarRows();
std::vector<ActionBarRow> bottomBarRows();
std::vector<ActionBarRow> overflowMenuRows();
std::vector<ClusterSpec> actionBarClusters(const std::vector<ActionBarRow>& rows);
int resolveBarCommandId(const ActionBarRow& row);
int toolbarHit(int x, int y, const ActionBarRect& bar, const std::vector<ActionBarRow>& rows);
bool currentTooltip(int w, int h, std::string& textOut, int& ax, int& ay, int& aw, int& ah);
// panel_render.cpp — the full LICE paint (drawn last-to-front into the caller's
// double buffer; BitBlt'd once by paintPanel).
void paintPanel(HWND hwnd, HDC hdc);
// panel_thumbnails.cpp — thumbnail compute + cache, and the bank-change fingerprint
// pass that owns the cache's generation key (bumps generation, clears the cache, and
// reconciles selection/shown-bank on any book mutation).
const Envelope& thumbnailFor(const Sample& sample, int width, const std::string& projectDir);
bool refreshFingerprint();
void reconcileShownBank();
// panel_audition.cpp — the preview engine (hot path: direct call-through, never
// virtual, no added header->TU indirection).
void initPreview();
void deinitPreview();
void stopAudition();
void startAudition(int idx);
// panel_input.cpp — click/wheel/key routing, accelerator, new-content detection,
// and the session-tail read/mutate helpers.
TailSetting currentTail();
// The registered activate action behind a footer mode segment, or 0 if unroutable.
// Shared with panel_render.cpp so paint and click resolve the same id.
int modeActivateCommandId(const std::string& modeId);
void handleClick(int x, int y);
bool handleWheel(int x, int y, int delta);
void registerAccel();
void unregisterAccel();
// panel_drag.cpp — the card-drag/hover state machine (pure mirror: core/ui/card_drag).
// Per-mouse-move work stays plain free-function calls.
void onMouseMove(int x, int y);
void onLBtnUp(int x, int y);
void handleRightClick(int x, int y);
void resetDragState();
void maybeShowTooltip();
} // namespace reasampler::panel