f2cdf676f3
Fixes a hidden-parent solo replay that could silence the mix. Also closes the N-mode segment silent no-op, amends the invariant comment, trims view.cpp under 600 lines, hedges two SDK inferences, drops a dead null-check.
505 lines
19 KiB
C++
505 lines
19 KiB
C++
#pragma once
|
|
// panel_state — INTERNAL shared state + cross-seam contract of the docked bank panel.
|
|
// Included ONLY by the panel's own TUs; outside consumers use the per-seam public
|
|
// headers (panel_window.h / panel_input.h / panel_bank_ops.h / panel_layout.h).
|
|
// Cross-seam calls are plain free functions — direct call-through, no virtual dispatch
|
|
// (audition and per-mouse-move paths must stay direct calls).
|
|
//
|
|
// REFERENCE-INVALIDATION GUARDRAIL: a bank-structural mutation (create/delete/
|
|
// evacuate/activate/move) can reallocate the book's vector, so a BankModel& / Bank*
|
|
// must NEVER be cached across one. Every seam resolves fresh after any mutation and
|
|
// passes bank IDS (not references) into the model ops.
|
|
|
|
#include <cstdint>
|
|
#include <string>
|
|
#include <unordered_map>
|
|
#include <vector>
|
|
|
|
// 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).
|
|
#ifdef _WIN32
|
|
#include <windows.h>
|
|
#else
|
|
#include <pthread.h>
|
|
#endif
|
|
#include "wdltypes.h"
|
|
#include "swell/swell.h"
|
|
|
|
// REAPER API function pointers are declared per-TU; main.cpp owns the definitions.
|
|
#include "reaper_plugin.h"
|
|
|
|
#include "core/audio/peaks.h"
|
|
#include "core/capture/capture_paths.h"
|
|
#include "core/capture/render_settings.h"
|
|
#include "core/capture/tail_control.h"
|
|
#include "core/model/bank_book.h"
|
|
#include "core/model/bank_model.h"
|
|
#include "core/ui/action_bar.h"
|
|
#include "core/ui/bank_grid.h"
|
|
#include "core/ui/card_drag.h"
|
|
#include "core/ui/card_meta.h"
|
|
#include "core/ui/component_geometry.h"
|
|
#include "core/ui/drag_out.h"
|
|
#include "core/ui/footer_bar.h"
|
|
#include "core/ui/mode_enable.h"
|
|
#include "core/ui/overflow_menu.h"
|
|
#include "core/ui/prune_button.h"
|
|
#include "core/ui/tab_strip.h"
|
|
#include "core/ui/theme.h"
|
|
#include "core/ui/tooltip.h"
|
|
#include "core/version/app_version.h"
|
|
#include "core/view/guid_diff.h"
|
|
#include "core/view/lane_keys.h"
|
|
#include "core/view/mode_switch.h"
|
|
#include "core/wire/instrument_drop.h"
|
|
|
|
#include "shell/panel/panel_layout.h"
|
|
|
|
namespace reasampler {
|
|
class ReaSamplerSession;
|
|
}
|
|
|
|
namespace reasampler::panel {
|
|
|
|
// core/ui
|
|
using ui::ActionBarRect;
|
|
using ui::ActionBarSlot;
|
|
using ui::ActionBarSpec;
|
|
using ui::ActionCluster;
|
|
using ui::ButtonRect;
|
|
using ui::CardGesture;
|
|
using ui::CellRect;
|
|
using ui::ClusterSpec;
|
|
using ui::CursorCue;
|
|
using ui::DragGesture;
|
|
using ui::DragModifiers;
|
|
using ui::DragState;
|
|
using ui::DropRegion;
|
|
using ui::FooterBarLayout;
|
|
using ui::FooterBarSpec;
|
|
using ui::FooterHit;
|
|
using ui::FooterRect;
|
|
using ui::GridSpec;
|
|
using ui::InteractionState;
|
|
using ui::KitBox;
|
|
using ui::KitButtonBox;
|
|
using ui::KitColor;
|
|
using ui::MenuBarRect;
|
|
using ui::MenuButtonRect;
|
|
using ui::MenuButtonSpec;
|
|
using ui::MusicalLength;
|
|
using ui::NavKey;
|
|
using ui::PanelClientRect;
|
|
using ui::PruneButtonSpec;
|
|
using ui::ResolvedSample;
|
|
using ui::Role;
|
|
using ui::Selection;
|
|
using ui::SlotCellRect;
|
|
using ui::TabHit;
|
|
using ui::TabHitKind;
|
|
using ui::TabRect;
|
|
using ui::TabStripLayout;
|
|
using ui::TabStripRect;
|
|
using ui::TabStripSpec;
|
|
using ui::TagTarget;
|
|
using ui::ThumbnailKey;
|
|
using ui::TooltipBox;
|
|
using ui::TooltipSpec;
|
|
using ui::applyClick;
|
|
using ui::assemblePathList;
|
|
using ui::clampTabScroll;
|
|
using ui::columnsForWidth;
|
|
using ui::computeBarSlots;
|
|
using ui::computeFooterBar;
|
|
using ui::computeMenuButton;
|
|
using ui::computePruneButton;
|
|
using ui::computeSlotRects;
|
|
using ui::computeSlotRectsForDrop;
|
|
using ui::computeTabRects;
|
|
using ui::computeTabStripLayout;
|
|
using ui::computeTooltip;
|
|
using ui::cursorForGesture;
|
|
using ui::decideCardGesture;
|
|
using ui::decideGesture;
|
|
using ui::formatBarsBeats;
|
|
using ui::formatSecondsMs;
|
|
using ui::hitTestActionBar;
|
|
using ui::hitTestFooterBar;
|
|
using ui::hitTestMenuButton;
|
|
using ui::hitTestPruneButton;
|
|
using ui::hitTestSlot;
|
|
using ui::hitTestTabStrip;
|
|
using ui::menuButtonReserve;
|
|
using ui::modeSegmentEnabled;
|
|
using ui::navigate;
|
|
using ui::roleColor;
|
|
using ui::stripActionPrefix;
|
|
using ui::tagButtonEnabled;
|
|
using ui::thumbnailKeyString;
|
|
using ui::waveformColumnCount;
|
|
|
|
// core/view
|
|
using view::GuidBaseline;
|
|
using view::HeaderRect;
|
|
using view::SegmentRect;
|
|
using view::computeSegmentRects;
|
|
using view::hitTestSegment;
|
|
using view::isOnManualLane;
|
|
|
|
// core/capture
|
|
using capture::CaptureActionDef;
|
|
using capture::TailMode;
|
|
using capture::TailSetting;
|
|
using capture::adjustManualMs;
|
|
using capture::captureActionTable;
|
|
using capture::clampManualMs;
|
|
using capture::cycleTailMode;
|
|
using capture::kManualStepMs;
|
|
using capture::normalizeSlashes;
|
|
using capture::resolveBankFile;
|
|
using capture::tailToggleLabel;
|
|
|
|
// core/audio
|
|
using audio::Envelope;
|
|
using audio::computeEnvelope;
|
|
|
|
// core/model
|
|
using model::BankModel;
|
|
using model::Sample;
|
|
|
|
// core/version
|
|
using version::actionDisplayPrefix;
|
|
using version::appVersion;
|
|
using version::channelCommandId;
|
|
using version::dockIdent;
|
|
using version::dockTitle;
|
|
|
|
// core/wire
|
|
using wire::buildInstrumentDropPreset;
|
|
|
|
// All color comes from `theme` by role, drawn through the kit. Only pixel layout
|
|
// metrics live here, shared by layout/render/input/drag so draw and hit-test can
|
|
// never drift.
|
|
|
|
inline const GridSpec kGrid{/*cellWidth=*/140, /*cellHeight=*/84, /*gap=*/10};
|
|
|
|
// Footer: [Arrange|Design] toggle, per-mode count, Tail button, Prune button
|
|
// (footer_bar left group + prune_button right).
|
|
inline constexpr int kFooterHeight = 30;
|
|
|
|
// Two task-grouped toolbars drawn through action_bar: top = capture + placement,
|
|
// bottom = Design-View tag/switch verbs, directly above the footer.
|
|
inline constexpr int kTopToolbarHeight = 28;
|
|
inline constexpr int kBottomToolbarHeight = 28;
|
|
|
|
// Hover-delay tooltip timing, driven off the OnTimer poll (no dedicated timer). Kit
|
|
// font is proportional, so char width is a generous estimate (pads, never clips).
|
|
inline constexpr unsigned int kTooltipDelayMs = 500;
|
|
inline constexpr int kTooltipCharPx = 7;
|
|
inline constexpr int kTooltipTextH = 14;
|
|
|
|
// Client area top to bottom: top toolbar | split body | bottom toolbar | footer.
|
|
// 26, not 24: Font::RegionTitle's line box (19px em + Segoe UI's leading) is ~25px, and
|
|
// DT_VCENTER clips to the rect.
|
|
inline constexpr int kRegionHeaderHeight = 26;
|
|
inline constexpr int kTabStripHeight = 26;
|
|
inline constexpr int kSplitDividerHeight = 3;
|
|
inline constexpr int kFullHtBtnWidth = 22;
|
|
inline constexpr int kCreateBtnWidth = 22;
|
|
|
|
inline const TabStripSpec kTabSpec{/*tabWidth=*/96, /*chevronWidth=*/20};
|
|
|
|
// Far-right More ("...") overflow-menu button: one source of truth for its
|
|
// geometry + the reserve action_bar leaves for it.
|
|
inline const MenuButtonSpec kMenuBtnSpec{/*buttonWidth=*/28, /*rightInset=*/6,
|
|
/*verticalInset=*/3, /*minLeftInset=*/40};
|
|
|
|
// Shared by both toolbars' draw and hit-test (identical button shape top and
|
|
// bottom). clusterGap is wider than buttonGap so semantic groups read as groups.
|
|
inline const ActionBarSpec kBarSpec{/*buttonWidth=*/108, /*buttonGap=*/4, /*clusterGap=*/24,
|
|
/*sidePad=*/8, /*verticalInset=*/3};
|
|
|
|
struct CachedThumbnail {
|
|
Envelope envelope;
|
|
int width = 0;
|
|
};
|
|
|
|
// Which of the two split regions currently owns the selection / receives keyboard
|
|
// input. The move/copy source is the focused region's displayed bank.
|
|
enum class Region { Pool, Banks };
|
|
|
|
// What a drag is dropping onto, resolved live under the pointer. BanksRegion fires
|
|
// when the pointer is anywhere in the named-banks grid that is NOT on a specific tab
|
|
// (tab takes precedence); the resolved bank is always shownBankId.
|
|
enum class DropKind { None, PoolRegion, Tab, BanksRegion };
|
|
|
|
// The hovered interactive element, resolved live in WM_MOUSEMOVE so the kit draws its
|
|
// hover state on that element only. SWELL exposes no WM_MOUSELEAVE (confirmed: no hit in
|
|
// vendor/WDL/WDL/swell), so hover is cleared by a move that resolves to None rather than a
|
|
// leave message; the panel is Windows-only but this stays portable-safe.
|
|
// `index` disambiguates within a kind (action-bar button index, tab index); -1 when N/A.
|
|
enum class HoverKind {
|
|
None,
|
|
TopBarButton, // index = flat action index into topBarRows
|
|
BottomBarButton, // index = flat action index into bottomBarRows
|
|
MoreButton,
|
|
PruneButton,
|
|
FullHtPool,
|
|
FullHtBanks,
|
|
CreateBank,
|
|
Tab, // index = tab ordinal
|
|
TailButton,
|
|
ModeSegment, // index = segment ordinal
|
|
};
|
|
|
|
struct Hover {
|
|
HoverKind kind = HoverKind::None;
|
|
int index = -1;
|
|
|
|
bool operator==(const Hover& o) const { return kind == o.kind && index == o.index; }
|
|
bool operator!=(const Hover& o) const { return !(*this == o); }
|
|
};
|
|
|
|
// Base rest/hover resolver; Active/Pressed are decided per-element by the caller
|
|
// (e.g. an active tab draws Active regardless of hover).
|
|
inline InteractionState hoverState(const Hover& hovered, HoverKind kind, int index) {
|
|
return (hovered.kind == kind && hovered.index == index) ? InteractionState::Hover
|
|
: InteractionState::Rest;
|
|
}
|
|
|
|
struct PanelState {
|
|
ReaSamplerSession* session = nullptr;
|
|
|
|
HWND hwnd = nullptr;
|
|
bool open = false;
|
|
|
|
std::string bankFingerprint;
|
|
std::uint64_t generation = 0;
|
|
|
|
std::unordered_map<std::string, CachedThumbnail> cache;
|
|
|
|
// One live selection, scoped to `focusedRegion`. Switching regions moves the
|
|
// selection with the focus.
|
|
Selection selection;
|
|
int selItemCount = 0;
|
|
Region focusedRegion = Region::Pool;
|
|
|
|
// Resolved on WM_MOUSEMOVE; repaint fires only when this changes.
|
|
Hover hovered;
|
|
|
|
// Custom hover-delay tooltip. hoverSinceTick resets on every hover change;
|
|
// tooltipShown latches once the delay elapses so the OnTimer poll repaints once.
|
|
unsigned int hoverSinceTick = 0;
|
|
bool tooltipShown = false;
|
|
|
|
// Polled on the OnTimer tick (REAPER exposes no transport-change callback). Feeds
|
|
// modeSegmentEnabled (core/ui/footer_bar.h). Cached rather than read per paint AND
|
|
// per click so the pixel the user saw and the click they made cannot disagree
|
|
// within a tick.
|
|
bool modeSwitchBlocked = false;
|
|
|
|
BankPanelFullHeight fullHeight = BankPanelFullHeight::Split;
|
|
|
|
// The named bank the banks region shows — distinct from the active/capture-target
|
|
// bank. Reconciled each fingerprint pass so it always names a live bank (or empty).
|
|
std::string shownBankId;
|
|
|
|
int tabScroll = 0;
|
|
|
|
// A drag begins only after the pointer moves past a threshold from a press
|
|
// that landed on a selected grid cell — disambiguates from the multi-select
|
|
// drag (which begins immediately on any grid press).
|
|
bool dragArmed = false;
|
|
bool dragging = false;
|
|
int dragStartX = 0, dragStartY = 0;
|
|
Region dragSourceRegion = Region::Pool;
|
|
std::string dragSourceBankId;
|
|
std::vector<std::string> dragSampleIds;
|
|
std::string dragPrimaryId; // the single card grabbed — the reorder/replace subject
|
|
DropKind dropKind = DropKind::None;
|
|
std::string dropBankId; // destination bank id when dropKind==Tab
|
|
|
|
// Resolved each mouse-move by card_drag::decideCardGesture. dragTargetSlot >= 0
|
|
// only for a Reorder/Replace over the source bank's own grid.
|
|
CardGesture cardGesture = CardGesture::None;
|
|
int dragTargetSlot = -1;
|
|
|
|
// While a single-capture drag is over REAPER's own UI, heading for a track's TCP FX
|
|
// button: on release this adds a ReaSampler 9000 preloaded with the capture. Null
|
|
// when the pointer is not over an FX button.
|
|
MediaTrack* instrumentDropTrack = nullptr;
|
|
|
|
// Latched once an OsDrag hand-off resolves "cannot hand off" for this gesture — skips
|
|
// re-running resolveDragPathsForOs (fs::exists per sample) each move. Cleared by resetDragState.
|
|
bool dragOsHandoffBlocked = false;
|
|
|
|
// Authoritative tail setting lives in ReaSamplerSession, not here; panel reads it for
|
|
// drawing and mutates via footer click / scroll-wheel. bankPanelTailSetting is the
|
|
// capture actions' read seam.
|
|
|
|
preview_register_t preview{};
|
|
PCM_source* previewSrc = nullptr;
|
|
bool previewActive = false;
|
|
bool previewInited = false; // guards double init / deinit
|
|
|
|
// Each timer tick diffs the live track+item GUID set against the previous tick to
|
|
// auto-tag new content. GuidBaseline self-arms on first observe() so pre-existing
|
|
// content is never mass-tagged. Project-load re-arm is driven by persist's load
|
|
// signal, not a ReaProject* compare — a recycled address previously mis-tagged tracks.
|
|
GuidBaseline contentBaseline;
|
|
bool reloadPending = false; // set by bankPanelNotifyProjectLoaded; drained next detect tick
|
|
};
|
|
|
|
// Defined in panel_window.cpp (the lifecycle owner).
|
|
extern PanelState g_panel;
|
|
|
|
// The grid draws in the bank's persisted SlotMap order (sparse, gap-preserving),
|
|
// NOT BankModel insertion order. regionDisplay (panel_layout.cpp) is the single
|
|
// place that resolves a region's display.
|
|
//
|
|
// TWO INDEX SPACES the whole panel must keep straight:
|
|
// * SLOT — display position 0..maxSlot; gaps are empty, valid drop targets.
|
|
// What pixels/hit-tests speak.
|
|
// * SELECTION — the dense occupied-ordinal [0, occupied) space the pure
|
|
// Selection/applyClick/navigate reason in. Selection index i <->
|
|
// orderedIds[i]. Keyboard nav therefore skips gaps.
|
|
// RegionDisplay carries both plus the translation, resolved fresh each call (never
|
|
// cached across a mutation, per the reference-invalidation guardrail).
|
|
struct RegionDisplay {
|
|
std::vector<std::string> orderedIds; // occupied ids in slot order (selection space)
|
|
std::vector<SlotCellRect> slotRects; // one rect per slot 0..maxSlot, viewport coords
|
|
const Bank* bank = nullptr;
|
|
|
|
// The id occupying `slot`, or "" for an empty slot / out of range.
|
|
std::string idAtSlot(int slot) const {
|
|
return bank ? bank->slots.idAt(slot) : std::string{};
|
|
}
|
|
// The slot a selection ordinal `sel` maps to, or -1. orderedIds[sel] -> its slot.
|
|
int slotForSelection(int sel) const {
|
|
if (sel < 0 || sel >= static_cast<int>(orderedIds.size()) || !bank) return -1;
|
|
return bank->slots.slotOf(orderedIds[static_cast<std::size_t>(sel)]);
|
|
}
|
|
// The selection ordinal for `slot` (index of its occupant in orderedIds), or -1 when
|
|
// the slot is empty. Inverse of slotForSelection.
|
|
int selectionForSlot(int slot) const {
|
|
const std::string id = idAtSlot(slot);
|
|
if (id.empty()) return -1;
|
|
for (std::size_t i = 0; i < orderedIds.size(); ++i)
|
|
if (orderedIds[i] == id) return static_cast<int>(i);
|
|
return -1;
|
|
}
|
|
int occupiedCount() const { return static_cast<int>(orderedIds.size()); }
|
|
};
|
|
|
|
// One action button. Row order IS the flat action index the pure action_bar slots
|
|
// carry. Built by panel_layout; consumed by render, input, drag.
|
|
struct ActionBarRow {
|
|
std::string suffix;
|
|
std::string shortLabel;
|
|
std::string fullName;
|
|
ActionCluster cluster = ActionCluster::Capture;
|
|
bool enabled = true; // opposite-mode gate for bottom-bar tag buttons; always true
|
|
// for the top bar (unconditional triggers)
|
|
};
|
|
|
|
// Modifier state at event time. Alt = the replace modifier.
|
|
inline bool ctrlDown() { return (GetAsyncKeyState(VK_CONTROL) & 0x8000) != 0; }
|
|
inline bool shiftDown() { return (GetAsyncKeyState(VK_SHIFT) & 0x8000) != 0; }
|
|
inline bool altDown() { return (GetAsyncKeyState(VK_MENU) & 0x8000) != 0; }
|
|
|
|
inline void invalidatePanel() {
|
|
if (g_panel.hwnd) InvalidateRect(g_panel.hwnd, nullptr, FALSE);
|
|
}
|
|
|
|
// Cross-seam contract, grouped by owning TU; all plain free functions.
|
|
|
|
// panel_bank_ops.cpp — book/bank accessors + the bank-CRUD verbs + menus.
|
|
BankBook* book();
|
|
const BankModel* indexForRegion(Region r);
|
|
std::string bankIdForRegion(Region r);
|
|
std::vector<const Bank*> namedBanks();
|
|
std::string currentProjectDir();
|
|
void doCreateBank();
|
|
void transferSamples(const std::vector<std::string>& sampleIds,
|
|
const std::string& srcBankId, const std::string& destBankId,
|
|
bool copy);
|
|
void removeSamples(const std::vector<std::string>& sampleIds,
|
|
const std::string& srcBankId);
|
|
std::vector<std::string> focusedSelectionIds();
|
|
std::vector<std::string> resolveDragPathsForOs();
|
|
void showTabMenu(int screenX, int screenY, const std::string& bankId);
|
|
void showSelectionMenu(int screenX, int screenY);
|
|
void showMoreMenu();
|
|
|
|
// panel_layout.cpp — toolbar/footer/menu rects, row/cluster builders, split geometry,
|
|
// region rects, the display bridge. Draw and hit-test both call these so they never drift.
|
|
int modeCount();
|
|
MenuButtonRect topMenuButtonRect(int w);
|
|
ActionBarRect topToolbarActionRect(int w);
|
|
RECT panelFooter(int w, int h);
|
|
FooterBarLayout footerBarLayoutFor(int w, int h);
|
|
ButtonRect pruneButtonRectFor(int w, int h);
|
|
bool pointInFooter(int x, int y);
|
|
ActionBarRect bottomToolbarRect(int w, int h);
|
|
RECT splitBody(int w, int h);
|
|
bool poolShown();
|
|
bool banksShown();
|
|
RECT poolRegionRect(int w, int h);
|
|
RECT banksRegionRect(int w, int h);
|
|
RECT regionHeaderRect(const RECT& region);
|
|
TabStripRect banksTabStripRect(const RECT& region);
|
|
RECT regionGridRect(const RECT& region, bool isBanks);
|
|
RECT fullHtBtnRect(const RECT& region);
|
|
RECT createBtnRect(const RECT& region);
|
|
RegionDisplay regionDisplay(const RECT& region, bool isBanks, Region reg);
|
|
RegionDisplay focusedDisplay();
|
|
bool regionAt(int x, int y, Region& out);
|
|
int footerToggleSegmentHit(int x, int y, int w, int h);
|
|
int columnsForRegion(Region reg);
|
|
std::vector<ActionBarRow> topBarRows();
|
|
std::vector<ActionBarRow> bottomBarRows();
|
|
std::vector<ActionBarRow> overflowMenuRows();
|
|
std::vector<ClusterSpec> actionBarClusters(const std::vector<ActionBarRow>& rows);
|
|
int resolveBarCommandId(const ActionBarRow& row);
|
|
int toolbarHit(int x, int y, const ActionBarRect& bar, const std::vector<ActionBarRow>& rows);
|
|
bool currentTooltip(int w, int h, std::string& textOut, int& ax, int& ay, int& aw, int& ah);
|
|
|
|
// panel_render.cpp — the full LICE paint (drawn last-to-front into the caller's
|
|
// double buffer; BitBlt'd once by paintPanel).
|
|
void paintPanel(HWND hwnd, HDC hdc);
|
|
|
|
// panel_thumbnails.cpp — thumbnail compute + cache, and the bank-change fingerprint
|
|
// pass that owns the cache's generation key (bumps generation, clears the cache, and
|
|
// reconciles selection/shown-bank on any book mutation).
|
|
const Envelope& thumbnailFor(const Sample& sample, int width, const std::string& projectDir);
|
|
bool refreshFingerprint();
|
|
void reconcileShownBank();
|
|
|
|
// panel_audition.cpp — the preview engine (hot path: direct call-through, never
|
|
// virtual, no added header->TU indirection).
|
|
void initPreview();
|
|
void deinitPreview();
|
|
void stopAudition();
|
|
void startAudition(int idx);
|
|
|
|
// panel_input.cpp — click/wheel/key routing, accelerator, new-content detection,
|
|
// and the session-tail read/mutate helpers.
|
|
TailSetting currentTail();
|
|
// The registered activate action behind a footer mode segment, or 0 if unroutable.
|
|
// Shared with panel_render.cpp so paint and click resolve the same id.
|
|
int modeActivateCommandId(const std::string& modeId);
|
|
void handleClick(int x, int y);
|
|
bool handleWheel(int x, int y, int delta);
|
|
void registerAccel();
|
|
void unregisterAccel();
|
|
|
|
// panel_drag.cpp — the card-drag/hover state machine (pure mirror: core/ui/card_drag).
|
|
// Per-mouse-move work stays plain free-function calls.
|
|
void onMouseMove(int x, int y);
|
|
void onLBtnUp(int x, int y);
|
|
void handleRightClick(int x, int y);
|
|
void resetDragState();
|
|
void maybeShowTooltip();
|
|
|
|
} // namespace reasampler::panel
|