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:
@@ -0,0 +1,558 @@
|
||||
// panel_layout.cpp — the geometry-glue seam of the docked bank panel (Q-W2 split of
|
||||
// bank_panel.cpp; the T4-01 NEW seam). Owns the toolbar/footer/menu rects, the
|
||||
// toolbar row/cluster builders, the vertical-split geometry + region rects, and the
|
||||
// L7 slot-order display bridge (regionDisplay/focusedDisplay). Every rect is derived
|
||||
// from the client size + fullHeight state, and BOTH paint (panel_render) and
|
||||
// hit-testing (panel_input / panel_drag) call these so they never drift.
|
||||
//
|
||||
// Also home of the public split-state seam (panel_layout.h): the B3-owned
|
||||
// BankPanelFullHeight toggles the render derives the region rects from.
|
||||
//
|
||||
// Compiled into the reaper_reasampler MODULE. Includes reaper_plugin_functions.h
|
||||
// WITHOUT REAPERAPI_IMPLEMENT — main.cpp owns the API pointers; here they are
|
||||
// extern (CLAUDE.md §contract). DAW-verified, not unit tested (the PURE tiling /
|
||||
// hit-test math lives in action_bar / footer_bar / prune_button / overflow_menu /
|
||||
// tab_strip / mode_switch / bank_grid / card_drag, unit-tested outside the DAW).
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "shell/panel/panel_state.h"
|
||||
#include "shell/panel/panel_layout.h"
|
||||
|
||||
#include "persist.h" // ReaSamplerSession — mode/view reads
|
||||
#include "core/view/view_mode_model.h" // ViewModeModel — modes()/activeModeId()
|
||||
|
||||
// Action-trigger buttons (M11): resolve each button's command id at runtime from the
|
||||
// composed named-command string and read its current key binding for the tooltip.
|
||||
// All main-section (SectionFromUniqueID(0)).
|
||||
#define REAPERAPI_MINIMAL
|
||||
#define REAPERAPI_WANT_NamedCommandLookup
|
||||
#define REAPERAPI_WANT_kbd_getTextFromCmd
|
||||
#define REAPERAPI_WANT_SectionFromUniqueID
|
||||
#include "reaper_plugin_functions.h"
|
||||
|
||||
namespace reasampler::panel {
|
||||
|
||||
// --- Mode toggle (D5; relocated to the footer at L4) --------------------------
|
||||
|
||||
int modeCount() {
|
||||
if (!g_panel.session) return 0;
|
||||
return static_cast<int>(g_panel.session->view().modes().size());
|
||||
}
|
||||
|
||||
// --- Top toolbar band (L4; L5 overflow-menu reserve) --------------------------
|
||||
//
|
||||
// The TOP toolbar (capture + placement) occupies the very top of the client. Degenerate
|
||||
// (height 0) when the client is too short to host it above the split body. The WHOLE band
|
||||
// (topToolbarRect) is what the far-right More button anchors into; the action_bar's frequent
|
||||
// buttons tile into the band MINUS the menu reserve (topToolbarActionRect), so they never run
|
||||
// under the menu button (L5 refinement 1).
|
||||
|
||||
ActionBarRect topToolbarRect(int w) {
|
||||
ActionBarRect s;
|
||||
s.x = 0;
|
||||
s.y = 0;
|
||||
s.width = w;
|
||||
s.height = kTopToolbarHeight;
|
||||
return s;
|
||||
}
|
||||
|
||||
// The band the More button occupies (the whole top toolbar band as a MenuBarRect).
|
||||
MenuBarRect topMenuBarRect(int w) {
|
||||
const ActionBarRect bar = topToolbarRect(w);
|
||||
return MenuBarRect{bar.x, bar.y, bar.width, bar.height};
|
||||
}
|
||||
|
||||
// The More button's rect (right-anchored in the top band). Empty when the band is too narrow
|
||||
// to place it clear of its left inset — the three variants stay reachable via their bindable
|
||||
// commands (graceful suppression).
|
||||
MenuButtonRect topMenuButtonRect(int w) {
|
||||
return computeMenuButton(topMenuBarRect(w), kMenuBtnSpec);
|
||||
}
|
||||
|
||||
// The rect the TOP toolbar's action_bar tiles into: the whole band MINUS the reserve for the
|
||||
// far-right More button, so the frequent buttons never overlap it. When the More button is
|
||||
// suppressed (band too narrow) the reserve is still subtracted (the reserve is 0 only for a
|
||||
// degenerate band), which keeps draw and hit-test consistent whether or not the button shows.
|
||||
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;
|
||||
}
|
||||
|
||||
// --- Footer (L4) --------------------------------------------------------------
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// The footer LEFT-group layout (mode toggle + count + Tail button), derived from the client
|
||||
// size. SINGLE source of truth for draw and hit-test. All-empty when the footer is degenerate.
|
||||
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{});
|
||||
}
|
||||
|
||||
// The prune button's rect within the footer, derived from the client size. SINGLE source
|
||||
// of truth for both draw and hit-test (they never drift). Empty when the footer is degenerate
|
||||
// or too narrow to place the button clear of the footer-left group / version readout — the
|
||||
// action stays reachable via its bindable command, so a suppressed button is graceful. Kept
|
||||
// set apart at the RIGHT (footer_bar reserves the matching space at its right so the two
|
||||
// groups never overlap). See prune_button.h §Placement contract.
|
||||
ButtonRect pruneButtonRectFor(int w, int h) {
|
||||
const RECT f = panelFooter(w, h);
|
||||
if (f.top >= f.bottom) return ButtonRect{}; // degenerate footer -> no button
|
||||
const FooterRect footer{f.left, f.top, f.right - f.left, f.bottom - f.top};
|
||||
return computePruneButton(footer, PruneButtonSpec{});
|
||||
}
|
||||
|
||||
// True iff client-relative (x, y) falls inside the (non-degenerate) footer strip. Used by the
|
||||
// scroll-wheel (Manual tail fine-adjust) so a wheel notch over the footer is claimed. The
|
||||
// Tail-cycle CLICK no longer uses this — it now hits the Tail button rect (footer_bar).
|
||||
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;
|
||||
}
|
||||
|
||||
// === Task-grouped toolbars (Phase L, L2 + L4) =================================
|
||||
//
|
||||
// L4 re-homes the button inventory around frequency and intent (DS-3 layout, not a re-skin)
|
||||
// across TWO toolbars, BOTH drawn through the pure action_bar module:
|
||||
// * the TOP toolbar (Capture + Placement) sits at the very top where the eye lands — the
|
||||
// two acts the tool exists for (L4 §1);
|
||||
// * the BOTTOM toolbar (the Design-View verbs: Tagging then Switching) sits above the
|
||||
// footer, in the space capture/placement vacated (L4 §2).
|
||||
// Each button is drawn with its action name (Font::Label) and live key binding on a Micro
|
||||
// sub-row (the L2 contract). action_bar owns the cluster tiling, the label/binding sub-rects,
|
||||
// the whole-trailing-button overflow, and the hit-test; only the kit draw + SDK binding query
|
||||
// + the NamedCommandLookup/Main_OnCommand dispatch live here.
|
||||
//
|
||||
// Each button resolves its command id at RUNTIME from the composed named-command string
|
||||
// (NamedCommandLookup on "_" + channelCommandId(suffix)), so it is channel-correct on stable
|
||||
// and beta and adds NO second registration. A cmd of 0 (action not registered on this channel)
|
||||
// draws Disabled and no-ops on click. L4 is layout-only: the SAME existing actions fire via the
|
||||
// SAME contract — no re-wiring, no command-id changes, and capture never auto-inserts.
|
||||
|
||||
// The TOP toolbar inventory (L6 refinement): the FREQUENT acts only — Capture (item / track)
|
||||
// then Re-capture (Maintenance, set between the two capture verbs and the placement verbs) then
|
||||
// Placement (insert / insert-conform). The FOUR RARE variants (Batch Items / Batch Razor /
|
||||
// Capture RT / Cancel RT) are ALL in the far-right "⋯" overflow menu (overflowMenuRows) —
|
||||
// same registered actions, same command-id contract, just a different home. Capture scopes come
|
||||
// from captureActionTable() (render_settings, pure); the rest are the registered M11/M10/M8
|
||||
// commands. Built once per draw/click. Each row carries its full (prefix-stripped) action name
|
||||
// for the hover tooltip.
|
||||
std::vector<ActionBarRow> topBarRows() {
|
||||
std::vector<ActionBarRow> rows;
|
||||
// Capture cluster — the primary gesture, leftmost. Face is a terse "Capture Item/Track";
|
||||
// the tooltip carries the full descriptionPhrase the action was registered with.
|
||||
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});
|
||||
}
|
||||
// Maintenance cluster — Re-capture from source (M10), placed BETWEEN the capture group and
|
||||
// the placement group so its position reads "refine the last capture before placing it".
|
||||
// Cancel RT lives in the overflow menu (both realtime verbs share that home — L6).
|
||||
rows.push_back({"RECAPTURE_FROM_SOURCE", "Re-capture",
|
||||
"re-capture from source", ActionCluster::Maintenance, true});
|
||||
// Placement cluster — the second act (still a distinct on-demand act; no auto-insert).
|
||||
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 TOP-toolbar OVERFLOW menu inventory (L6): four items pulled off the visible bar into the
|
||||
// far-right "⋯" menu button's popup — the three rare batch/realtime capture variants plus
|
||||
// Cancel RT (both realtime verbs share the menu home). Each fires the SAME existing registered
|
||||
// command id via the SAME NamedCommandLookup/Main_OnCommand contract — no action changes. The
|
||||
// fullName is the popup entry text (the terse shortLabel is unused for menu items; the popup has
|
||||
// room for the full name). Batch entries first, then the two realtime verbs.
|
||||
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},
|
||||
};
|
||||
}
|
||||
|
||||
// The active mode id the opposite-mode gate + footer toggle both read (ONE source of truth for
|
||||
// "which mode is active"). Empty when no session (every button then falls to fail-open live).
|
||||
std::string activeModeIdOrEmpty() {
|
||||
if (!g_panel.session) return {};
|
||||
return g_panel.session->view().activeModeId();
|
||||
}
|
||||
|
||||
// The BOTTOM toolbar inventory (L5 refinement 3): FOUR Item/Track x Arrange/Design tag buttons
|
||||
// then a set-apart Show Both. The suffixes are the ACTUAL registered command-id strings from
|
||||
// actions.cpp (VIEW_MOVE_ITEMS_ARRANGE / VIEW_MOVE_ITEMS_DESIGN for the item moves;
|
||||
// VIEW_TAG_ARRANGE / VIEW_TAG_DESIGN for the track tags; VIEW_SHOW_BOTH) — grepped, not
|
||||
// paraphrased. "…: Arrange" routes through the untag/arrange path (Arrange = absence of a tag).
|
||||
// The Toggle + both Activate buttons are REMOVED (L5 refinement 4 / settled inventory): the
|
||||
// footer [Arrange|Design] toggle owns mode switching.
|
||||
//
|
||||
// OPPOSITE-MODE ENABLEMENT (L5): a tag button is LIVE only for the OPPOSITE of the active mode
|
||||
// (you tag into the mode you are not in). The pure mode_enable::tagButtonEnabled decides it from
|
||||
// the active mode id; Show Both is unconditional (not a tag target). enabled=false rows draw
|
||||
// Disabled and no-op on click. The Item/Track axis is display-only here — both the Item and the
|
||||
// Track button for a target share the target's enablement.
|
||||
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;
|
||||
// Tagging cluster — the four Item/Track x Arrange/Design tag buttons.
|
||||
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});
|
||||
// Switching cluster — Show Both, set apart (the only survivor of the old switching group).
|
||||
rows.push_back({"VIEW_SHOW_BOTH", "Show Both",
|
||||
"show both for selected tracks", ActionCluster::Switching, true});
|
||||
return rows;
|
||||
}
|
||||
|
||||
// The cluster button-count specs for a given row set, in the row list's cluster order (so the
|
||||
// pure action_bar's flat index lines up with the row list). Handles all five cluster kinds;
|
||||
// empty clusters contribute a 0-count spec (action_bar skips them, emitting no gap). The spec
|
||||
// order follows each toolbar's fixed layout order (top: Capture, Maintenance, Placement —
|
||||
// Re-capture sits between the two capture verbs and the placement verbs; bottom: Tagging,
|
||||
// Switching). The bottom bar's Maintenance count is 0, so the order change is transparent there.
|
||||
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},
|
||||
};
|
||||
}
|
||||
|
||||
// The BOTTOM toolbar band: a fixed-height band directly above the footer (below the split
|
||||
// body). Degenerate (height 0) when the client is 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;
|
||||
// Keep the bar below the top toolbar; if the client is too short, collapse it.
|
||||
if (s.y < kTopToolbarHeight) { s.y = footerTop; s.height = 0; }
|
||||
return s;
|
||||
}
|
||||
|
||||
// Resolves a row's composed named command to its runtime command id (0 if not registered).
|
||||
// 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());
|
||||
}
|
||||
|
||||
// The current key binding string for a command in the MAIN section, or "" (unbound / not
|
||||
// registered). Queried via kbd_getTextFromCmd (SectionFromUniqueID(0)).
|
||||
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 {};
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
// --- Tooltip (L5 refinement 2) ------------------------------------------------
|
||||
//
|
||||
// A custom hover-delay tooltip: the full, prefix-stripped action name of the hovered toolbar
|
||||
// button. Resolves the hovered element to its (anchor rect, text); returns false when the current
|
||||
// hover has no tooltip (grid / chrome / the More button — the More button's own popup is its
|
||||
// affordance). The tooltip DRAW lives in panel_render (drawTooltip); timing
|
||||
// (kTooltipDelayMs) is applied by the caller.
|
||||
|
||||
// The full (prefix-stripped) tooltip text for the currently hovered toolbar button, plus its
|
||||
// anchor rect. Returns false when the hover is not a tooltip-bearing toolbar button.
|
||||
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;
|
||||
|
||||
// The hovered button's slot rect (the anchor). computeBarSlots is the same layout the 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;
|
||||
|
||||
// The full name is stored already prefix-free, but strip defensively in case a source ever
|
||||
// carries the "ReaSampler:" display prefix (the tooltip must never show it — L5 refinement 2).
|
||||
// L6: the keybinding sub-row was removed from the button face, so the tooltip now carries
|
||||
// both the name AND the binding (when bound) — e.g. "capture selected item — F5". When the
|
||||
// action is unbound the tooltip shows only the name (no "(unbound)" noise in the tooltip).
|
||||
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;
|
||||
}
|
||||
|
||||
// --- Split geometry -----------------------------------------------------------
|
||||
//
|
||||
// Every rect below is derived from the client size + fullHeight state, and BOTH paint
|
||||
// and hit-testing call these so they never drift. All are top-left origin.
|
||||
|
||||
// The body band between the TOP toolbar and the BOTTOM toolbar (L4). Its top edge is below the
|
||||
// top toolbar; its bottom edge is the bottom toolbar's top. 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;
|
||||
}
|
||||
|
||||
// True when both regions are shown (the split is live). Otherwise one region fills
|
||||
// the body.
|
||||
bool poolShown() { return g_panel.fullHeight != BankPanelFullHeight::BanksOnly; }
|
||||
bool banksShown() { return g_panel.fullHeight != BankPanelFullHeight::PoolOnly; }
|
||||
|
||||
// The pool region's rect (whole-region: header band + grid). 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; // pool full-height: the whole body
|
||||
// Split: pool gets the top half (minus the divider).
|
||||
RECT rc = body;
|
||||
rc.bottom = body.top + (body.bottom - body.top - kSplitDividerHeight) / 2;
|
||||
if (rc.bottom < rc.top) rc.bottom = rc.top;
|
||||
return rc;
|
||||
}
|
||||
|
||||
// The named-banks region's rect (whole-region: header band + tab strip + grid).
|
||||
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;
|
||||
}
|
||||
|
||||
// A region's header band (the top kRegionHeaderHeight of the region).
|
||||
RECT regionHeaderRect(const RECT& region) {
|
||||
RECT rc = region;
|
||||
rc.bottom = region.top + kRegionHeaderHeight;
|
||||
if (rc.bottom > region.bottom) rc.bottom = region.bottom;
|
||||
return rc;
|
||||
}
|
||||
|
||||
// The named-banks region's tab strip (below its header band).
|
||||
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;
|
||||
}
|
||||
|
||||
// The full-height toggle button rect inside a region header (right-aligned).
|
||||
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;
|
||||
}
|
||||
|
||||
// The "+" create-bank button rect inside the named-banks region header (left of the
|
||||
// full-height button).
|
||||
RECT createBtnRect(const RECT& region) {
|
||||
RECT ft = fullHtBtnRect(region);
|
||||
RECT rc = ft;
|
||||
rc.right = ft.left - 4;
|
||||
rc.left = rc.right - kCreateBtnWidth;
|
||||
return rc;
|
||||
}
|
||||
|
||||
// Resolves a region's display for the currently-shown bank. Empty (no bank / no width)
|
||||
// yields an empty display. orderedSampleIds reconciles the bank's SlotMap against live
|
||||
// membership, so a freshly-migrated or out-of-band-mutated bank always yields a complete
|
||||
// order (trailing empties are trimmed by the model — maxSlot walks only live occupants).
|
||||
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;
|
||||
}
|
||||
|
||||
// The FOCUSED region's display (the slot-order bridge for the region holding the live
|
||||
// selection). Mirrors columnsForRegion's client read.
|
||||
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);
|
||||
}
|
||||
|
||||
// Which region (if any) contains client point (x, y); returns false via `out` set to
|
||||
// Pool by default when the point is in neither region body.
|
||||
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;
|
||||
}
|
||||
|
||||
// The footer mode-toggle segment (Arrange|Design) under (x, y), or -1. Segments are tiled by
|
||||
// mode_switch inside footer_bar's toggle box, so both draw and hit-test use the same box.
|
||||
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());
|
||||
}
|
||||
|
||||
// The column count for a region's current grid width (nav needs the layout's wrap).
|
||||
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
|
||||
|
||||
// --- Public API (the split-state seam — panel_layout.h) ------------------------
|
||||
|
||||
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
|
||||
Reference in New Issue
Block a user