Files
reasampler/src/shell/panel/panel_layout.cpp
T

460 lines
17 KiB
C++

// 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 <string>
#include <vector>
#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<int>(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<ActionBarRow> topBarRows() {
std::vector<ActionBarRow> 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<ActionBarRow> 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<ActionBarRow> bottomBarRows() {
const std::string active = activeModeIdOrEmpty();
const bool arrangeLive = tagButtonEnabled(active, TagTarget::Arrange);
const bool designLive = tagButtonEnabled(active, TagTarget::Design);
std::vector<ActionBarRow> 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<ClusterSpec> actionBarClusters(const std::vector<ActionBarRow>& 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<ActionBarRow>& 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<ActionBarRow> 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<int>(rows.size())) return false;
// computeBarSlots is the same layout draw + hit-test use, so the anchor
// matches the drawn button exactly.
const std::vector<ClusterSpec> clusters = actionBarClusters(rows);
const std::vector<ActionBarSlot> 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<std::size_t>(hv.index)].fullName,
actionDisplayPrefix());
const int cmd = resolveBarCommandId(rows[static_cast<std::size_t>(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