refactor: centralize font-relative UI icon sizing
This commit is contained in:
@@ -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)?
|
||||
|
||||
@@ -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") }
|
||||
}
|
||||
@@ -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 }
|
||||
|
||||
@@ -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)
|
||||
])
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user