refactor: centralize font-relative UI icon sizing

This commit is contained in:
Valdemar Lorenzen
2026-10-15 13:16:12 +02:00
parent db8150c312
commit 6a7a0eb0ab
8 changed files with 124 additions and 46 deletions
@@ -271,25 +271,24 @@ final class ApplicationActionsController: NSViewController, NSSearchFieldDelegat
private struct ActionEntry {
let view = LauncherRowView()
let button: NSButton
let icon = ActionIconView()
let icon: IconView
let returnKey = ShortcutIndicator("↵")
init(title: String, symbol: String, color: NSColor) {
let font = NSFont.systemFont(ofSize: LauncherRowView.textSize, weight: .medium)
icon = IconView(font: font)
button = NSButton(title: title, target: nil, action: nil)
button.isBordered = false
button.alignment = .left
button.font = .systemFont(ofSize: LauncherRowView.textSize, weight: .medium)
button.font = font
button.contentTintColor = color
button.attributedTitle = NSAttributedString(string: title, attributes: [
.foregroundColor: color, .font: NSFont.systemFont(ofSize: LauncherRowView.textSize, weight: .medium)
.foregroundColor: color, .font: font
])
button.setAccessibilityLabel(title)
icon.image = NSImage(systemSymbolName: symbol, accessibilityDescription: nil)
?? (symbol == "finder" ? NSWorkspace.shared.icon(forFile: "/System/Library/CoreServices/Finder.app") : nil)
icon.contentTintColor = color
icon.symbolConfiguration = .init(pointSize: LauncherRowView.textSize, weight: .regular, scale: .medium)
icon.imageScaling = .scaleProportionallyDown
icon.setAccessibilityElement(false)
for child in [icon, button, returnKey] {
child.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(child)
@@ -298,8 +297,6 @@ private struct ActionEntry {
view.heightAnchor.constraint(equalToConstant: LauncherRowView.rowHeight),
icon.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 8),
icon.centerYAnchor.constraint(equalTo: view.centerYAnchor),
icon.widthAnchor.constraint(equalToConstant: LauncherRowView.iconSize),
icon.heightAnchor.constraint(equalToConstant: LauncherRowView.iconSize),
button.leadingAnchor.constraint(equalTo: icon.trailingAnchor, constant: LauncherRowView.iconToTextSpacing),
button.centerYAnchor.constraint(equalTo: view.centerYAnchor),
button.trailingAnchor.constraint(equalTo: returnKey.leadingAnchor, constant: -12),
@@ -318,12 +315,6 @@ private final class ActionsPanelView: NSView {
}
}
@MainActor
private final class ActionIconView: NSImageView {
// SF Symbol optical insets otherwise enlarge a 22-point icon box beyond application icon sizing.
override var alignmentRectInsets: NSEdgeInsets { NSEdgeInsets(top: 0, left: 0, bottom: 0, right: 0) }
}
@MainActor
private final class ActionsSearchField: NSSearchField {
var closeActions: (() -> Void)?
+40
View File
@@ -0,0 +1,40 @@
import AppKit
@MainActor
enum IconStyle {
// Preserve the approved 22-point application artwork slot beside 14-point text.
static func applicationSize(for font: NSFont) -> CGFloat {
font.pointSize * (22.0 / 14.0)
}
static func symbolConfiguration(for font: NSFont) -> NSImage.SymbolConfiguration {
.init(pointSize: font.pointSize, weight: .regular, scale: .medium)
}
static func symbol(_ name: String, for font: NSFont, accessibilityDescription: String? = nil) -> NSImage? {
NSImage(systemSymbolName: name, accessibilityDescription: accessibilityDescription)?
.withSymbolConfiguration(symbolConfiguration(for: font))
}
}
/// A shared artwork slot: font-sized symbols or proportionally sized application/file icons.
@MainActor
final class IconView: NSImageView {
init(font: NSFont) {
super.init(frame: .zero)
symbolConfiguration = IconStyle.symbolConfiguration(for: font)
imageScaling = .scaleProportionallyDown
setAccessibilityElement(false)
translatesAutoresizingMaskIntoConstraints = false
let size = IconStyle.applicationSize(for: font)
NSLayoutConstraint.activate([
widthAnchor.constraint(equalToConstant: size),
heightAnchor.constraint(equalToConstant: size)
])
}
// SF Symbol optical insets must not enlarge the shared artwork slot.
override var alignmentRectInsets: NSEdgeInsets { NSEdgeInsets(top: 0, left: 0, bottom: 0, right: 0) }
required init?(coder: NSCoder) { fatalError("Conjure creates icons programmatically") }
}
+19 -21
View File
@@ -226,10 +226,9 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
searchField.delegate = self
searchField.setAccessibilityLabel("Search applications, calculate, or convert units and currencies")
let symbol = NSImageView()
let symbol = IconView(font: searchFont)
symbol.image = NSImage(systemSymbolName: "sparkles", accessibilityDescription: nil)
symbol.contentTintColor = SunsetPalette.accent
symbol.setAccessibilityElement(false)
let header = NSStackView(views: [symbol, searchField])
header.orientation = .horizontal
header.alignment = .centerY
@@ -324,8 +323,6 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
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: -10),
@@ -565,8 +562,7 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
cell.locationLabel.isHidden = true
cell.kindLabel.stringValue = isCurrencyCalculation ? "Daily rates"
: calculation.copyValue == nil ? "Error" : "Calculator"
cell.imageView?.image = NSImage(systemSymbolName: "equal", accessibilityDescription: nil)?
.withSymbolConfiguration(.init(pointSize: LauncherRowView.iconSize, weight: .regular))
cell.imageView?.image = NSImage(systemSymbolName: "equal", accessibilityDescription: nil)
cell.imageView?.contentTintColor = SunsetPalette.accent
cell.toolTip = calculation.detail
} else if results[row].command != nil {
@@ -574,7 +570,8 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
cell.textField?.stringValue = entry.name
cell.textField?.setAccessibilityLabel(history.isFavorite(entry) ? "\(entry.name), Favorite" : nil)
cell.locationLabel.isHidden = true
cell.kindLabel.stringValue = history.isFavorite(entry) ? "★ Command" : "Command"
cell.kindLabel.attributedStringValue = history.isFavorite(entry)
? ApplicationCell.favoriteCommandKind : ApplicationCell.commandKind
cell.imageView?.image = NSImage(systemSymbolName: "character.bubble", accessibilityDescription: nil)
cell.imageView?.contentTintColor = SunsetPalette.accent
cell.toolTip = "Translate English to Japanese"
@@ -832,21 +829,26 @@ final class ApplicationCell: NSTableCellView {
private static let fadedMask = [NSColor.black.cgColor, NSColor.clear.cgColor]
private static let timing = CAMediaTimingFunction(name: .easeOut)
static let applicationKind = NSAttributedString(string: "Application", attributes: [
.font: NSFont.systemFont(ofSize: 14), .foregroundColor: SunsetPalette.secondaryText
.font: NSFont.systemFont(ofSize: LauncherRowView.textSize), .foregroundColor: SunsetPalette.secondaryText
])
static let favoriteKind: NSAttributedString = {
static let commandKind = NSAttributedString(string: "Command", attributes: applicationKind.attributes(at: 0, effectiveRange: nil))
static let favoriteKind = favorited(applicationKind)
static let favoriteCommandKind = favorited(commandKind)
private static func favorited(_ kind: NSAttributedString) -> NSAttributedString {
let font = kind.attribute(.font, at: 0, effectiveRange: nil) as! NSFont
let attachment = NSTextAttachment()
attachment.image = NSImage(systemSymbolName: "star.fill", accessibilityDescription: "Favorite")?
.withSymbolConfiguration(NSImage.SymbolConfiguration(pointSize: 12, weight: .regular)
.withSymbolConfiguration(IconStyle.symbolConfiguration(for: font)
.applying(.init(paletteColors: [SunsetPalette.secondaryText])))
let size = attachment.image?.size ?? .zero
attachment.bounds = NSRect(x: 0, y: (NSFont.systemFont(ofSize: 14).capHeight - size.height) / 2,
attachment.bounds = NSRect(x: 0, y: (font.capHeight - size.height) / 2,
width: size.width, height: size.height)
let title = NSMutableAttributedString(attachment: attachment)
title.append(NSAttributedString(string: " "))
title.append(applicationKind)
title.append(kind)
return title
}()
}
let locationLabel = NSTextField(labelWithString: "")
let kindLabel = NSTextField(labelWithString: "Application")
@@ -860,11 +862,10 @@ final class ApplicationCell: NSTableCellView {
override init(frame frameRect: NSRect) {
super.init(frame: frameRect)
let icon = NSImageView()
icon.imageScaling = .scaleProportionallyDown
icon.setAccessibilityElement(false)
let font = NSFont.systemFont(ofSize: LauncherRowView.textSize, weight: .medium)
let icon = IconView(font: font)
let label = NSTextField(labelWithString: "")
label.font = .systemFont(ofSize: LauncherRowView.textSize, weight: .medium)
label.font = font
label.textColor = SunsetPalette.text
label.lineBreakMode = .byTruncatingTail
locationLabel.font = .systemFont(ofSize: 12)
@@ -876,7 +877,7 @@ final class ApplicationCell: NSTableCellView {
title.orientation = .horizontal
title.alignment = .centerY
title.spacing = 8
kindLabel.font = .systemFont(ofSize: 14)
kindLabel.font = .systemFont(ofSize: font.pointSize)
kindLabel.textColor = SunsetPalette.secondaryText
kindLabel.setContentCompressionResistancePriority(.required, for: .horizontal)
kindLabel.setAccessibilityElement(false)
@@ -897,8 +898,6 @@ final class ApplicationCell: NSTableCellView {
NSLayoutConstraint.activate([
icon.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 8),
icon.centerYAnchor.constraint(equalTo: centerYAnchor),
icon.widthAnchor.constraint(equalToConstant: LauncherRowView.iconSize),
icon.heightAnchor.constraint(equalToConstant: LauncherRowView.iconSize),
title.leadingAnchor.constraint(equalTo: icon.trailingAnchor, constant: LauncherRowView.iconToTextSpacing),
title.centerYAnchor.constraint(equalTo: centerYAnchor),
title.trailingAnchor.constraint(lessThanOrEqualTo: kindLabel.leadingAnchor, constant: -12),
@@ -1181,7 +1180,6 @@ final class LauncherPanel: NSPanel {
final class LauncherRowView: NSTableRowView {
static let rowHeight: CGFloat = 38
static let textSize: CGFloat = 14
static let iconSize: CGFloat = 22
static let iconToTextSpacing: CGFloat = 12
var topSpacing: CGFloat = 0 {
didSet { needsLayout = true; needsDisplay = true }
+1 -1
View File
@@ -26,7 +26,7 @@ final class TranslationController: NSViewController, NSTextFieldDelegate, NSText
let title = NSTextField(labelWithString: "Conjure Translate · English → Japanese")
title.font = .systemFont(ofSize: 13, weight: .semibold)
let back = NSButton(title: "", target: self, action: #selector(goBack))
back.image = NSImage(systemSymbolName: "chevron.left", accessibilityDescription: nil)
back.image = IconStyle.symbol("chevron.left", for: title.font!)
back.imageScaling = .scaleProportionallyDown
back.isBordered = false
back.contentTintColor = SunsetPalette.secondaryText
@@ -88,7 +88,7 @@ final class UninstallReviewController: NSViewController, NSSearchFieldDelegate,
back.target = self
back.action = #selector(goBack)
back.title = ""
back.image = NSImage(systemSymbolName: "chevron.left", accessibilityDescription: nil)
back.image = IconStyle.symbol("chevron.left", for: search.font!)
back.imageScaling = .scaleProportionallyDown
back.isBordered = false
back.contentTintColor = SunsetPalette.secondaryText
@@ -429,11 +429,13 @@ private final class UninstallFileCell: NSTableCellView {
let name = NSTextField(labelWithString: "")
let detail = NSTextField(labelWithString: "")
let size = NSTextField(labelWithString: "")
let fileIcon = NSImageView()
let fileIcon: IconView
init() {
let font = NSFont.systemFont(ofSize: 14, weight: .medium)
fileIcon = IconView(font: font)
super.init(frame: .zero)
name.font = .systemFont(ofSize: 14, weight: .medium)
name.font = font
detail.font = .systemFont(ofSize: 11)
detail.textColor = SunsetPalette.secondaryText
for label in [name, detail] {
@@ -447,7 +449,6 @@ private final class UninstallFileCell: NSTableCellView {
size.textColor = SunsetPalette.secondaryText
size.font = .systemFont(ofSize: 12)
size.setContentCompressionResistancePriority(.required, for: .horizontal)
fileIcon.imageScaling = .scaleProportionallyDown
let row = NSStackView(views: [toggle, labels, NSView(), size, fileIcon])
row.orientation = .horizontal
row.alignment = .centerY
@@ -458,9 +459,7 @@ private final class UninstallFileCell: NSTableCellView {
row.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 8),
row.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -8),
row.topAnchor.constraint(equalTo: topAnchor, constant: 4),
row.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -4),
fileIcon.widthAnchor.constraint(equalToConstant: 22),
fileIcon.heightAnchor.constraint(equalToConstant: 22)
row.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -4)
])
}