119 lines
5.5 KiB
C++
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
|