Files
reasampler/src/core/ui/bank_grid.h
T

119 lines
5.5 KiB
C++

#pragma once
#include "core/ui/rect.h"
// 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>
#include <string>
#include <vector>
namespace reasampler::ui {
// One cell's pixel rect, top-left origin. Draw bounds for one sample's thumbnail.
using CellRect = Rect;
// 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;
};
// 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. 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);
// 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. 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;
std::uint64_t generation = 0;
bool operator==(const ThumbnailKey& o) const {
return sampleId == o.sampleId && width == o.width &&
generation == o.generation;
}
};
// 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: hit-test, selection, keyboard nav --------------------------
// 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);
// 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 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;
int anchor = -1;
bool operator==(const Selection& o) const {
return indices == o.indices && focus == o.focus && anchor == o.anchor;
}
bool contains(int index) const;
bool empty() const { return indices.empty(); }
};
// 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);
// 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 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 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 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