style: match Raycast launcher layout and appearance

This commit is contained in:
2026-10-11 19:11:37 +02:00
parent 0d033667ff
commit eb75f652a7
2 changed files with 164 additions and 30 deletions
+158 -27
View File
@@ -14,14 +14,19 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
private var isLaunching = false
init() {
let window = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 640, height: 480),
styleMask: [.titled, .closable, .miniaturizable, .resizable],
let window = LauncherPanel(
contentRect: NSRect(x: 0, y: 0, width: 750, height: 474),
styleMask: [.borderless],
backing: .buffered,
defer: false
)
window.title = "Conjure"
window.contentMinSize = NSSize(width: 420, height: 300)
window.appearance = NSAppearance(named: .darkAqua)
window.backgroundColor = .clear
window.isOpaque = false
window.hasShadow = true
window.isMovableByWindowBackground = true
window.hidesOnDeactivate = false
window.isReleasedWhenClosed = false
window.center()
super.init(window: window)
@@ -41,6 +46,7 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
NSApp.activate()
window?.makeKeyAndOrderFront(nil)
window?.makeFirstResponder(searchField)
(searchField.currentEditor() as? NSTextView)?.insertionPointColor = .labelColor
refreshApplications()
}
@@ -50,21 +56,56 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
}
private func configureContent(in window: NSWindow) {
guard let content = window.contentView else { return }
searchField.placeholderString = "Search applications"
searchField.controlSize = .large
searchField.font = .systemFont(ofSize: NSFont.systemFontSize(for: .large))
let content = NSVisualEffectView()
content.material = .hudWindow
content.blendingMode = .behindWindow
content.state = .active
content.wantsLayer = true
content.layer?.cornerRadius = 24
content.layer?.masksToBounds = true
content.layer?.borderWidth = 0.5
window.contentView = content
let tint = NSView(frame: content.bounds)
tint.autoresizingMask = [.width, .height]
tint.wantsLayer = true
content.addSubview(tint)
searchField.placeholderString = "Search for apps…"
searchField.font = .systemFont(ofSize: 20)
searchField.textColor = .labelColor
searchField.isBordered = false
searchField.drawsBackground = false
searchField.focusRingType = .none
if let cell = searchField.cell as? NSSearchFieldCell {
cell.searchButtonCell = nil
cell.cancelButtonCell = nil
}
searchField.sendsSearchStringImmediately = true
searchField.sendsWholeSearchString = false
searchField.delegate = self
searchField.setAccessibilityLabel("Search applications")
let symbol = NSImageView()
symbol.image = NSImage(systemSymbolName: "sparkles", accessibilityDescription: nil)
symbol.contentTintColor = .tertiaryLabelColor
symbol.setAccessibilityElement(false)
let header = NSStackView(views: [symbol, searchField])
header.orientation = .horizontal
header.alignment = .centerY
header.spacing = 12
let sectionLabel = NSTextField(labelWithString: "Applications")
sectionLabel.font = .systemFont(ofSize: 12, weight: .semibold)
sectionLabel.textColor = .secondaryLabelColor
let column = NSTableColumn(identifier: NSUserInterfaceItemIdentifier("application"))
column.resizingMask = .autoresizingMask
tableView.addTableColumn(column)
tableView.headerView = nil
tableView.style = .inset
tableView.rowHeight = 44
tableView.style = .plain
tableView.backgroundColor = .clear
tableView.rowHeight = 38
tableView.intercellSpacing = .zero
tableView.columnAutoresizingStyle = .uniformColumnAutoresizingStyle
tableView.allowsMultipleSelection = false
tableView.allowsEmptySelection = false
@@ -85,43 +126,93 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
scrollView.documentView = tableView
scrollView.hasVerticalScroller = true
scrollView.autohidesScrollers = true
scrollView.scrollerStyle = .overlay
scrollView.borderType = .noBorder
scrollView.drawsBackground = false
scrollView.contentView.drawsBackground = false
scrollView.horizontalScrollElasticity = .none
statusLabel.font = .systemFont(ofSize: NSFont.smallSystemFontSize)
statusLabel.font = .systemFont(ofSize: 12)
statusLabel.textColor = .secondaryLabelColor
statusLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
emptyLabel.textColor = .secondaryLabelColor
emptyLabel.alignment = .center
openButton.bezelStyle = .rounded
openButton.isBordered = false
openButton.font = .systemFont(ofSize: 14, weight: .medium)
openButton.contentTintColor = .labelColor
openButton.keyEquivalent = "\r"
openButton.target = self
openButton.action = #selector(openSelection)
openButton.isEnabled = false
openButton.toolTip = "Open the selected application (Return). Use ↑ and ↓ to select."
let returnKey = NSTextField(labelWithString: "↵")
returnKey.font = .systemFont(ofSize: 14)
returnKey.textColor = .secondaryLabelColor
returnKey.alignment = .center
returnKey.wantsLayer = true
returnKey.layer?.cornerRadius = 5
returnKey.layer?.borderWidth = 1
returnKey.setAccessibilityElement(false)
let footer = NSStackView(views: [statusLabel, NSView(), openButton])
let actionPill = NSVisualEffectView()
actionPill.material = .popover
actionPill.blendingMode = .withinWindow
actionPill.state = .active
actionPill.wantsLayer = true
actionPill.layer?.cornerRadius = 18
actionPill.layer?.masksToBounds = true
actionPill.layer?.borderWidth = 0.5
for view in [openButton, returnKey] {
view.translatesAutoresizingMaskIntoConstraints = false
actionPill.addSubview(view)
}
let footer = NSStackView(views: [statusLabel, NSView(), actionPill])
footer.orientation = .horizontal
footer.alignment = .centerY
footer.spacing = 12
for view in [searchField, scrollView, emptyLabel, footer] {
for view in [header, sectionLabel, scrollView, emptyLabel, footer] {
view.translatesAutoresizingMaskIntoConstraints = false
content.addSubview(view)
}
NSLayoutConstraint.activate([
searchField.topAnchor.constraint(equalTo: content.topAnchor, constant: 16),
searchField.leadingAnchor.constraint(equalTo: content.leadingAnchor, constant: 16),
searchField.trailingAnchor.constraint(equalTo: content.trailingAnchor, constant: -16),
scrollView.topAnchor.constraint(equalTo: searchField.bottomAnchor, constant: 12),
scrollView.leadingAnchor.constraint(equalTo: content.leadingAnchor),
scrollView.trailingAnchor.constraint(equalTo: content.trailingAnchor),
scrollView.bottomAnchor.constraint(equalTo: footer.topAnchor, constant: -12),
header.topAnchor.constraint(equalTo: content.topAnchor, constant: 20),
header.leadingAnchor.constraint(equalTo: content.leadingAnchor, constant: 16),
header.trailingAnchor.constraint(equalTo: content.trailingAnchor, constant: -24),
header.heightAnchor.constraint(equalToConstant: 32),
symbol.widthAnchor.constraint(equalToConstant: 22),
symbol.heightAnchor.constraint(equalToConstant: 22),
sectionLabel.topAnchor.constraint(equalTo: header.bottomAnchor, constant: 16),
sectionLabel.leadingAnchor.constraint(equalTo: content.leadingAnchor, constant: 16),
scrollView.topAnchor.constraint(equalTo: sectionLabel.bottomAnchor, constant: 6),
scrollView.leadingAnchor.constraint(equalTo: content.leadingAnchor, constant: 8),
scrollView.trailingAnchor.constraint(equalTo: content.trailingAnchor, constant: -8),
scrollView.bottomAnchor.constraint(equalTo: footer.topAnchor, constant: -8),
emptyLabel.centerXAnchor.constraint(equalTo: scrollView.centerXAnchor),
emptyLabel.centerYAnchor.constraint(equalTo: scrollView.centerYAnchor),
emptyLabel.leadingAnchor.constraint(greaterThanOrEqualTo: content.leadingAnchor, constant: 16),
emptyLabel.trailingAnchor.constraint(lessThanOrEqualTo: content.trailingAnchor, constant: -16),
footer.leadingAnchor.constraint(equalTo: content.leadingAnchor, constant: 16),
footer.trailingAnchor.constraint(equalTo: content.trailingAnchor, constant: -16),
footer.bottomAnchor.constraint(equalTo: content.bottomAnchor, constant: -12),
footer.trailingAnchor.constraint(equalTo: content.trailingAnchor, constant: -8),
footer.bottomAnchor.constraint(equalTo: content.bottomAnchor, constant: -8),
footer.heightAnchor.constraint(equalToConstant: 36),
actionPill.heightAnchor.constraint(equalToConstant: 36),
openButton.leadingAnchor.constraint(equalTo: actionPill.leadingAnchor, constant: 14),
openButton.centerYAnchor.constraint(equalTo: actionPill.centerYAnchor),
returnKey.leadingAnchor.constraint(equalTo: openButton.trailingAnchor, constant: 8),
returnKey.trailingAnchor.constraint(equalTo: actionPill.trailingAnchor, constant: -12),
returnKey.centerYAnchor.constraint(equalTo: actionPill.centerYAnchor),
returnKey.widthAnchor.constraint(equalToConstant: 18),
returnKey.heightAnchor.constraint(equalToConstant: 18),
])
window.effectiveAppearance.performAsCurrentDrawingAppearance {
let border = NSColor.separatorColor.cgColor
content.layer?.borderColor = border
returnKey.layer?.borderColor = border
actionPill.layer?.borderColor = border
tint.layer?.backgroundColor = NSColor.textBackgroundColor.withAlphaComponent(0.85).cgColor
}
window.initialFirstResponder = searchField
}
@@ -207,6 +298,14 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
return cell
}
func tableView(_ tableView: NSTableView, rowViewForRow row: Int) -> NSTableRowView? {
let identifier = NSUserInterfaceItemIdentifier("applicationRow")
let view = (tableView.makeView(withIdentifier: identifier, owner: self) as? LauncherRowView)
?? LauncherRowView()
view.identifier = identifier
return view
}
private func makeCell(identifier: NSUserInterfaceItemIdentifier) -> NSTableCellView {
let cell = NSTableCellView()
cell.identifier = identifier
@@ -214,21 +313,29 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
icon.imageScaling = .scaleProportionallyDown
icon.setAccessibilityElement(false)
let label = NSTextField(labelWithString: "")
label.font = .systemFont(ofSize: 14, weight: .medium)
label.lineBreakMode = .byTruncatingTail
let kind = NSTextField(labelWithString: "Application")
kind.font = .systemFont(ofSize: 14)
kind.textColor = .secondaryLabelColor
kind.setContentCompressionResistancePriority(.required, for: .horizontal)
kind.setAccessibilityElement(false)
cell.imageView = icon
cell.textField = label
for view in [icon, label] {
for view in [icon, label, kind] {
view.translatesAutoresizingMaskIntoConstraints = false
cell.addSubview(view)
}
NSLayoutConstraint.activate([
icon.leadingAnchor.constraint(equalTo: cell.leadingAnchor, constant: 8),
icon.centerYAnchor.constraint(equalTo: cell.centerYAnchor),
icon.widthAnchor.constraint(equalToConstant: 28),
icon.heightAnchor.constraint(equalToConstant: 28),
icon.widthAnchor.constraint(equalToConstant: 22),
icon.heightAnchor.constraint(equalToConstant: 22),
label.leadingAnchor.constraint(equalTo: icon.trailingAnchor, constant: 12),
label.centerYAnchor.constraint(equalTo: cell.centerYAnchor),
label.trailingAnchor.constraint(equalTo: cell.trailingAnchor, constant: -8),
label.trailingAnchor.constraint(lessThanOrEqualTo: kind.leadingAnchor, constant: -12),
kind.centerYAnchor.constraint(equalTo: cell.centerYAnchor),
kind.trailingAnchor.constraint(equalTo: cell.trailingAnchor, constant: -8),
])
return cell
}
@@ -278,3 +385,27 @@ private final class ApplicationTableView: NSTableView {
}
}
}
@MainActor
private final class LauncherPanel: NSPanel {
override var canBecomeKey: Bool { true }
override var canBecomeMain: Bool { true }
override func performClose(_ sender: Any?) {
close()
}
}
@MainActor
private final class LauncherRowView: NSTableRowView {
override func drawSelection(in dirtyRect: NSRect) {
let selection = NSBezierPath(roundedRect: bounds, xRadius: 10, yRadius: 10)
NSColor.unemphasizedSelectedContentBackgroundColor.withAlphaComponent(0.55).setFill()
selection.fill()
if NSWorkspace.shared.accessibilityDisplayShouldIncreaseContrast {
NSColor.labelColor.setStroke()
selection.lineWidth = 1
selection.stroke()
}
}
}