// Standalone tests for reasampler::footer_bar — no REAPER, no test framework. Same fast loop // as the sibling pure tests (prune_button / mode_switch / action_bar): assert the L4 footer // LEFT-group layout (toggle . count . Tail) and hit-testing directly. // // Covers (L4 brief §footer test cases): // * Layout: toggle + count + Tail placed left-to-right in the specified order without // overlap, starting at leftPad, vertically inset; the narrow toggle sits at the LEFT. // * The group stays clear of the reserved RIGHT region (prune + version) — nothing crosses // footer.right - rightReserve. // * Suppression: a too-narrow footer drops later affordances (Tail first, then count), // the toggle surviving longest; a degenerate footer yields an all-empty layout. // * countWidth <= 0 hides the count label and the Tail sits right after the toggle. // * Hit-test: Toggle / Tail returned for in-bounds points, None outside AND on the count // label (a passive readout, never a control); half-open bounds; suppressed box claims none. #include "../src/core/ui/footer_bar.h" #include using namespace reasampler; using namespace reasampler::ui; static int g_fail = 0; #define CHECK(cond) do { if(!(cond)) { \ std::printf("FAIL line %d: %s\n", __LINE__, #cond); ++g_fail; } } while(0) // A spec with round numbers so expected pixels are hand-checkable. static FooterBarSpec roundSpec() { FooterBarSpec s; s.toggleWidth = 130; s.countWidth = 60; s.tailWidth = 130; s.gap = 6; s.leftPad = 8; s.verticalInset = 4; s.rightReserve = 168; return s; } // --- Layout: wide footer, all three placed, left-to-right --------------------- // A wide footer fits all three. Verify order, gaps, no overlap, vertical inset, and that the // Tail (last) stays clear of the reserved right region. static void testWideFooterAllPlaced() { const FooterBarSpec s = roundSpec(); // Need room for toggle(130)+gap(6)+count(60)+gap(6)+tail(130) = 332 from leftPad(8) => 340, // plus rightReserve(168). Width 700 is comfortable. FooterRect f{0, 100, 700, 26}; const FooterBarLayout L = computeFooterBar(f, s); const int top = 100 + 4, boxH = 26 - 8; // 104, 18 CHECK((L.toggle == FooterBarRect{8, top, 130, boxH})); CHECK((L.count == FooterBarRect{8 + 130 + 6, top, 60, boxH})); // x = 144 CHECK((L.tail == FooterBarRect{8 + 130 + 6 + 60 + 6, top, 130, boxH})); // x = 210 // Order + no overlap: each starts at-or-after the previous end. CHECK(L.count.x >= L.toggle.x + L.toggle.width); CHECK(L.tail.x >= L.count.x + L.count.width); // The whole group clears the reserved right region. CHECK(L.tail.x + L.tail.width <= f.x + f.width - s.rightReserve); } // Origin offset honoured: the group anchors to THIS footer's left, not 0. static void testOffsetFooterAnchorsLeft() { const FooterBarSpec s = roundSpec(); FooterRect f{20, 200, 700, 26}; const FooterBarLayout L = computeFooterBar(f, s); CHECK(L.toggle.x == 20 + 8); CHECK(!L.toggle.empty() && !L.count.empty() && !L.tail.empty()); } // --- Suppression: narrowing drops later affordances first --------------------- // A footer wide enough for toggle + count but not the Tail: Tail suppressed, toggle+count stay. static void testTailSuppressedWhenTight() { const FooterBarSpec s = roundSpec(); // Right bound = width - 168. Toggle ends at 8+130=138; count ends at 144+60=204. // Tail would start at 210 and end at 340; need 340 <= width-168 => width >= 508 for Tail. // count needs 204 <= width-168 => width >= 372. Pick width 480: count fits, Tail does not. FooterRect f{0, 0, 480, 26}; const FooterBarLayout L = computeFooterBar(f, s); CHECK(!L.toggle.empty()); CHECK(!L.count.empty()); CHECK(L.tail.empty()); } // Narrower still: only the toggle survives (count and Tail both drop). When the count does not // fit, the Tail is offered the count's slot — but if the Tail also does not fit there, both are // empty and only the toggle remains. static void testOnlyToggleSurvives() { const FooterBarSpec s = roundSpec(); // Toggle needs 138 <= width-168 => width >= 306. count needs width >= 372. // At the count's slot (x=144) the Tail (130) would end at 274; needs 274 <= width-168 => // width >= 442. Pick width 330: toggle fits (306), count does not (372), Tail-at-count-slot // does not (442) -> only toggle. FooterRect f{0, 0, 330, 26}; const FooterBarLayout L = computeFooterBar(f, s); CHECK(!L.toggle.empty()); CHECK(L.count.empty()); CHECK(L.tail.empty()); } // When the count does NOT fit but the Tail WOULD fit at the count's slot, the Tail takes it // (a passive label yields to the interactive button). Right after the toggle, no count gap. static void testTailTakesCountSlotWhenCountDrops() { const FooterBarSpec s = roundSpec(); // Want: count does not fit, Tail-at-count-slot fits. count fits at width >= 372. // Tail at count slot x=144, ends 274, fits at width >= 442. Choose width 420: // 372 > 420? no — count fits at 420. Need width in [442-eps? ] hmm: count fits iff // width>=372, Tail-at-slot fits iff width>=442. For "count drops but tail-at-slot fits" // we need width < 372 AND width >= 442 — impossible with countWidth==tailWidth region. // So shrink countWidth to make its slot further right than tail's need. Use a spec where // countWidth is large so it drops before a smaller Tail. FooterBarSpec s2 = s; s2.countWidth = 200; // large passive label s2.tailWidth = 80; // small button // toggle ends 138. count slot x = 144, count ends 344; fits iff 344 <= width-168 => // width >= 512. Tail at count slot x=144 ends 224; fits iff width >= 392. // Choose width 420: count drops (needs 512), Tail-at-slot fits (needs 392). FooterRect f{0, 0, 420, 26}; const FooterBarLayout L = computeFooterBar(f, s2); CHECK(!L.toggle.empty()); CHECK(L.count.empty()); CHECK(!L.tail.empty()); // Tail sits at the count's slot (right after the toggle + gap), NOT past a phantom count. CHECK(L.tail.x == L.toggle.x + L.toggle.width + s2.gap); } // A degenerate footer yields an all-empty layout (nothing placed, no crash). static void testDegenerateFooterEmpty() { const FooterBarSpec s = roundSpec(); const FooterBarLayout a = computeFooterBar(FooterRect{0, 0, 0, 26}, s); CHECK(a.toggle.empty() && a.count.empty() && a.tail.empty()); const FooterBarLayout b = computeFooterBar(FooterRect{0, 0, 700, 0}, s); CHECK(b.toggle.empty() && b.count.empty() && b.tail.empty()); // Height <= 2*verticalInset -> boxH <= 0 -> all empty. const FooterBarLayout c = computeFooterBar(FooterRect{0, 0, 700, 8}, s); CHECK(c.toggle.empty() && c.count.empty() && c.tail.empty()); } // A footer too narrow even for the toggle: all empty (toggle drops last, so nothing survives). static void testTooNarrowForToggle() { const FooterBarSpec s = roundSpec(); // Toggle fits iff 8+130 <= width-168 => width >= 306. Width 200 suppresses everything. const FooterBarLayout L = computeFooterBar(FooterRect{0, 0, 200, 26}, s); CHECK(L.toggle.empty() && L.count.empty() && L.tail.empty()); } // countWidth <= 0 hides the count label and the Tail sits right after the toggle (no count gap). static void testCountHiddenByZeroWidth() { FooterBarSpec s = roundSpec(); s.countWidth = 0; FooterRect f{0, 0, 700, 26}; const FooterBarLayout L = computeFooterBar(f, s); CHECK(!L.toggle.empty()); CHECK(L.count.empty()); CHECK(!L.tail.empty()); CHECK(L.tail.x == L.toggle.x + L.toggle.width + s.gap); // right after the toggle } // --- Hit-test ----------------------------------------------------------------- static void testHitToggleAndTail() { const FooterBarSpec s = roundSpec(); FooterRect f{0, 100, 700, 26}; const FooterBarLayout L = computeFooterBar(f, s); // Toggle interior. CHECK(hitTestFooterBar(L.toggle.x, L.toggle.y, L) == FooterHit::Toggle); CHECK(hitTestFooterBar(L.toggle.x + L.toggle.width - 1, L.toggle.y + L.toggle.height - 1, L) == FooterHit::Toggle); // Tail interior. CHECK(hitTestFooterBar(L.tail.x + L.tail.width / 2, L.tail.y + L.tail.height / 2, L) == FooterHit::Tail); // The count label is a passive readout — never a hit target. CHECK(hitTestFooterBar(L.count.x + L.count.width / 2, L.count.y + L.count.height / 2, L) == FooterHit::None); // Between the toggle and the count (the gap) is a clean miss. CHECK(hitTestFooterBar(L.toggle.x + L.toggle.width, L.toggle.y, L) == FooterHit::None); } // Half-open bounds: far edges excluded; points outside every box miss. static void testHitEdgesAndOutside() { const FooterBarSpec s = roundSpec(); FooterRect f{0, 100, 700, 26}; const FooterBarLayout L = computeFooterBar(f, s); CHECK(hitTestFooterBar(L.toggle.x - 1, L.toggle.y, L) == FooterHit::None); CHECK(hitTestFooterBar(L.tail.x + L.tail.width, L.tail.y, L) == FooterHit::None); // right edge excl CHECK(hitTestFooterBar(L.toggle.x, L.toggle.y - 1, L) == FooterHit::None); // above CHECK(hitTestFooterBar(L.toggle.x, L.toggle.y + L.toggle.height, L) == FooterHit::None); // below excl // Far right (the prune/version region) is not this module's — a clean None here. CHECK(hitTestFooterBar(f.x + f.width - 10, L.toggle.y, L) == FooterHit::None); } // A suppressed box claims no point — a Tail hit-test where the Tail was dropped is None. static void testSuppressedClaimsNothing() { const FooterBarSpec s = roundSpec(); FooterRect f{0, 0, 480, 26}; // Tail suppressed (see testTailSuppressedWhenTight) const FooterBarLayout L = computeFooterBar(f, s); CHECK(L.tail.empty()); // A point where the Tail WOULD have been claims nothing. CHECK(hitTestFooterBar(210 + 10, 104, L) != FooterHit::Tail); } // --- Resize sweep: no overlap or cross-into-reserved across a width range ----- static void testResizeSweepNoOverlap() { const FooterBarSpec s = roundSpec(); const int rightBoundFor = -s.rightReserve; // + width applied per iteration for (int w = 40; w <= 1200; w += 7) { FooterRect f{0, 0, w, 26}; const FooterBarLayout L = computeFooterBar(f, s); const int rightBound = f.x + w + rightBoundFor; // = w - rightReserve // Order + no overlap among placed boxes. if (!L.toggle.empty() && !L.count.empty()) CHECK(L.count.x >= L.toggle.x + L.toggle.width); if (!L.count.empty() && !L.tail.empty()) CHECK(L.tail.x >= L.count.x + L.count.width); if (!L.toggle.empty() && L.count.empty() && !L.tail.empty()) CHECK(L.tail.x >= L.toggle.x + L.toggle.width); // Nothing placed crosses into the reserved right region. if (!L.toggle.empty()) CHECK(L.toggle.x + L.toggle.width <= rightBound); if (!L.count.empty()) CHECK(L.count.x + L.count.width <= rightBound); if (!L.tail.empty()) CHECK(L.tail.x + L.tail.width <= rightBound); // A later affordance is never placed while an earlier one is dropped (greedy order): // count/tail present implies toggle present. if (!L.count.empty() || !L.tail.empty()) CHECK(!L.toggle.empty()); } } // --- Mode-segment enablement (the playback gate's visible half) --------------- static void testModeSegmentsAreLiveWithTheTransportStopped() { CHECK(modeSegmentEnabled(/*isActiveSegment=*/false, /*transportRunning=*/false)); CHECK(modeSegmentEnabled(/*isActiveSegment=*/true, /*transportRunning=*/false)); } static void testInactiveSegmentGoesDeadWhileTheTransportRuns() { // The one that would fire a real switch — refused while playing/recording, so it // must read dead rather than invite a click that silently does nothing. CHECK(!modeSegmentEnabled(/*isActiveSegment=*/false, /*transportRunning=*/true)); } static void testActiveSegmentStaysLiveWhileTheTransportRuns() { // Clicking the mode you are already in is a reapply, which is never gated; // dimming it would read as "this mode is unavailable". CHECK(modeSegmentEnabled(/*isActiveSegment=*/true, /*transportRunning=*/true)); } int main() { testWideFooterAllPlaced(); testOffsetFooterAnchorsLeft(); testTailSuppressedWhenTight(); testOnlyToggleSurvives(); testTailTakesCountSlotWhenCountDrops(); testDegenerateFooterEmpty(); testTooNarrowForToggle(); testCountHiddenByZeroWidth(); testHitToggleAndTail(); testHitEdgesAndOutside(); testSuppressedClaimsNothing(); testResizeSweepNoOverlap(); testModeSegmentsAreLiveWithTheTransportStopped(); testInactiveSegmentGoesDeadWhileTheTransportRuns(); testActiveSegmentStaysLiveWhileTheTransportRuns(); if (g_fail == 0) std::printf("footer_bar: all tests passed\n"); else std::printf("footer_bar: %d CHECK(s) FAILED\n", g_fail); return g_fail == 0 ? 0 : 1; }