Make the background FX drain visible: a busy cursor over the panel and a footer readout that names its direction, driven off fxReclaimActivity

This commit is contained in:
2026-08-21 14:45:25 -04:00
parent da8401239a
commit 052fbb0d52
9 changed files with 182 additions and 6 deletions
+12
View File
@@ -62,6 +62,18 @@ live in `shell/bank_ops`, a sibling directory, not here.
- **No external UI framework.** iPlug2 / JUCE / VSTGUI are rejected (DS-1). LICE +
reused WDL pieces are the toolkit; reject any path that pulls in a new framework.
- **The FX-drain indicator asks `fxReclaimActivity()` and decides nothing itself.**
`panel_input` polls it once per tick into `g_panel.fxDrain`; the footer readout and the
`WM_SETCURSOR` busy cursor both read that cached state, so paint and cursor cannot
disagree within a tick. The transport gate, the park debounce and the open-undo-block
stand-off are all already folded into that answer, against the reclaim queue's own pinned
project — do not re-evaluate any of them here, and never drive this off `fxReclaimPending`
(true throughout the debounce, when only the opposite direction is eligible).
The indicator reaches only our own window: over REAPER's arrange or mixer the cursor is
REAPER's, and the SDK exposes nothing host-wide. A global `SetSystemCursor` or a message
hook would reach further and is rejected — do not reintroduce either. `IDC_APPSTARTING`
is Windows-only (SWELL ships no equivalent), so on mac/Linux the readout is the whole
indicator.
- **No hit-test geometry in untestable shell code.** Even when reusing a WDL piece
(e.g. a `vwnd` control for a long scroll list), layout/hit-test math stays in pure
CTest-covered modules — do not import a WDL control's retained-mode object model
+21
View File
@@ -20,6 +20,7 @@
#include "core/view/view_mode_model.h" // autoTagNewContent / NewItem / AutoTag
#include "shell/capture/item_read.h" // itemGuid / itemLaneName — shared item-read seam
#include "shell/capture/track_guid.h" // guidString — canonical track GUID key
#include "shell/view/fx_reclaim.h" // fxReclaimActivity — the drain-direction read
#include "shell/view/view.h" // transportBlocksModeSwitch
#include "shell/view/view_lanes.h" // mintManagedLanes
@@ -476,6 +477,17 @@ int translateAccel(MSG* msg, accelerator_register_t* /*ctx*/) {
accelerator_register_t g_accel{translateAccel, true, nullptr};
bool g_accelRegistered = false;
// The ONE map from the reclaimer's answer to the pure readout's vocabulary. Name-for-name
// so a divergence reads as one; the decision itself is entirely fxReclaimActivity's.
FxDrainState drainStateNow() {
switch (fxReclaimActivity()) {
case FxReclaimActivity::Online: return FxDrainState::Online;
case FxReclaimActivity::Offline: return FxDrainState::Offline;
case FxReclaimActivity::Idle: break;
}
return FxDrainState::Idle;
}
} // namespace
void registerAccel() {
@@ -530,6 +542,15 @@ void bankPanelRefresh() {
InvalidateRect(panel::g_panel.hwnd, nullptr, FALSE);
}
// The drain finishes without a callback, so its direction is polled here alongside the
// transport and repainted only on an edge.
const panel::FxDrainState drain = panel::drainStateNow();
if (drain != panel::g_panel.fxDrain) {
panel::g_panel.fxDrain = drain;
panel::refreshBusyCursor();
InvalidateRect(panel::g_panel.hwnd, nullptr, FALSE);
}
// The custom hover-delay tooltip is driven off this poll tick (no dedicated timer) — if
// a toolbar button has rested under the pointer past the delay, latch + repaint it.
panel::maybeShowTooltip();
+7 -5
View File
@@ -147,13 +147,15 @@ void drawFooter(LICE_IBitmap* bmp, int w, int h) {
}
}
// Per-mode member count, a compact dim readout beside the toggle. Passive text, not a control.
// The one passive readout beside the toggle: a moving FX drain, else the per-mode member
// count (footerReadout owns which). Passive text, not a control. The drain draws
// text/primary against the count's text/dim so a transient state reads as one.
if (!fb.count.empty()) {
const int members = activeModeMemberCount();
const std::string countLabel =
std::to_string(members) + (members == 1 ? " track" : " tracks");
const bool draining = g_panel.fxDrain != FxDrainState::Idle;
const std::string label = footerReadout(g_panel.fxDrain, activeModeMemberCount());
kitText(bmp, KitBox{fb.count.x, fb.count.y, fb.count.width, fb.count.height},
countLabel.c_str(), Font::Micro, Role::TextDim, Align::Center);
label.c_str(), Font::Micro,
draining ? Role::TextPrimary : Role::TextDim, Align::Center);
}
// Tail BUTTON — a real kit button with rest/hover states; its click cycles the tail
+13
View File
@@ -83,6 +83,7 @@ using ui::FooterBarLayout;
using ui::FooterBarSpec;
using ui::FooterHit;
using ui::FooterRect;
using ui::FxDrainState;
using ui::GridSpec;
using ui::InteractionState;
using ui::KitBox;
@@ -127,6 +128,7 @@ using ui::cueForDropClass;
using ui::cursorForGesture;
using ui::decideCardGesture;
using ui::decideDropClass;
using ui::footerReadout;
using ui::formatBarsBeats;
using ui::formatSecondsMs;
using ui::hitTestActionBar;
@@ -322,6 +324,12 @@ struct PanelState {
// within a tick.
bool modeSwitchBlocked = false;
// Which direction the background FX reclaimer is MOVING, polled on the same tick as
// modeSwitchBlocked. The footer readout and the busy cursor both read THIS rather than
// polling again, so the pixel the user saw and the cursor under their pointer cannot
// disagree within a tick.
FxDrainState fxDrain = FxDrainState::Idle;
BankPanelFullHeight fullHeight = BankPanelFullHeight::Split;
// The named bank the banks region shows — distinct from the active/capture-target
@@ -442,6 +450,11 @@ inline void invalidatePanel() {
// Cross-seam contract, grouped by owning TU; all plain free functions.
// panel_window.cpp — re-resolves the pointer's cursor after g_panel.fxDrain changed. The
// window answers WM_SETCURSOR from that state, but the OS only re-asks on pointer
// movement, so a drain that starts or ends under a still pointer needs this nudge.
void refreshBusyCursor();
// panel_bank_ops.cpp — book/bank accessors + the bank-CRUD verbs + menus.
BankBook* book();
const BankModel* indexForRegion(Region r);
+44
View File
@@ -94,8 +94,39 @@ void handleDropFiles(HDROP hDrop) {
if (!paths.empty()) ingestDroppedFiles(paths);
}
// IDC_APPSTARTING — arrow plus hourglass, "busy in the background, still usable" — is the
// exact reading for the FX reclaimer's drain, and IDC_WAIT is not: nothing here blocks.
// SWELL defines neither anywhere in vendor/WDL/WDL/swell, so on mac/Linux there is no busy
// cursor at all and the footer readout carries the state alone. The cursor reaches OUR
// windows only; REAPER's arrange and mixer answer their own WM_SETCURSOR and the SDK
// exposes nothing host-wide to ask for. A global SetSystemCursor or a message hook would
// reach further and is deliberately not done.
#ifdef _WIN32
HCURSOR busyCursor() { return LoadCursor(nullptr, IDC_APPSTARTING); }
#else
HCURSOR busyCursor() { return nullptr; }
#endif
// A pure read of the polled drain state at query time, never a latched mode — which is what
// makes it clear itself on every path the drain can end by. A live card drag outranks it:
// that cue tells the user what a release will DO, and is the more urgent read.
bool busyCursorApplies() {
return busyCursor() && g_panel.fxDrain != FxDrainState::Idle && !g_panel.dragArmed &&
!g_panel.dragging;
}
WDL_DLGRET dlgProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) {
switch (msg) {
case WM_SETCURSOR:
// Claiming the message (non-zero) is what stops the default arrow from being
// reapplied underneath us — on Win32 via DefDlgProc, on SWELL via the same
// return test in swell-wnd-generic.cpp. Falling through leaves every other
// cursor decision exactly where it was (panel_drag's per-move SetCursor).
if (busyCursorApplies()) {
SetCursor(busyCursor());
return 1;
}
return 0;
case WM_DROPFILES:
handleDropFiles(reinterpret_cast<HDROP>(wParam));
return 0;
@@ -146,6 +177,9 @@ WDL_DLGRET dlgProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) {
resetDragState();
g_panel.hovered = Hover{};
g_panel.tooltipShown = false;
// Nothing polls the drain while the panel is closed, so a stale non-Idle here
// would be the first thing a reopened panel painted.
g_panel.fxDrain = FxDrainState::Idle;
g_panel.hwnd = nullptr;
g_panel.open = false;
return 0;
@@ -205,6 +239,16 @@ void closePanel() {
} // namespace
void refreshBusyCursor() {
if (!g_panel.hwnd) return;
POINT pt{};
GetCursorPos(&pt); // SWELL's returns void, so its result is deliberately not tested
// Only when our own window is the one under the pointer: a menu or another window
// covering the panel owns the cursor there, and setting one from here would fight it.
if (WindowFromPoint(pt) != g_panel.hwnd) return;
SetCursor(busyCursorApplies() ? busyCursor() : LoadCursor(nullptr, IDC_ARROW));
}
} // namespace reasampler::panel
namespace reasampler {