Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

Select the types of activity you want to include in your feed.

slab/menuband: 0.9 — YWFT bold for real + popover refinements

YWFT title (the actual fix this time):
- 0.7/0.8 each tried a different name-based lookup for the bold cut
and shipped the title in system black anyway. The trap is that
`NSFont(descriptor:)` almost never returns nil on a miss — it
silently substitutes the system font and reports success — so
every `??`-style fallback chain was unreachable.
- AppDelegate.registerBundledFonts now also parses each .ttf URL
with CTFontManagerCreateFontDescriptorsFromURL and caches the
per-cut descriptors as static vars on AppDelegate. Descriptors
built from a file URL are bound to that specific file, sidestepping
the shared-PostScript-name collision entirely.
- MenuBandPopover resolves the cached bold descriptor and asserts
`familyName == "YWFT Processing"` before drawing. If anything is
off (descriptor missing, family mismatch), it logs loudly and
falls back to system black so the next regression can't hide the
way 0.7/0.8 did.

Popover diagram:
- Removed the keyboard trackpad slab — the wide rounded recess
below the keys read as decorative noise once the chassis was
doing real work.
- Bottom strip 144 → 88. With the trackpad gone the chassis only
needs to wrap qwerty (46h) + arrows (30h) + insets.
- Chassis gets perceived volume + right-side perspective: a soft
drop shadow (opacity 0.30 light / 0.55 dark, 6pt blur, 2pt
offset down) plus a CATransform3D with m34 = -1/900 and a 5°
Y-rotation so the right edge recedes. Keys are siblings of the
deck (not children), so they stay flat — only the substrate tilts.

Info.plist + landing page bumped to 0.9.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

+129 -34
+2 -2
slab/menuband/Info.plist
··· 17 17 <key>CFBundlePackageType</key> 18 18 <string>APPL</string> 19 19 <key>CFBundleShortVersionString</key> 20 - <string>0.8</string> 20 + <string>0.9</string> 21 21 <key>CFBundleVersion</key> 22 - <string>8</string> 22 + <string>9</string> 23 23 <key>LSMinimumSystemVersion</key> 24 24 <string>11.0</string> 25 25 <key>LSUIElement</key>
+30 -4
slab/menuband/Sources/MenuBand/AppDelegate.swift
··· 324 324 menuBand.shutdown() 325 325 } 326 326 327 + /// YWFT Processing descriptors built straight from the .ttf URLs. 328 + /// Both bundled cuts share the PostScript name "YWFT-Processing", 329 + /// so any name-based lookup (`NSFont(name:)` or 330 + /// `NSFontDescriptor(fontAttributes: [.family: ...])` + symbolic 331 + /// traits) silently returns the wrong cut or falls through to the 332 + /// system font without nil-ing. Loading the descriptor directly 333 + /// from the file URL is the only way to be sure a given draw call 334 + /// gets that exact .ttf. 335 + static var ywftBoldDescriptor: NSFontDescriptor? 336 + static var ywftRegularDescriptor: NSFontDescriptor? 337 + 327 338 /// Register the YWFT Processing font files we ship in the SPM 328 - /// bundle so AppKit can find them by PostScript name. Called 329 - /// once at launch — the system caches the registration for the 330 - /// process lifetime. Failures are logged but non-fatal; the 331 - /// caller should fall back to the system font. 339 + /// bundle so AppKit can find them by PostScript name, AND cache 340 + /// per-cut descriptors built directly from the .ttf URLs. 341 + /// Registration alone is unreliable for these specific files 342 + /// because both cuts share a PostScript name (the original 0.7/0.8 343 + /// title-rendering bug), so callers should prefer the cached 344 + /// descriptors. Called once at launch. 332 345 private static func registerBundledFonts() { 333 346 let bundle = Bundle.module 334 347 for name in ["ywft-processing-regular", "ywft-processing-bold"] { ··· 340 353 if !CTFontManagerRegisterFontsForURL(url as CFURL, .process, &error) { 341 354 NSLog("MenuBand: font register failed for \(name): \(error?.takeRetainedValue().localizedDescription ?? "?")") 342 355 } 356 + guard let descs = CTFontManagerCreateFontDescriptorsFromURL(url as CFURL) as? [NSFontDescriptor], 357 + let desc = descs.first else { 358 + NSLog("MenuBand: no descriptor parsed for \(name).ttf") 359 + continue 360 + } 361 + if name.hasSuffix("bold") { 362 + ywftBoldDescriptor = desc 363 + } else { 364 + ywftRegularDescriptor = desc 365 + } 366 + } 367 + if ywftBoldDescriptor == nil { 368 + NSLog("MenuBand: ywft-processing-bold descriptor unavailable — title will fall back to system font") 343 369 } 344 370 } 345 371
+95 -26
slab/menuband/Sources/MenuBand/MenuBandPopover.swift
··· 73 73 private var instrumentTitleRow: NSStackView! 74 74 private var arrowsHint: ArrowKeysIndicator! 75 75 private var qwertyMap: QwertyLayoutView! 76 + private var keyboardDeck: NSView! 76 77 /// Horizontal stack of small floating boxes, one per currently- 77 78 /// held note. Sits above the visualizer; empty (zero-height) at 78 79 /// rest so the layout doesn't wobble when notes come and go. ··· 515 516 let palettePanel = NSView() 516 517 palettePanel.translatesAutoresizingMaskIntoConstraints = false 517 518 palettePanel.addSubview(instrumentList) 519 + 520 + // MacBook-style keyboard chassis behind the QWERTY map + 521 + // arrow keys. Layer-painted rounded slab tinted to read as 522 + // brushed silver in light mode and space-gray in dark mode. 523 + // Added BEFORE the keycap views so it sits beneath them in 524 + // z-order — keys render on top of the chassis. Substrate 525 + // colors are applied per-appearance in 526 + // `applyAppearanceToKeyboardDeck`. 527 + // 528 + // Volume + right-side perspective: a soft drop shadow gives 529 + // the slab perceived thickness, and a tiny Y-axis rotation 530 + // (with m34 perspective) tilts the right edge slightly back, 531 + // so the chassis reads as a real laptop angled away from 532 + // the viewer. The keys are siblings of the deck (not 533 + // children), so they stay flat — only the substrate tilts. 534 + keyboardDeck = NSView() 535 + keyboardDeck.wantsLayer = true 536 + keyboardDeck.layer?.cornerRadius = 7 537 + keyboardDeck.layer?.borderWidth = 0.5 538 + keyboardDeck.layer?.shadowOpacity = 0.35 539 + keyboardDeck.layer?.shadowRadius = 6 540 + keyboardDeck.layer?.shadowOffset = CGSize(width: 0, height: -2) 541 + keyboardDeck.layer?.shadowColor = NSColor.black.cgColor 542 + var deckTransform = CATransform3DIdentity 543 + deckTransform.m34 = -1.0 / 900 // perspective foreshortening 544 + deckTransform = CATransform3DRotate(deckTransform, -.pi / 36, 0, 1, 0) 545 + keyboardDeck.layer?.transform = deckTransform 546 + keyboardDeck.translatesAutoresizingMaskIntoConstraints = false 547 + palettePanel.addSubview(keyboardDeck) 548 + 518 549 arrowsHint = ArrowKeysIndicator() 519 550 arrowsHint.toolTip = "Arrow keys move the selection." 520 551 arrowsHint.translatesAutoresizingMaskIntoConstraints = false ··· 525 556 } 526 557 palettePanel.addSubview(arrowsHint) 527 558 let cornerInset: CGFloat = 4 528 - // Strip below the grid: full-width QWERTY keymap on top of 529 - // the strip, arrow-keys cluster anchored to its bottom-right 530 - // corner. Reads like a tiny laptop keyboard glued to the 531 - // base of the voice grid. 532 - let strip: CGFloat = 100 559 + // Strip below the grid: keyboard chassis wraps the QWERTY 560 + // map on top and the arrow-keys cluster tucked into its 561 + // bottom-right corner. Reads like a tiny laptop keyboard 562 + // glued to the base of the voice grid. Sized to hug the 563 + // qwerty (46h) + arrows (30h) + insets — no trackpad slab 564 + // anymore, so the strip can be tighter. 565 + let strip: CGFloat = 88 533 566 qwertyMap = QwertyLayoutView() 534 567 qwertyMap.translatesAutoresizingMaskIntoConstraints = false 535 568 // Pointer-driven play: clicks/drags on caps route through the ··· 547 580 instrumentList.leadingAnchor.constraint(equalTo: palettePanel.leadingAnchor), 548 581 instrumentList.trailingAnchor.constraint(equalTo: palettePanel.trailingAnchor), 549 582 instrumentList.heightAnchor.constraint(equalToConstant: InstrumentListView.preferredHeight), 550 - qwertyMap.centerXAnchor.constraint(equalTo: palettePanel.centerXAnchor), 551 - // Anchor the QWERTY map to the BOTTOM of the strip 552 - // (just above the arrow keys) instead of the top — 553 - // visually clusters it with the arrow-keys cluster as 554 - // one keyboard ornament, and frees space above for the 555 - // grid to breathe. 556 - qwertyMap.bottomAnchor.constraint(equalTo: arrowsHint.topAnchor, constant: -4), 583 + 584 + // Deck wraps the keys + trackpad. Spans the full panel 585 + // width so the chassis reads as a real laptop deck under 586 + // the voice grid. 587 + keyboardDeck.leadingAnchor.constraint(equalTo: palettePanel.leadingAnchor), 588 + keyboardDeck.trailingAnchor.constraint(equalTo: palettePanel.trailingAnchor), 589 + keyboardDeck.topAnchor.constraint(equalTo: instrumentList.bottomAnchor, constant: 6), 590 + keyboardDeck.bottomAnchor.constraint(equalTo: palettePanel.bottomAnchor), 591 + 592 + // QWERTY map sits at the top of the chassis with a 593 + // small inset from the deck's rounded edge. 594 + qwertyMap.centerXAnchor.constraint(equalTo: keyboardDeck.centerXAnchor), 595 + qwertyMap.topAnchor.constraint(equalTo: keyboardDeck.topAnchor, constant: 6), 557 596 qwertyMap.widthAnchor.constraint(equalToConstant: QwertyLayoutView.intrinsicSize.width), 558 597 qwertyMap.heightAnchor.constraint(equalToConstant: QwertyLayoutView.intrinsicSize.height), 559 - arrowsHint.trailingAnchor.constraint(equalTo: palettePanel.trailingAnchor, constant: -cornerInset), 560 - arrowsHint.bottomAnchor.constraint(equalTo: palettePanel.bottomAnchor, constant: -cornerInset), 598 + 599 + // Arrow cluster nestles below the QWERTY rows on the 600 + // right edge — same inverted-T position a real laptop 601 + // would put it. 602 + arrowsHint.trailingAnchor.constraint(equalTo: keyboardDeck.trailingAnchor, constant: -cornerInset), 603 + arrowsHint.topAnchor.constraint(equalTo: qwertyMap.bottomAnchor, constant: 2), 561 604 ]) 562 605 stack.addArrangedSubview(palettePanel) 563 606 palettePanel.widthAnchor.constraint(equalToConstant: InstrumentListView.preferredWidth).isActive = true ··· 920 963 ?? famColor) 921 964 shadow.shadowOffset = NSSize(width: 1, height: -1) 922 965 shadow.shadowBlurRadius = 0 923 - // YWFT Processing — both bundled .ttf files share the same 924 - // PostScript name ("YWFT-Processing"), so `NSFont(name:)` 925 - // only resolves whichever one was registered last. Resolve by 926 - // family + bold trait via NSFontDescriptor instead so we 927 - // actually get the bold cut, then the regular as a same-family 928 - // fallback, then system black as a last resort. 929 - let famDesc = NSFontDescriptor(fontAttributes: [.family: "YWFT Processing"]) 930 - let boldDesc = famDesc.withSymbolicTraits(.bold) 931 - let titleFont = NSFont(descriptor: boldDesc, size: 18) 932 - ?? NSFont(descriptor: famDesc, size: 18) 933 - ?? NSFont(name: "YWFT-Processing", size: 18) 934 - ?? NSFont.systemFont(ofSize: 18, weight: .black) 966 + // YWFT Processing — see AppDelegate.registerBundledFonts. 967 + // 0.7/0.8 tried to resolve the bold cut by PostScript name and 968 + // by family+symbolic-traits. Both paths returned a non-nil 969 + // wrong font (NSFont(descriptor:) silently substitutes the 970 + // system font on a miss instead of nil-ing), so the title 971 + // shipped in system black for two releases without any 972 + // visible signal of the failure. The reliable path is the 973 + // descriptor parsed directly from the .ttf URL at launch. 974 + // Verify familyName before accepting; log and fall back if 975 + // anything is off so the next regression can't hide. 976 + let titleFont: NSFont = { 977 + if let desc = AppDelegate.ywftBoldDescriptor, 978 + let f = NSFont(descriptor: desc, size: 18), 979 + f.familyName == "YWFT Processing" { 980 + return f 981 + } 982 + NSLog("MenuBand: YWFT bold descriptor unavailable; title falling back to system font") 983 + return NSFont.systemFont(ofSize: 18, weight: .black) 984 + }() 935 985 instrumentReadout.attributedStringValue = NSAttributedString( 936 986 string: title, 937 987 attributes: [ ··· 976 1026 // recessed-housing effect as the dark mode 0.06 → 0.0 step. 977 1027 waveformBezel?.layer?.backgroundColor = 978 1028 NSColor(white: 0.82, alpha: 1.0).cgColor 1029 + } 1030 + applyAppearanceToKeyboardDeck(isDark: isDark) 1031 + } 1032 + 1033 + /// Repaint the keyboard chassis against the current appearance. 1034 + /// Light mode reads as brushed silver aluminum; dark mode reads 1035 + /// as space-gray. Shadow tints darker in light mode (more 1036 + /// contrast against the bright substrate) and a touch lighter 1037 + /// against the dark popover background. 1038 + private func applyAppearanceToKeyboardDeck(isDark: Bool) { 1039 + guard let deck = keyboardDeck?.layer else { return } 1040 + if isDark { 1041 + deck.backgroundColor = NSColor(white: 0.18, alpha: 1.0).cgColor 1042 + deck.borderColor = NSColor(white: 0.30, alpha: 1.0).cgColor 1043 + deck.shadowOpacity = 0.55 1044 + } else { 1045 + deck.backgroundColor = NSColor(white: 0.86, alpha: 1.0).cgColor 1046 + deck.borderColor = NSColor(white: 0.68, alpha: 1.0).cgColor 1047 + deck.shadowOpacity = 0.30 979 1048 } 980 1049 } 981 1050
+2 -2
system/public/menuband/index.html
··· 571 571 <p class="tagline">Taking macOS' standard instruments out of the 🎸 Garage and kickin' it on the curb!</p> 572 572 573 573 <div class="button-row"> 574 - <a class="aqua" href="https://assets.aesthetic.computer/menuband/Menu-Band-0.8.dmg" download> 574 + <a class="aqua" href="https://assets.aesthetic.computer/menuband/Menu-Band-0.9.dmg" download> 575 575 Download 576 - <small>0.8 · Apple Silicon · 2.1 MB</small> 576 + <small>0.9 · Apple Silicon · 2.1 MB</small> 577 577 </a> 578 578 </div> 579 579 <p class="aux"><a href="https://tangled.org/@aesthetic.computer/core/tree/main/slab/menuband">view source</a> · by <a href="https://aesthetic.computer">aesthetic.computer</a></p>