#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 #include #include #include 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 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& 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 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