Files
reasampler/src/tab_strip.h
T
daniel a67a2f9479 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.
2026-07-25 14:37:57 -04:00

136 lines
6.8 KiB
C++
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#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 ~812
// 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