refactor(capture): expose offline tail as docked-panel toggle

Replace the ...-with-tail variant actions with a pure tail_control module +
a docked-panel footer toggle (Off/Auto/Manual) read by CAPTURE_ITEM/TRACK.
Default None (byte-identical); Manual fixed 2s, in-memory session lifetime.
Tail mechanism unchanged; retired the variant ids.
This commit is contained in:
2026-07-23 17:32:19 -04:00
parent 2d225258b4
commit 9a9339b90b
11 changed files with 363 additions and 90 deletions
+98 -5
View File
@@ -42,6 +42,7 @@
#include "mode_switch.h"
#include "peaks.h"
#include "persist.h"
#include "tail_control.h" // TailSetting, cycleTailMode, tailToggleLabel (pure)
#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);
@@ -134,6 +135,20 @@ const LICE_pixel kColSegBorder = LICE_RGBA(70, 70, 76, 255); // segment di
const COLORREF kRgbSegText = RGB(170, 170, 176); // inactive label
const COLORREF kRgbSegActiveText = RGB(220, 235, 228); // active label
// --- Tail-mode footer (T1 exposure) -------------------------------------------
// A fixed-height strip at the BOTTOM of the client area holding the tail-mode
// toggle ("Tail: Off / Auto / Manual"). Clicking anywhere in it cycles the mode
// (None -> Auto -> Manual -> None). Display/settings only: it mutates the panel's
// in-memory tail setting the plain capture actions read — NEVER the project/bank/
// arrange. The cycle/label logic is the pure tail_control module; only the draw +
// click routing is here. The grid viewport is shortened by this strip's height so
// cells never draw under it.
constexpr int kFooterHeight = 26; // px; fixed strip at the bottom
const LICE_pixel kColFooterBg = LICE_RGBA(20, 20, 22, 255); // footer strip fill
const LICE_pixel kColFooterBorder = LICE_RGBA(70, 70, 76, 255); // top divider
const COLORREF kRgbFooterText = RGB(190, 205, 198); // toggle label
// --- Panel state --------------------------------------------------------------
// A computed thumbnail: the per-channel envelope at a known width. Held in the
@@ -173,6 +188,12 @@ struct PanelState {
// clear the selection rather than risk indices pointing past the new count.
int selItemCount = 0;
// --- Tail-mode toggle (T1 exposure) ---------------------------------------
// The current tail setting the plain capture actions read (bankPanelTailSetting).
// Default None (exact bounds). In-memory only — resets on panel teardown; project
// persistence is a noted follow-on. Mutated ONLY by a click in the footer strip.
TailSetting tail;
// --- Audition preview (Wave B) --------------------------------------------
//
// The stock preview register we hand to PlayPreview/StopPreview. Its cs/mutex
@@ -424,6 +445,45 @@ void drawModeSwitch(LICE_IBitmap* bmp, int w) {
}
}
// The tail-toggle footer rect for a client of width `w` and height `h`: the
// full-width strip of fixed height pinned to the BOTTOM. A RECT (not HeaderRect)
// since the whole strip is one hit target — a click anywhere in it cycles the mode.
// Shared by paint and click routing so both agree on the band. Degenerate (empty)
// when the client is too short to host it above the header.
RECT panelFooter(int w, int h) {
RECT rc{};
rc.left = 0;
rc.right = w;
rc.top = h - kFooterHeight;
rc.bottom = h;
// Clamp so the footer never rides up into (or above) the header band on a very
// short panel — it collapses to empty rather than overlapping the mode switch.
if (rc.top < kHeaderHeight) rc.top = rc.bottom; // empty: top == bottom
return rc;
}
// Draws the tail-mode toggle into the footer strip: a filled band, a top divider,
// and the current mode's label ("Tail: Off / Auto / Manual") from the pure
// tail_control module. READ-ONLY: reads g_panel.tail; the click handler mutates it.
void drawTailFooter(LICE_IBitmap* bmp, int w, int h) {
const RECT f = panelFooter(w, h);
if (f.top >= f.bottom) return; // no room — skip (short panel)
LICE_FillRect(bmp, f.left, f.top, w, kFooterHeight, kColFooterBg, 1.0f, 0);
// Top divider so the strip reads as distinct from the grid above it.
LICE_Line(bmp, f.left, f.top, f.right, f.top, kColFooterBorder, 1.0f, 0, false);
HDC dc = bmp->getDC();
if (!dc) return;
const std::string label = tailToggleLabel(g_panel.tail);
RECT rc = f;
rc.left += 8; // small left pad so the label is not flush against the edge
SetTextColor(dc, kRgbFooterText);
SetBkMode(dc, TRANSPARENT);
DrawText(dc, label.c_str(), -1, &rc,
DT_LEFT | 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
@@ -469,11 +529,15 @@ void paintPanel(HWND hwnd, HDC hdc) {
// 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;
// Cells must not draw under the footer strip: the visible grid stops at the
// footer top (or the client bottom when the panel is too short for a footer).
const RECT footer = panelFooter(w, h);
const int gridBottom = footer.top < footer.bottom ? footer.top : h;
for (std::size_t i = 0; i < rects.size(); ++i) {
const CellRect& rect = rects[i];
// Skip cells entirely below the viewport (Wave A has no scroll; this
// just avoids computing thumbnails that cannot be seen).
if (rect.y >= h) continue;
// Skip cells entirely below the visible grid area (Wave A has no scroll;
// this just avoids computing thumbnails that cannot be seen).
if (rect.y >= gridBottom) continue;
const int idx = static_cast<int>(i);
const bool selected = g_panel.selection.contains(idx);
const bool focused = g_panel.selection.focus == idx;
@@ -482,9 +546,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.
// The mode switch and tail footer draw LAST so their bands overlay the top/bottom
// of the grid / empty-state area regardless of which branch ran above.
drawModeSwitch(&bmp, w);
drawTailFooter(&bmp, w, h);
BitBlt(hdc, 0, 0, w, h, bmp.getDC(), 0, 0, SRCCOPY);
}
@@ -675,6 +740,23 @@ void handleClick(int x, int y) {
}
}
// Tail-mode footer: a click anywhere in the bottom strip cycles the tail mode
// (None -> Auto -> Manual -> None) and repaints. Settings-only — it mutates the
// panel's in-memory tail setting the capture actions read, and NOTHING in the
// project/bank/arrange. Checked before the grid so a footer click never selects.
{
RECT cr{};
GetClientRect(g_panel.hwnd, &cr);
const int w = cr.right - cr.left;
const int h = cr.bottom - cr.top;
const RECT f = panelFooter(w, h);
if (f.top < f.bottom && x >= f.left && x < f.right && y >= f.top && y < f.bottom) {
g_panel.tail.mode = cycleTailMode(g_panel.tail.mode);
invalidatePanel();
return; // footer 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();
@@ -906,6 +988,17 @@ void bankPanelRefresh() {
InvalidateRect(g_panel.hwnd, nullptr, FALSE);
}
TailSetting bankPanelTailSetting() {
// In-memory for the extension's lifetime (g_panel is static): the toggle's mode
// survives panel open/close and bank changes, and resets to the default None only
// on extension unload. Persistence across project reload is a noted follow-on.
// manualMs is clamped here so a caller always receives a within-cap length, even
// if a future fine-adjust UI stored an over-cap value.
TailSetting s = g_panel.tail;
s.manualMs = clampManualMs(s.manualMs);
return s;
}
void bankPanelShutdown() {
closePanel(); // stops audition + destroys the window
deinitPreview(); // destroy the preview lock (after the last stop)