feat(bank_panel): B4 vertical-split UI — LICE tab strip, id-keyed bank ops, move/copy drag
Pool grid on top, LICE-drawn named-banks tab strip below with overflow-scroll (new pure tab_strip seam, unit-tested). Full-height toggles, unmistakable active-bank readout distinct from the shown tab, tab context menu (activate/rename/delete/evacuate/create) with rich confirm-on-non-empty-delete, and move/copy via menu + drag with drop-highlighting. Fold-in: deserialize auto-disambiguates duplicate folded bank names instead of rejecting the book.
This commit is contained in:
+135
@@ -0,0 +1,135 @@
|
||||
#pragma once
|
||||
// tab_strip — the REAPER-free layout + hit-test math behind the bank_panel's
|
||||
// named-banks tab strip (Phase B, Wave 4 — B4). The named-banks region of the
|
||||
// vertical-split bank window is a LICE-drawn tab strip (one tab per named bank,
|
||||
// NOT a SWELL-native tab control), and — from the start — it must scroll when the
|
||||
// tabs overflow the strip width (a naive fixed-width strip breaks down at ~8–12
|
||||
// tabs). What is NOT DAW-bound — how N fixed-width tabs tile a strip of a given
|
||||
// pixel width, where the overflow chevrons sit, which tab/chevron a click lands in,
|
||||
// and how far the strip may scroll — lives here so it is unit-tested outside the
|
||||
// DAW (CLAUDE.md §load-bearing split). The panel shell (bank_panel.cpp) owns the
|
||||
// SWELL window, LICE drawing, and the live BankBook read; it calls into this seam
|
||||
// for every rect and every hit. Mirror of mode_switch / bank_grid.
|
||||
//
|
||||
// PURE MODULE: NO REAPER types, NO SWELL, NO vendor/ includes. Standard library
|
||||
// only. Builds and unit-tests without REAPER.
|
||||
|
||||
#include <vector>
|
||||
|
||||
namespace reasampler {
|
||||
|
||||
// The strip the tabs are drawn into, top-left origin (SWELL/LICE convention).
|
||||
// (x, y) is the top-left corner; width/height are the strip extents. The panel
|
||||
// reserves this as a fixed-height band at the top of the named-banks region.
|
||||
struct TabStripRect {
|
||||
int x = 0;
|
||||
int y = 0;
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
|
||||
bool operator==(const TabStripRect& o) const {
|
||||
return x == o.x && y == o.y && width == o.width && height == o.height;
|
||||
}
|
||||
};
|
||||
|
||||
// Fixed inputs that shape the strip. tabWidth is the pixel width of each tab (fixed
|
||||
// so the strip reads as a uniform segmented control and overflow math stays simple —
|
||||
// labels ellipsize within the tab, they do not resize it). chevronWidth is the width
|
||||
// reserved at each end for the scroll affordance WHEN the tabs overflow; when they
|
||||
// fit, no chevron is reserved and the tabs use the full strip width.
|
||||
struct TabStripSpec {
|
||||
int tabWidth = 96;
|
||||
int chevronWidth = 20;
|
||||
};
|
||||
|
||||
// One tab's pixel rectangle within the strip, top-left origin, ALREADY translated
|
||||
// by the current scroll offset and clipped to the visible track. `index` is the
|
||||
// tab's index in the caller's list (ordinal order) so the shell can label/light it
|
||||
// without re-deriving. A tab scrolled fully out of view is omitted from the result
|
||||
// (the shell only draws what computeTabRects returns), so every returned rect is at
|
||||
// least partially visible.
|
||||
struct TabRect {
|
||||
int index = 0;
|
||||
int x = 0;
|
||||
int y = 0;
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
|
||||
bool operator==(const TabRect& o) const {
|
||||
return index == o.index && x == o.x && y == o.y &&
|
||||
width == o.width && height == o.height;
|
||||
}
|
||||
};
|
||||
|
||||
// The scrollable track's geometry: where the tabs may be drawn (between the
|
||||
// chevrons when overflowing, or the whole strip when they fit) and whether each
|
||||
// chevron is present. Derived once and shared by layout + hit-testing so both agree.
|
||||
struct TabStripLayout {
|
||||
bool overflow = false; // true iff N tabs at tabWidth exceed the track width
|
||||
int trackX = 0; // left edge of the tab track (past the left chevron)
|
||||
int trackWidth = 0; // width available to tabs (strip minus both chevrons)
|
||||
int maxScroll = 0; // largest valid scroll offset (0 when no overflow)
|
||||
bool leftChevron = false; // a left-scroll affordance is reserved this frame
|
||||
bool rightChevron = false;// a right-scroll affordance is reserved this frame
|
||||
};
|
||||
|
||||
// Computes the strip layout for `tabCount` tabs of `spec.tabWidth` in `strip`,
|
||||
// given the current `scrollOffset`. Pure geometry:
|
||||
// * No overflow (all tabs fit the strip width): overflow=false, no chevrons, the
|
||||
// track IS the strip, maxScroll=0.
|
||||
// * Overflow: both chevrons are reserved (chevronWidth each), the track is the
|
||||
// strip minus both chevrons, and maxScroll is the pixels by which the tab run
|
||||
// exceeds the track (so the last tab's right edge can reach the track's right
|
||||
// edge but not scroll past it). Chevrons are always both present under overflow
|
||||
// (a fixed affordance is simpler and unambiguous than hiding one at an end;
|
||||
// clicking a chevron at a scroll limit is a harmless no-op the shell clamps).
|
||||
// tabCount <= 0 or a non-positive strip width returns a zeroed layout (no overflow,
|
||||
// track == strip, maxScroll 0).
|
||||
TabStripLayout computeTabStripLayout(const TabStripRect& strip, int tabCount,
|
||||
const TabStripSpec& spec, int scrollOffset);
|
||||
|
||||
// Clamps a desired scroll offset into [0, maxScroll] for the given layout. The shell
|
||||
// calls this after a chevron click / wheel so the strip never scrolls past either
|
||||
// end. maxScroll is 0 when the tabs fit, so a fitting strip always clamps to 0.
|
||||
int clampTabScroll(int desiredOffset, const TabStripLayout& layout);
|
||||
|
||||
// Tiles `tabCount` fixed-width tabs left-to-right into the layout's track, shifted
|
||||
// left by `scrollOffset`, and returns the rects that are at least partially visible
|
||||
// (in tab-index order). Each tab i sits at trackX + i*tabWidth - scrollOffset; a tab
|
||||
// whose visible extent is empty (fully left of or right of the track) is omitted.
|
||||
// Returned rects are CLIPPED to the track horizontally so a partially-scrolled tab
|
||||
// does not draw under a chevron. The caller passes the SAME scrollOffset it passed
|
||||
// to computeTabStripLayout (the shell clamps once, then uses the clamped value for
|
||||
// both). tabCount <= 0 -> empty.
|
||||
std::vector<TabRect> computeTabRects(const TabStripRect& strip, int tabCount,
|
||||
const TabStripSpec& spec, int scrollOffset);
|
||||
|
||||
// What a point in the strip resolves to.
|
||||
enum class TabHitKind {
|
||||
None, // outside the strip, or in dead space between visible tabs
|
||||
Tab, // a tab — `index` is the tab's index in the caller's list
|
||||
ScrollLeft, // the left overflow chevron
|
||||
ScrollRight, // the right overflow chevron
|
||||
};
|
||||
|
||||
// The outcome of hit-testing a point against the strip. For Tab, `index` is the tab
|
||||
// index; for the chevrons and None it is -1.
|
||||
struct TabHit {
|
||||
TabHitKind kind = TabHitKind::None;
|
||||
int index = -1;
|
||||
|
||||
bool operator==(const TabHit& o) const {
|
||||
return kind == o.kind && index == o.index;
|
||||
}
|
||||
};
|
||||
|
||||
// Hit-tests a point (SWELL/LICE top-left client coords) against the strip laid out
|
||||
// for `tabCount` tabs at `scrollOffset`. Chevrons take precedence over tabs at the
|
||||
// strip ends (a click in the reserved chevron band is a scroll, never a tab), and a
|
||||
// point outside the strip band, or in the track but not on any visible tab, is None.
|
||||
// Half-open bounds match computeTabRects / the chevron bands so no pixel is claimed
|
||||
// twice. The shell passes the SAME clamped scrollOffset it drew with.
|
||||
TabHit hitTestTabStrip(int px, int py, const TabStripRect& strip, int tabCount,
|
||||
const TabStripSpec& spec, int scrollOffset);
|
||||
|
||||
} // namespace reasampler
|
||||
Reference in New Issue
Block a user