Retire the zone system: one capture = one parameter set, and re-seam the engine and Sample face into bands
This commit is contained in:
@@ -2,7 +2,11 @@
|
||||
|
||||
#include "core/instrument/ui/browser_scroll.h"
|
||||
|
||||
#include "core/instrument/ui/editor_geometry.h" // kPad / kTitleHeight / kNavButtonWidth
|
||||
// The Browse modal is a full-window sheet drawn over the Sample face, so it reuses that
|
||||
// face's chrome metrics rather than minting its own — a divergent title height or pad would
|
||||
// make the sheet visibly not line up with what it covers.
|
||||
#include "core/instrument/ui/sample_bands.h" // kPad / kTitleHeight
|
||||
#include "core/instrument/ui/sample_chrome.h" // kNavButtonWidth (the Back button's slot)
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
|
||||
@@ -1,273 +0,0 @@
|
||||
// editor_geometry.cpp — see editor_geometry.h. Pure math; no host types.
|
||||
|
||||
#include "core/instrument/ui/editor_geometry.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace reasampler::instrument::ui {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr int kTitleBarHeight = 28;
|
||||
constexpr int kButtonMargin = 10;
|
||||
constexpr int kButtonWidth = 120;
|
||||
constexpr int kButtonHeight = 24;
|
||||
|
||||
} // namespace
|
||||
|
||||
EditorLayout layoutEditor(int w, int h) {
|
||||
// Clamp to non-negative extents so a degenerate view can't produce inverted rects.
|
||||
const int cw = std::max(0, w);
|
||||
const int ch = std::max(0, h);
|
||||
|
||||
EditorLayout out;
|
||||
|
||||
const int titleH = std::min(kTitleBarHeight, ch);
|
||||
out.titleBar = Rect::ltrb(0, 0, cw, titleH);
|
||||
out.canvas = Rect::ltrb(0, titleH, cw, ch);
|
||||
|
||||
// Button inset from the canvas top-left, clamped so it never overhangs a small view.
|
||||
const int bx = out.canvas.x + kButtonMargin;
|
||||
const int by = out.canvas.y + kButtonMargin;
|
||||
const int bRight = std::min(bx + kButtonWidth, out.canvas.right());
|
||||
const int bBottom = std::min(by + kButtonHeight, out.canvas.bottom());
|
||||
out.button = Rect::ltrb(bx, by, std::max(bx, bRight), std::max(by, bBottom));
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
HitTarget hitTest(const EditorLayout& layout, int x, int y) {
|
||||
if (contains(layout.button, x, y)) return HitTarget::kButton;
|
||||
return HitTarget::kNone;
|
||||
}
|
||||
|
||||
Rect sampleRowRect(const EditorLayout& layout, int index) {
|
||||
if (index < 0) return Rect{};
|
||||
const int top = layout.canvas.y + index * kSampleRowHeight;
|
||||
return Rect::ltrb(layout.canvas.x, top, layout.canvas.right(), top + kSampleRowHeight);
|
||||
}
|
||||
|
||||
int sampleRowHitTest(const EditorLayout& layout, int rowCount, int x, int y) {
|
||||
if (rowCount <= 0) return -1;
|
||||
if (x < layout.canvas.x || x >= layout.canvas.right()) return -1;
|
||||
if (y < layout.canvas.y) return -1;
|
||||
if (y >= layout.canvas.bottom()) return -1;
|
||||
const int index = (y - layout.canvas.y) / kSampleRowHeight;
|
||||
if (index < 0 || index >= rowCount) return -1;
|
||||
const Rect r = sampleRowRect(layout, index);
|
||||
if (y >= r.bottom()) return -1;
|
||||
return index;
|
||||
}
|
||||
|
||||
// --- Keymap editor -----------------------------------------------------------
|
||||
|
||||
KeymapEditorLayout layoutKeymapEditor(int w, int h) {
|
||||
KeymapEditorLayout out;
|
||||
out.base = layoutEditor(w, h);
|
||||
const Rect& canvas = out.base.canvas;
|
||||
|
||||
const int canvasW = std::max(0, canvas.width);
|
||||
const int splitW = canvasW / kZonePanelFraction; // width of the zone panel
|
||||
const int splitX = std::max(canvas.x, canvas.right() - splitW);
|
||||
|
||||
out.sampleList = Rect::ltrb(canvas.x, canvas.y, splitX, canvas.bottom());
|
||||
out.zonePanel = Rect::ltrb(splitX, canvas.y, canvas.right(), canvas.bottom());
|
||||
|
||||
const int addH = std::min(kAddZoneHeight, std::max(0, out.zonePanel.height));
|
||||
out.addZoneButton =
|
||||
Rect::ltrb(out.zonePanel.x, out.zonePanel.y, out.zonePanel.right(),
|
||||
out.zonePanel.y + addH);
|
||||
|
||||
out.zoneRowArea = Rect::ltrb(out.zonePanel.x, out.addZoneButton.bottom(),
|
||||
out.zonePanel.right(), out.zonePanel.bottom());
|
||||
return out;
|
||||
}
|
||||
|
||||
Rect keymapSampleRowRect(const KeymapEditorLayout& layout, int index) {
|
||||
if (index < 0) return Rect{};
|
||||
const int top = layout.sampleList.y + index * kSampleRowHeight;
|
||||
return Rect::ltrb(layout.sampleList.x, top, layout.sampleList.right(),
|
||||
top + kSampleRowHeight);
|
||||
}
|
||||
|
||||
int keymapSampleRowHitTest(const KeymapEditorLayout& layout, int rowCount, int x, int y) {
|
||||
if (rowCount <= 0) return -1;
|
||||
const Rect& list = layout.sampleList;
|
||||
if (x < list.x || x >= list.right()) return -1;
|
||||
if (y < list.y || y >= list.bottom()) return -1;
|
||||
const int index = (y - list.y) / kSampleRowHeight;
|
||||
if (index < 0 || index >= rowCount) return -1;
|
||||
const Rect r = keymapSampleRowRect(layout, index);
|
||||
if (y >= r.bottom()) return -1;
|
||||
return index;
|
||||
}
|
||||
|
||||
Rect zoneRowRect(const KeymapEditorLayout& layout, int index) {
|
||||
if (index < 0) return Rect{};
|
||||
const int top = layout.zoneRowArea.y + index * kZoneRowHeight;
|
||||
return Rect::ltrb(layout.zoneRowArea.x, top, layout.zoneRowArea.right(),
|
||||
top + kZoneRowHeight);
|
||||
}
|
||||
|
||||
ZoneHit zoneHitTest(const KeymapEditorLayout& layout, int zoneCount, int x, int y) {
|
||||
if (zoneCount <= 0) return ZoneHit{};
|
||||
const Rect& area = layout.zoneRowArea;
|
||||
if (x < area.x || x >= area.right()) return ZoneHit{};
|
||||
if (y < area.y || y >= area.bottom()) return ZoneHit{};
|
||||
const int index = (y - area.y) / kZoneRowHeight;
|
||||
if (index < 0 || index >= zoneCount) return ZoneHit{};
|
||||
const Rect row = zoneRowRect(layout, index);
|
||||
if (y >= row.bottom()) return ZoneHit{};
|
||||
|
||||
// Seven mini-buttons pinned to the right edge, each kZoneCtrlWidth wide, in slot
|
||||
// order 0..6; a click left of the leftmost is the label ("select").
|
||||
const ZoneField fields[7] = {
|
||||
ZoneField::kLowDown, ZoneField::kLowUp, ZoneField::kHighDown,
|
||||
ZoneField::kHighUp, ZoneField::kRootDown, ZoneField::kRootUp,
|
||||
ZoneField::kDelete,
|
||||
};
|
||||
const int slots = 7;
|
||||
const int ctrlBlockLeft = row.right() - slots * kZoneCtrlWidth;
|
||||
if (x < ctrlBlockLeft) return ZoneHit{index, ZoneField::kZoneNone}; // label -> select
|
||||
const int slot = (x - ctrlBlockLeft) / kZoneCtrlWidth;
|
||||
if (slot < 0 || slot >= slots) return ZoneHit{index, ZoneField::kZoneNone};
|
||||
return ZoneHit{index, fields[slot]};
|
||||
}
|
||||
|
||||
bool addZoneHitTest(const KeymapEditorLayout& layout, int x, int y) {
|
||||
return contains(layout.addZoneButton, x, y);
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr int kHeroMinHeight = 150; // elastic hero's floor
|
||||
constexpr int kClusterHeight = 52; // root strip + preview + vel knob + curve btn + channel toggle
|
||||
constexpr int kStripBandHeight = 40; // keyboard-strip band height (root strip + zone strip)
|
||||
|
||||
// Cluster's fixed right-anchored run: Preview button, vel knob cell, curve button, Mono|Stereo.
|
||||
constexpr int kPreviewBtnW = 64;
|
||||
constexpr int kVelCellW = 48;
|
||||
constexpr int kCurveBtnSize = 28;
|
||||
|
||||
constexpr int kChanSegW = 52;
|
||||
constexpr int kChanSegH = 18;
|
||||
|
||||
} // namespace
|
||||
|
||||
// Band order: title (fixed) -> hero (elastic, absorbs remaining height, floor
|
||||
// kHeroMinHeight) -> cluster (fixed) -> deck (fixed height `deckH`, bottom-anchored). A
|
||||
// window too short for the floor keeps the hero at its floor and clips lower bands.
|
||||
SampleBands computeSampleBands(int w, int h, int deckH) {
|
||||
SampleBands b;
|
||||
const int titleH = (std::min)(kTitleHeight, h);
|
||||
b.title = Rect::ltrb(0, 0, w, titleH);
|
||||
// Two nav buttons right-anchored in the title band (Browse then Zone).
|
||||
const int navTop = 2;
|
||||
const int navBot = (std::max)(navTop, titleH - 2);
|
||||
const Rect zone = Rect::ltrb(w - kPad - kNavButtonWidth, navTop, w - kPad, navBot);
|
||||
const Rect browse = Rect::ltrb(zone.x - 4 - kNavButtonWidth, navTop, zone.x - 4, navBot);
|
||||
b.navBrowse = browse;
|
||||
b.navZone = zone;
|
||||
|
||||
int deckTop = h - kPad - deckH;
|
||||
int clusterTop = deckTop - kClusterHeight - 4;
|
||||
int heroBottom = clusterTop - 4;
|
||||
if (heroBottom - titleH < kHeroMinHeight) {
|
||||
heroBottom = titleH + kHeroMinHeight; // hero floor wins; lower bands clip below
|
||||
clusterTop = heroBottom + 4;
|
||||
deckTop = clusterTop + kClusterHeight + 4;
|
||||
}
|
||||
b.hero = Rect::ltrb(kPad, titleH, w - kPad, heroBottom);
|
||||
b.cluster = Rect::ltrb(0, clusterTop, w, clusterTop + kClusterHeight);
|
||||
b.deck = Rect::ltrb(kPad, deckTop, w - kPad, deckTop + deckH);
|
||||
return b;
|
||||
}
|
||||
|
||||
ClusterRects clusterRects(const Rect& cluster, const Rect& chanMono, int knobSize) {
|
||||
ClusterRects r;
|
||||
const int stripTop = cluster.y + (cluster.height - kStripBandHeight) / 2;
|
||||
const int stripBot = stripTop + kStripBandHeight;
|
||||
const int curveTop = cluster.y + (cluster.height - kCurveBtnSize) / 2;
|
||||
r.curveBtn = Rect::ltrb(chanMono.x - kPad - kCurveBtnSize, curveTop,
|
||||
chanMono.x - kPad, curveTop + kCurveBtnSize);
|
||||
r.velCell = Rect::ltrb(r.curveBtn.x - kPad - kVelCellW, stripTop,
|
||||
r.curveBtn.x - kPad, stripBot);
|
||||
const int knobLeft = r.velCell.x + (kVelCellW - knobSize) / 2;
|
||||
r.velKnob = Rect::ltrb(knobLeft, r.velCell.y, knobLeft + knobSize,
|
||||
r.velCell.y + knobSize);
|
||||
r.velLabel = Rect::ltrb(r.velCell.x, r.velKnob.bottom(), r.velCell.right(), r.velCell.bottom());
|
||||
r.preview = Rect::ltrb(r.velCell.x - kPad - kPreviewBtnW, stripTop,
|
||||
r.velCell.x - kPad, stripBot);
|
||||
r.rootStrip = Rect::ltrb(cluster.x + kPad, stripTop, r.preview.x - kPad, stripBot);
|
||||
return r;
|
||||
}
|
||||
|
||||
ChannelToggleRects channelToggleRects(const Rect& area) {
|
||||
const int top = area.y + (area.height - kChanSegH) / 2;
|
||||
const int right = area.right() - kPad;
|
||||
const Rect stereo = Rect::ltrb(right - kChanSegW, top, right, top + kChanSegH);
|
||||
const Rect mono = Rect::ltrb(stereo.x - kChanSegW, top, stereo.x, top + kChanSegH);
|
||||
return {mono, stereo};
|
||||
}
|
||||
|
||||
Rect zoneContentArea(int w, int h) {
|
||||
const int titleH = (std::min)(kTitleHeight, h);
|
||||
return Rect::ltrb(0, titleH, w, h);
|
||||
}
|
||||
|
||||
Rect zoneBackRect(int w, int h) {
|
||||
return Rect::ltrb(w - kPad - kNavButtonWidth, 2, w - kPad,
|
||||
(std::max)(2, (std::min)(kTitleHeight, h) - 2));
|
||||
}
|
||||
|
||||
Rect zoneAddRect(const Rect& content) {
|
||||
return Rect::ltrb(content.x + kPad, content.y + 4, content.x + kPad + 96,
|
||||
content.y + 4 + 20);
|
||||
}
|
||||
|
||||
Rect zoneDeleteRect(const Rect& addR) {
|
||||
return Rect::ltrb(addR.right() + 8, addR.y, addR.right() + 8 + 64, addR.bottom());
|
||||
}
|
||||
|
||||
// Sits below the "+ Add Zone" affordance (top+4, height 20) with a 12px gap.
|
||||
Rect zonesStripArea(const Rect& content) {
|
||||
const int stripTop = content.y + 4 + 20 + 12; // addR.bottom() + 12
|
||||
return Rect::ltrb(content.x + kPad, stripTop, content.right() - kPad,
|
||||
stripTop + kStripBandHeight);
|
||||
}
|
||||
|
||||
// Anchored off zonesStripArea.bottom() so the legend top tracks the strip bottom.
|
||||
Rect noteEntryFieldsArea(const Rect& content) {
|
||||
const int stripBottom = zonesStripArea(content).bottom();
|
||||
const int top = stripBottom + 8; // legendTop (== zonesStripArea.bottom() + 8)
|
||||
return Rect::ltrb(content.x + 8 + 128, top, content.right() - 8, top + 18);
|
||||
}
|
||||
|
||||
Rect noteEntryFieldRect(const Rect& fields, int f) {
|
||||
if (f < 0 || f > 2 || fields.width <= 0) return Rect{};
|
||||
const int segW = fields.width / 3;
|
||||
const int left = fields.x + f * segW + (f > 0 ? 4 : 0); // small inter-field gap
|
||||
const int right = (f == 2) ? fields.right() : fields.x + (f + 1) * segW;
|
||||
return Rect::ltrb(left, fields.y, right, fields.bottom());
|
||||
}
|
||||
|
||||
Rect zonesControlPanel(const Rect& content) {
|
||||
const Rect strip = zonesStripArea(content);
|
||||
const int panelTop = strip.bottom() + 8 + 18 + 8; // strip + the 18px legend row + gap
|
||||
return Rect::ltrb(content.x + kPad, panelTop, content.right() - kPad,
|
||||
content.bottom() - 4);
|
||||
}
|
||||
|
||||
// Top-anchored; reserves a column at the panel's right for the curve-preview button so
|
||||
// no deck row starts inside it.
|
||||
Rect zonesDeckArea(const Rect& content) {
|
||||
const Rect panel = zonesControlPanel(content);
|
||||
return Rect::ltrb(panel.x, panel.y, panel.right() - kCurveBtnSize - kPad, panel.bottom());
|
||||
}
|
||||
|
||||
Rect zonesCurveButton(const Rect& content) {
|
||||
const Rect panel = zonesControlPanel(content);
|
||||
return Rect::ltrb(panel.right() - kCurveBtnSize, panel.y, panel.right(), panel.y + kCurveBtnSize);
|
||||
}
|
||||
|
||||
} // namespace reasampler::instrument::ui
|
||||
@@ -1,8 +1,9 @@
|
||||
// editor_geometry.h — view geometry + hit-test for the VST3 IPlugView LICE editor. The
|
||||
// IPlugView shell owns window/bitmap/SWELL plumbing; the rectangle math and hit-testing
|
||||
// live here so they can be unit-tested outside the DAW.
|
||||
|
||||
#pragma once
|
||||
// editor_geometry.h — the shared geometry vocabulary for the VST3 editor's pure modules:
|
||||
// the one concrete `Rect` (aliased from core/ui) and its half-open `contains()`. Every
|
||||
// instrument UI module speaks these types, so they live in one place rather than each
|
||||
// module reaching into core/ui separately. The Sample face's own layout lives in
|
||||
// sample_bands (the band-stack allocator) and the per-band modules.
|
||||
|
||||
#include "core/ui/rect.h"
|
||||
|
||||
@@ -11,174 +12,4 @@ namespace reasampler::instrument::ui {
|
||||
using Rect = ::reasampler::ui::Rect;
|
||||
using ::reasampler::ui::contains;
|
||||
|
||||
// Title band + one button + remaining canvas, clamped so a degenerate (too-small) view
|
||||
// never yields a region spilling outside the surface.
|
||||
struct EditorLayout {
|
||||
Rect titleBar;
|
||||
Rect button;
|
||||
Rect canvas;
|
||||
};
|
||||
|
||||
// Divide a (w x h) client area into the editor's top-level regions. Pure.
|
||||
EditorLayout layoutEditor(int w, int h);
|
||||
|
||||
enum class HitTarget {
|
||||
kNone,
|
||||
kButton,
|
||||
};
|
||||
|
||||
// Classify a click at (x, y) against a layout.
|
||||
HitTarget hitTest(const EditorLayout& layout, int x, int y);
|
||||
|
||||
// --- Sample-selection list ---------------------------------------------------
|
||||
//
|
||||
// A vertical stack of fixed-height rows below the title bar; clicking a row selects that
|
||||
// sample. Pure geometry only — the shell draws names and routes the click.
|
||||
|
||||
inline constexpr int kSampleRowHeight = 22;
|
||||
|
||||
// Rect for row `index` (0-based), laid out top-down inside the layout's canvas. Rows
|
||||
// beyond what the canvas can show are still computed (the shell clips at paint time); a
|
||||
// negative index yields an empty rect.
|
||||
Rect sampleRowRect(const EditorLayout& layout, int index);
|
||||
|
||||
// Row index a click at (x, y) lands on given `rowCount` rows, or -1 for a click outside
|
||||
// the list.
|
||||
int sampleRowHitTest(const EditorLayout& layout, int rowCount, int x, int y);
|
||||
|
||||
// --- Keymap editor ------------------------------------------------------------
|
||||
//
|
||||
// Splits the canvas into a LEFT bank-sample list (the sample-selection rows above, reused
|
||||
// as the "sample to add / fallback pick") and a RIGHT zone panel listing the performance
|
||||
// map's zones. An "Add Zone" button sits at the top of the zone panel; each zone row
|
||||
// carries nudge/delete mini-buttons (LICE has no native numeric entry field).
|
||||
|
||||
inline constexpr int kZoneRowHeight = 24;
|
||||
inline constexpr int kZonePanelFraction = 2; // zone panel gets the RIGHT 1/2 of the canvas
|
||||
inline constexpr int kZoneCtrlWidth = 20; // width of one nudge/delete mini-button
|
||||
inline constexpr int kAddZoneHeight = 22; // "Add Zone" button band height
|
||||
|
||||
// Clamps every rect to the canvas so a degenerate view still yields in-bounds rects.
|
||||
struct KeymapEditorLayout {
|
||||
EditorLayout base;
|
||||
Rect sampleList; // LEFT column
|
||||
Rect zonePanel; // RIGHT column
|
||||
Rect addZoneButton; // top of the zone panel
|
||||
Rect zoneRowArea; // below addZoneButton
|
||||
};
|
||||
|
||||
KeymapEditorLayout layoutKeymapEditor(int w, int h);
|
||||
|
||||
// Rect for bank-sample row `index` inside the LEFT column. Negative index -> empty.
|
||||
Rect keymapSampleRowRect(const KeymapEditorLayout& layout, int index);
|
||||
|
||||
// Bank-sample row a click lands on inside the left list, or -1 outside it.
|
||||
int keymapSampleRowHitTest(const KeymapEditorLayout& layout, int rowCount, int x, int y);
|
||||
|
||||
// Rect for zone row `index` inside zoneRowArea. Negative index -> empty.
|
||||
Rect zoneRowRect(const KeymapEditorLayout& layout, int index);
|
||||
|
||||
// A zone row's interactive fields: a label on the left, then seven fixed-width
|
||||
// mini-buttons on the right (low-, low+, high-, high+, root-, root+, delete). kZoneNone
|
||||
// means the click missed a control (e.g. the label) — the shell may still treat that as
|
||||
// "select this zone".
|
||||
enum class ZoneField {
|
||||
kZoneNone,
|
||||
kLowDown,
|
||||
kLowUp,
|
||||
kHighDown,
|
||||
kHighUp,
|
||||
kRootDown,
|
||||
kRootUp,
|
||||
kDelete,
|
||||
};
|
||||
|
||||
// Which zone row (or -1) and which field within it a click landed on. A click on
|
||||
// "Add Zone" is reported separately by addZoneHitTest.
|
||||
struct ZoneHit {
|
||||
int zoneIndex = -1;
|
||||
ZoneField field = ZoneField::kZoneNone;
|
||||
};
|
||||
|
||||
// Classify a click at (x, y) against `zoneCount` zone rows. {-1, kZoneNone} for a miss.
|
||||
// Within a row, the seven mini-buttons occupy fixed-width slots on the right edge; a
|
||||
// click left of those slots is {index, kZoneNone} (the label area — "select").
|
||||
ZoneHit zoneHitTest(const KeymapEditorLayout& layout, int zoneCount, int x, int y);
|
||||
|
||||
bool addZoneHitTest(const KeymapEditorLayout& layout, int x, int y);
|
||||
|
||||
// --- Sample / Zone face layout ------------------------------------------------
|
||||
//
|
||||
// The capture-first editor's band/cluster/zone-surface layout math. Draw and hit-test
|
||||
// both derive every rect from these formulas so they can never drift; the shell only
|
||||
// draws + routes. The Browse-modal layout lives in browser_scroll (its search box
|
||||
// height feeds it).
|
||||
|
||||
inline constexpr int kPad = 8;
|
||||
inline constexpr int kTitleHeight = 26;
|
||||
inline constexpr int kNavButtonWidth = 62; // Browse / Zone / Back title-band buttons
|
||||
|
||||
// Sample-face bands (top->bottom): TITLE (name + Browse/Zone nav), a full-width elastic
|
||||
// HERO (absorbs all height left after the fixed bands, floored), the root+preview
|
||||
// CLUSTER, and the bottom-anchored knob DECK (height `deckH` from knob_deck's wrap). A
|
||||
// window shorter than the hero floor clips the lower bands past the window bottom.
|
||||
struct SampleBands {
|
||||
Rect title;
|
||||
Rect navBrowse;
|
||||
Rect navZone;
|
||||
Rect hero; // waveform + envelope overlay
|
||||
Rect cluster; // root strip + preview + vel knob + curve button + channel toggle
|
||||
Rect deck;
|
||||
};
|
||||
SampleBands computeSampleBands(int w, int h, int deckH);
|
||||
|
||||
// Cluster sub-rects: the root strip keeps the left side at remainder width; the right
|
||||
// side is the fixed-width right-anchored run (Preview · vel knob cell · curve button ·
|
||||
// Mono|Stereo). `knobSize` is the deck knob square, passed in so this module does not
|
||||
// depend on knob_deck.
|
||||
struct ClusterRects {
|
||||
Rect rootStrip;
|
||||
Rect preview;
|
||||
Rect velCell; // preview-velocity knob cell (knob + label band)
|
||||
Rect velKnob;
|
||||
Rect velLabel;
|
||||
Rect curveBtn; // opens the curve-preview popup
|
||||
};
|
||||
ClusterRects clusterRects(const Rect& cluster, const Rect& chanMono, int knobSize);
|
||||
|
||||
// Mono/stereo toggle: a two-segment control right-anchored in `area`, vertically centered.
|
||||
struct ChannelToggleRects {
|
||||
Rect mono;
|
||||
Rect stereo;
|
||||
};
|
||||
ChannelToggleRects channelToggleRects(const Rect& area);
|
||||
|
||||
// Zone-view content area: the whole window below the title band.
|
||||
Rect zoneContentArea(int w, int h);
|
||||
|
||||
// Zone/Browse "Back" button — the same slot the Sample face's Zone nav button occupies.
|
||||
Rect zoneBackRect(int w, int h);
|
||||
|
||||
// "+ Add Zone" affordance and the "Delete" button beside it (Delete only draws/hits
|
||||
// when a zone is selected).
|
||||
Rect zoneAddRect(const Rect& content);
|
||||
Rect zoneDeleteRect(const Rect& addR);
|
||||
|
||||
// Zone-view keyboard strip rect: below "+ Add Zone" with a 12px gap, padded kPad
|
||||
// horizontally.
|
||||
Rect zonesStripArea(const Rect& content);
|
||||
|
||||
// Numeric-entry field row area inside the Zones legend, and the rect of field `f`
|
||||
// (0=low, 1=high, 2=root) within it — three equal segments left-to-right. Out-of-range
|
||||
// index yields an empty rect.
|
||||
Rect noteEntryFieldsArea(const Rect& content);
|
||||
Rect noteEntryFieldRect(const Rect& fields, int f);
|
||||
|
||||
// Per-zone parameter panel below the strip + legend, running to the content bottom; the
|
||||
// knob-deck area within it (a right column reserved for the curve-preview button); and
|
||||
// that button's rect (right-anchored at the panel top).
|
||||
Rect zonesControlPanel(const Rect& content);
|
||||
Rect zonesDeckArea(const Rect& content);
|
||||
Rect zonesCurveButton(const Rect& content);
|
||||
|
||||
} // namespace reasampler::instrument::ui
|
||||
|
||||
@@ -15,7 +15,7 @@ int clampNote(int n) {
|
||||
}
|
||||
|
||||
// Maps a key boundary (0..128) to an x pixel; keyEdge==128 maps to the band's right. A
|
||||
// zone's left uses floor(low) and its right uses floor(high+1), tiling adjacent zones
|
||||
// span's left uses floor(low) and its right uses floor(high+1), tiling adjacent spans
|
||||
// without a seam.
|
||||
int keyEdgeToX(int bandLeft, int bandWidth, int keyEdge) {
|
||||
if (keyEdge <= 0) return bandLeft;
|
||||
@@ -44,31 +44,19 @@ EmbedLayout layoutEmbed(int w, int h) {
|
||||
return out;
|
||||
}
|
||||
|
||||
Rect zoneSegmentRect(const EmbedLayout& layout, int lowNote, int highNote) {
|
||||
Rect keySpanRect(const EmbedLayout& layout, int lowNote, int highNote) {
|
||||
const Rect& band = layout.keymap;
|
||||
const int bandWidth = std::max(0, band.width);
|
||||
|
||||
int lo = clampNote(lowNote);
|
||||
int hi = clampNote(highNote);
|
||||
if (lo > hi) lo = hi; // defensive: a malformed zone collapses rather than inverts
|
||||
if (lo > hi) lo = hi; // defensive: a malformed span collapses rather than inverts
|
||||
|
||||
const int leftX = keyEdgeToX(band.x, bandWidth, lo);
|
||||
const int rightX = keyEdgeToX(band.x, bandWidth, hi + 1);
|
||||
return Rect::ltrb(leftX, band.y, std::max(leftX, rightX), band.bottom());
|
||||
}
|
||||
|
||||
int zoneAtPoint(const EmbedLayout& layout, const EmbedZone* zones, int zoneCount, int x,
|
||||
int y) {
|
||||
if (zoneCount <= 0 || zones == nullptr) return -1;
|
||||
if (!contains(layout.keymap, x, y)) return -1;
|
||||
// First covering zone in draw order wins (first-match, mirroring the core's resolve).
|
||||
for (int i = 0; i < zoneCount; ++i) {
|
||||
const Rect r = zoneSegmentRect(layout, zones[i].lowNote, zones[i].highNote);
|
||||
if (contains(r, x, y)) return i;
|
||||
}
|
||||
return -1; // on the band but on an uncovered key
|
||||
}
|
||||
|
||||
Rect levelFillRect(const EmbedLayout& layout, double level) {
|
||||
const Rect& band = layout.levelBand;
|
||||
if (band.width <= 0 || band.height <= 0) return Rect{};
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
// bitmap + mouse coords) into these functions.
|
||||
//
|
||||
// A single compact band REAPER draws inline in the track/mixer control panel via the
|
||||
// Cockos embedded-UI surface: each performance zone as a horizontal segment across the
|
||||
// keyboard span (MIDI 0..127 mapped to the strip width), plus a thin activity level band
|
||||
// at the bottom. Interaction is zone selection only — no editing.
|
||||
// Cockos embedded-UI surface: the loaded capture across the keyboard span (MIDI 0..127
|
||||
// mapped to the strip width) with its root marked, plus a thin activity level band at the
|
||||
// bottom. Read-only — the strip displays, it never edits.
|
||||
|
||||
#pragma once
|
||||
|
||||
@@ -19,18 +19,10 @@ inline constexpr int kEmbedKeyCount = 128;
|
||||
inline constexpr int kEmbedLevelBandHeight = 4;
|
||||
inline constexpr int kEmbedKeymapMinHeight = 6;
|
||||
|
||||
// One zone rendered on the strip: its inclusive MIDI key range — the minimal projection
|
||||
// of a PerformanceZone the strip needs (no sample ids or PCM). Expected in [0,127] with
|
||||
// low <= high; layout clamps defensively regardless.
|
||||
struct EmbedZone {
|
||||
int lowNote = 0;
|
||||
int highNote = 127;
|
||||
};
|
||||
|
||||
// Clamped to the area so a degenerate (tiny) size never yields a region spilling outside
|
||||
// the surface.
|
||||
struct EmbedLayout {
|
||||
Rect keymap; // top: zone-segment band
|
||||
Rect keymap; // top: keyboard-span band
|
||||
Rect levelBand; // bottom: level/activity indicator
|
||||
};
|
||||
|
||||
@@ -39,16 +31,11 @@ struct EmbedLayout {
|
||||
// kEmbedKeymapMinHeight); the keymap takes the rest.
|
||||
EmbedLayout layoutEmbed(int w, int h);
|
||||
|
||||
// Horizontal sub-rect of the keymap band for a zone spanning [lowNote, highNote]
|
||||
// (inclusive). Spans the half-open pixel range so adjacent zones tile without a gap or
|
||||
// overlap. Notes clamp to [0,127] and low clamps to <= high.
|
||||
Rect zoneSegmentRect(const EmbedLayout& layout, int lowNote, int highNote);
|
||||
|
||||
// Zone a click at (x, y) lands on, given zones in draw order, or -1 for a miss. When
|
||||
// zones overlap on a key, the first covering zone in order wins — mirroring the sampler
|
||||
// core's first-match Keymap::resolve, so selection agrees with playback.
|
||||
int zoneAtPoint(const EmbedLayout& layout, const EmbedZone* zones, int zoneCount, int x,
|
||||
int y);
|
||||
// Horizontal sub-rect of the keymap band for the inclusive key span [lowNote, highNote].
|
||||
// Spans the half-open pixel range so adjacent spans tile without a gap or overlap. Notes
|
||||
// clamp to [0,127] and low clamps to <= high. The loaded capture uses the full span; a
|
||||
// single-key span (low == high) is the root marker.
|
||||
Rect keySpanRect(const EmbedLayout& layout, int lowNote, int highNote);
|
||||
|
||||
// Filled portion of the level band for a 0..1 level (clamped); left sub-rect of levelBand
|
||||
// whose width is level * band width.
|
||||
|
||||
@@ -15,7 +15,7 @@ int clampNote(int n) {
|
||||
}
|
||||
|
||||
// Maps a key boundary (0..128) to an x pixel. Key N's left is keyEdgeToX(N), right is
|
||||
// keyEdgeToX(N+1) — tiles adjacent keys/zones without a seam. Mirrors embed_strip::keyEdgeToX.
|
||||
// keyEdgeToX(N+1) — tiles adjacent keys without a seam. Mirrors embed_strip::keyEdgeToX.
|
||||
int keyEdgeToX(int bandLeft, int bandWidth, int keyEdge) {
|
||||
if (keyEdge <= 0) return bandLeft;
|
||||
if (keyEdge >= kStripKeyCount) return bandLeft + bandWidth;
|
||||
@@ -62,41 +62,6 @@ int keyAtPoint(const StripLayout& layout, int x, int y) {
|
||||
return clampNote(note);
|
||||
}
|
||||
|
||||
Rect zoneBarRect(const StripLayout& layout, int lowNote, int highNote) {
|
||||
int lo = clampNote(lowNote);
|
||||
int hi = clampNote(highNote);
|
||||
if (lo > hi) lo = hi; // malformed zone collapses rather than inverts
|
||||
const int leftX = keyLeftX(layout, lo);
|
||||
const int rightX = keyLeftX(layout, hi + 1);
|
||||
return Rect::ltrb(leftX, layout.keys.y, std::max(leftX, rightX), layout.keys.bottom());
|
||||
}
|
||||
|
||||
ZoneGrab zoneGrabAt(const StripLayout& layout, int lowNote, int highNote, int x, int y) {
|
||||
const Rect bar = zoneBarRect(layout, lowNote, highNote);
|
||||
if (!contains(bar, x, y)) return ZoneGrab::kNone;
|
||||
|
||||
const int barW = bar.width;
|
||||
// A narrow bar has no body: split at the midpoint, low edge wins the tie.
|
||||
if (barW < 2 * kStripEdgeGrabWidth) {
|
||||
const int mid = bar.x + barW / 2;
|
||||
return x <= mid ? ZoneGrab::kLowEdge : ZoneGrab::kHighEdge;
|
||||
}
|
||||
if (x < bar.x + kStripEdgeGrabWidth) return ZoneGrab::kLowEdge;
|
||||
if (x >= bar.right() - kStripEdgeGrabWidth) return ZoneGrab::kHighEdge;
|
||||
return ZoneGrab::kBody;
|
||||
}
|
||||
|
||||
ZoneBarHit zoneBarAtPoint(const StripLayout& layout, const int* lows, const int* highs,
|
||||
int count, int x, int y) {
|
||||
if (count <= 0 || lows == nullptr || highs == nullptr) return ZoneBarHit{};
|
||||
if (!contains(layout.keys, x, y)) return ZoneBarHit{};
|
||||
for (int i = 0; i < count; ++i) {
|
||||
const ZoneGrab g = zoneGrabAt(layout, lows[i], highs[i], x, y);
|
||||
if (g != ZoneGrab::kNone) return ZoneBarHit{i, g};
|
||||
}
|
||||
return ZoneBarHit{}; // on the band but on no bar
|
||||
}
|
||||
|
||||
bool isNaturalKey(int note) {
|
||||
const int n = note < 0 ? 0 : (note > kStripKeyCount - 1 ? kStripKeyCount - 1 : note);
|
||||
static constexpr bool kNatural[12] = {
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
// keyboard_strip.h — layout + hit-test + drag math for the capture-first editor's
|
||||
// keyboard strip. Mirror of editor_geometry/embed_strip/mode_switch; the shell draws
|
||||
// and marshals mouse events into these functions.
|
||||
// keyboard_strip.h — layout + hit-test + drag math for the editor's keyboard strip.
|
||||
// Mirror of embed_strip/mode_switch; the shell draws and marshals mouse events into these
|
||||
// functions.
|
||||
//
|
||||
// The strip maps the full 128-key MIDI span across a horizontal band (the same idiom
|
||||
// embed_strip uses) and serves two faces: the single-capture fast path (a root marker,
|
||||
// click-a-key or drag it to set root) and the opt-in zones panel (each zone drawn as a
|
||||
// bar with edge-grab resize handles + a body move-handle).
|
||||
// embed_strip uses). The loaded capture responds across that whole span, so the strip's
|
||||
// job is the root marker: click a key, or drag the marker, to set the root.
|
||||
|
||||
#pragma once
|
||||
|
||||
@@ -17,10 +16,6 @@ namespace reasampler::instrument::ui {
|
||||
// stay independent.
|
||||
inline constexpr int kStripKeyCount = 128;
|
||||
|
||||
// Pixel width of a zone bar's edge-grab region. A zone narrower than 2x this has no
|
||||
// body move-handle (both edges win their halves).
|
||||
inline constexpr int kStripEdgeGrabWidth = 6;
|
||||
|
||||
// The keys band takes the whole strip area today; clamped so a degenerate size never
|
||||
// yields an inverted rect.
|
||||
struct StripLayout {
|
||||
@@ -37,44 +32,15 @@ int keyLeftX(const StripLayout& layout, int note);
|
||||
// Half-open rect of a single key `note`, clamped to [0,127].
|
||||
Rect keyRect(const StripLayout& layout, int note);
|
||||
|
||||
// Root-marker rect for the single-capture fast path; equivalent to
|
||||
// keyRect(layout, rootNote) but named so the intent reads at the call site.
|
||||
// Root-marker rect; equivalent to keyRect(layout, rootNote) but named so the intent reads
|
||||
// at the call site.
|
||||
Rect rootMarkerRect(const StripLayout& layout, int rootNote);
|
||||
|
||||
// MIDI note a point (x, y) lands on, or -1 outside the keys band.
|
||||
int keyAtPoint(const StripLayout& layout, int x, int y);
|
||||
|
||||
// Horizontal sub-rect for a zone spanning [lowNote, highNote] inclusive. Notes clamp to
|
||||
// [0,127] and low clamps to <= high, so a malformed zone never yields an inverted rect.
|
||||
Rect zoneBarRect(const StripLayout& layout, int lowNote, int highNote);
|
||||
|
||||
// Which part of a zone bar a grab landed on: an edge resizes that boundary, the body
|
||||
// moves the whole span, kNone means the grab missed the bar.
|
||||
enum class ZoneGrab {
|
||||
kNone,
|
||||
kLowEdge,
|
||||
kHighEdge,
|
||||
kBody,
|
||||
};
|
||||
|
||||
// Classify a grab at (x, y) against one zone's bar. A narrow bar (< 2*kStripEdgeGrabWidth)
|
||||
// resolves the near half to each edge (no body); the low edge wins a tie at the exact
|
||||
// midpoint.
|
||||
ZoneGrab zoneGrabAt(const StripLayout& layout, int lowNote, int highNote, int x, int y);
|
||||
|
||||
// Zone (index into the parallel `lows`/`highs` arrays, draw order) whose bar a grab
|
||||
// lands on, plus which part, or {-1, kNone} for a miss. First covering zone in draw
|
||||
// order wins.
|
||||
struct ZoneBarHit {
|
||||
int zoneIndex = -1;
|
||||
ZoneGrab grab = ZoneGrab::kNone;
|
||||
};
|
||||
ZoneBarHit zoneBarAtPoint(const StripLayout& layout, const int* lows, const int* highs,
|
||||
int count, int x, int y);
|
||||
|
||||
// Resolves a drag to a new MIDI note: `startNote` shifted by round(dxPixels / keyWidth),
|
||||
// clamped to [0,127]. The one arithmetic behind edge-resize, body-move (apply to both
|
||||
// edges with the same delta to preserve span), and root-marker drag.
|
||||
// clamped to [0,127]. The one arithmetic behind the root-marker drag.
|
||||
int resolveDragNote(const StripLayout& layout, int startNote, int dxPixels);
|
||||
|
||||
// True when `note` (clamped to [0,127]) is a natural (white) key in 12-tone equal
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
// sample_bands.cpp — see sample_bands.h. Pure math; no host types.
|
||||
|
||||
#include "core/instrument/ui/sample_bands.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace reasampler::instrument::ui {
|
||||
|
||||
SampleBands computeSampleBands(int w, int h, int deckHeight) {
|
||||
const int cw = std::max(0, w);
|
||||
const int ch = std::max(0, h);
|
||||
const int deckH = std::max(0, deckHeight);
|
||||
|
||||
SampleBands b;
|
||||
const int chromeH = std::min(kTitleHeight + kChromeRowHeight, ch);
|
||||
b.chrome = Rect::ltrb(0, 0, cw, chromeH);
|
||||
|
||||
// Decks are bottom-anchored so the deck row sits on the window edge at any height; the
|
||||
// waveform absorbs whatever is left. When that leaves less than the two-lane floor the
|
||||
// FLOOR WINS and the deck band is pushed past the window bottom (clipped) rather than
|
||||
// squeezing the waveform into an unreadable sliver.
|
||||
int deckTop = ch - kPad - deckH;
|
||||
int waveTop = chromeH + kBandGap;
|
||||
int waveBottom = deckTop - kBandGap;
|
||||
if (waveBottom - waveTop < kWaveformMinHeight) {
|
||||
waveBottom = waveTop + kWaveformMinHeight;
|
||||
deckTop = waveBottom + kBandGap;
|
||||
}
|
||||
|
||||
b.waveform = Rect::ltrb(kPad, waveTop, std::max(kPad, cw - kPad), waveBottom);
|
||||
b.decks = Rect::ltrb(kPad, deckTop, std::max(kPad, cw - kPad), deckTop + deckH);
|
||||
return b;
|
||||
}
|
||||
|
||||
WaveformLanes waveformLanes(const Rect& waveform, bool stereo) {
|
||||
WaveformLanes lanes;
|
||||
if (waveform.empty()) return lanes;
|
||||
if (!stereo) {
|
||||
lanes.upper = waveform; // one lane; `lower` stays empty
|
||||
return lanes;
|
||||
}
|
||||
// Split the usable height evenly, giving the seam to the gap. An odd remainder goes to
|
||||
// the upper (left) lane so the two lanes never disagree about the seam row.
|
||||
const int usable = std::max(0, waveform.height - kLaneGap);
|
||||
const int lowerH = usable / 2;
|
||||
const int upperH = usable - lowerH;
|
||||
const int upperBottom = waveform.y + upperH;
|
||||
lanes.upper = Rect::ltrb(waveform.x, waveform.y, waveform.right(), upperBottom);
|
||||
lanes.lower = Rect::ltrb(waveform.x, upperBottom + kLaneGap, waveform.right(),
|
||||
waveform.bottom());
|
||||
return lanes;
|
||||
}
|
||||
|
||||
} // namespace reasampler::instrument::ui
|
||||
@@ -0,0 +1,53 @@
|
||||
#pragma once
|
||||
// sample_bands.h — THE band-stack allocator for the Sample face: the one module that owns
|
||||
// the editor's vertical inventory. Three bands, top to bottom — CHROME (toolbar + control
|
||||
// row), WAVEFORM (elastic, sized to hold two stacked channel lanes), DECKS (the knob-deck
|
||||
// row). Everything else in the editor fills a band it is handed; nothing else allocates
|
||||
// vertical space, so a band's owner can re-lay its interior without moving its neighbours.
|
||||
|
||||
#include "core/instrument/ui/editor_geometry.h" // Rect
|
||||
|
||||
namespace reasampler::instrument::ui {
|
||||
|
||||
// Shared outer inset every band honours horizontally.
|
||||
inline constexpr int kPad = 8;
|
||||
|
||||
// Chrome band: the toolbar row (title + nav) stacked over the control row (piano strip,
|
||||
// preview, velocity knob, curve button, channel toggle). sample_chrome partitions it.
|
||||
inline constexpr int kTitleHeight = 26;
|
||||
inline constexpr int kChromeRowHeight = 52;
|
||||
|
||||
// Waveform band floor: two stacked lanes plus the seam between them. The band never shrinks
|
||||
// below this — a window too short for it clips the bands beneath instead, so the waveform
|
||||
// stays a usable two-lane surface at every size.
|
||||
inline constexpr int kLaneMinHeight = 74;
|
||||
inline constexpr int kLaneGap = 2;
|
||||
inline constexpr int kWaveformMinHeight = 2 * kLaneMinHeight + kLaneGap;
|
||||
|
||||
// Vertical seam between adjacent bands.
|
||||
inline constexpr int kBandGap = 4;
|
||||
|
||||
// The vertical inventory. Bands never overlap and are returned top-to-bottom; a band may be
|
||||
// empty() on a degenerate window, in which case its owner draws and hit-tests nothing.
|
||||
struct SampleBands {
|
||||
Rect chrome; // full width: toolbar row + control row
|
||||
Rect waveform; // kPad-inset, elastic, >= kWaveformMinHeight
|
||||
Rect decks; // kPad-inset, bottom-anchored, height `deckHeight`
|
||||
};
|
||||
|
||||
// Divide a (w x h) client area into the three bands. `deckHeight` is the knob deck's own
|
||||
// wrapped height (from knob_deck) — the only interior measurement the allocator needs, so
|
||||
// the deck band is exactly as tall as its content. Pure.
|
||||
SampleBands computeSampleBands(int w, int h, int deckHeight);
|
||||
|
||||
// The waveform band's two channel lanes: L above R, separated by kLaneGap. In mono only
|
||||
// `upper` is populated (it takes the whole band) and `lower` is empty — a mono capture has
|
||||
// no second lane to draw, and overlays that ride the waveform draw ONCE across the whole
|
||||
// band in either mode, never per lane.
|
||||
struct WaveformLanes {
|
||||
Rect upper;
|
||||
Rect lower; // empty() in mono
|
||||
};
|
||||
WaveformLanes waveformLanes(const Rect& waveform, bool stereo);
|
||||
|
||||
} // namespace reasampler::instrument::ui
|
||||
@@ -0,0 +1,71 @@
|
||||
// sample_chrome.cpp — see sample_chrome.h. Pure math; no host types.
|
||||
|
||||
#include "core/instrument/ui/sample_chrome.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include "core/instrument/ui/sample_bands.h" // kPad / kTitleHeight / kChromeRowHeight
|
||||
|
||||
namespace reasampler::instrument::ui {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr int kStripBandHeight = 40; // the root/piano strip's own height inside the row
|
||||
|
||||
// The control row's fixed right-anchored run, right to left.
|
||||
constexpr int kChanSegW = 52;
|
||||
constexpr int kChanSegH = 18;
|
||||
constexpr int kCurveBtnSize = 28;
|
||||
constexpr int kVelCellW = 48;
|
||||
constexpr int kPreviewBtnW = 64;
|
||||
|
||||
} // namespace
|
||||
|
||||
ChromeRects chromeRects(const Rect& chrome, int knobSize) {
|
||||
ChromeRects r;
|
||||
if (chrome.empty()) return r;
|
||||
|
||||
const int titleH = std::min(kTitleHeight, chrome.height);
|
||||
r.toolbar = Rect::ltrb(chrome.x, chrome.y, chrome.right(), chrome.y + titleH);
|
||||
r.controls = Rect::ltrb(chrome.x, r.toolbar.bottom(), chrome.right(), chrome.bottom());
|
||||
|
||||
const int navTop = r.toolbar.y + 2;
|
||||
const int navBot = std::max(navTop, r.toolbar.bottom() - 2);
|
||||
r.navBrowse = Rect::ltrb(std::max(chrome.x, chrome.right() - kPad - kNavButtonWidth),
|
||||
navTop, std::max(chrome.x, chrome.right() - kPad), navBot);
|
||||
|
||||
if (r.controls.empty()) return r;
|
||||
const Rect& row = r.controls;
|
||||
|
||||
// Vertically centre the two heights the row uses: the tall strip band (which the preview
|
||||
// button and velocity cell align to) and the smaller square/segment controls.
|
||||
const int stripTop = row.y + (row.height - kStripBandHeight) / 2;
|
||||
const int stripBot = stripTop + kStripBandHeight;
|
||||
|
||||
const int chanTop = row.y + (row.height - kChanSegH) / 2;
|
||||
const int chanRight = row.right() - kPad;
|
||||
r.chanStereo = Rect::ltrb(chanRight - kChanSegW, chanTop, chanRight, chanTop + kChanSegH);
|
||||
r.chanMono = Rect::ltrb(r.chanStereo.x - kChanSegW, chanTop, r.chanStereo.x,
|
||||
chanTop + kChanSegH);
|
||||
|
||||
const int curveTop = row.y + (row.height - kCurveBtnSize) / 2;
|
||||
r.curveBtn = Rect::ltrb(r.chanMono.x - kPad - kCurveBtnSize, curveTop,
|
||||
r.chanMono.x - kPad, curveTop + kCurveBtnSize);
|
||||
|
||||
r.velCell = Rect::ltrb(r.curveBtn.x - kPad - kVelCellW, stripTop,
|
||||
r.curveBtn.x - kPad, stripBot);
|
||||
const int knobLeft = r.velCell.x + (kVelCellW - knobSize) / 2;
|
||||
r.velKnob = Rect::ltrb(knobLeft, r.velCell.y, knobLeft + knobSize,
|
||||
r.velCell.y + knobSize);
|
||||
r.velLabel = Rect::ltrb(r.velCell.x, r.velKnob.bottom(), r.velCell.right(),
|
||||
r.velCell.bottom());
|
||||
|
||||
r.preview = Rect::ltrb(r.velCell.x - kPad - kPreviewBtnW, stripTop,
|
||||
r.velCell.x - kPad, stripBot);
|
||||
// Remainder width; clamped so a narrow window collapses the strip rather than inverting it.
|
||||
r.rootStrip = Rect::ltrb(row.x + kPad, stripTop,
|
||||
std::max(row.x + kPad, r.preview.x - kPad), stripBot);
|
||||
return r;
|
||||
}
|
||||
|
||||
} // namespace reasampler::instrument::ui
|
||||
@@ -0,0 +1,34 @@
|
||||
#pragma once
|
||||
// sample_chrome.h — interior geometry of the Sample face's CHROME band: the toolbar row
|
||||
// (title + Browse) over the control row (root/piano strip, preview trigger, preview-velocity
|
||||
// knob cell, curve-preview button, Mono|Stereo toggle). Reads the band rect the allocator
|
||||
// hands it (sample_bands) and never allocates vertical space of its own.
|
||||
|
||||
#include "core/instrument/ui/editor_geometry.h" // Rect
|
||||
|
||||
namespace reasampler::instrument::ui {
|
||||
|
||||
inline constexpr int kNavButtonWidth = 62; // the Browse toolbar button
|
||||
|
||||
// Every interactive rect inside the chrome band, in one pass so draw and hit-test cannot
|
||||
// derive them differently. The control row's right-anchored run is fixed-width (preview,
|
||||
// velocity cell, curve button, channel toggle) and the root strip takes the remainder, so
|
||||
// the strip grows with the window.
|
||||
struct ChromeRects {
|
||||
Rect toolbar; // full-width top row
|
||||
Rect navBrowse; // right-anchored in the toolbar
|
||||
Rect controls; // full-width second row
|
||||
Rect rootStrip; // remainder-width, left
|
||||
Rect preview;
|
||||
Rect velCell; // preview-velocity knob cell (knob + label band)
|
||||
Rect velKnob;
|
||||
Rect velLabel;
|
||||
Rect curveBtn; // opens the velocity-curve popup
|
||||
Rect chanMono;
|
||||
Rect chanStereo;
|
||||
};
|
||||
|
||||
// `knobSize` is the deck knob square, passed in so this module does not depend on knob_deck.
|
||||
ChromeRects chromeRects(const Rect& chrome, int knobSize);
|
||||
|
||||
} // namespace reasampler::instrument::ui
|
||||
Reference in New Issue
Block a user