import AppKit // MARK: - Slider Menu Item View class SliderMenuItemView: NSView { let slider: NSSlider let valueLabel: NSTextField private let titleLabel: NSTextField var onChanged: ((Double) -> Void)? init(title: String, value: Double, minValue: Double, maxValue: Double, width: CGFloat = 280) { titleLabel = NSTextField(labelWithString: title) slider = NSSlider(value: value, minValue: minValue, maxValue: maxValue, target: nil, action: nil) valueLabel = NSTextField(labelWithString: String(format: "%.2fx", value)) super.init(frame: NSRect(x: 0, y: 0, width: width, height: 50)) titleLabel.font = NSFont.systemFont(ofSize: 12, weight: .medium) titleLabel.textColor = .labelColor titleLabel.frame = NSRect(x: 16, y: 28, width: width - 32, height: 16) addSubview(titleLabel) let sliderW = width - 80 slider.frame = NSRect(x: 16, y: 4, width: sliderW, height: 20) slider.target = self slider.action = #selector(sliderMoved) slider.isContinuous = true addSubview(slider) valueLabel.font = NSFont.monospacedSystemFont(ofSize: 11, weight: .regular) valueLabel.textColor = .secondaryLabelColor valueLabel.alignment = .right valueLabel.frame = NSRect(x: sliderW + 20, y: 4, width: 44, height: 20) addSubview(valueLabel) } required init?(coder: NSCoder) { fatalError() } @objc private func sliderMoved() { let v = slider.doubleValue valueLabel.stringValue = String(format: "%.2fx", v) onChanged?(v) } func update(value: Double) { slider.doubleValue = value valueLabel.stringValue = String(format: "%.2fx", value) } } // MARK: - Color Swatch Menu Item View class ColorSwatchMenuItemView: NSView { private let titleLabel: NSTextField private var swatchButtons: [NSButton] = [] private var selectedIndex: Int = -1 var onChanged: ((NSColor) -> Void)? static let presetColors: [(String, NSColor)] = [ ("Teal", .systemTeal), ("Blue", .systemBlue), ("Purple", .systemPurple), ("Pink", .systemPink), ("Red", .systemRed), ("Orange", .systemOrange), ("Yellow", .systemYellow), ("Green", .systemGreen), ("Indigo", .systemIndigo), ] init(title: String, currentColor: NSColor, width: CGFloat = 280) { titleLabel = NSTextField(labelWithString: title) super.init(frame: NSRect(x: 0, y: 0, width: width, height: 56)) titleLabel.font = NSFont.systemFont(ofSize: 12, weight: .medium) titleLabel.textColor = .labelColor titleLabel.frame = NSRect(x: 16, y: 34, width: width - 32, height: 16) addSubview(titleLabel) let swatchSize: CGFloat = 22 let gap: CGFloat = 4 let count = Self.presetColors.count let totalW = CGFloat(count) * swatchSize + CGFloat(count - 1) * gap var x = (width - totalW) / 2 for (i, (_, color)) in Self.presetColors.enumerated() { let btn = NSButton(frame: NSRect(x: x, y: 6, width: swatchSize, height: swatchSize)) btn.wantsLayer = true btn.isBordered = false btn.title = "" btn.layer?.backgroundColor = color.cgColor btn.layer?.cornerRadius = swatchSize / 2 btn.layer?.borderWidth = 2 btn.layer?.borderColor = NSColor.clear.cgColor btn.tag = i btn.target = self btn.action = #selector(swatchClicked(_:)) addSubview(btn) swatchButtons.append(btn) x += swatchSize + gap } highlightClosest(to: currentColor) } required init?(coder: NSCoder) { fatalError() } @objc private func swatchClicked(_ sender: NSButton) { let idx = sender.tag guard idx >= 0 && idx < Self.presetColors.count else { return } setSelected(idx) onChanged?(Self.presetColors[idx].1) } private func setSelected(_ index: Int) { // Clear previous for btn in swatchButtons { btn.layer?.borderColor = NSColor.clear.cgColor } // Highlight new if index >= 0 && index < swatchButtons.count { swatchButtons[index].layer?.borderColor = NSColor.white.cgColor selectedIndex = index } } func highlightClosest(to target: NSColor) { let t = target.usingColorSpace(.sRGB) ?? target var bestIdx = 0 var bestDist: CGFloat = .greatestFiniteMagnitude for (i, (_, color)) in Self.presetColors.enumerated() { let c = color.usingColorSpace(.sRGB) ?? color let dr = t.redComponent - c.redComponent let dg = t.greenComponent - c.greenComponent let db = t.blueComponent - c.blueComponent let dist = dr*dr + dg*dg + db*db if dist < bestDist { bestDist = dist; bestIdx = i } } setSelected(bestIdx) } func update(color: NSColor) { highlightClosest(to: color) } } // MARK: - Settings Menu Builder class SettingsMenuBuilder { private var intensityView: SliderMenuItemView? private var durationView: SliderMenuItemView? private var colorView: ColorSwatchMenuItemView? func buildSettingsSubmenu() -> NSMenuItem { let submenu = NSMenu(title: "Settings") // Color swatches let cv = ColorSwatchMenuItemView(title: "Gradient Color:", currentColor: Settings.shared.gradientColor) cv.onChanged = { color in Settings.shared.gradientColor = color } colorView = cv let colorItem = NSMenuItem() colorItem.view = cv submenu.addItem(colorItem) submenu.addItem(NSMenuItem.separator()) // Intensity let iv = SliderMenuItemView(title: "Intensity", value: Settings.shared.intensityScale, minValue: 0.25, maxValue: 2.0) iv.onChanged = { val in Settings.shared.intensityScale = val } intensityView = iv let intensityItem = NSMenuItem() intensityItem.view = iv submenu.addItem(intensityItem) submenu.addItem(NSMenuItem.separator()) // Blink Duration let dv = SliderMenuItemView(title: "Blink Duration", value: Settings.shared.blinkDurationScale, minValue: 0.25, maxValue: 3.0) dv.onChanged = { val in Settings.shared.blinkDurationScale = val } durationView = dv let durationItem = NSMenuItem() durationItem.view = dv submenu.addItem(durationItem) submenu.addItem(NSMenuItem.separator()) // Reset let resetItem = NSMenuItem(title: "Reset to Defaults", action: #selector(resetDefaults), keyEquivalent: "") resetItem.target = self submenu.addItem(resetItem) let parentItem = NSMenuItem(title: "Settings", action: nil, keyEquivalent: ",") parentItem.submenu = submenu return parentItem } func refreshValues() { let s = Settings.shared intensityView?.update(value: s.intensityScale) durationView?.update(value: s.blinkDurationScale) colorView?.update(color: s.gradientColor) } @objc private func resetDefaults() { Settings.shared.resetToDefaults() refreshValues() } }