#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 #include #include #include // 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 #else #include #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 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 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 orderedIds; // occupied ids in slot order (selection space) std::vector 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(orderedIds.size()) || !bank) return -1; return bank->slots.slotOf(orderedIds[static_cast(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(i); return -1; } int occupiedCount() const { return static_cast(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 namedBanks(); std::string currentProjectDir(); void doCreateBank(); void transferSamples(const std::vector& sampleIds, const std::string& srcBankId, const std::string& destBankId, bool copy); void removeSamples(const std::vector& sampleIds, const std::string& srcBankId); std::vector focusedSelectionIds(); std::vector 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 topBarRows(); std::vector bottomBarRows(); std::vector overflowMenuRows(); std::vector actionBarClusters(const std::vector& rows); int resolveBarCommandId(const ActionBarRow& row); int toolbarHit(int x, int y, const ActionBarRect& bar, const std::vector& 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