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..