// panel_layout.cpp — geometry-glue seam of the docked bank panel: toolbar/footer/menu // rects, toolbar row/cluster builders, vertical-split geometry + region rects, and // the slot-order display bridge. Every rect is derived from client size + fullHeight // state, and both paint and hit-testing call these so they never drift. Also home of // the public split-state seam (panel_layout.h). // // main.cpp owns the API pointers; here they are extern. DAW-verified, not unit tested // (the pure tiling/hit-test math lives in action_bar / footer_bar / etc.). #include #include #include "shell/panel/panel_state.h" #include "shell/panel/panel_layout.h" #include "shell/persist/session.h" #include "core/view/view_mode_model.h" // Resolves each button's command id at runtime from the composed named-command // string, and reads its current key binding for the tooltip. Main-section only. #define REAPERAPI_MINIMAL #define REAPERAPI_WANT_NamedCommandLookup #define REAPERAPI_WANT_kbd_getTextFromCmd #define REAPERAPI_WANT_SectionFromUniqueID #include "reaper_plugin_functions.h" namespace reasampler::panel { int modeCount() { if (!g_panel.session) return 0; return static_cast(g_panel.session->view().modes().size()); } // topToolbarRect is what the far-right More button anchors into; the action_bar // buttons tile into the band minus the menu reserve (topToolbarActionRect) so // they never run under it. namespace { ActionBarRect topToolbarRect(int w) { ActionBarRect s; s.x = 0; s.y = 0; s.width = w; s.height = kTopToolbarHeight; return s; } MenuBarRect topMenuBarRect(int w) { const ActionBarRect bar = topToolbarRect(w); return MenuBarRect{bar.x, bar.y, bar.width, bar.height}; } } // namespace // Empty when the band is too narrow to place the button clear of its left // inset — suppressed gracefully; still reachable via its bindable command. MenuButtonRect topMenuButtonRect(int w) { return computeMenuButton(topMenuBarRect(w), kMenuBtnSpec); } // Reserve is still subtracted even when the button is suppressed (0 only for a // degenerate band), keeping draw and hit-test consistent either way. ActionBarRect topToolbarActionRect(int w) { ActionBarRect bar = topToolbarRect(w); const int reserve = menuButtonReserve(topMenuBarRect(w), kMenuBtnSpec); bar.width -= reserve; if (bar.width < 0) bar.width = 0; return bar; } RECT panelFooter(int w, int h) { RECT rc{}; rc.left = 0; rc.right = w; rc.top = h - kFooterHeight; rc.bottom = h; // Keep the footer below the top toolbar; if the client is too short, collapse it. if (rc.top < kTopToolbarHeight) rc.top = rc.bottom; return rc; } // Left-group layout (mode toggle + count + Tail button). Single source of truth // for draw and hit-test. FooterBarLayout footerBarLayoutFor(int w, int h) { const RECT f = panelFooter(w, h); if (f.top >= f.bottom) return FooterBarLayout{}; const FooterRect footer{f.left, f.top, f.right - f.left, f.bottom - f.top}; return computeFooterBar(footer, FooterBarSpec{}); } // Empty when the footer is degenerate or too narrow to clear the left group / // version readout — reachable via its bindable command regardless. Set apart at // the right (footer_bar reserves matching space so the groups never overlap). ButtonRect pruneButtonRectFor(int w, int h) { const RECT f = panelFooter(w, h); if (f.top >= f.bottom) return ButtonRect{}; const FooterRect footer{f.left, f.top, f.right - f.left, f.bottom - f.top}; return computePruneButton(footer, PruneButtonSpec{}); } // Used by the scroll-wheel (Manual tail fine-adjust); the Tail-cycle click hits // the Tail button rect (footer_bar) instead. bool pointInFooter(int x, int y) { if (!g_panel.hwnd) return false; RECT cr{}; GetClientRect(g_panel.hwnd, &cr); const RECT f = panelFooter(cr.right - cr.left, cr.bottom - cr.top); return f.top < f.bottom && x >= f.left && x < f.right && y >= f.top && y < f.bottom; } // Frequent acts only: Capture (item/track), Re-capture (between capture and // placement), Placement (insert/insert-conform). The four rare variants live in // the overflow menu (overflowMenuRows) — same actions, different home. std::vector topBarRows() { std::vector rows; for (const CaptureActionDef& def : captureActionTable()) { std::string label = def.commandSuffix; if (label == "CAPTURE_ITEM") label = "Capture Item"; else if (label == "CAPTURE_TRACK") label = "Capture Track"; rows.push_back({def.commandSuffix, label, def.descriptionPhrase, ActionCluster::Capture, true}); } rows.push_back({"RECAPTURE_FROM_SOURCE", "Re-capture", "re-capture from source", ActionCluster::Maintenance, true}); rows.push_back({"INSERT_SELECTED", "Insert", "insert selected sample at edit cursor", ActionCluster::Placement, true}); rows.push_back({"INSERT_SELECTED_CONFORM", "Insert Conform", "insert selected sample at edit cursor (conform to tempo)", ActionCluster::Placement, true}); return rows; } // The four rare batch/realtime capture variants pulled off the visible bar, plus // Cancel RT. fullName is the popup entry text (shortLabel is unused for menu items). std::vector overflowMenuRows() { return { {"CAPTURE_BATCH_ITEMS", "Batch Items", "batch capture selected items (one per item)", ActionCluster::Capture, true}, {"CAPTURE_BATCH_RAZOR", "Batch Razor", "batch capture razor areas (one per area)", ActionCluster::Capture, true}, {"CAPTURE_TRACK_REALTIME", "Capture RT", "capture selected track (realtime)", ActionCluster::Capture, true}, {"CANCEL_REALTIME_CAPTURE", "Cancel RT", "cancel realtime capture", ActionCluster::Maintenance, true}, }; } namespace { std::string activeModeIdOrEmpty() { if (!g_panel.session) return {}; return g_panel.session->view().activeModeId(); } } // namespace // Four Item/Track x Arrange/Design tag buttons then a set-apart Show Both. A tag // button is live only for the OPPOSITE of the active mode (tag into the mode // you're not in) — decided by the pure mode_enable::tagButtonEnabled; disabled // rows draw Disabled and no-op on click. Show Both is unconditional. std::vector bottomBarRows() { const std::string active = activeModeIdOrEmpty(); const bool arrangeLive = tagButtonEnabled(active, TagTarget::Arrange); const bool designLive = tagButtonEnabled(active, TagTarget::Design); std::vector rows; rows.push_back({"VIEW_MOVE_ITEMS_ARRANGE", "Item: Arrange", "move selected items -> Arrange", ActionCluster::Tagging, arrangeLive}); rows.push_back({"VIEW_MOVE_ITEMS_DESIGN", "Item: Design", "move selected items -> Design", ActionCluster::Tagging, designLive}); rows.push_back({"VIEW_TAG_ARRANGE", "Track: Arrange", "tag selected tracks -> Arrange", ActionCluster::Tagging, arrangeLive}); rows.push_back({"VIEW_TAG_DESIGN", "Track: Design", "tag selected tracks -> Design", ActionCluster::Tagging, designLive}); rows.push_back({"VIEW_SHOW_BOTH", "Show Both", "show both for selected tracks", ActionCluster::Switching, true}); return rows; } // Cluster button-count specs in the row list's cluster order, so action_bar's // flat index lines up with the row list. Empty clusters contribute a 0-count // spec (action_bar skips them, no gap). std::vector actionBarClusters(const std::vector& rows) { int nCap = 0, nPlace = 0, nMaint = 0, nTag = 0, nSwitch = 0; for (const ActionBarRow& r : rows) { switch (r.cluster) { case ActionCluster::Capture: ++nCap; break; case ActionCluster::Placement: ++nPlace; break; case ActionCluster::Maintenance: ++nMaint; break; case ActionCluster::Tagging: ++nTag; break; case ActionCluster::Switching: ++nSwitch; break; } } return { {ActionCluster::Capture, nCap}, {ActionCluster::Maintenance, nMaint}, {ActionCluster::Placement, nPlace}, {ActionCluster::Tagging, nTag}, {ActionCluster::Switching, nSwitch}, }; } // Fixed-height band directly above the footer; degenerate (height 0) when too // short to host it above the footer. ActionBarRect bottomToolbarRect(int w, int h) { ActionBarRect s; const RECT footer = panelFooter(w, h); const int footerTop = (footer.top < footer.bottom) ? footer.top : h; s.x = 0; s.width = w; s.height = kBottomToolbarHeight; s.y = footerTop - kBottomToolbarHeight; if (s.y < kTopToolbarHeight) { s.y = footerTop; s.height = 0; } return s; } // The named-command lookup string is "_" + the channel-qualified id (REAPER's convention). int resolveBarCommandId(const ActionBarRow& row) { if (!NamedCommandLookup) return 0; const std::string named = "_" + channelCommandId(row.suffix); return NamedCommandLookup(named.c_str()); } namespace { // "" when unbound or not registered. std::string barBindingText(int cmd) { if (cmd != 0 && kbd_getTextFromCmd && SectionFromUniqueID) { const char* t = kbd_getTextFromCmd(cmd, SectionFromUniqueID(0)); if (t) return std::string(t); } return {}; } } // namespace // The flat action index under (x, y) in `bar` for the given row set, or -1 (miss). Pure hit-test. int toolbarHit(int x, int y, const ActionBarRect& bar, const std::vector& rows) { if (bar.height <= 0) return -1; return hitTestActionBar(x, y, bar, actionBarClusters(rows), kBarSpec); } // Resolves the hovered element to (anchor rect, text); false when the hover has // no tooltip (grid / chrome / More button — its own popup is its affordance). // Draw lives in panel_render; timing is applied by the caller. bool currentTooltip(int w, int h, std::string& textOut, int& ax, int& ay, int& aw, int& ah) { const Hover& hv = g_panel.hovered; std::vector rows; ActionBarRect bar{}; if (hv.kind == HoverKind::TopBarButton) { rows = topBarRows(); bar = topToolbarActionRect(w); } else if (hv.kind == HoverKind::BottomBarButton) { rows = bottomBarRows(); bar = bottomToolbarRect(w, h); } else { return false; } if (hv.index < 0 || hv.index >= static_cast(rows.size())) return false; // computeBarSlots is the same layout draw + hit-test use, so the anchor // matches the drawn button exactly. const std::vector clusters = actionBarClusters(rows); const std::vector slots = computeBarSlots(bar, clusters, kBarSpec); const ActionBarSlot* slot = nullptr; for (const ActionBarSlot& s : slots) if (s.index == hv.index) { slot = &s; break; } if (!slot) return false; // fullName is stored prefix-free; strip defensively in case a source ever // carries the display prefix. Tooltip carries name + binding when bound // (e.g. "capture selected item — F5"), name only when unbound. const std::string phrase = stripActionPrefix(rows[static_cast(hv.index)].fullName, actionDisplayPrefix()); const int cmd = resolveBarCommandId(rows[static_cast(hv.index)]); const std::string binding = barBindingText(cmd); textOut = binding.empty() ? phrase : phrase + " \xe2\x80\x94 " + binding; // " — " (em dash, UTF-8) ax = slot->x; ay = slot->y; aw = slot->width; ah = slot->height; return true; } // Between the top and bottom toolbars. When the bottom bar collapses on a short // client, bottomToolbarRect returns its y at the footer top, so the body still // ends there. RECT splitBody(int w, int h) { RECT rc{}; rc.left = 0; rc.right = w; rc.top = kTopToolbarHeight; const ActionBarRect bar = bottomToolbarRect(w, h); rc.bottom = bar.y; if (rc.bottom < rc.top) rc.bottom = rc.top; return rc; } bool poolShown() { return g_panel.fullHeight != BankPanelFullHeight::BanksOnly; } bool banksShown() { return g_panel.fullHeight != BankPanelFullHeight::PoolOnly; } // Empty when hidden. RECT poolRegionRect(int w, int h) { const RECT body = splitBody(w, h); if (!poolShown()) return RECT{0, 0, 0, 0}; if (!banksShown()) return body; RECT rc = body; rc.bottom = body.top + (body.bottom - body.top - kSplitDividerHeight) / 2; if (rc.bottom < rc.top) rc.bottom = rc.top; return rc; } RECT banksRegionRect(int w, int h) { const RECT body = splitBody(w, h); if (!banksShown()) return RECT{0, 0, 0, 0}; if (!poolShown()) return body; // banks full-height: the whole body RECT rc = body; rc.top = body.top + (body.bottom - body.top - kSplitDividerHeight) / 2 + kSplitDividerHeight; if (rc.top > rc.bottom) rc.top = rc.bottom; return rc; } RECT regionHeaderRect(const RECT& region) { RECT rc = region; rc.bottom = region.top + kRegionHeaderHeight; if (rc.bottom > region.bottom) rc.bottom = region.bottom; return rc; } TabStripRect banksTabStripRect(const RECT& region) { const RECT hdr = regionHeaderRect(region); TabStripRect s; s.x = region.left; s.y = hdr.bottom; s.width = region.right - region.left; s.height = kTabStripHeight; if (s.y + s.height > region.bottom) s.height = region.bottom - s.y; if (s.height < 0) s.height = 0; return s; } // A region's grid viewport (below the header band, and below the tab strip for the // banks region). This is where cells tile. RECT regionGridRect(const RECT& region, bool isBanks) { RECT rc = region; rc.top = region.top + kRegionHeaderHeight; if (isBanks) rc.top += kTabStripHeight; if (rc.top > rc.bottom) rc.top = rc.bottom; return rc; } RECT fullHtBtnRect(const RECT& region) { const RECT hdr = regionHeaderRect(region); RECT rc = hdr; rc.right = hdr.right - 4; rc.left = rc.right - kFullHtBtnWidth; rc.top = hdr.top + 2; rc.bottom = hdr.bottom - 2; return rc; } RECT createBtnRect(const RECT& region) { RECT ft = fullHtBtnRect(region); RECT rc = ft; rc.right = ft.left - 4; rc.left = rc.right - kCreateBtnWidth; return rc; } // orderedSampleIds reconciles the bank's SlotMap against live membership, so a // freshly-migrated or out-of-band-mutated bank always yields a complete order. RegionDisplay regionDisplay(const RECT& region, bool isBanks, Region reg) { RegionDisplay d; BankBook* b = book(); if (!b) return d; const std::string bankId = bankIdForRegion(reg); if (bankId.empty()) return d; d.bank = b->bank(bankId); if (!d.bank) return d; d.orderedIds = b->orderedSampleIds(bankId); // occupied ids, slot order (reconciles) if (d.orderedIds.empty()) return d; const RECT grid = regionGridRect(region, isBanks); const int w = grid.right - grid.left; if (w <= 0) return d; d.slotRects = computeSlotRects(d.bank->slots.maxSlot(), w, kGrid); for (SlotCellRect& r : d.slotRects) { r.x += grid.left; r.y += grid.top; } return d; } RegionDisplay focusedDisplay() { RECT cr{}; GetClientRect(g_panel.hwnd, &cr); const int w = cr.right - cr.left, h = cr.bottom - cr.top; const bool isBanks = g_panel.focusedRegion == Region::Banks; const RECT region = isBanks ? banksRegionRect(w, h) : poolRegionRect(w, h); return regionDisplay(region, isBanks, g_panel.focusedRegion); } bool regionAt(int x, int y, Region& out) { RECT cr{}; GetClientRect(g_panel.hwnd, &cr); const int w = cr.right - cr.left, h = cr.bottom - cr.top; if (poolShown()) { const RECT r = poolRegionRect(w, h); if (x >= r.left && x < r.right && y >= r.top && y < r.bottom) { out = Region::Pool; return true; } } if (banksShown()) { const RECT r = banksRegionRect(w, h); if (x >= r.left && x < r.right && y >= r.top && y < r.bottom) { out = Region::Banks; return true; } } return false; } int footerToggleSegmentHit(int x, int y, int w, int h) { if (!g_panel.session) return -1; const FooterBarLayout fb = footerBarLayoutFor(w, h); if (fb.toggle.empty()) return -1; const HeaderRect th{fb.toggle.x, fb.toggle.y, fb.toggle.width, fb.toggle.height}; return hitTestSegment(x, y, th, modeCount()); } int columnsForRegion(Region reg) { RECT cr{}; GetClientRect(g_panel.hwnd, &cr); const int w = cr.right - cr.left, h = cr.bottom - cr.top; const RECT region = reg == Region::Banks ? banksRegionRect(w, h) : poolRegionRect(w, h); const RECT grid = regionGridRect(region, reg == Region::Banks); return columnsForWidth(grid.right - grid.left, kGrid); } } // namespace reasampler::panel namespace reasampler { BankPanelFullHeight bankPanelFullHeight() { return panel::g_panel.fullHeight; } static void setFullHeight(BankPanelFullHeight target) { panel::g_panel.fullHeight = (panel::g_panel.fullHeight == target) ? BankPanelFullHeight::Split : target; if (panel::g_panel.hwnd) InvalidateRect(panel::g_panel.hwnd, nullptr, FALSE); } void bankPanelToggledPoolFullHeight() { setFullHeight(BankPanelFullHeight::PoolOnly); } void bankPanelToggledBanksFullHeight() { setFullHeight(BankPanelFullHeight::BanksOnly); } } // namespace reasampler