Fix color picker with preset swatches and add DMG volume icon

Replace NSColorWell (breaks inside NSMenu submenu) with a row of 9
clickable color swatch circles that work reliably in the dropdown.
Set the boot-logo .icns as the DMG volume icon via SetFile -a C.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Leopere 2026-02-06 16:03:19 -05:00
parent c8691636a6
commit 837bc32abd
Signed by: colin
SSH Key Fingerprint: SHA256:nRPCQTeMFLdGytxRQmPVK9VXY3/ePKQ5lGRyJhT5DY8
4 changed files with 81 additions and 20 deletions

Binary file not shown.

View File

@ -83,6 +83,8 @@ dmg: bundle
@mkdir -p $(BUILD_DIR)/dmg-staging
@cp -R $(APP_BUNDLE) $(BUILD_DIR)/dmg-staging/
@ln -s /Applications $(BUILD_DIR)/dmg-staging/Applications
@cp Resources/$(APP_NAME).icns $(BUILD_DIR)/dmg-staging/.VolumeIcon.icns
@SetFile -a C $(BUILD_DIR)/dmg-staging
@hdiutil create -volname "$(APP_NAME)" \
-srcfolder $(BUILD_DIR)/dmg-staging \
-ov -format UDZO \

View File

@ -48,38 +48,98 @@ class SliderMenuItemView: NSView {
}
}
// MARK: - Color Menu Item View
class ColorMenuItemView: NSView {
let colorWell: NSColorWell
// 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)?
init(title: String, color: NSColor, width: CGFloat = 280) {
titleLabel = NSTextField(labelWithString: title)
colorWell = NSColorWell(frame: .zero)
static let presetColors: [(String, NSColor)] = [
("Teal", .systemTeal),
("Blue", .systemBlue),
("Purple", .systemPurple),
("Pink", .systemPink),
("Red", .systemRed),
("Orange", .systemOrange),
("Yellow", .systemYellow),
("Green", .systemGreen),
("Indigo", .systemIndigo),
]
super.init(frame: NSRect(x: 0, y: 0, width: width, height: 34))
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: 7, width: 120, height: 16)
titleLabel.frame = NSRect(x: 16, y: 34, width: width - 32, height: 16)
addSubview(titleLabel)
colorWell.frame = NSRect(x: 140, y: 4, width: 40, height: 26)
colorWell.color = color
colorWell.target = self
colorWell.action = #selector(colorPicked)
addSubview(colorWell)
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 colorPicked() {
onChanged?(colorWell.color)
@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) {
colorWell.color = color
highlightClosest(to: color)
}
}
@ -87,14 +147,13 @@ class ColorMenuItemView: NSView {
class SettingsMenuBuilder {
private var intensityView: SliderMenuItemView?
private var durationView: SliderMenuItemView?
private var colorView: ColorMenuItemView?
private var colorView: ColorSwatchMenuItemView?
/// Build settings items and append them to the given menu as a submenu
func buildSettingsSubmenu() -> NSMenuItem {
let submenu = NSMenu(title: "Settings")
// Color
let cv = ColorMenuItemView(title: "Gradient Color:", color: Settings.shared.gradientColor)
// 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()

Binary file not shown.