docs(multi-bank): settle Phase B forks 1-4; draft fork-5 options analysis
Fold Daniel's decisions into CONTEXT.md, PLAN.md, and the product notes: - Fork 1 (persistence): pool folds into the `banks` blob; legacy `bank_index` key retired after a one-way lossless migration. - Fork 2 (delete): delete drops member index entries; add an `evacuate` verb (B1 pure op + B3 action) returning members to the pool. Design the orphaned-until-prune window and confirm-on-non-empty-delete guardrail. - Fork 3 (move): move is the default gesture, copy the deliberate secondary. - Fork 4 (active bank): active-bank and shown-tab stay distinct; the active-bank indicator must be visually unmistakable. Fork 5 (tab rendering + move affordance) remains open with an options analysis and pending recommendations. Additive; M0-M11 and Phase D untouched.
This commit is contained in:
+287
-90
@@ -6,8 +6,9 @@ detail in `CONTEXT.md` (§Multi-bank). This doc holds the *why* — the workflow
|
||||
narrative, the pool-privilege reasoning, the movement semantics, and the
|
||||
design-direction recommendations — so those don't clutter the build docs.
|
||||
|
||||
Status: framed by product-designer (2026-07-23), pending Daniel review. Open items
|
||||
for Daniel are listed at the bottom.
|
||||
Status: framed by product-designer (2026-07-23). Forks 1–4 settled by Daniel
|
||||
(2026-07-23); fork 5 (tab rendering + move affordance) still open — options
|
||||
analysis at the bottom. Settled forks are folded into the prose below.
|
||||
|
||||
---
|
||||
|
||||
@@ -18,9 +19,11 @@ banks.** Today ReaSampler has exactly one bank (`BankIndex`) — every capture l
|
||||
in it, and it is the whole library. That is fine at ten samples and unusable at two
|
||||
hundred. Multi-bank keeps the one bank as **the pool** — the default catch-all
|
||||
every capture still lands in — and lets the user create **named banks** ("Drums",
|
||||
"1-Shots", "Synth Hits") that group samples for a purpose. Samples move (or copy)
|
||||
freely between any banks, including to and from the pool. One bank at a time is the
|
||||
**active bank**: the capture target. The pool is active by default.
|
||||
"1-Shots", "Synth Hits") that group samples for a purpose. **Move** is the primary
|
||||
gesture — a sample relocates from one bank to another, living in exactly one bank at
|
||||
a time — with **copy** the deliberate secondary act when the user wants a sample in
|
||||
two banks at once. One bank at a time is the **active bank**: the capture target. The
|
||||
pool is active by default.
|
||||
|
||||
**It is not a new file layout.** Banks are *logical groupings over one shared file
|
||||
pool*, not folders on disk. Moving a sample between banks moves an index entry, not
|
||||
@@ -94,14 +97,17 @@ tested core, not a modification of it.
|
||||
|
||||
## Movement semantics (the settled rules, in prose)
|
||||
|
||||
- **Move is index-only.** Moving a sample from bank A to bank B removes the `Sample`
|
||||
from A's `BankIndex` and adds it to B's. The file never moves — one shared file
|
||||
pool under the project bank folder, exactly where M4 put it. Cheap,
|
||||
non-destructive, immune to the path-rewrite bug class M4 closed.
|
||||
- **Copy is index-only too.** Copy adds the sample to B and *leaves it in A*. Two
|
||||
index entries, one file, two banks. Copy is the mechanism that lets a sample live
|
||||
in the pool *and* in a named group at once — the pool stays the complete library,
|
||||
and "Drums" is a curated view into part of it.
|
||||
- **Move is index-only, and is the default gesture.** Moving a sample from bank A to
|
||||
bank B removes the `Sample` from A's `BankIndex` and adds it to B's. The file never
|
||||
moves — one shared file pool under the project bank folder, exactly where M4 put it.
|
||||
Cheap, non-destructive, immune to the path-rewrite bug class M4 closed. After a move
|
||||
the sample lives in exactly one bank; this is the primary, low-friction affordance
|
||||
(see design-direction, below).
|
||||
- **Copy is index-only too, and is the deliberate secondary act.** Copy adds the
|
||||
sample to B and *leaves it in A*. Two index entries, one file, two banks. Copy is
|
||||
the mechanism that lets a sample live in the pool *and* in a named group at once —
|
||||
or in two named groups. It is the explicit way to keep a sample in more than one
|
||||
place; move (the default) puts it in one place.
|
||||
- **Dedup-by-hash is per-bank, and collapse is observed across a move.** Each
|
||||
`BankIndex` dedups within itself, unchanged. If you move (or copy) a sample into a
|
||||
bank that already holds its hash, the destination *collapses* onto its existing
|
||||
@@ -127,10 +133,18 @@ trivially correct.
|
||||
## Persistence and migration
|
||||
|
||||
The book rides the existing `"reasampler"` project ext-state namespace under a new
|
||||
key, `banks`, alongside `bank_index`, `view_state`, and `project_guid`. Same
|
||||
namespace, same travel-with-the-`.rpp` guarantee, same GUID-primary identity and
|
||||
Save-As-relocation machinery from M4. One shared physical bank folder; one
|
||||
ext-state namespace; now three logical sections.
|
||||
key, `banks`, alongside `view_state` and `project_guid`. Same namespace, same
|
||||
travel-with-the-`.rpp` guarantee, same GUID-primary identity and Save-As-relocation
|
||||
machinery from M4. One shared physical bank folder; one ext-state namespace.
|
||||
|
||||
**The pool folds into the `banks` blob; the legacy `bank_index` key is retired
|
||||
(fork 1, settled (a)).** The pool's index is stored *inside* the `banks` blob as
|
||||
bank-zero, exactly like every named bank — one blob, one section, one JSON shape.
|
||||
The old top-level `bank_index` key is not written going forward; after migration the
|
||||
`banks` blob is the single authoritative source for all bank state including the
|
||||
pool. This is the cleaner shape and keeps the "pool is just bank-zero" data-model
|
||||
uniformity honest all the way down to storage: the pool is not persisted
|
||||
differently from a named bank.
|
||||
|
||||
**Migration of an existing single-bank project is the load-bearing edge case.** A
|
||||
project saved before this phase has a `bank_index` key and no `banks` key. On load,
|
||||
@@ -140,12 +154,17 @@ the user opens their old project and finds everything in the pool exactly as bef
|
||||
now with the ability to add named banks. This must be a first-class, tested path
|
||||
(B1 pure-model migration + B2 in-DAW load), not an afterthought.
|
||||
|
||||
There is one genuine fork here, called out in the open items: whether the pool's
|
||||
index lives *inside* the `banks` blob (retire the legacy `bank_index` key) or the
|
||||
`bank_index` key is *kept* as the pool's canonical slot with `banks` holding only
|
||||
named banks. Both migrate cleanly; the trade is cleanliness (one blob) vs.
|
||||
additive-minimalism (existing pool persistence untouched, named banks pure
|
||||
addition). The lean recommendation is the conservative one — see open items.
|
||||
**The migration is a one-time promotion, and it is one-way.** On load: legacy
|
||||
`bank_index` present, `banks` absent → promote the legacy index into the pool inside
|
||||
a freshly-minted `banks` blob, then treat the blob as authoritative. Once the `banks`
|
||||
blob exists it is the only source consulted; the legacy key is not read back or
|
||||
kept in sync. A project touched by this version and saved will carry `banks` and
|
||||
(going forward) will not carry a written `bank_index` — the retirement is complete
|
||||
after the first save. The promotion is lossless: no sample, no path, no ordinal is
|
||||
dropped. This trades a small amount of forward-only commitment (a project saved by
|
||||
this version will not re-open cleanly in a pre-multi-bank build that only knows
|
||||
`bank_index`) for the clean single-blob shape — an acceptable, one-directional
|
||||
migration, consistent with how M4 project state already moves forward.
|
||||
|
||||
---
|
||||
|
||||
@@ -159,31 +178,87 @@ are properties of the capture path and the file. The invariants it *does* touch:
|
||||
`BankIndex` in the book, by the exact code that enforces it today. Movement adds
|
||||
no path handling because files never relocate.
|
||||
- **Non-destructive** extends to bank operations: create / rename / delete /
|
||||
activate and sample move / copy mutate only index + ext-state. No file is written,
|
||||
moved, or deleted; no timeline item is touched. In particular, **deleting a named
|
||||
bank does not delete its samples' files** — a file may be referenced by the pool
|
||||
or another bank via copy, and file lifecycle stays owned by the capture/prune
|
||||
path, never the bank container.
|
||||
activate / evacuate and sample move / copy mutate only index + ext-state. No file
|
||||
is written, moved, or deleted; no timeline item is touched. In particular,
|
||||
**deleting a named bank drops its member index entries but does not delete their
|
||||
files** — file lifecycle stays owned by the capture/prune path, never the bank
|
||||
container (fork 2, settled). A file that was referenced *only* by the deleted bank
|
||||
becomes an orphan on disk — present but indexed by no bank — until the
|
||||
capture/prune path reclaims it. That orphaned-until-prune window is a designed
|
||||
consequence of "delete drops members, files persist," not an accident (see
|
||||
*design-direction → evacuate* below for how the workflow avoids it).
|
||||
- **Travels-with-the-.rpp** is preserved by riding the M4 machinery unchanged.
|
||||
|
||||
---
|
||||
|
||||
## Design-direction recommendations (opinionated)
|
||||
## Design-direction (settled forks 2–4, plus rationale)
|
||||
|
||||
### The pool stays the complete library; named banks are curated views
|
||||
### Move is the default; the pool is a bank, not a permanent mirror
|
||||
|
||||
The strongest mental model — and the one the index-only, copy-friendly design is
|
||||
built for — is: **the pool is everything you've captured; named banks are curated
|
||||
subsets you assemble by hand.** Copy (not move) into a named bank keeps the pool
|
||||
complete, so the user can always fall back to "it's in the pool somewhere" while
|
||||
"Drums" stays a clean working set. Move is there for the user who wants a strict
|
||||
partition, but copy is the gentler default the UI should make easy. This is
|
||||
borrowed from playlist-vs-library models (music apps, Lightroom collections): the
|
||||
library is authoritative and complete; collections are lightweight views over it.
|
||||
**Settled (fork 3): move is the primary, low-friction gesture; copy is the
|
||||
deliberate secondary act.** This inverts the earlier framing (which leaned
|
||||
copy-as-default on a "pool is the complete library, banks are views" model). Daniel's
|
||||
call is a *partition* mental model: a sample lives in one bank at a time, and moving
|
||||
it to "Drums" takes it *out* of the pool. Copy remains available for the user who
|
||||
genuinely wants a sample in two banks at once, but it is the explicit, deliberate act
|
||||
— not the default.
|
||||
|
||||
Recommendation: make **copy the low-friction gesture** (drag, or a one-click "add to
|
||||
bank") and **move the deliberate one** (explicit "move" menu item). Do not force the
|
||||
user to choose partition semantics up front.
|
||||
This changes what "the pool is the complete library" means, and the docs must be
|
||||
honest about it. Under move-as-default the pool is **not** a permanent mirror of
|
||||
everything ever captured — it is the default catch-all every capture lands in *and*
|
||||
the from-which/back-to-which home for organizing, but once a user moves a sample into
|
||||
a named bank it is no longer in the pool. The honest framing is:
|
||||
|
||||
- **The pool is the default home and the staging ground**, not a guaranteed superset.
|
||||
Fresh captures land there; from there the user *moves* them into named banks. What
|
||||
is "in the pool" is what hasn't been filed yet plus whatever the user deliberately
|
||||
keeps there.
|
||||
- **Named banks are where filed samples live**, not lightweight views over a
|
||||
still-complete library. A sample in "Drums" is *in Drums* and nowhere else unless
|
||||
copied.
|
||||
- **Copy is the escape hatch** for the "I want this in two places" case — borrowed
|
||||
from the move-vs-copy distinction every file manager makes. It is deliberately the
|
||||
less prominent of the two so the default stays a clean partition.
|
||||
|
||||
Prior art: this is the file-manager / DAW-media-pool model (move relocates, copy
|
||||
duplicates) rather than the playlist-library model. It suits a capture-heavy workflow
|
||||
where the user is actively filing rather than curating views over a fixed corpus.
|
||||
|
||||
### Evacuate: the "keep the samples, drop the grouping" gesture
|
||||
|
||||
**Settled (fork 2): delete drops a bank's member index entries; a separate
|
||||
*evacuate* operation moves all members back to the pool.** These are two distinct
|
||||
verbs for two distinct intents:
|
||||
|
||||
- **Delete** removes the bank *and its member index entries*. Files are not touched
|
||||
(capture/prune owns file lifecycle). Use it when you want the grouping *and its
|
||||
contents' index entries* gone.
|
||||
- **Evacuate** moves every member of the bank back to the pool (index-only, observing
|
||||
the same destination-collapse-by-hash as a normal move), leaving the bank empty.
|
||||
Use it when you want to dissolve the grouping *but keep the samples*.
|
||||
|
||||
The intended workflow is **"evacuate, then delete"** when the user wants to keep the
|
||||
samples and drop only the grouping, versus a **plain delete** when they want the
|
||||
grouping and its members gone. Evacuate is a pure `bank_book` operation (B1) and a
|
||||
bindable action (B3). Pool-privilege rules still apply: you cannot evacuate the pool
|
||||
(it has nowhere to evacuate *to*, and it is the evacuation destination).
|
||||
|
||||
**The non-empty-delete hazard, and the recommended guardrail.** Because delete drops
|
||||
members (rather than reabsorbing them, the earlier lean) and move is the default (so a
|
||||
sample is often in exactly one bank), a plain delete of a *non-empty* named bank can
|
||||
orphan those samples out of every index — their files persist on disk until prune,
|
||||
but no bank references them. That is the orphaned-until-prune window called out in the
|
||||
precision-invariant section. It is a real footgun: "I deleted Drums and my kick is
|
||||
gone from the library."
|
||||
|
||||
Recommendation (B4, panel-polish, pending Daniel): **confirm on non-empty delete**,
|
||||
with the confirmation naming the escape hatch. Something like *"Delete 'Drums'? Its 14
|
||||
samples will be removed from the library (files remain on disk until pruned).
|
||||
Evacuate to pool instead?"* — a three-way affordance (Delete / Evacuate then delete /
|
||||
Cancel) turns the hazard into a teaching moment for the evacuate verb. An *empty*
|
||||
bank deletes with no confirmation (nothing to lose). This keeps delete fast where it's
|
||||
safe and guarded where it bites. I recommend adopting the confirm-on-non-empty
|
||||
guardrail; the exact copy and button set are a B4 detail.
|
||||
|
||||
### The vertical split: pool on top, named banks as a tab strip below
|
||||
|
||||
@@ -203,20 +278,25 @@ curated views are the swappable bottom region.
|
||||
(hide the pool — "I'm organizing"). These are mode-of-work toggles, not layout
|
||||
fiddling, and they map to the two halves of the workflow (capture vs. curate).
|
||||
|
||||
### Keep "active bank" distinct from "shown tab"
|
||||
### Active bank and shown tab stay distinct (settled fork 4)
|
||||
|
||||
A subtle but important call: the **capture-active** bank (where new captures land)
|
||||
and the **currently-shown** named-bank tab are *different* concepts. You might be
|
||||
looking at "Drums" while capturing into the pool, or vice versa. Conflating them
|
||||
("the tab you're viewing is the capture target") would make it too easy to
|
||||
capture into the wrong bank by merely browsing.
|
||||
**Settled (fork 4): the capture-active bank and the currently-shown named-bank tab
|
||||
are distinct concepts, and the active bank carries an unmistakable visual
|
||||
indicator.** The **capture-active** bank (where new captures land) and the
|
||||
**currently-shown** tab are *different* things — you might be looking at "Drums" while
|
||||
capturing into the pool, or vice versa. Browsing a tab **never** retargets capture.
|
||||
Conflating them ("the tab you're viewing is the capture target") was considered and
|
||||
rejected: it makes it too easy to capture into the wrong bank by merely browsing, and
|
||||
capture-into-wrong-bank is a silent error the user won't notice until later.
|
||||
|
||||
Recommendation: **active bank is an explicit state with a clear indicator**, set by
|
||||
an explicit "activate" affordance (and action), *not* implied by which tab is shown.
|
||||
The pool is active by default; activating a named bank is a deliberate act. The
|
||||
indicator should be unmistakable — the capture target is a thing you want to be sure
|
||||
of before you hit capture. Placement (per-region header vs. single readout) is a
|
||||
panel-polish open item.
|
||||
Active bank is an explicit state, set by an explicit "activate" affordance (and
|
||||
action), *not* implied by which tab is shown. The pool is active by default;
|
||||
activating a named bank is a deliberate act. **The indicator must be visually
|
||||
unmistakable** — this is now a settled constraint, not a suggestion. The capture
|
||||
target is a thing the user must be able to verify at a glance before hitting capture.
|
||||
The indicator's exact placement (per-region header vs. single header readout vs.
|
||||
lit-tab treatment) remains a B4 panel-polish detail — but that it reads unmistakably
|
||||
is fixed.
|
||||
|
||||
### The Design View mode switch and multi-bank are orthogonal — and should read that way
|
||||
|
||||
@@ -247,11 +327,14 @@ Stable command-id strings follow the sampler family prefix. Bank-activate and
|
||||
move/copy are MIDI-bindable to suit the capture-heavy, hands-on workflow.
|
||||
|
||||
- **Create bank** / **Rename bank** / **Delete bank** — manage the named-bank set.
|
||||
(Pool is un-deletable / un-renamable — the actions refuse on the pool.)
|
||||
(Pool is un-deletable / un-renamable — the actions refuse on the pool.) Delete on a
|
||||
non-empty bank confirms (see evacuate, above).
|
||||
- **Evacuate bank → pool** — move all of a bank's members back to the pool, leaving
|
||||
it empty; the "keep the samples, drop the grouping" verb. (Refuses on the pool.)
|
||||
- **Activate bank** (direct-by-id) / **Cycle active bank** — set the capture target;
|
||||
distinct from browsing a tab.
|
||||
- **Move selected samples → bank** / **Copy selected samples → bank** — the two
|
||||
movement verbs; copy the low-friction default per the recommendation above.
|
||||
movement verbs; move is the low-friction default, copy the deliberate secondary.
|
||||
- **Pool full-height** / **Banks full-height** (toggles) — collapse the split.
|
||||
|
||||
---
|
||||
@@ -269,20 +352,25 @@ Mirrors the capture and Design View pillars exactly.
|
||||
- Active-bank id (get/set, defaults to pool); resolve the active bank's `BankIndex`.
|
||||
- Move / copy a sample between banks — index-only, destination collapse-by-hash
|
||||
observed, move removes the source entry.
|
||||
- Evacuate a bank — move every member to the pool (index-only, same destination
|
||||
collapse-by-hash as move), leaving the bank empty; pool cannot be evacuated.
|
||||
- JSON round-trip of the whole book (banks + per-bank indices + ordinals + active
|
||||
id); legacy-`bank_index`→pool migration on parse.
|
||||
id), pool included as bank-zero *inside* the blob; legacy-`bank_index`→pool
|
||||
migration on parse (one-way, blob becomes authoritative).
|
||||
- `BankIndex` is untouched — `bank_book` wraps, never modifies it.
|
||||
|
||||
**`persist` slice:**
|
||||
- Serialize/deserialize the book under the `banks` key in `"reasampler"` (shared
|
||||
blob, distinct section from `bank_index` / `view_state`).
|
||||
- Migrate a legacy `bank_index` key into the pool on first load.
|
||||
namespace, distinct section from `view_state`; the pool rides *inside* the `banks`
|
||||
blob as bank-zero — no separate `bank_index` key going forward).
|
||||
- Migrate a legacy `bank_index` key into the pool on first load (one-way, lossless;
|
||||
after migration the `banks` blob is authoritative and the legacy key is retired).
|
||||
- Reload-on-open and Save-As survival via the existing M4 machinery; the session
|
||||
exposes the book; the active bank's `BankIndex` is the capture add target.
|
||||
|
||||
**`actions` entries:** the set listed above, registered with the
|
||||
`command_id` / `gaccel` / `hookcommand` pattern; bank-activate + move/copy
|
||||
MIDI-bindable.
|
||||
**`actions` entries:** the set listed above (including **evacuate bank → pool**),
|
||||
registered with the `command_id` / `gaccel` / `hookcommand` pattern; bank-activate +
|
||||
move/copy + evacuate MIDI-bindable.
|
||||
|
||||
**UI (extending the M5 `bank_panel`):** the vertical split (pool grid top,
|
||||
named-banks tab-page region bottom); the two full-height toggles; the active-bank
|
||||
@@ -292,33 +380,142 @@ the M5 LICE grid render loop per region.
|
||||
|
||||
---
|
||||
|
||||
## Open items for Daniel
|
||||
## Settled forks (Daniel, 2026-07-23)
|
||||
|
||||
1. **`bank_index` key retirement vs. retention.** Two persistence shapes: (a) fold
|
||||
the pool's index *into* the `banks` blob and retire the legacy `bank_index` key
|
||||
after a one-way migration — cleaner, one blob, one section; (b) *keep* the
|
||||
`bank_index` key as the pool's canonical storage slot and store only named banks
|
||||
+ ordering + active id under `banks` — more conservative, existing pool
|
||||
persistence untouched, named banks a pure addition. I lean **(b)** because M7/M8
|
||||
persist work is in flight in another worktree and (b) leaves the existing
|
||||
`bank_index` write path alone. But it's a genuine fork with a real trade — your
|
||||
call before B2 is scoped.
|
||||
2. **Named-bank delete → member disposition.** When a user deletes a *named* bank
|
||||
holding samples: (i) *reabsorb* — members move back to the pool, nothing is ever
|
||||
lost to a delete; (ii) *orphan-check* — delete members no other bank references,
|
||||
keep the rest; (iii) *forbid non-empty delete* — require emptying first. I lean
|
||||
**(i)** — a named bank is a grouping, deleting the group returns things home, and
|
||||
it pairs naturally with index-only movement. Confirm or redirect.
|
||||
3. **Move vs. copy defaults.** I'm recommending **copy as the low-friction gesture**
|
||||
(drag / one-click) and **move as the deliberate one** (explicit menu), on the
|
||||
"pool is the complete library" model. If you'd rather move be the default (strict
|
||||
partition mental model), say so — it changes the primary affordance.
|
||||
4. **Active-bank vs. shown-tab separation.** I'm recommending these stay *distinct*
|
||||
(browsing a tab does not change the capture target; activation is explicit). If
|
||||
you'd prefer the simpler "the tab you're viewing is the capture target" coupling,
|
||||
flag it — it's a real simplification but risks capturing into the wrong bank by
|
||||
browsing.
|
||||
5. **Tab rendering + move affordance mechanics.** SWELL-native tab control vs.
|
||||
LICE-drawn tabs matching the grid aesthetic; drag-between-regions vs. menu as the
|
||||
primary move gesture. Panel-build detail — noted for B4, not phase-defining, and
|
||||
the SWELL tab-control availability needs verification against the M5 reference.
|
||||
Four of the five open forks are now decided; their implications are folded into the
|
||||
prose above. Recorded here as a scan-line so the decisions don't have to be
|
||||
reconstructed from the body text.
|
||||
|
||||
1. **Persistence key — (a), retire `bank_index`.** The pool folds into the `banks`
|
||||
blob as bank-zero; the legacy `bank_index` key is retired after a one-way,
|
||||
lossless migration; the blob is authoritative thereafter. (See *persistence and
|
||||
migration*.)
|
||||
2. **Delete drops members; add an *evacuate* verb.** Deleting a named bank drops its
|
||||
member index entries (files untouched, owned by capture/prune). A separate
|
||||
*evacuate* operation moves all members back to the pool. Workflow: "evacuate then
|
||||
delete" to keep samples; plain delete to drop the grouping and its members.
|
||||
Non-empty delete should confirm and offer evacuate. (See *design-direction →
|
||||
evacuate*; orphaned-until-prune consequence in *precision-invariant implications*.)
|
||||
3. **Move is the default gesture; copy is the deliberate secondary.** A sample lives
|
||||
in one bank at a time; copy is the explicit "in two places" escape hatch. (See
|
||||
*design-direction → move is the default*.)
|
||||
4. **Active bank and shown tab stay distinct; active bank gets an unmistakable
|
||||
indicator.** Browsing never retargets capture; the "visually unmistakable"
|
||||
requirement is a settled constraint (placement remains a B4 detail). (See
|
||||
*design-direction → active bank and shown tab stay distinct*.)
|
||||
|
||||
---
|
||||
|
||||
## Fork 5 — options for discussion, decision pending
|
||||
|
||||
The one fork Daniel wants to weigh before deciding. Two sub-questions, both B4
|
||||
panel-build mechanics, neither phase-defining but both shaping the panel's feel.
|
||||
Nothing below is decided; the recommendations at the end are *pending Daniel's call*.
|
||||
|
||||
### 5a — Named-banks tab rendering: SWELL-native tabs vs. LICE-drawn custom tabs
|
||||
|
||||
The named-banks region needs a tab strip (one tab per named bank). Two ways to draw
|
||||
it.
|
||||
|
||||
**Option A — SWELL-native tab control** (`SWELL`'s tab-control, the cross-platform
|
||||
Win32 `SysTabControl32` analog). *Verify against the SWELL headers* — SWELL tab-control
|
||||
availability and behavior-parity across Win/mac/Linux is a known soft spot and must be
|
||||
checked, not assumed; the SWS extension is the reference for which SWELL controls are
|
||||
safe to lean on.
|
||||
|
||||
- **Pros:** near-zero custom draw/hit-test code; keyboard navigation (arrow between
|
||||
tabs, focus handling) comes for free on platforms where SWELL implements it;
|
||||
gets native-ish affordances (overflow, tooltips) without hand-rolling.
|
||||
- **Cons:** *visual inconsistency* — a native tab control will not match the M5 LICE
|
||||
grid aesthetic or the Design View segmented switch; it will read as a bolted-on OS
|
||||
widget in an otherwise custom-drawn surface. HiDPI/scaling behavior of SWELL controls
|
||||
is uneven across platforms. Cross-platform parity is the real risk: the control may
|
||||
look and behave three different ways on Win/mac/Linux, and SWELL's tab
|
||||
implementation is less battle-tested than its buttons/edits.
|
||||
- **Scaling:** native tab controls handle many tabs via built-in overflow/scroll, so
|
||||
it degrades more gracefully past ~10–15 banks than a naive custom strip would.
|
||||
|
||||
**Option B — LICE-drawn custom tabs** matching the grid aesthetic (the same render
|
||||
path M5 already uses, and the same approach the Design View segmented mode switch
|
||||
`[ Arrange | Design ]` took).
|
||||
|
||||
- **Pros:** *visual consistency* — tabs match the grid and the existing segmented
|
||||
switch; one coherent custom-drawn surface. Full control over HiDPI (draw at the
|
||||
panel's own scale factor, as M5 already must). Identical look and behavior on all
|
||||
three platforms because we own every pixel — no SWELL-parity risk. Precedent
|
||||
exists in-repo: the Design View segmented switch is already a LICE-drawn
|
||||
multi-segment selector, so the pattern and hit-testing are partly solved.
|
||||
- **Cons:** we hand-roll hit-testing, keyboard navigation, and overflow. Keyboard nav
|
||||
(tab/arrow to move between banks) is real work we'd otherwise get free from a native
|
||||
control. Overflow past the visible width needs a scroll or chevron affordance we
|
||||
build ourselves.
|
||||
- **Scaling:** a naive fixed-width custom strip breaks down when named banks exceed the
|
||||
region width (~8–12 tabs depending on name length) unless we build scroll/overflow.
|
||||
This is the sharpest cost of B — but the Design View switch only ever has two
|
||||
segments, so its precedent doesn't cover the many-tabs case; we'd extend it.
|
||||
|
||||
*Framing note:* the choice is largely "buy keyboard-nav + overflow from SWELL and pay
|
||||
in visual inconsistency + cross-platform risk" (A) vs. "pay to build keyboard-nav +
|
||||
overflow and get visual consistency + platform certainty" (B). How many named banks
|
||||
we expect in practice is the swing factor: if a user rarely exceeds ~6–8 banks, B's
|
||||
overflow cost is small and its consistency win is large; if banks routinely run into
|
||||
the dozens, A's built-in overflow starts to matter.
|
||||
|
||||
### 5b — Move affordance: drag-between-regions vs. "send to bank" menu vs. both
|
||||
|
||||
The move gesture (now the default, per fork 3) needs a surface. Three shapes.
|
||||
|
||||
**Option A — drag between regions** (drag selected samples from the pool grid onto a
|
||||
named-bank tab, or between tabs).
|
||||
|
||||
- **Pros:** spatially intuitive; matches the mental model ("put this there"); direct.
|
||||
- **Cons:** *precision / mis-drop risk* is real on a dense grid — dragging onto the
|
||||
wrong tab or dropping in dead space is easy, and a mis-drop that silently moves a
|
||||
sample to the wrong bank is exactly the "silent wrong-bank" error we're guarding
|
||||
against elsewhere. Not keyboard- or MIDI-bindable — but B3 already specifies the
|
||||
move action *is* MIDI-bindable, so drag can't be the *only* surface. Drag also has
|
||||
to interoperate cleanly with the M5 grid's existing multi-select drag behavior
|
||||
(verify there's no gesture collision).
|
||||
|
||||
**Option B — "send to bank" / "move to bank" menu** on the current selection (a
|
||||
context menu or a toolbar dropdown listing banks; the selected samples move to the
|
||||
chosen bank).
|
||||
|
||||
- **Pros:** *precise* — you pick the destination from a named list, no mis-drop. Maps
|
||||
directly onto the B3 bindable action (the menu is just a visual front-end for the
|
||||
same `move → bankId` operation, so it's consistent with keyboard/MIDI binding).
|
||||
Consistent with how the M5 grid already handles selection (act-on-current-selection,
|
||||
not per-item widgets — the Design View tag/untag already works this way). Scales to
|
||||
many banks (a menu list handles dozens of destinations fine). Cheapest to build in
|
||||
the LICE/SWELL surface (a menu, not a drag-and-drop hit-test system).
|
||||
- **Cons:** less *discoverable* than drag for a first-time user; an extra click vs. a
|
||||
direct drag; feels less "direct-manipulation" than dragging.
|
||||
|
||||
**Option C — both.** Menu as the precise, bindable, discoverable-via-right-click
|
||||
primary; drag as the direct-manipulation accelerator for users who prefer it.
|
||||
|
||||
- **Pros:** covers both interaction styles; the menu carries the bindable/precise
|
||||
path while drag serves direct manipulation. This is what most media managers do
|
||||
(Finder, Lightroom, Ableton browser all offer drag *and* a menu/right-click move).
|
||||
- **Cons:** most implementation cost (build both, and make them agree); drag still
|
||||
carries its mis-drop risk even as a secondary path.
|
||||
|
||||
### Recommendations (pending Daniel's decision)
|
||||
|
||||
- **5a — recommend LICE-drawn custom tabs (Option B).** Visual consistency with the
|
||||
M5 grid and the Design View segmented switch is worth the hand-rolled keyboard-nav
|
||||
and overflow, *and* it removes the cross-platform SWELL-parity risk entirely (we own
|
||||
every pixel on all three platforms). The Design View segmented switch is a partial
|
||||
precedent to extend rather than a from-scratch build. The one thing that would flip
|
||||
me to native tabs: evidence that users routinely run dozens of named banks, where
|
||||
built-in overflow earns its keep. Recommend B; revisit if the expected bank count is
|
||||
high. *Verify SWELL tab-control availability against the headers regardless* — even
|
||||
choosing B, we should know what the native fallback offers.
|
||||
- **5b — recommend the "send to bank" menu as primary (Option B), with drag as a
|
||||
later accelerator (toward C).** The menu is precise, cheap, scales to many banks,
|
||||
is the natural front-end for the already-bindable B3 move action, and matches how
|
||||
the M5 grid and Design View already act on the current selection. It sidesteps the
|
||||
mis-drop hazard that would otherwise fight our "no silent wrong-bank" stance. Drag
|
||||
is a nice accelerator but should not be the primary or only surface (it can't be
|
||||
MIDI-bound, and B3 requires the action be bindable). Recommend menu-first; add drag
|
||||
in a later B4 pass if it earns the cost. Net: ship B, leave the seam for C.
|
||||
|
||||
Reference in New Issue
Block a user