From 67524206fbc5b74687a87cf12569a746f9a61d27 Mon Sep 17 00:00:00 2001 From: daniel-c-harvey Date: Wed, 5 Aug 2026 16:21:17 -0400 Subject: [PATCH] 2nd Pass Render --- CMakeLists.txt | 4 +- src/app/CMakeLists.txt | 1 + src/core/capture/render_settings.cpp | 4 + src/core/capture/render_settings.h | 4 + src/core/capture/tail_control.cpp | 14 +- src/core/capture/tail_control.h | 9 +- src/core/json/json.cpp | 6 + src/core/json/json.h | 1 + src/core/ui/footer_bar.cpp | 19 ++- src/core/ui/footer_bar.h | 11 +- src/shell/capture/capture.cpp | 6 +- src/shell/capture/capture.h | 6 +- src/shell/capture/capture_orchestrator.cpp | 1 + src/shell/capture/render_in_place.cpp | 1 + src/shell/instrument/editor_paint_chrome.cpp | 2 +- src/shell/panel/footer_input.cpp | 134 +++++++++++++++++++ src/shell/panel/footer_input.h | 9 ++ src/shell/panel/panel_drag.cpp | 4 +- src/shell/panel/panel_input.cpp | 71 +--------- src/shell/panel/panel_render.cpp | 13 ++ src/shell/panel/panel_state.h | 1 + tests/test_footer_bar.cpp | 14 +- tests/test_tail_control.cpp | 5 +- 23 files changed, 251 insertions(+), 89 deletions(-) create mode 100644 src/shell/panel/footer_input.cpp create mode 100644 src/shell/panel/footer_input.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 3c5a95b..6a477b7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -21,9 +21,9 @@ cmake_minimum_required(VERSION 3.19) # invariant (reconstruct-from-components inside app_version.cpp) via a permanent synthetic # "0.9.01" fixture there that must NEVER be bumped on release. It cannot see this line # becoming a CMake derivation — that is this comment's job. -set(REASAMPLER_VERSION "1.6.0") +set(REASAMPLER_VERSION "1.7.3") -project(reaper_reasampler VERSION 1.6.0 LANGUAGES CXX) +project(reaper_reasampler VERSION 1.7.3 LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) diff --git a/src/app/CMakeLists.txt b/src/app/CMakeLists.txt index 0724ac9..d3c84f4 100644 --- a/src/app/CMakeLists.txt +++ b/src/app/CMakeLists.txt @@ -27,6 +27,7 @@ add_library(reaper_reasampler MODULE ${REASAMPLER_SRC_DIR}/shell/panel/panel_audition.cpp ${REASAMPLER_SRC_DIR}/shell/panel/panel_bank_ops.cpp ${REASAMPLER_SRC_DIR}/shell/panel/panel_drag.cpp + ${REASAMPLER_SRC_DIR}/shell/panel/footer_input.cpp ${REASAMPLER_SRC_DIR}/shell/panel/panel_input.cpp ${REASAMPLER_SRC_DIR}/shell/panel/panel_layout.cpp ${REASAMPLER_SRC_DIR}/shell/panel/panel_render.cpp diff --git a/src/core/capture/render_settings.cpp b/src/core/capture/render_settings.cpp index 1d72782..5fb690d 100644 --- a/src/core/capture/render_settings.cpp +++ b/src/core/capture/render_settings.cpp @@ -100,6 +100,10 @@ RenderSettingsChoice renderSettingsFor(SourceMode mode, double /*wetDry*/) { return c; } +void applySecondPassRenderSettings(const bool secondPass, RenderSettingsChoice& choice) { + if (secondPass) { choice.settings |= kSecondPassRender; } +} + const char* renderSourceLabel(SourceMode mode) { switch (mode) { // MasterMix and TimeSelection share this label because they ARE the same diff --git a/src/core/capture/render_settings.h b/src/core/capture/render_settings.h index 3fdb844..e15e1ac 100644 --- a/src/core/capture/render_settings.h +++ b/src/core/capture/render_settings.h @@ -20,6 +20,7 @@ inline constexpr int kRenderMasterMix = 0; // (&(1|2))==0, no sour inline constexpr int kRenderSelItems = 32; // &32 selected media items inline constexpr int kRenderSelItemsViaMaster = 64; // &64 selected media items via master inline constexpr int kRenderSelTracksViaMaster = 128; // &128 selected tracks via master +inline constexpr int kSecondPassRender = 2048; // &2048 2nd Pass Render inline constexpr int kRenderRazorEdits = 4096; // &4096 render razor edits // kRenderPreFaderStems (&8192) is deliberately NOT used — REAPER offline render // has no true pre-FX "dry" bit. FX scoping is done by the FX-bypass-around-render @@ -113,6 +114,9 @@ struct RenderSettingsChoice { // SelectedItems -> &32|single-file; RazorArea -> &4096|single-file. RenderSettingsChoice renderSettingsFor(SourceMode mode, double wetDry); +// Apply the 2nd Pass Render setting to the RENDER_SETTINGS block +void applySecondPassRenderSettings(const bool secondPass, RenderSettingsChoice& choice); + // The render source a mode drives, in words. Exists for the offline backend's // bounds refusal: the two ways a render can miss its window — a source that // derives its own bounds (selected items, razor edits) versus a time-bounded diff --git a/src/core/capture/tail_control.cpp b/src/core/capture/tail_control.cpp index 1c5563a..9cd057b 100644 --- a/src/core/capture/tail_control.cpp +++ b/src/core/capture/tail_control.cpp @@ -18,6 +18,10 @@ TailMode cycleTailMode(TailMode current) { return TailMode::None; // unreachable for a valid enum; fail to the safe default } +bool toggleSecondPass(bool current) { + return !current; // wow +} + double clampManualMs(double manualMs) { return std::clamp(manualMs, 0.0, kMaxTailMs); } @@ -77,6 +81,7 @@ std::string serializeTailSetting(const TailSetting& setting) { json::Writer w(out); w.keyRaw("mode", json::numToStr(modeToInt(setting.mode))); w.keyRaw("manualMs", json::numToStr(setting.manualMs)); + w.keyRaw("secondPass", json::boolToStr(setting.secondPass)); } // Writer closes the object here (see bank_model's NRVO note) return out; } @@ -87,7 +92,8 @@ std::optional deserializeTailSetting(const std::string& blob) { int modeInt = 0; double ms = 0.0; - bool haveMode = false, haveMs = false; + bool secondPass = false; + bool haveMode = false, haveMs = false, haveSecondPass = false; r.skipWs(); if (!r.consume('}')) { do { @@ -99,13 +105,16 @@ std::optional deserializeTailSetting(const std::string& blob) { } else if (key == "manualMs") { if (!r.parseDouble(ms)) return std::nullopt; haveMs = true; + } else if (key == "secondPass") { + if (!r.parseBool(secondPass)) return std::nullopt; + haveSecondPass = true; } else { if (!r.skipValue()) return std::nullopt; // forward-compat } } while (r.consume(',')); if (!r.consume('}')) return std::nullopt; } - if (!haveMode || !haveMs) return std::nullopt; // absent key -> malformed -> default + if (!haveMode || !haveMs || !haveSecondPass) return std::nullopt; // absent key -> malformed -> default const std::optional mode = modeFromInt(modeInt); if (!mode) return std::nullopt; @@ -113,6 +122,7 @@ std::optional deserializeTailSetting(const std::string& blob) { TailSetting out; out.mode = *mode; out.manualMs = ms; + out.secondPass = secondPass; return out; } diff --git a/src/core/capture/tail_control.h b/src/core/capture/tail_control.h index fba7687..f22458c 100644 --- a/src/core/capture/tail_control.h +++ b/src/core/capture/tail_control.h @@ -21,14 +21,19 @@ inline constexpr double kManualStepMs = 250.0; // The panel's current tail setting: mode + the length used only when Manual. // Default None so a capture with no explicit choice stays exact-bounds. // `manualMs` is clamped to kMaxTailMs before it ever reaches a CaptureRequest. +// 2nd-Pass Render optional, off by default struct TailSetting { - TailMode mode = TailMode::None; - double manualMs = kDefaultManualTailMs; + TailMode mode = TailMode::None; + double manualMs = kDefaultManualTailMs; + bool secondPass = false; }; // Cycles the tail mode: None -> Auto -> Manual -> None. TailMode cycleTailMode(TailMode current); +// Toggles the 2nd-Pass Render +bool toggleSecondPass(bool current); + // The effective manual length a Manual capture uses: clamped to [0, kMaxTailMs]. // Exposed so the panel can show the clamped value. Meaningful only for Manual. double clampManualMs(double manualMs); diff --git a/src/core/json/json.cpp b/src/core/json/json.cpp index 0aadb59..33686c0 100644 --- a/src/core/json/json.cpp +++ b/src/core/json/json.cpp @@ -54,6 +54,12 @@ std::string numToStr(int v) { return buf; } +std::string boolToStr(bool v) { + char buf[6]; + std::snprintf(buf, sizeof(buf), "%s", v ? "true" : "false"); + return buf; +} + void writeStringArray(std::string& out, const std::vector& v) { out += '['; for (std::size_t i = 0; i < v.size(); ++i) { diff --git a/src/core/json/json.h b/src/core/json/json.h index 1b4c869..9698190 100644 --- a/src/core/json/json.h +++ b/src/core/json/json.h @@ -34,6 +34,7 @@ void writeEscaped(std::string& out, const std::string& s); std::string numToStr(double v); std::string numToStr(std::int64_t v); std::string numToStr(int v); +std::string boolToStr(bool v); // Flat homogeneous arrays: ["a","b"] / [1,2]. Empty vector -> "[]". void writeStringArray(std::string& out, const std::vector& v); diff --git a/src/core/ui/footer_bar.cpp b/src/core/ui/footer_bar.cpp index e945ef7..a424b2b 100644 --- a/src/core/ui/footer_bar.cpp +++ b/src/core/ui/footer_bar.cpp @@ -2,6 +2,8 @@ #include "core/ui/footer_bar.h" +#include + namespace reasampler::ui { namespace { @@ -50,8 +52,18 @@ FooterBarLayout computeFooterBar(const FooterRect& footer, const FooterBarSpec& } // Tail button. - if (fitsLeftOf(cursorX, spec.tailWidth, rightBound)) + if (fitsLeftOf(cursorX, spec.tailWidth, rightBound)) { out.tail = FooterBarRect{cursorX, top, spec.tailWidth, boxH}; + cursorX += spec.tailWidth + spec.gap; + } + // 2nd Pass Render button. + if (fitsLeftOf(cursorX, spec.secondPassWidth, rightBound)) { + out.secondPass = FooterBarRect{cursorX,top, spec.secondPassWidth, boxH}; + // no need to increment the cursor for final element + } + + // Package the whole footer bounds into the layout object + out.footer = FooterRect{footer}; return out; } @@ -61,7 +73,10 @@ FooterHit hitTestFooterBar(int px, int py, const FooterBarLayout& layout) { // count label is a passive readout — never a hit target. if (pointIn(px, py, layout.toggle)) return FooterHit::Toggle; if (pointIn(px, py, layout.tail)) return FooterHit::Tail; - return FooterHit::None; + if (pointIn(px, py, layout.secondPass)) return FooterHit::SecondPass; + // Finally test if it's within the footer at all + if (pointIn(px, py, layout.footer)) return FooterHit::Invalid; + return FooterHit::Outside; } bool modeSegmentEnabled(bool isActiveSegment, bool transportRunning, bool routable) { diff --git a/src/core/ui/footer_bar.h b/src/core/ui/footer_bar.h index 9b2c562..7142c5b 100644 --- a/src/core/ui/footer_bar.h +++ b/src/core/ui/footer_bar.h @@ -25,18 +25,24 @@ using FooterBarRect = Rect; // left of the reserved right margin; placement is greedy left-to-right (toggle survives longest, // Tail drops first on a very narrow footer). struct FooterBarLayout { + FooterRect footer; FooterBarRect toggle; FooterBarRect count; FooterBarRect tail; + FooterBarRect secondPass; bool operator==(const FooterBarLayout& o) const { - return toggle == o.toggle && count == o.count && tail == o.tail; + return footer == o.footer && + toggle == o.toggle && + count == o.count && + tail == o.tail && + secondPass == o.secondPass; } }; // Which footer LEFT-group affordance a point landed on. Prune is hit-tested separately via // hitTestPruneButton. -enum class FooterHit { None, Toggle, Tail }; +enum class FooterHit { Invalid, Toggle, Tail, SecondPass, Outside }; // Layout inputs, in pixels; defaults are the bank_panel footer metrics. // * rightReserve — pixels reserved at the footer's right for the prune button + version @@ -45,6 +51,7 @@ struct FooterBarSpec { int toggleWidth = 132; int countWidth = 64; int tailWidth = 132; + int secondPassWidth = 96; int gap = 6; int leftPad = 8; int verticalInset = 4; diff --git a/src/shell/capture/capture.cpp b/src/shell/capture/capture.cpp index 1bd3993..8c6e892 100644 --- a/src/shell/capture/capture.cpp +++ b/src/shell/capture/capture.cpp @@ -367,7 +367,7 @@ CaptureResult OfflineRenderBackend::capture(const CaptureRequest& request) { // SourceMode::Realtime is refused here — that's the realtime backend's job — // so the offline path never silently renders the wrong thing. - const RenderSettingsChoice choice = + RenderSettingsChoice choice = renderSettingsFor(request.sourceMode, request.wetDry); if (!choice.supported) { result.status = CaptureStatus::UnsupportedMode; @@ -487,8 +487,12 @@ CaptureResult OfflineRenderBackend::capture(const CaptureRequest& request) { static_cast(tail.tailFlag), true); GetSetProjectInfo(proj, "RENDER_TAILMS", tail.tailMs, true); + // 2nd Pass Render + applySecondPassRenderSettings(request.secondPass, choice); + // Source-selection bits for this mode (SDK header ~3041), all wet-only: // master mix = 0; tracks = &128; items = &32|single-file; razor = &4096|single-file. + // Also is the door to control the 2nd Pass Render setting. GetSetProjectInfo(proj, "RENDER_SETTINGS", static_cast(choice.settings), true); diff --git a/src/shell/capture/capture.h b/src/shell/capture/capture.h index dc640f1..14666a2 100644 --- a/src/shell/capture/capture.h +++ b/src/shell/capture/capture.h @@ -59,11 +59,13 @@ struct CaptureRequest { // bounds, no added silence — the only mode valid for null-test/verify captures. // tailMs applies only to Manual (clamped to 8s by the pure mapping); Auto uses // the 8s cap + -72 dB trim internally, None ignores it. - TailMode tailMode = TailMode::None; - double tailMs = 0.0; + TailMode tailMode = TailMode::None; + double tailMs = 0.0; + bool secondPass = false; // 0 sampleRate => follow project rate. int sampleRate = 0; + // What the RENDER is asked for (RENDER_CHANNELS / the realtime record mode), not // what the capture lands as: a dual-mono render is collapsed to 1 channel after // the fact, and the Sample's count comes from the produced file. diff --git a/src/shell/capture/capture_orchestrator.cpp b/src/shell/capture/capture_orchestrator.cpp index a59ecdd..d0ef312 100644 --- a/src/shell/capture/capture_orchestrator.cpp +++ b/src/shell/capture/capture_orchestrator.cpp @@ -257,6 +257,7 @@ CaptureResult captureAndIndexOne(ReaSamplerSession& session, req.wetDry = 1.0; // wet post the FX left enabled by the scope req.tailMode = tail.mode; // None / Auto / Manual, from the panel toggle req.tailMs = tail.manualMs; // Manual-only (clamped); ignored for None/Auto + req.secondPass = tail.secondPass; // Whether 2nd-Pass Render should be performed req.sampleRate = 0; // follow project rate req.channelCount = 2; req.bitDepth = WavBitDepth::Float32; // deterministic, no dither diff --git a/src/shell/capture/render_in_place.cpp b/src/shell/capture/render_in_place.cpp index e0434aa..247e908 100644 --- a/src/shell/capture/render_in_place.cpp +++ b/src/shell/capture/render_in_place.cpp @@ -101,6 +101,7 @@ void RunRenderTrackInPlace(ReaSamplerSession& session) { req.wetDry = 1.0; req.tailMode = tail.mode; req.tailMs = tail.manualMs; + req.secondPass = tail.secondPass; req.sampleRate = 0; // follow project rate req.channelCount = 2; req.bitDepth = WavBitDepth::Float32; diff --git a/src/shell/instrument/editor_paint_chrome.cpp b/src/shell/instrument/editor_paint_chrome.cpp index 3ef2df0..ac3cc52 100644 --- a/src/shell/instrument/editor_paint_chrome.cpp +++ b/src/shell/instrument/editor_paint_chrome.cpp @@ -102,7 +102,7 @@ void ReaSamplerEditor::paintChrome(LICE_IBitmap* bmp, const FaceLayout& fl, bool // Toolbar: product name + live readout. The beta channel gets no distinct accent; the // channel-derived vstPluginName is the only beta-vs-stable signal. - std::string title = version::vstPluginName(); + std::string title = version::vstPluginName() + " v" + version::appVersion(); if (processor_ && processor_->bridge().isConnected()) { // The instance's own loaded state outranks bank availability (the bank is a browser // source, not the instrument's identity) — a self-contained instance names its sound diff --git a/src/shell/panel/footer_input.cpp b/src/shell/panel/footer_input.cpp new file mode 100644 index 0000000..0a58540 --- /dev/null +++ b/src/shell/panel/footer_input.cpp @@ -0,0 +1,134 @@ +#include "shell/panel/footer_input.h" +#include "shell/panel/panel_state.h" +#include "core/view/view_mode_model.h" +#include "shell/actions/bank_actions.h" +#include "shell/persist/session.h" // ReaSamplerSession — view/tail reads + mutation + +#define REAPERAPI_MINIMAL +#define REAPERAPI_WANT_EnumProjects +#define REAPERAPI_WANT_MarkProjectDirty +#define REAPERAPI_WANT_Main_OnCommand +#include "reaper_plugin_functions.h" + +namespace reasampler::panel { + + namespace { + // Commits the current tail setting to ext state and marks the active project dirty so the + // change travels inside the .rpp on Ctrl+S — closes the gap where toggle/scroll would dirty + // the project but never write the new value. No-ops cleanly on an unsaved project. + void markTailDirty() + { + if (g_panel.session) g_panel.session->saveToActiveProject(); + ReaProject* proj = EnumProjects(-1, nullptr, 0); + if (proj) MarkProjectDirty(proj); + } + + // click was confirmed on the toggle button; + // detect which mode segment was hit (if any) + bool clickToggle(int x, int y, ui::FooterBarRect toggleRect) + { + const int seg = hitTestSegment(x, y, toggleRect, modeCount()); + if (seg >= 0) { + const ViewModeModel& view = g_panel.session->view(); + const std::vector& modes = view.modes().all(); + + if (seg < static_cast(modes.size())) { + const std::string& id = modes[static_cast(seg)].id; + const bool isActive = id == view.activeModeId(); + const int cmd = modeActivateCommandId(id); + if (modeSegmentEnabled(isActive, g_panel.modeSwitchBlocked, cmd != 0)) { + if (cmd != 0 && Main_OnCommand) Main_OnCommand(cmd, 0); + } + } + return true; + } + return false; + } + + // click was confirmed on the Tail button + void clickTail() + { + TailSetting& tail = g_panel.session->tail(); + tail.mode = cycleTailMode(tail.mode); + markTailDirty(); + invalidatePanel(); + } + + void clickSecondPass() + { + TailSetting& tail = g_panel.session->tail(); + tail.secondPass = capture::toggleSecondPass(tail.secondPass); + markTailDirty(); + invalidatePanel(); + } + + // click was confirmed on the prune button + void clickPrune() + { + // Fires through its registered command id (not the session directly) so the panel + // affordance and the bindable action share the one guarded dry-run/confirm/delete + // path in doBankPruneFolder. + const int cmd = bankPruneCommandId(); + if (cmd != 0) Main_OnCommand(cmd, 0); + } + } + + + + // Returns: true if the click was inside the footer bar, + // false if the click was outside the footer bar. + bool handleFooterClick(int x, int y, int w, int h) + { + + const ButtonRect pb = pruneButtonRectFor(w, h); + if (hitTestPruneButton(x, y, pb)) { + clickPrune(); + return true; + } + + const FooterBarLayout fb = footerBarLayoutFor(w, h); + if (g_panel.session) { + FooterHit clicked = hitTestFooterBar(x, y, fb); + + // The short-circuit cases + if (clicked == FooterHit::Outside) { return false; } + if (clicked == FooterHit::Invalid) { return true; } + + // Check actual clickable elements: + + // Cycles None -> Auto -> Manual -> None + if (clicked == FooterHit::Tail) { clickTail(); } + + // Detect which segment was selected and activate that mode + if (clicked == FooterHit::Toggle) { clickToggle(x, y, fb.toggle); } + + // Toggle 2nd Pass Render + if (clicked == FooterHit::SecondPass) { clickSecondPass(); } + + return true; + } + + // If we reach this point, no valid click target inside the footer was hit. + return false; + } + + bool handleFooterWheel(int x, int y, int delta) { + if (!g_panel.session) return false; + + TailSetting& tail = g_panel.session->tail(); + if (tail.mode != TailMode::Manual) return false; // fine-adjust is Manual-only + + // One notch is WHEEL_DELTA (120); accumulate whole notches so a high-res trackpad + // that sends fractional deltas still steps predictably. Sign carries direction. + const int notches = delta / 120; + if (notches == 0) return false; // sub-notch movement — nothing to apply yet + + const double before = tail.manualMs; + tail.manualMs = adjustManualMs(tail.manualMs, notches, kManualStepMs); + if (tail.manualMs == before) return true; // already at a bound — consumed, no change + + markTailDirty(); + invalidatePanel(); // label shows the new length live + return true; + } +} diff --git a/src/shell/panel/footer_input.h b/src/shell/panel/footer_input.h new file mode 100644 index 0000000..63fb98d --- /dev/null +++ b/src/shell/panel/footer_input.h @@ -0,0 +1,9 @@ +#pragma once + +namespace reasampler::panel { + + bool handleFooterClick(int x, int y, int w, int h); + + bool handleFooterWheel(int x, int y, int delta); + +} diff --git a/src/shell/panel/panel_drag.cpp b/src/shell/panel/panel_drag.cpp index d4a5a16..001143b 100644 --- a/src/shell/panel/panel_drag.cpp +++ b/src/shell/panel/panel_drag.cpp @@ -274,7 +274,9 @@ Hover resolveHover(int x, int y) { const int seg = footerToggleSegmentHit(x, y, w, h); if (seg >= 0) return Hover{HoverKind::ModeSegment, seg}; const FooterBarLayout fb = footerBarLayoutFor(w, h); - if (hitTestFooterBar(x, y, fb) == FooterHit::Tail) return Hover{HoverKind::TailButton, -1}; + const FooterHit footerHit = hitTestFooterBar(x, y, fb); + if (footerHit == FooterHit::Tail) return Hover{HoverKind::TailButton, -1}; + if (footerHit == FooterHit::SecondPass) return Hover{HoverKind::SecondPassButton, -1}; const ButtonRect pb = pruneButtonRectFor(w, h); if (hitTestPruneButton(x, y, pb)) return Hover{HoverKind::PruneButton, -1}; } diff --git a/src/shell/panel/panel_input.cpp b/src/shell/panel/panel_input.cpp index 3a06328..9cdf01c 100644 --- a/src/shell/panel/panel_input.cpp +++ b/src/shell/panel/panel_input.cpp @@ -13,6 +13,7 @@ #include "shell/panel/panel_state.h" #include "shell/panel/panel_input.h" +#include "shell/panel/footer_input.h" #include "shell/actions/bank_actions.h" // bankPruneCommandId — the footer Prune dispatch #include "shell/persist/session.h" // ReaSamplerSession — view/tail reads + mutation @@ -61,15 +62,6 @@ int modeActivateCommandId(const std::string& modeId) { namespace { -// Commits the current tail setting to ext state and marks the active project dirty so the -// change travels inside the .rpp on Ctrl+S — closes the gap where toggle/scroll would dirty -// the project but never write the new value. No-ops cleanly on an unsaved project. -void markTailDirty() { - if (g_panel.session) g_panel.session->saveToActiveProject(); - ReaProject* proj = EnumProjects(-1, nullptr, 0); - if (proj) MarkProjectDirty(proj); -} - // Routes a click in a toolbar to the hit button's action via Main_OnCommand. Returns true // iff the click was inside the bar band, so the caller stops before grid handling. bool handleToolbarClick(int x, int y, const ActionBarRect& bar, @@ -332,46 +324,9 @@ void handleClick(int x, int y) { // the More button) so a click there is inert chrome, never a fall-through to the grid. if (y >= 0 && y < kTopToolbarHeight && x >= 0 && x < w) return; - // Footer: mode toggle (left) -> Tail button -> Prune (right). Checked before the bottom + // Footer: mode toggle (left) -> Tail button -> 2nd Pass Render -> Prune (right). Checked before the bottom // toolbar / grid so a footer click never selects a cell. - { - const int seg = footerToggleSegmentHit(x, y, w, h); - if (seg >= 0) { - const ViewModeModel& view = g_panel.session->view(); - const std::vector& modes = view.modes().all(); - if (seg < static_cast(modes.size())) { - const std::string& id = modes[static_cast(seg)].id; - const bool isActive = id == view.activeModeId(); - const int cmd = modeActivateCommandId(id); - // Disabled/dead rationale: core/ui/footer_bar.h. Claimed but inert, the - // same shape a disabled toolbar row takes — never falls through to the grid. - if (modeSegmentEnabled(isActive, g_panel.modeSwitchBlocked, cmd != 0)) { - if (cmd != 0 && Main_OnCommand) Main_OnCommand(cmd, 0); - } - } - return; - } - - const FooterBarLayout fb = footerBarLayoutFor(w, h); - if (g_panel.session && hitTestFooterBar(x, y, fb) == FooterHit::Tail) { - // Cycles None -> Auto -> Manual -> None; touches NOTHING in the bank/arrange. - TailSetting& tail = g_panel.session->tail(); - tail.mode = cycleTailMode(tail.mode); - markTailDirty(); - invalidatePanel(); - return; - } - - // Fires through its registered command id (not the session directly) so the panel - // affordance and the bindable action share the one guarded dry-run/confirm/delete - // path in doBankPruneFolder. - const ButtonRect pb = pruneButtonRectFor(w, h); - if (hitTestPruneButton(x, y, pb)) { - const int cmd = bankPruneCommandId(); - if (cmd != 0) Main_OnCommand(cmd, 0); - return; - } - } + if (handleFooterClick(x, y, w, h)) return; if (handleToolbarClick(x, y, bottomToolbarRect(w, h), bottomBarRows())) return; @@ -456,24 +411,10 @@ void handleClick(int x, int y) { // [0, kMaxTailMs]. Otherwise does nothing (returns false so the caller can let REAPER/the // docker handle the wheel normally). Returns true iff the wheel was consumed. bool handleWheel(int x, int y, int delta) { - if (!g_panel.session) return false; - if (!pointInFooter(x, y)) return false; + if (pointInFooter(x, y)) return handleFooterWheel(x, y, delta); - TailSetting& tail = g_panel.session->tail(); - if (tail.mode != TailMode::Manual) return false; // fine-adjust is Manual-only - - // One notch is WHEEL_DELTA (120); accumulate whole notches so a high-res trackpad - // that sends fractional deltas still steps predictably. Sign carries direction. - const int notches = delta / 120; - if (notches == 0) return false; // sub-notch movement — nothing to apply yet - - const double before = tail.manualMs; - tail.manualMs = adjustManualMs(tail.manualMs, notches, kManualStepMs); - if (tail.manualMs == before) return true; // already at a bound — consumed, no change - - markTailDirty(); - invalidatePanel(); // label shows the new length live - return true; + // Unhandled + return false; } namespace { diff --git a/src/shell/panel/panel_render.cpp b/src/shell/panel/panel_render.cpp index ada77d0..0f2ca20 100644 --- a/src/shell/panel/panel_render.cpp +++ b/src/shell/panel/panel_render.cpp @@ -165,6 +165,19 @@ void drawFooter(LICE_IBitmap* bmp, int w, int h) { drawButton(bmp, box, label.c_str(), state, /*warn=*/false); } + // 2nd Pass Render BUTTON + if (!fb.secondPass.empty()) { + const TailSetting tail = currentTail(); + const InteractionState state = tail.secondPass + ? InteractionState::Active + : hoverState(g_panel.hovered, HoverKind::SecondPassButton, -1); + const std::string label = "2nd Pass"; + const KitButtonBox box{ + KitBox{fb.secondPass.x, fb.secondPass.y, fb.secondPass.width, fb.secondPass.height} + }; + drawButton(bmp, box, label.c_str(), state, /*warn=*/false); + } + // Version/channel readout. appVersion() renders the configured version string on // stable and that string plus "-beta" on beta, so a beta panel self-identifies. kitText(bmp, KitBox{f.left, f.top, (f.right - f.left) - 8, f.bottom - f.top}, diff --git a/src/shell/panel/panel_state.h b/src/shell/panel/panel_state.h index 70bf02c..ed96f71 100644 --- a/src/shell/panel/panel_state.h +++ b/src/shell/panel/panel_state.h @@ -272,6 +272,7 @@ enum class HoverKind { CreateBank, Tab, // index = tab ordinal TailButton, + SecondPassButton, ModeSegment, // index = segment ordinal }; diff --git a/tests/test_footer_bar.cpp b/tests/test_footer_bar.cpp index 88717dd..8544e4a 100644 --- a/tests/test_footer_bar.cpp +++ b/tests/test_footer_bar.cpp @@ -174,9 +174,9 @@ static void testHitToggleAndTail() { 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); + L.count.y + L.count.height / 2, L) == FooterHit::Invalid); // 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); + CHECK(hitTestFooterBar(L.toggle.x + L.toggle.width, L.toggle.y, L) == FooterHit::Invalid); } // Half-open bounds: far edges excluded; points outside every box miss. @@ -184,12 +184,12 @@ 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 + CHECK(hitTestFooterBar(L.toggle.x - 1, L.toggle.y, L) == FooterHit::Invalid); + CHECK(hitTestFooterBar(L.tail.x + L.tail.width, L.tail.y, L) == FooterHit::Invalid); // right edge excl + CHECK(hitTestFooterBar(L.toggle.x, L.toggle.y - 1, L) == FooterHit::Invalid); // above + CHECK(hitTestFooterBar(L.toggle.x, L.toggle.y + L.toggle.height, L) == FooterHit::Invalid); // 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); + CHECK(hitTestFooterBar(f.x + f.width - 10, L.toggle.y, L) == FooterHit::Invalid); } // A suppressed box claims no point — a Tail hit-test where the Tail was dropped is None. diff --git a/tests/test_tail_control.cpp b/tests/test_tail_control.cpp index 20e6e27..bfe3b01 100644 --- a/tests/test_tail_control.cpp +++ b/tests/test_tail_control.cpp @@ -106,12 +106,13 @@ static void testDefaultSettingIsOff() { CHECK(s.mode == TailMode::None); CHECK(s.manualMs == kDefaultManualTailMs); CHECK(tailToggleLabel(s) == "Tail: Off"); + CHECK(s.secondPass == false); } // --- serialize/deserialize: per-project persistence round-trip ---------------- static bool settingsEqual(const TailSetting& a, const TailSetting& b) { - return a.mode == b.mode && a.manualMs == b.manualMs; + return a.mode == b.mode && a.manualMs == b.manualMs && a.secondPass == b.secondPass; } static void testRoundTripNoneDefault() { @@ -142,7 +143,7 @@ static void testSerializeByteIdentity() { // and re-serializing a round-tripped setting must be byte-identical — the blob // lives in the .rpp, so a byte shift would dirty every saved project. TailSetting s; // None + 2000.0 default - CHECK(serializeTailSetting(s) == "{\"mode\":0,\"manualMs\":2000}"); + CHECK(serializeTailSetting(s) == "{\"mode\":0,\"manualMs\":2000,\"secondPass\":false}"); TailSetting man; man.mode = TailMode::Manual; man.manualMs = 3141.592653589793; const std::string json = serializeTailSetting(man); auto back = deserializeTailSetting(json);