diff --git a/CMakeLists.txt b/CMakeLists.txt index ea6285a..3e41106 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -45,6 +45,16 @@ target_include_directories(capture_paths PUBLIC src) add_library(bank_grid STATIC src/bank_grid.cpp) target_include_directories(bank_grid PUBLIC src) +# --------------------------------------------------------------------------- +# 2c') Pure mode_switch layout — NO REAPER, NO SWELL. The Design-View mode-switch +# geometry (D5): header rect + N modes -> N equal segment rects (exact tiling), +# and point -> segment hit-test. Split out so the switch's layout math is +# unit-tested outside the DAW; the bank_panel header strip that draws it and +# routes clicks to view::applyMode is DAW-verified. Mirror of bank_grid. +# --------------------------------------------------------------------------- +add_library(mode_switch STATIC src/mode_switch.cpp) +target_include_directories(mode_switch PUBLIC src) + # --------------------------------------------------------------------------- # 2d) Pure view_mode_model library — NO REAPER, NO SWELL. The Design View heart # (Phase D1): mode registry + GUID-keyed membership index + folder-tree-aware @@ -106,6 +116,10 @@ add_executable(bank_grid_tests tests/test_bank_grid.cpp) target_link_libraries(bank_grid_tests PRIVATE bank_grid) add_test(NAME bank_grid_tests COMMAND bank_grid_tests) +add_executable(mode_switch_tests tests/test_mode_switch.cpp) +target_link_libraries(mode_switch_tests PRIVATE mode_switch) +add_test(NAME mode_switch_tests COMMAND mode_switch_tests) + add_executable(view_mode_model_tests tests/test_view_mode_model.cpp) target_link_libraries(view_mode_model_tests PRIVATE view_mode_model) add_test(NAME view_mode_model_tests COMMAND view_mode_model_tests) @@ -141,6 +155,7 @@ add_library(reaper_reasampler MODULE src/capture.cpp src/persist.cpp src/bank_panel.cpp + src/mode_switch.cpp src/insert.cpp src/insert_plan.cpp ${LICE_SRC} @@ -151,7 +166,7 @@ add_library(reaper_reasampler MODULE src/actions.cpp src/render_settings.cpp ) -target_link_libraries(reaper_reasampler PRIVATE bank_model capture_paths peaks bank_grid view_mode_model insert_plan render_settings) +target_link_libraries(reaper_reasampler PRIVATE bank_model capture_paths peaks bank_grid mode_switch view_mode_model insert_plan render_settings) target_include_directories(reaper_reasampler PRIVATE ${SDK_INC} ${WDL_INC}) set_target_properties(reaper_reasampler PROPERTIES PREFIX "" OUTPUT_NAME "reaper_reasampler") diff --git a/src/bank_panel.cpp b/src/bank_panel.cpp index f87bcd7..ca47774 100644 --- a/src/bank_panel.cpp +++ b/src/bank_panel.cpp @@ -39,8 +39,10 @@ #include "bank_grid.h" #include "bank_model.h" #include "capture_paths.h" +#include "mode_switch.h" #include "peaks.h" #include "persist.h" +#include "view.h" // applyMode — the D2/D4 mode-activation entrypoint the switch fires // SWELL / LICE. On macOS/Linux SWELL is provided by the host (SWELL_PROVIDED_BY_APP); // on Windows we use native Win32 (windows.h first, then swell.h no-ops on _WIN32). @@ -116,6 +118,22 @@ const LICE_pixel kColSelBg = LICE_RGBA(38, 66, 58, 255); // selected fil const LICE_pixel kColSelBorder = LICE_RGBA(120, 200, 160, 255);// selected border const LICE_pixel kColFocusBorder = LICE_RGBA(210, 230, 220, 255);// focused-cell border +// --- Mode-switch header (D5) -------------------------------------------------- +// A fixed-height segmented control at the top of the client area: one segment per +// registered Design-View mode, the active one lit. The grid is offset below it. +// Layout math (segment rects, hit-test) lives in the pure mode_switch module; only +// the draw + click routing is here. +constexpr int kHeaderHeight = 30; // px; fixed strip, grid starts below it + +const LICE_pixel kColHeaderBg = LICE_RGBA(20, 20, 22, 255); // header strip fill +const LICE_pixel kColSegBg = LICE_RGBA(44, 44, 48, 255); // inactive segment +const LICE_pixel kColSegActiveBg = LICE_RGBA(58, 96, 84, 255); // active (lit) segment +const LICE_pixel kColSegBorder = LICE_RGBA(70, 70, 76, 255); // segment divider + +// Segment label colors are COLORREFs (SetTextColor takes RGB, not LICE_pixel). +const COLORREF kRgbSegText = RGB(170, 170, 176); // inactive label +const COLORREF kRgbSegActiveText = RGB(220, 235, 228); // active label + // --- Panel state -------------------------------------------------------------- // A computed thumbnail: the per-channel envelope at a known width. Held in the @@ -350,10 +368,77 @@ void drawEmptyState(HWND hwnd, LICE_IBitmap* bmp, int w, int h) { DrawText(dc, msg, -1, &rc, DT_CENTER | DT_VCENTER | DT_SINGLELINE | DT_WORDBREAK); } -// The cell rects for the panel's CURRENT client width and bank size. Both paint -// and mouse hit-testing call this so they share identical geometry (no drift -// between what is drawn and what a click resolves to). Returns empty when the -// window is gone or the bank is empty. +// The mode-switch header rect for a client of width `w`: the full-width strip of +// fixed height at the top. Pure geometry (mode_switch owns the segment division); +// this just sizes the band. Shared by paint and click routing so both agree. +HeaderRect panelHeader(int w) { + return HeaderRect{0, 0, w, kHeaderHeight}; +} + +// The number of registered Design-View modes (segments to draw). 0 when no session. +int modeCount() { + if (!g_panel.session) return 0; + return static_cast(g_panel.session->view().modes().size()); +} + +// Draws the segmented mode switch into the header strip of width `w`: one segment +// per registered mode (ordinal order), the active mode lit, each labeled with its +// display name and a cheap membership indicator (count of tracks tagged into that +// mode). READ-ONLY: reads g_session->view() live; never mutates the model here +// (activation happens on click, in handleClick). +void drawModeSwitch(LICE_IBitmap* bmp, int w) { + if (!g_panel.session) return; + const ViewModeModel& view = g_panel.session->view(); + const std::vector& modes = view.modes().all(); + const int n = static_cast(modes.size()); + + // Strip background first (so an empty/absent switch still reads as a header band). + LICE_FillRect(bmp, 0, 0, w, kHeaderHeight, kColHeaderBg, 1.0f, 0); + if (n <= 0) return; + + const HeaderRect header = panelHeader(w); + const std::vector segs = computeSegmentRects(header, n); + if (segs.empty()) return; + + // Cheap per-mode membership count: how many tagged leaves opted into this mode. + // Iterate the membership index once per mode (tiny N of modes; the index is the + // set of TAGGED tracks, not all tracks — bounded and cheap). Untagged tracks are + // Arrange members by default but are NOT in the index, so this is a "tagged into" + // count, which is the sensible, cheap indicator (not a full tree walk). + const std::string& activeId = view.activeModeId(); + HDC dc = bmp->getDC(); + + for (int i = 0; i < n; ++i) { + const SegmentRect& s = segs[static_cast(i)]; + const Mode& mode = modes[static_cast(i)]; + const bool active = mode.id == activeId; + + LICE_FillRect(bmp, s.x, s.y, s.width, s.height, + active ? kColSegActiveBg : kColSegBg, 1.0f, 0); + LICE_DrawRect(bmp, s.x, s.y, s.width, s.height, kColSegBorder, 1.0f, 0); + + if (!dc) continue; + + int members = 0; + for (const auto& entry : view.membership().all()) + if (entry.second.modeIds.count(mode.id) != 0) ++members; + + // "DisplayName (count)" centered in the segment. A single-line centered + // label; the segment is wide enough for the seed modes' short names. + std::string label = mode.displayName + " (" + std::to_string(members) + ")"; + RECT rc{s.x, s.y, s.x + s.width, s.y + s.height}; + SetTextColor(dc, active ? kRgbSegActiveText : kRgbSegText); + SetBkMode(dc, TRANSPARENT); + DrawText(dc, label.c_str(), -1, &rc, + DT_CENTER | DT_VCENTER | DT_SINGLELINE | DT_END_ELLIPSIS); + } +} + +// The cell rects for the panel's CURRENT client width and bank size, translated +// DOWN by the header height so the grid sits below the mode switch. Both paint and +// mouse hit-testing call this so they share identical geometry (no drift between +// what is drawn and what a click resolves to). Returns empty when the window is +// gone or the bank is empty. std::vector panelRects() { if (!g_panel.hwnd) return {}; const BankIndex* bank = g_panel.session ? &g_panel.session->bank() : nullptr; @@ -362,7 +447,10 @@ std::vector panelRects() { GetClientRect(g_panel.hwnd, &cr); const int w = cr.right - cr.left; if (w <= 0) return {}; - return computeCellRects(static_cast(bank->size()), w, kGrid); + std::vector rects = + computeCellRects(static_cast(bank->size()), w, kGrid); + for (CellRect& r : rects) r.y += kHeaderHeight; // offset below the header + return rects; } // The full paint: build/refresh the LICE backing bitmap at client size, draw the @@ -385,8 +473,9 @@ void paintPanel(HWND hwnd, HDC hdc) { LICE_Clear(&bmp, kColBackground); const std::string projectDir = currentProjectDir(); const std::vector& samples = bank->all(); - const std::vector rects = + std::vector rects = computeCellRects(static_cast(samples.size()), w, kGrid); + for (CellRect& r : rects) r.y += kHeaderHeight; // grid sits below the header // Draw each cell's thumbnail. Inner drawable width == cell width - inset; // compute the envelope at the cell's inner column count so bins map 1:1. const int binWidth = kGrid.cellWidth - 4; @@ -403,6 +492,10 @@ void paintPanel(HWND hwnd, HDC hdc) { } } + // The mode switch draws LAST so its header band overlays the top of the grid / + // empty-state area regardless of which branch ran above. + drawModeSwitch(&bmp, w); + BitBlt(hdc, 0, 0, w, h, bmp.getDC(), 0, 0, SRCCOPY); } @@ -571,6 +664,27 @@ void invalidatePanel() { // on empty space (gap/margin/below grid) clears the selection AND stops audition // (deselect stop path). READ-ONLY: never mutates the bank/project. void handleClick(int x, int y) { + // Mode-switch header takes precedence: a click in the header band activates the + // clicked mode via the D2/D4 view shell (the same action the user can bind) and + // repaints. Load-bearing principle preserved — this fires a Design-View toggle, + // it never inserts into the arrange or mutates the bank. + if (g_panel.session) { + RECT cr{}; + GetClientRect(g_panel.hwnd, &cr); + const int w = cr.right - cr.left; + const int n = modeCount(); + const int seg = hitTestSegment(x, y, panelHeader(w), n); + if (seg >= 0) { + const std::vector& modes = g_panel.session->view().modes().all(); + if (seg < static_cast(modes.size())) { + applyMode(g_panel.session->view(), modes[static_cast(seg)].id, + nullptr); + invalidatePanel(); // active-segment highlight + parked-track redraw + } + return; // header click consumed; do NOT fall through to grid selection + } + } + const std::vector rects = panelRects(); const int hit = hitTestCell(x, y, rects); const int count = bankItemCount(); diff --git a/src/mode_switch.cpp b/src/mode_switch.cpp new file mode 100644 index 0000000..204c481 --- /dev/null +++ b/src/mode_switch.cpp @@ -0,0 +1,64 @@ +// mode_switch — pure implementation. See mode_switch.h. NO REAPER / SWELL / vendor. + +#include "mode_switch.h" + +#include + +namespace reasampler { + +namespace { + +// The left edge of segment i in a header of the given x-origin and width divided +// into `count` segments. Boundary i is x + (i * width) / count, so segment i spans +// [edge(i), edge(i+1)). Because every boundary is derived from the same formula, +// consecutive segments share an exact edge (no gap, no overlap) and edge(count) +// == x + width precisely. count assumed >= 1 by callers. +int segmentEdge(int x, int width, int i, int count) { + return x + (i * width) / count; +} + +} // namespace + +std::vector computeSegmentRects(const HeaderRect& header, + int segmentCount) { + std::vector rects; + if (segmentCount <= 0 || header.width <= 0) return rects; + + rects.reserve(static_cast(segmentCount)); + for (int i = 0; i < segmentCount; ++i) { + const int left = segmentEdge(header.x, header.width, i, segmentCount); + const int right = segmentEdge(header.x, header.width, i + 1, segmentCount); + SegmentRect r; + r.x = left; + r.y = header.y; + r.width = right - left; // absorbs rounding; adjacent segments abut exactly + r.height = header.height; + rects.push_back(r); + } + return rects; +} + +int hitTestSegment(int px, int py, const HeaderRect& header, int segmentCount) { + if (segmentCount <= 0 || header.width <= 0 || header.height <= 0) return -1; + + // Reject anything outside the header band first (half-open bounds match the + // segment rects). Below the header is where the grid lives — the panel falls + // through to grid handling on a -1. + if (px < header.x || px >= header.x + header.width || + py < header.y || py >= header.y + header.height) + return -1; + + // Inside the band: find the segment whose [edge(i), edge(i+1)) contains px. + // Linear over N (N is tiny — one per mode); mirrors the boundary formula so the + // hit matches the drawn segment exactly. + for (int i = 0; i < segmentCount; ++i) { + const int left = segmentEdge(header.x, header.width, i, segmentCount); + const int right = segmentEdge(header.x, header.width, i + 1, segmentCount); + if (px >= left && px < right) return i; + } + // Guard: px == header.x + header.width would fail the < above but was already + // excluded by the band check. Any residual falls to -1 (defensive, unreachable). + return -1; +} + +} // namespace reasampler diff --git a/src/mode_switch.h b/src/mode_switch.h new file mode 100644 index 0000000..8580094 --- /dev/null +++ b/src/mode_switch.h @@ -0,0 +1,66 @@ +#pragma once +// mode_switch — the REAPER-free layout math behind the bank_panel's Design-View +// mode switch (Phase D, Wave 4 — D5). A segmented control `[ Arrange | Design ]` +// (N-mode general, one segment per registered mode) drawn in a fixed-height header +// strip at the top of the docked panel. The panel shell (bank_panel.cpp) owns the +// SWELL window, LICE drawing, and the live ViewModeModel read + mode activation — +// all REAPER-bound, DAW-verified. What is NOT DAW-bound — how N segments tile a +// header rectangle, and which segment a click lands in — lives here so it is +// unit-tested outside the DAW (CLAUDE.md §load-bearing split). Mirror of bank_grid. +// +// PURE MODULE: NO REAPER types, NO SWELL, NO vendor/ includes. Standard library +// only. Builds and unit-tests without REAPER. + +#include + +namespace reasampler { + +// The header strip the switch is 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 at the top of its client area and offsets the grid below. +struct HeaderRect { + int x = 0; + int y = 0; + int width = 0; + int height = 0; + + bool operator==(const HeaderRect& o) const { + return x == o.x && y == o.y && width == o.width && height == o.height; + } +}; + +// One segment's pixel rectangle within the header, top-left origin. These are the +// draw bounds for one mode's button; the panel draws the mode's label + membership +// indicator inside it and lights it when it is the active mode. +struct SegmentRect { + int x = 0; + int y = 0; + int width = 0; + int height = 0; + + bool operator==(const SegmentRect& o) const { + return x == o.x && y == o.y && width == o.width && height == o.height; + } +}; + +// Divides `header` into `segmentCount` equal segments left-to-right, in the caller's +// order (the panel passes modes in ordinal order). Returns exactly segmentCount +// rects. The division tiles the header EXACTLY: each segment's left edge is +// header.x + (i * width) / segmentCount, so integer rounding is absorbed at the +// boundaries — segments abut with no gap and no overlap, and the last segment +// reaches header.x + header.width precisely (individual widths may differ by one +// pixel when width does not divide evenly). Each segment inherits the header's full +// y/height. segmentCount <= 0 or a non-positive header width returns empty. +std::vector computeSegmentRects(const HeaderRect& header, + int segmentCount); + +// Hit-tests a point (SWELL/LICE top-left client coords) against the segmented +// control laid out in `header` with `segmentCount` segments. Returns the index of +// the segment containing the point, or -1 for a miss: a point outside the header +// bounds entirely (including below it, where the grid lives), or when segmentCount +// <= 0. Half-open bounds [x, x+width) x [y, y+height) match computeSegmentRects, so +// adjacent segments never both claim a pixel and the point maps to the same segment +// the panel drew there. +int hitTestSegment(int px, int py, const HeaderRect& header, int segmentCount); + +} // namespace reasampler diff --git a/tests/test_mode_switch.cpp b/tests/test_mode_switch.cpp new file mode 100644 index 0000000..3110165 --- /dev/null +++ b/tests/test_mode_switch.cpp @@ -0,0 +1,172 @@ +// Standalone tests for reasampler::mode_switch — no REAPER, no test framework. +// Same fast loop as the sibling pure tests (bank_grid et al.): assert the segmented +// mode-switch layout math and hit-testing directly. +// +// Covers (D5 brief §unit-test): N=2 and N=3 segment layout; exact tiling (adjacent +// segments abut with no gap/overlap, half-open boundaries — no pixel claimed twice, +// last segment reaches the header's right edge even when width doesn't divide +// evenly); hit-test hits per segment and misses (outside the band above/below/ +// left/right, boundary pixels); degenerate widths and counts. + +#include "../src/mode_switch.h" + +#include +#include +#include + +using namespace reasampler; + +static int g_fail = 0; +#define CHECK(cond) do { if(!(cond)) { \ + std::printf("FAIL line %d: %s\n", __LINE__, #cond); ++g_fail; } } while(0) + +// --- Layout: N=2 ------------------------------------------------------------- + +// A header 100 wide at origin (10, 4), height 24, split in two: segments +// [10,60) and [60,110), each 50 wide, full header y/height. +static void testTwoSegmentsEvenSplit() { + HeaderRect h{10, 4, 100, 24}; + auto rects = computeSegmentRects(h, 2); + CHECK(rects.size() == 2); + CHECK((rects[0] == SegmentRect{10, 4, 50, 24})); + CHECK((rects[1] == SegmentRect{60, 4, 50, 24})); +} + +// --- Layout: N=3, uneven width absorbed at boundaries ------------------------ + +// 100 wide / 3 doesn't divide evenly: boundaries at floor(i*100/3) = +// 0, 33, 66, 100 -> widths 33, 33, 34. The rounding lands in the LAST segment; +// segments still abut exactly and the last reaches header.x + width (0+100). +static void testThreeSegmentsUnevenTilesExactly() { + HeaderRect h{0, 0, 100, 20}; + auto rects = computeSegmentRects(h, 3); + CHECK(rects.size() == 3); + CHECK((rects[0] == SegmentRect{0, 0, 33, 20})); + CHECK((rects[1] == SegmentRect{33, 0, 33, 20})); + CHECK((rects[2] == SegmentRect{66, 0, 34, 20})); + // Adjacent segments abut (no gap, no overlap): each left edge == prior right. + CHECK(rects[1].x == rects[0].x + rects[0].width); + CHECK(rects[2].x == rects[1].x + rects[1].width); + // Last segment's right edge reaches the header's right edge exactly. + CHECK(rects[2].x + rects[2].width == h.x + h.width); +} + +// --- Layout: full coverage invariant for a range of widths ------------------- + +// For any width and count, the segments must tile [x, x+width) with no gap/overlap: +// first left == x, last right == x+width, every mid boundary shared. +static void testTilingCoversHeaderForVariousWidths() { + for (int width = 1; width <= 200; ++width) { + for (int count = 1; count <= 5; ++count) { + HeaderRect h{7, 3, width, 16}; + auto rects = computeSegmentRects(h, count); + CHECK(static_cast(rects.size()) == count); + CHECK(rects.front().x == h.x); + CHECK(rects.back().x + rects.back().width == h.x + h.width); + for (std::size_t i = 1; i < rects.size(); ++i) + CHECK(rects[i].x == rects[i - 1].x + rects[i - 1].width); + } + } +} + +// --- Layout: degenerate -------------------------------------------------------- + +// segmentCount <= 0 or non-positive width yields no rects (the panel draws nothing +// / falls back to full-client grid). +static void testLayoutDegenerate() { + CHECK(computeSegmentRects(HeaderRect{0, 0, 100, 20}, 0).empty()); + CHECK(computeSegmentRects(HeaderRect{0, 0, 100, 20}, -3).empty()); + CHECK(computeSegmentRects(HeaderRect{0, 0, 0, 20}, 2).empty()); + CHECK(computeSegmentRects(HeaderRect{0, 0, -5, 20}, 2).empty()); +} + +// A single segment fills the whole header. +static void testSingleSegmentFillsHeader() { + HeaderRect h{5, 5, 80, 24}; + auto rects = computeSegmentRects(h, 1); + CHECK(rects.size() == 1); + CHECK((rects[0] == SegmentRect{5, 5, 80, 24})); +} + +// --- Hit-test: hits ----------------------------------------------------------- + +// A point inside each segment maps to that segment's index. Header 100 wide at +// x=10, two segments [10,60) and [60,110). +static void testHitTestHitsEachSegment() { + HeaderRect h{10, 4, 100, 24}; + CHECK(hitTestSegment(10, 4, h, 2) == 0); // top-left corner of segment 0 + CHECK(hitTestSegment(35, 15, h, 2) == 0); // middle of segment 0 + CHECK(hitTestSegment(59, 27, h, 2) == 0); // last pixel of segment 0 (band) + CHECK(hitTestSegment(60, 15, h, 2) == 1); // first pixel of segment 1 + CHECK(hitTestSegment(109, 4, h, 2) == 1); // last pixel column of segment 1 +} + +// The boundary pixel belongs to exactly ONE segment (half-open): px==60 is the +// start of segment 1, never the end of segment 0 — so no pixel is double-claimed. +static void testHitTestBoundaryHalfOpen() { + HeaderRect h{10, 4, 100, 24}; + CHECK(hitTestSegment(59, 10, h, 2) == 0); + CHECK(hitTestSegment(60, 10, h, 2) == 1); + // Three-way boundaries at floor(i*100/2)+x are consistent for N=3 too. + HeaderRect h3{0, 0, 99, 20}; // 99/3 = 33 exactly -> edges 0,33,66,99 + CHECK(hitTestSegment(32, 5, h3, 3) == 0); + CHECK(hitTestSegment(33, 5, h3, 3) == 1); + CHECK(hitTestSegment(65, 5, h3, 3) == 1); + CHECK(hitTestSegment(66, 5, h3, 3) == 2); + CHECK(hitTestSegment(98, 5, h3, 3) == 2); +} + +// --- Hit-test: misses --------------------------------------------------------- + +// Points outside the header band on any side miss (-1). Below the band is where +// the grid lives; the panel treats -1 as "fall through to grid handling". +static void testHitTestMissesOutsideBand() { + HeaderRect h{10, 4, 100, 24}; + CHECK(hitTestSegment(9, 10, h, 2) == -1); // left of header + CHECK(hitTestSegment(110, 10, h, 2) == -1); // right edge (px == x+width, excluded) + CHECK(hitTestSegment(50, 3, h, 2) == -1); // above the band + CHECK(hitTestSegment(50, 28, h, 2) == -1); // below the band (into the grid) + CHECK(hitTestSegment(200, 200, h, 2) == -1); // far away +} + +// Degenerate hit-test inputs miss safely. +static void testHitTestDegenerate() { + HeaderRect h{10, 4, 100, 24}; + CHECK(hitTestSegment(50, 15, h, 0) == -1); // no segments + CHECK(hitTestSegment(50, 15, h, -2) == -1); + CHECK(hitTestSegment(50, 15, HeaderRect{10, 4, 0, 24}, 2) == -1); // zero width + CHECK(hitTestSegment(50, 15, HeaderRect{10, 4, 100, 0}, 2) == -1); // zero height +} + +// --- Cross-check: every point in the header hit-tests to the segment that DREW +// it (hit-test and layout agree, the load-bearing consistency invariant). ------ + +static void testHitTestMatchesLayout() { + HeaderRect h{4, 2, 87, 18}; // deliberately awkward width/origin + const int count = 3; + auto rects = computeSegmentRects(h, count); + for (int px = h.x; px < h.x + h.width; ++px) { + const int seg = hitTestSegment(px, h.y + 1, h, count); + CHECK(seg >= 0); + // The returned segment's rect must contain px (half-open). + const SegmentRect& r = rects[static_cast(seg)]; + CHECK(px >= r.x && px < r.x + r.width); + } +} + +int main() { + testTwoSegmentsEvenSplit(); + testThreeSegmentsUnevenTilesExactly(); + testTilingCoversHeaderForVariousWidths(); + testLayoutDegenerate(); + testSingleSegmentFillsHeader(); + + testHitTestHitsEachSegment(); + testHitTestBoundaryHalfOpen(); + testHitTestMissesOutsideBand(); + testHitTestDegenerate(); + testHitTestMatchesLayout(); + + if (g_fail == 0) std::printf("All tests passed.\n"); + return g_fail ? 1 : 0; +}