diff --git a/slab/menuband/Sources/MenuBand/InstrumentMapView.swift b/slab/menuband/Sources/MenuBand/InstrumentMapView.swift
index 69f6c0aaf2..eec93b5ef4 100644
--- a/slab/menuband/Sources/MenuBand/InstrumentMapView.swift
+++ b/slab/menuband/Sources/MenuBand/InstrumentMapView.swift
@@ -1,56 +1,39 @@
import AppKit
-/// Two-column sectioned browser for the 128 General MIDI programs. Left
-/// column carries the first 8 GM families (Piano … Brass), right column
-/// carries the last 8 (Reed … Sound FX). Each family has a colored
-/// header + 8 instrument rows beneath it. Click a row → onCommit; the
-/// popover hands that to the controller, which sets the program and
-/// auditions middle-C through the local synth so the user hears their
-/// pick.
+/// Tiny numeric flat-map of all 128 General MIDI programs. 8 columns × 16
+/// rows, one cell per program — number-only, family-colored background.
+/// No scrolling, no names: a glance shows the whole instrument set.
+/// Click a cell → onCommit; the popover commits the program and plays a
+/// preview note in it.
///
-/// No hover audio. Hover only nudges the row's background slightly so
-/// the cursor's resting target reads at a glance.
+/// Replaces the scrollable named-list because the named list ate the
+/// popover's vertical real estate. The numeric map is ~224 × 224 px and
+/// fits comfortably alongside the rest of the controls.
final class InstrumentListView: NSView {
- static let rowHeight: CGFloat = 18
- static let headerHeight: CGFloat = 22
- static let stripeWidth: CGFloat = 4
- static let sectionPadding: CGFloat = 8
+ static let cols = 8
+ static let rows = 16
+ static let cellW: CGFloat = 28
+ static let cellH: CGFloat = 14
- static let columnWidth: CGFloat = 220
- static let columnGap: CGFloat = 8
- static let totalColumns = 2
- static let familiesPerColumn = 8 // 16 families / 2 columns
- static let programsPerFamily = 8
-
- static let sectionHeight: CGFloat =
- headerHeight + CGFloat(programsPerFamily) * rowHeight + sectionPadding
-
- static let preferredWidth: CGFloat =
- CGFloat(totalColumns) * columnWidth + CGFloat(totalColumns - 1) * columnGap
-
- static let intrinsicHeight: CGFloat = CGFloat(familiesPerColumn) * sectionHeight
+ static let preferredWidth: CGFloat = cellW * CGFloat(cols) // 224
+ static let preferredHeight: CGFloat = cellH * CGFloat(rows) // 224
var selectedProgram: UInt8 = 0 { didSet { needsDisplay = true } }
private(set) var hoveredProgram: UInt8?
-
- /// Only `onCommit` is exposed — the design dropped hover audio, so we
- /// don't even surface a hover callback. The view tracks hover for the
- /// background highlight only.
var onCommit: ((Int) -> Void)?
private var trackingArea: NSTrackingArea?
- /// Rows top-down (y=0 → topmost row), matching reading order.
- override var isFlipped: Bool { true }
+ override var isFlipped: Bool { true } // top-down rows, reading order
override init(frame frameRect: NSRect) {
super.init(frame: frameRect)
- setFrameSize(NSSize(width: Self.preferredWidth, height: Self.intrinsicHeight))
+ setFrameSize(NSSize(width: Self.preferredWidth, height: Self.preferredHeight))
}
required init?(coder: NSCoder) { fatalError() }
override var intrinsicContentSize: NSSize {
- NSSize(width: Self.preferredWidth, height: Self.intrinsicHeight)
+ NSSize(width: Self.preferredWidth, height: Self.preferredHeight)
}
override func updateTrackingAreas() {
@@ -66,175 +49,77 @@ final class InstrumentListView: NSView {
trackingArea = ta
}
- // MARK: - Layout helpers
-
- /// Origin x of the given column (0 or 1).
- private func columnX(_ col: Int) -> CGFloat {
- CGFloat(col) * (Self.columnWidth + Self.columnGap)
- }
-
- /// Origin y (top-down) of family index `i` within its column.
- private func sectionY(_ familyInColumn: Int) -> CGFloat {
- CGFloat(familyInColumn) * Self.sectionHeight
- }
-
- private func rowRect(programInFamily: Int, familyInColumn: Int, column: Int) -> NSRect {
- let x = columnX(column) + Self.stripeWidth
- let y = sectionY(familyInColumn) + Self.headerHeight + CGFloat(programInFamily) * Self.rowHeight
- return NSRect(x: x, y: y,
- width: Self.columnWidth - Self.stripeWidth,
- height: Self.rowHeight)
- }
-
- private func headerRect(familyInColumn: Int, column: Int) -> NSRect {
- NSRect(x: columnX(column),
- y: sectionY(familyInColumn),
- width: Self.columnWidth,
- height: Self.headerHeight)
- }
+ // MARK: - Geometry
- private func stripeRect(familyInColumn: Int, column: Int) -> NSRect {
- NSRect(x: columnX(column),
- y: sectionY(familyInColumn) + Self.headerHeight,
- width: Self.stripeWidth,
- height: CGFloat(Self.programsPerFamily) * Self.rowHeight)
+ private func cellRect(program p: Int) -> NSRect {
+ let col = p % Self.cols
+ let row = p / Self.cols
+ return NSRect(x: CGFloat(col) * Self.cellW,
+ y: CGFloat(row) * Self.cellH,
+ width: Self.cellW,
+ height: Self.cellH)
}
- /// Given a point in the view, return the program (0..127) under it,
- /// or nil if the point falls in a header / column gap / padding area.
private func program(at point: NSPoint) -> Int? {
- // Column.
- let stride = Self.columnWidth + Self.columnGap
- let col = Int(point.x / stride)
- guard col >= 0, col < Self.totalColumns else { return nil }
- let xInColumn = point.x - CGFloat(col) * stride
- guard xInColumn >= 0, xInColumn <= Self.columnWidth else { return nil }
-
- // Family within column.
- let familyInCol = Int(point.y / Self.sectionHeight)
- guard familyInCol >= 0, familyInCol < Self.familiesPerColumn else { return nil }
- let yInSection = point.y - CGFloat(familyInCol) * Self.sectionHeight
-
- // Skip headers + bottom padding.
- if yInSection < Self.headerHeight { return nil }
- let yInBody = yInSection - Self.headerHeight
- let progInFam = Int(yInBody / Self.rowHeight)
- guard progInFam >= 0, progInFam < Self.programsPerFamily else { return nil }
-
- let familyAbs = col * Self.familiesPerColumn + familyInCol
- return familyAbs * Self.programsPerFamily + progInFam
+ guard bounds.contains(point) else { return nil }
+ let col = Int(point.x / Self.cellW)
+ let row = Int(point.y / Self.cellH)
+ guard col >= 0, col < Self.cols, row >= 0, row < Self.rows else { return nil }
+ let p = row * Self.cols + col
+ return p < 128 ? p : nil
}
- // MARK: - Colors
-
- private func familyColor(_ familyAbs: Int) -> NSColor {
- NSColor(hue: CGFloat(familyAbs) / CGFloat(GeneralMIDI.families.count),
- saturation: 0.55, brightness: 0.85, alpha: 1.0)
+ private func familyColor(forProgram p: Int) -> NSColor {
+ // 16 families × 8 programs each. Each row IS a family (since cols=8).
+ let famIdx = p / 8
+ return NSColor(hue: CGFloat(famIdx) / 16.0,
+ saturation: 0.55, brightness: 0.88, alpha: 1.0)
}
// MARK: - Drawing
- private static let headerAttrs: [NSAttributedString.Key: Any] = [
- .font: NSFont.systemFont(ofSize: 10, weight: .bold),
- .foregroundColor: NSColor.labelColor,
- .kern: 0.6,
+ private static let numAttrs: [NSAttributedString.Key: Any] = [
+ .font: NSFont.monospacedDigitSystemFont(ofSize: 9, weight: .medium),
+ .foregroundColor: NSColor.labelColor.withAlphaComponent(0.85),
]
- private static let numberAttrs: [NSAttributedString.Key: Any] = [
- .font: NSFont.monospacedDigitSystemFont(ofSize: 10, weight: .regular),
- .foregroundColor: NSColor.tertiaryLabelColor,
- ]
- private static let nameAttrs: [NSAttributedString.Key: Any] = [
- .font: NSFont.systemFont(ofSize: 11, weight: .regular),
- .foregroundColor: NSColor.labelColor,
- ]
- private static let nameAttrsSelected: [NSAttributedString.Key: Any] = [
- .font: NSFont.systemFont(ofSize: 11, weight: .semibold),
- .foregroundColor: NSColor.labelColor,
+ private static let numAttrsSelected: [NSAttributedString.Key: Any] = [
+ .font: NSFont.monospacedDigitSystemFont(ofSize: 9, weight: .bold),
+ .foregroundColor: NSColor.white,
]
override func draw(_ dirtyRect: NSRect) {
super.draw(dirtyRect)
-
- for col in 0..