Files
MetalPOC/Sources/App/HUDWindowController.swift
james.maesandClaude Sonnet 4.6 878aa28b31 feat(widgets): LocalNetwork widget — Bonjour scanner + CoreLocation place name
Adds a real-data LocalNetworkWidget to the HUD showing the Mac's approximate
location (city/region via CLGeocoder reverse-geocode), the user's phone presence
on the LAN (● HERE / ○ AWAY via Bonjour NWBrowser matching configured name
patterns), and a live list of Bonjour-advertising LAN devices (iPhones, iPads,
Macs, Apple TVs, HomePods, etc.) with a total count.

All data is real — NWBrowser browses 13 service types continuously;
CoreLocation supplies GPS coordinates reverse-geocoded to a place name.
On first launch, macOS will prompt for Location Services permission; the widget
gracefully shows "(awaiting permission)" until granted. Bonjour discovery is
async — the device list starts empty and populates within a few seconds.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 15:41:22 -05:00

201 lines
8.1 KiB
Swift

import Cocoa
enum HUDMode {
case full // orb at center + all widgets + ornaments visible
case compact // only orb visible, positioned in lower-right corner
case hidden // window not visible
}
final class HUDWindowController: NSWindowController {
private let hudWindow: HUDWindow
private let hudView: HUDView
private var backdropViews: [NSVisualEffectView] = []
private var mode: HUDMode = .full
// Persistent element references — reused across mode switches.
private let orbElement = OrbElement()
private let ornamentElement = OrnamentElement()
private let vitalsWidget = VitalsRingWidget()
private let spectrumWidget = SpectrumBarsWidget()
// Framed text widgets (strong references so state is preserved across mode switches).
private let kpi: KPIClusterWidget
private let llm: LLMTelemetryWidget
private let clocks: WorldClocksWidget
private let log: LogStreamWidget
private let tasks: TaskListWidget
private let compass: CompassWidget
private let schedule: ScheduleStripWidget
private let ticker: StatusTickerWidget
// New procedural framed widgets
private let forceGraph: ForceGraphWidget
private let networkTopology: NetworkTopologyWidget
private let wireframeModel: WireframeModelWidget
// Real-data widgets
private let localNetwork: LocalNetworkWidget
init() {
let screenFrame = (NSScreen.main ?? NSScreen.screens.first!).frame
self.hudView = HUDView(frame: NSRect(origin: .zero, size: screenFrame.size))
self.hudWindow = HUDWindow()
// Build staggered-reveal framed widgets.
let kpi_ = KPIClusterWidget(); kpi_.revealDelay = Theme.Reveal.kpiCluster
let llm_ = LLMTelemetryWidget(); llm_.revealDelay = Theme.Reveal.llmTelemetry
let clocks_ = WorldClocksWidget(); clocks_.revealDelay = Theme.Reveal.worldClocks
let log_ = LogStreamWidget(); log_.revealDelay = Theme.Reveal.logStream
let tasks_ = TaskListWidget(); tasks_.revealDelay = Theme.Reveal.taskList
let compass_ = CompassWidget(); compass_.revealDelay = Theme.Reveal.compass
let schedule_ = ScheduleStripWidget(); schedule_.revealDelay = Theme.Reveal.scheduleStrip
let ticker_ = StatusTickerWidget(); ticker_.revealDelay = Theme.Reveal.statusTicker
let forceGraph_ = ForceGraphWidget(); forceGraph_.revealDelay = Theme.Reveal.forceGraph
let networkTopology_ = NetworkTopologyWidget(); networkTopology_.revealDelay = Theme.Reveal.networkTopology
let wireframeModel_ = WireframeModelWidget(); wireframeModel_.revealDelay = Theme.Reveal.wireframeModel
let localNetwork_ = LocalNetworkWidget(); localNetwork_.revealDelay = Theme.Reveal.localNetwork
self.kpi = kpi_
self.llm = llm_
self.clocks = clocks_
self.log = log_
self.tasks = tasks_
self.compass = compass_
self.schedule = schedule_
self.ticker = ticker_
self.forceGraph = forceGraph_
self.networkTopology = networkTopology_
self.wireframeModel = wireframeModel_
self.localNetwork = localNetwork_
super.init(window: hudWindow)
// Collect all framed widgets to build backdrops.
let framedWidgets: [HUDElement] = [
kpi, llm, clocks, log, tasks, compass, schedule, ticker,
forceGraph, networkTopology, wireframeModel, localNetwork
]
let container = hudWindow.containerView
let sz = screenFrame.size
let aspect = sz.width / sz.height
// Add NSVisualEffectView backdrops for every framed widget (beneath the MTKView).
for element in framedWidgets {
guard let framed = element as? FramedHUDWidget else { continue }
let widgetRect = NSRect(
x: CGFloat(framed.origin.x) * sz.width,
y: CGFloat(framed.origin.y) * sz.height,
width: CGFloat(framed.size.x) * sz.width,
height: CGFloat(framed.size.y) * sz.height
)
let backdrop = NSVisualEffectView(frame: widgetRect)
backdrop.material = Theme.Backdrop.blurMaterial
backdrop.blendingMode = .behindWindow
backdrop.state = .active
backdrop.appearance = NSAppearance(named: Theme.Backdrop.blurAppearanceName)
backdrop.wantsLayer = true
backdrop.autoresizingMask = []
let halfXadj = CGFloat(framed.size.x) * aspect * 0.5
let halfY = CGFloat(framed.size.y) * 0.5
let chamferLeg = 2.0 * min(halfXadj, halfY) * 0.10 * sz.height
let chamferPath = HUDWindowController.chamferedRectPath(
width: widgetRect.width,
height: widgetRect.height,
chamfer: chamferLeg
)
let mask = CAShapeLayer()
mask.frame = CGRect(origin: .zero, size: widgetRect.size)
mask.path = chamferPath
mask.fillColor = NSColor.black.cgColor
backdrop.layer?.mask = mask
let darkTint = CALayer()
darkTint.frame = CGRect(origin: .zero, size: widgetRect.size)
darkTint.backgroundColor = Theme.Backdrop.tintColor
backdrop.layer?.addSublayer(darkTint)
container.addSubview(backdrop)
backdropViews.append(backdrop)
}
// Add HUDView (MTKView, transparent) on top of all backdrops.
hudView.frame = NSRect(origin: .zero, size: sz)
hudView.autoresizingMask = [.width, .height]
container.addSubview(hudView)
// Wire up the scene via setMode so there is one code path.
setMode(.full)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) is not supported")
}
override func showWindow(_ sender: Any?) {
hudWindow.orderFrontRegardless()
}
// MARK: - Mode switching
func setMode(_ newMode: HUDMode) {
mode = newMode
switch newMode {
case .hidden:
hudWindow.orderOut(nil)
case .full:
orbElement.center = SIMD2(0, 0)
orbElement.radius = Theme.Orb.radius
hudView.renderer.scene = [
ornamentElement,
orbElement,
vitalsWidget,
spectrumWidget,
kpi, llm, clocks, log, tasks, compass, schedule, ticker,
forceGraph, networkTopology, wireframeModel, localNetwork
]
for v in backdropViews { v.isHidden = false }
hudWindow.orderFrontRegardless()
case .compact:
orbElement.center = Theme.Orb.compactCenter
orbElement.radius = Theme.Orb.compactRadius
// Only the orb: no ornament, no vitals/spectrum (sized for center), no framed widgets.
hudView.renderer.scene = [orbElement]
for v in backdropViews { v.isHidden = true }
hudWindow.orderFrontRegardless()
}
}
func cycleMode() {
let next: HUDMode
switch mode {
case .full: next = .compact
case .compact: next = .hidden
case .hidden: next = .full
}
setMode(next)
}
func setFull() { setMode(.full) }
func setCompact() { setMode(.compact) }
func setHidden() { setMode(.hidden) }
// MARK: - Chamfered backdrop path
/// Builds a CGPath for a rectangle with top-left and bottom-right 45° chamfered corners
/// (top-right and bottom-left stay square) — matches the shape of the Metal widget frame.
private static func chamferedRectPath(width w: CGFloat, height h: CGFloat, chamfer n: CGFloat) -> CGPath {
let clampedN = max(0, min(n, min(w, h) * 0.5))
let path = CGMutablePath()
path.move(to: CGPoint(x: 0, y: 0))
path.addLine(to: CGPoint(x: w - clampedN, y: 0))
path.addLine(to: CGPoint(x: w, y: clampedN))
path.addLine(to: CGPoint(x: w, y: h))
path.addLine(to: CGPoint(x: clampedN, y: h))
path.addLine(to: CGPoint(x: 0, y: h - clampedN))
path.closeSubpath()
return path
}
}