Cut core/ui and core/audio comment bloat ~60% (comments only, zero code change)
This commit is contained in:
+49
-109
@@ -1,14 +1,7 @@
|
||||
#pragma once
|
||||
#include "core/ui/rect.h"
|
||||
// bank_grid — the REAPER-free layout math and cache-key logic behind the docked
|
||||
// bank_panel (M5, Wave A). The panel shell (shell/panel/) owns the SWELL window,
|
||||
// LICE drawing, and PCM reads; ALL of that is REAPER-bound and DAW-verified. What
|
||||
// is NOT DAW-bound — how N sample cells tile a panel of a given pixel size, and
|
||||
// the key that identifies a cached thumbnail — lives here so it is unit-tested
|
||||
// outside the DAW (CLAUDE.md §load-bearing split).
|
||||
//
|
||||
// PURE MODULE: NO REAPER types, NO SWELL, NO vendor/ includes. Standard library
|
||||
// only. Builds and unit-tests without REAPER.
|
||||
// bank_grid — layout math, hit-test, selection, and keyboard nav for the docked bank_panel grid,
|
||||
// plus its thumbnail cache-key. The panel shell owns SWELL/LICE/PCM; this is the DAW-free half.
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
@@ -17,50 +10,34 @@
|
||||
|
||||
namespace reasampler::ui {
|
||||
|
||||
// A single cell's pixel rectangle within the panel, top-left origin (SWELL/LICE
|
||||
// convention). (x, y) is the top-left corner; width/height are the cell extents.
|
||||
// These are the draw bounds for one sample's thumbnail; the panel draws its
|
||||
// waveform envelope inside this rect (minus any internal padding it applies).
|
||||
using CellRect = Rect; // Q-W1: the shared concrete ui::Rect (core/ui/rect.h), role-aliased
|
||||
// One cell's pixel rect, top-left origin. Draw bounds for one sample's thumbnail.
|
||||
using CellRect = Rect;
|
||||
|
||||
// Fixed inputs that shape the grid. All in pixels. cellWidth/cellHeight are the
|
||||
// TARGET cell size; the layout fits as many whole columns as the panel width
|
||||
// allows (>= 1) and wraps to as many rows as N requires. gap is the pixel spacing
|
||||
// between adjacent cells (and the outer margin), so cells never touch.
|
||||
// cellWidth/cellHeight are the target cell size; layout fits as many whole columns as the panel
|
||||
// width allows (>= 1) and wraps rows as needed. gap is the spacing between cells and the margin.
|
||||
struct GridSpec {
|
||||
int cellWidth = 120;
|
||||
int cellHeight = 72;
|
||||
int gap = 8;
|
||||
};
|
||||
|
||||
// Computes the number of columns that fit in a panel of the given pixel width for
|
||||
// the spec. Always >= 1 (a panel narrower than one cell still shows one column,
|
||||
// clipped by the window). Pure arithmetic — the panel passes its live client
|
||||
// width here and to computeCellRects.
|
||||
// Columns that fit a panel of the given width. Always >= 1 (a too-narrow panel still shows one
|
||||
// clipped column).
|
||||
int columnsForWidth(int panelWidth, const GridSpec& spec);
|
||||
|
||||
// Tiles `itemCount` cells left-to-right, top-to-bottom into a panel of the given
|
||||
// pixel width, honoring the spec's cell size and gap. Returns exactly itemCount
|
||||
// rects in item order (rect i is sample i). A partial last row is left-aligned
|
||||
// and simply shorter — no centering, no stretching. itemCount == 0 -> empty.
|
||||
// panelWidth is used only to derive the column count; the returned rects may
|
||||
// extend below any fixed viewport height (the panel scrolls/clips in Wave B).
|
||||
// Tiles itemCount cells left-to-right, top-to-bottom. Returns exactly itemCount rects in item
|
||||
// order. A partial last row is left-aligned, not centered or stretched. itemCount == 0 -> empty.
|
||||
std::vector<CellRect> computeCellRects(int itemCount,
|
||||
int panelWidth,
|
||||
const GridSpec& spec);
|
||||
|
||||
// The total pixel height the grid occupies for itemCount cells at the given panel
|
||||
// width and spec (top margin + rows*cellHeight + inter-row gaps + bottom margin).
|
||||
// 0 when itemCount == 0. The panel uses this to know its full content height
|
||||
// (scroll extent in Wave B; for Wave A it sizes the empty-vs-populated decision).
|
||||
// Total pixel height the grid occupies (top margin + rows*cellHeight + inter-row gaps + bottom
|
||||
// margin); 0 when itemCount == 0.
|
||||
int contentHeight(int itemCount, int panelWidth, const GridSpec& spec);
|
||||
|
||||
// Identifies one cached thumbnail. A cached envelope is valid only while the
|
||||
// sample's identity, the draw width it was computed at, and the bank generation
|
||||
// it was computed under all match. Width is part of the key because the envelope
|
||||
// has exactly `width` bins per channel (peaks::computeEnvelope is width-driven);
|
||||
// a resized panel needs a fresh envelope. Generation lets the panel invalidate
|
||||
// every entry when the bank changes (capture / project load) without diffing.
|
||||
// Identifies one cached thumbnail. Valid only while sample identity, the draw width it was
|
||||
// computed at (the envelope has exactly `width` bins per channel), and bank generation all match;
|
||||
// generation bump invalidates every cached entry without diffing.
|
||||
struct ThumbnailKey {
|
||||
std::string sampleId;
|
||||
int width = 0;
|
||||
@@ -72,35 +49,22 @@ struct ThumbnailKey {
|
||||
}
|
||||
};
|
||||
|
||||
// A stable string form of the key, suitable as a map key. Deterministic: the same
|
||||
// key always yields the same string, distinct keys always differ (the sampleId is
|
||||
// length-prefixed so an id containing the delimiter cannot collide with another).
|
||||
// Stable string form of the key for use as a map key. sampleId is length-prefixed so a delimiter
|
||||
// byte inside an id can't forge a collision.
|
||||
std::string thumbnailKeyString(const ThumbnailKey& key);
|
||||
|
||||
// --- Interaction (M5 Wave B): hit-test, selection, keyboard nav --------------
|
||||
//
|
||||
// All REAPER-free so the panel's interaction LOGIC is unit-tested outside the DAW,
|
||||
// exactly as the layout math is. The panel shell (shell/panel/) reads live mouse
|
||||
// coordinates / key codes / modifier state via SWELL and calls into these; it owns
|
||||
// no selection arithmetic of its own.
|
||||
// --- Interaction: hit-test, selection, keyboard nav --------------------------
|
||||
|
||||
// Hit-tests a point (SWELL/LICE top-left client coords) against a cell-rect list.
|
||||
// Returns the index of the FIRST rect that contains the point, or -1 for a miss
|
||||
// (a click in the inter-cell gap, the margin, or below the last row). Half-open
|
||||
// bounds [x, x+width) x [y, y+height) so adjacent rects never both claim a pixel.
|
||||
// Index of the first rect containing (px, py), or -1 for a miss (gap, margin, below last row).
|
||||
// Half-open bounds so adjacent rects never both claim a pixel.
|
||||
int hitTestCell(int px, int py, const std::vector<CellRect>& rects);
|
||||
|
||||
// The panel's selection state. `indices` is the selected set as a SORTED, unique
|
||||
// ascending vector (deterministic for tests and for highlight iteration). `focus`
|
||||
// is the cell the caret sits on — the audition/extend target — or -1 when nothing
|
||||
// is focused. `anchor` is the fixed end of a shift-range (the cell a range extends
|
||||
// FROM); -1 when there is no active range origin. An empty selection has focus and
|
||||
// anchor both -1.
|
||||
// Panel selection state. `indices` is sorted unique ascending (deterministic for tests and
|
||||
// highlight order). `focus` is the caret cell (audition/extend target), -1 when none. `anchor` is
|
||||
// the fixed end a shift-range extends from, -1 when none. Empty selection: focus == anchor == -1.
|
||||
//
|
||||
// Invariants (upheld by the pure mutators below, asserted in tests):
|
||||
// * indices is sorted ascending with no duplicates;
|
||||
// * every index (and focus/anchor when >= 0) is in [0, itemCount);
|
||||
// * focus, when >= 0, is a member of indices.
|
||||
// Invariants upheld by the mutators below: indices sorted/unique; every index (and focus/anchor
|
||||
// when >= 0) is in [0, itemCount); focus, when >= 0, is a member of indices.
|
||||
struct Selection {
|
||||
std::vector<int> indices;
|
||||
int focus = -1;
|
||||
@@ -113,66 +77,42 @@ struct Selection {
|
||||
bool empty() const { return indices.empty(); }
|
||||
};
|
||||
|
||||
// Applies a mouse click on cell `index` to `current`, returning the new selection.
|
||||
// Modifier semantics (standard multi-select, matching file-manager conventions):
|
||||
// * plain (no modifier): select ONLY `index`; focus = anchor = index.
|
||||
// * ctrl: TOGGLE `index` in/out of the set; focus = index. Anchor moves to
|
||||
// index on add, and to index on remove too (a ctrl-click reseeds the
|
||||
// range origin at the clicked cell). If the toggle empties the set,
|
||||
// focus stays at index (the caret) but the set is empty.
|
||||
// * shift: select the inclusive RANGE from `anchor` to `index` (replacing the
|
||||
// set); focus = index, anchor unchanged. With no prior anchor (anchor
|
||||
// == -1) shift behaves like a plain click (anchor seeds at index).
|
||||
// `index` out of [0, itemCount) or itemCount <= 0 returns `current` unchanged.
|
||||
// ctrl and shift together: shift takes precedence (range select), matching common
|
||||
// UI; documented so the panel need not special-case it.
|
||||
// Applies a click on cell `index` to `current`. Modifier semantics (file-manager convention):
|
||||
// * plain: select only `index`; focus = anchor = index.
|
||||
// * ctrl: toggle `index` in/out; focus = index; anchor reseeds to index either way.
|
||||
// * shift: select the inclusive range [anchor, index]; focus = index, anchor unchanged.
|
||||
// No prior anchor behaves like a plain click.
|
||||
// ctrl+shift together: shift wins (range select). index out of range or itemCount <= 0: no-op.
|
||||
Selection applyClick(const Selection& current, int index, bool ctrl, bool shift,
|
||||
int itemCount);
|
||||
|
||||
// A directional key for keyboard navigation. REAPER-free (the shell maps VK_* to
|
||||
// these) so nav math is testable without SWELL. Enter/Space/Esc are NOT here: they
|
||||
// drive audition, which is a shell concern (no selection math), so the shell reads
|
||||
// those key codes directly.
|
||||
// Directional key for nav; Enter/Space/Esc drive audition and are a shell concern, not modelled
|
||||
// here.
|
||||
enum class NavKey { Left, Right, Up, Down, Home, End };
|
||||
|
||||
// Moves the focus by one step for `key` in a grid of `cols` columns holding
|
||||
// `itemCount` cells, returning the new selection. `cols` >= 1.
|
||||
// * Left/Right move by one cell in linear (row-major) order; Up/Down move by
|
||||
// `cols`. Movement CLAMPS at the grid ends (no wrap): Right on the last cell,
|
||||
// Left on the first, Up on the top row, Down past the last cell all stay put.
|
||||
// (Clamp, not wrap: wrap on a partial last row is surprising and error-prone;
|
||||
// clamp is the predictable choice — flagged as the deliberate decision.)
|
||||
// * Down from the second-to-last row into a column with no cell in the last row
|
||||
// clamps to the last cell rather than overshooting past itemCount.
|
||||
// * Without shift: the moved-to cell becomes the sole selection; focus = anchor
|
||||
// = newIndex (a plain arrow reseeds the range origin).
|
||||
// * With shift: focus moves to newIndex and the selection becomes the inclusive
|
||||
// range from anchor to newIndex (anchor unchanged); a first shift-arrow with no
|
||||
// anchor seeds the anchor at the ORIGIN cell before moving.
|
||||
// * Empty selection (focus == -1): the first arrow focuses cell 0 (Home-like),
|
||||
// so an arrow press on a fresh panel starts navigation predictably.
|
||||
// Moves focus by one step for `key` in a `cols`-column grid of `itemCount` cells.
|
||||
// * Left/Right move linearly; Up/Down move by `cols`. Movement CLAMPS at the grid edges (no
|
||||
// wrap) — deliberate: wrap on a partial last row is surprising.
|
||||
// * Down from the row above a missing partial-last-row cell clamps to the last cell rather than
|
||||
// overshooting past itemCount.
|
||||
// * Without shift: moved-to cell becomes the sole selection (focus = anchor = newIndex).
|
||||
// * With shift: focus moves to newIndex, selection becomes the inclusive range from anchor
|
||||
// (seeded at the origin cell on first extend).
|
||||
// * Empty selection: first arrow focuses cell 0 without moving.
|
||||
// itemCount <= 0 returns `current` unchanged.
|
||||
Selection navigate(const Selection& current, NavKey key, int cols, int itemCount,
|
||||
bool shift);
|
||||
|
||||
// --- Waveform display compression --------------------------------------------
|
||||
//
|
||||
// Maps a raw linear amplitude magnitude to a perceptual display fraction so
|
||||
// quiet and medium content remains visible in the thumbnail.
|
||||
//
|
||||
// The floor below which amplitude is treated as silence (display fraction 0).
|
||||
// At -60 dB, 0.001 linear magnitude maps to ~0. Tune this constant in-DAW to
|
||||
// taste — it is the only knob for the compression curve.
|
||||
// Maps raw linear amplitude to a perceptual display fraction so quiet content stays visible.
|
||||
|
||||
// Below this, amplitude is treated as silence (display fraction 0). Only knob for the curve.
|
||||
constexpr float kDisplayFloorDb = -60.0f;
|
||||
|
||||
// Maps a signed linear amplitude value in [-1, 1] (a raw envelope extreme such
|
||||
// as PeakBin::max or PeakBin::min) to a signed display fraction in [-1, 1].
|
||||
//
|
||||
// The magnitude |linear| is converted to dB, clamped to [kDisplayFloorDb, 0],
|
||||
// then normalized so kDisplayFloorDb -> 0 and 0 dB -> 1. The original sign is
|
||||
// re-applied so positive max values still map positive (draw up) and negative
|
||||
// min values still map negative (draw down). Exact-zero input returns 0.0f
|
||||
// (stays on the midline). Full-scale (|linear| == 1.0f) returns exactly ±1.0f.
|
||||
// Maps a signed linear amplitude in [-1, 1] (a raw envelope extreme, e.g. PeakBin::max/min) to a
|
||||
// signed display fraction in [-1, 1]: magnitude -> dB, clamped to [kDisplayFloorDb, 0] and
|
||||
// normalized so the floor -> 0 and 0 dB -> 1, then the original sign is re-applied. Exact zero
|
||||
// stays 0; full-scale (|linear| == 1.0f) returns exactly +-1.0f.
|
||||
float compressAmplitudeForDisplay(float linear);
|
||||
|
||||
} // namespace reasampler::ui
|
||||
|
||||
Reference in New Issue
Block a user