#pragma once #include "core/ui/rect.h" // 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 namespace reasampler::ui { // 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. using TabStripRect = Rect; // Q-W1: the shared concrete ui::Rect (core/ui/rect.h), role-aliased // 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 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::ui