Add D5 Design-View mode switch to bank panel
New pure mode_switch module (header-rect -> N equal segment rects + point hit-test, unit-tested) plus a segmented switch in the bank_panel header: one lit segment per registered mode, click activates via view::applyMode. Grid offset below the header.
This commit is contained in:
+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();
|
||||
|
||||
Reference in New Issue
Block a user