Merge Phase D5: segmented Design View mode switch in bank panel
This commit is contained in:
+16
-1
@@ -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")
|
||||
|
||||
|
||||
+120
-6
@@ -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<int>(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<Mode>& modes = view.modes().all();
|
||||
const int n = static_cast<int>(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<SegmentRect> 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<std::size_t>(i)];
|
||||
const Mode& mode = modes[static_cast<std::size_t>(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<CellRect> panelRects() {
|
||||
if (!g_panel.hwnd) return {};
|
||||
const BankIndex* bank = g_panel.session ? &g_panel.session->bank() : nullptr;
|
||||
@@ -362,7 +447,10 @@ std::vector<CellRect> panelRects() {
|
||||
GetClientRect(g_panel.hwnd, &cr);
|
||||
const int w = cr.right - cr.left;
|
||||
if (w <= 0) return {};
|
||||
return computeCellRects(static_cast<int>(bank->size()), w, kGrid);
|
||||
std::vector<CellRect> rects =
|
||||
computeCellRects(static_cast<int>(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<Sample>& samples = bank->all();
|
||||
const std::vector<CellRect> rects =
|
||||
std::vector<CellRect> rects =
|
||||
computeCellRects(static_cast<int>(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<Mode>& modes = g_panel.session->view().modes().all();
|
||||
if (seg < static_cast<int>(modes.size())) {
|
||||
applyMode(g_panel.session->view(), modes[static_cast<std::size_t>(seg)].id,
|
||||
nullptr);
|
||||
invalidatePanel(); // active-segment highlight + parked-track redraw
|
||||
}
|
||||
return; // header click consumed; do NOT fall through to grid selection
|
||||
}
|
||||
}
|
||||
|
||||
const std::vector<CellRect> rects = panelRects();
|
||||
const int hit = hitTestCell(x, y, rects);
|
||||
const int count = bankItemCount();
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
// mode_switch — pure implementation. See mode_switch.h. NO REAPER / SWELL / vendor.
|
||||
|
||||
#include "mode_switch.h"
|
||||
|
||||
#include <cstddef>
|
||||
|
||||
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<SegmentRect> computeSegmentRects(const HeaderRect& header,
|
||||
int segmentCount) {
|
||||
std::vector<SegmentRect> rects;
|
||||
if (segmentCount <= 0 || header.width <= 0) return rects;
|
||||
|
||||
rects.reserve(static_cast<std::size_t>(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
|
||||
@@ -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 <vector>
|
||||
|
||||
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<SegmentRect> 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
|
||||
@@ -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 <cstddef>
|
||||
#include <cstdio>
|
||||
#include <vector>
|
||||
|
||||
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<int>(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<std::size_t>(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;
|
||||
}
|
||||
Reference in New Issue
Block a user