From cd5650d8baaf8c236374542a1c24cb50d14989d3 Mon Sep 17 00:00:00 2001 From: "prompt.ac/@jeffrey" Date: Wed, 29 Apr 2026 01:16:10 -0700 Subject: [PATCH] slab/menuband: tiny numeric instrument map + Whistle default + name readout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit InstrumentListView rewritten as a non-scrollable 8 × 16 numeric grid: - 128 cells, one per GM program, each ~28 × 14 px showing just the 3- digit number ("078") in monospaced. Family-tinted background per row (16 families = 16 colored stripes top to bottom). Selected cell flashes the family color full-strength + bold white number. - No scroll, no names inside cells — the popover gets way smaller and the whole instrument set is readable at a glance. - Hover only nudges the row's tint; clicks commit + audition. Below the grid, a single monospaced readout line shows the selected program's number + name ("078 Whistle") so the user still sees what they picked. Updated on syncFromController + handleInstrumentCommit. Default instrument: Whistle (GM 078) — playful + breathy, more distinctive than acoustic grand on a fresh install. Fresh-install default written in bootstrap if the UserDefault isn't set; this user's pref also flipped to 78 via `defaults write …notepat.melodicProgram`. About / Crash row: - Distribution flipped from .fillEqually to .fill, with About hugging low (expands) and Crash hugging high (sticks to its content). When there are no crashes, About claims the whole row instead of leaving empty space on the right. - About body font bumped 10 → 10.5 with preferredMaxLayoutWidth = the instrument-map width so the paragraph wraps to the popover width. Site: - Bump download cache-buster query string to ?v=237f18c. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../Sources/MenuBand/InstrumentMapView.swift | 300 +++++------------- .../Sources/MenuBand/MenuBandController.swift | 6 + .../Sources/MenuBand/MenuBandPopover.swift | 52 +-- system/public/menuband/index.html | 2 +- 4 files changed, 119 insertions(+), 241 deletions(-) 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..