A collection of user interface components and drawing routines for building tasteful apps using AppKit.
appkit swift aqua ui mac

Initial commit

+879
+4
.gitignore
··· 1 + xcuserdata 2 + .swiftpm 3 + build 4 + .build
+14
Package.swift
··· 1 + // swift-tools-version: 6.2 2 + 3 + import PackageDescription 4 + 5 + let package = Package( 6 + name: "AquaKit", 7 + platforms: [.macOS(.v26)], 8 + products: [ 9 + .library(name: "AquaKit", targets: ["AquaKit"]) 10 + ], 11 + targets: [ 12 + .target(name: "AquaKit") 13 + ] 14 + )
+73
Sources/AquaKit/Controls and Views/AquaResizeHandle.swift
··· 1 + import AppKit 2 + 3 + open class AquaResizeHandle: NSControl { 4 + public struct Drag { 5 + public var initialLocation: NSPoint = .zero 6 + public var delta: CGFloat = 0 7 + } 8 + 9 + override open var mouseDownCanMoveWindow: Bool { false } 10 + public private(set) var activeDrag: Drag? 11 + 12 + public override init(frame frameRect: NSRect) { 13 + super.init(frame: frameRect) 14 + 15 + addTrackingArea( 16 + NSTrackingArea( 17 + rect: .zero, 18 + options: [ 19 + .mouseEnteredAndExited, 20 + .mouseMoved, 21 + .cursorUpdate, 22 + .activeInKeyWindow, 23 + .inVisibleRect, 24 + ], 25 + owner: self, 26 + userInfo: nil 27 + ) 28 + ) 29 + 30 + NSLayoutConstraint.activate([ 31 + widthAnchor.constraint(equalToConstant: 16) 32 + ]) 33 + } 34 + 35 + public required init?(coder: NSCoder) { 36 + fatalError("init(coder:) has not been implemented") 37 + } 38 + 39 + override open func draw(_ dirtyRect: NSRect) { 40 + let path = NSBezierPath() 41 + let bounds = bounds.insetBy(dx: 4, dy: 0) 42 + path.move(to: NSPoint(x: bounds.minX, y: bounds.minY)) 43 + path.line(to: NSPoint(x: bounds.minX, y: bounds.maxY)) 44 + path.move(to: NSPoint(x: bounds.midX, y: bounds.minY)) 45 + path.line(to: NSPoint(x: bounds.midX, y: bounds.maxY)) 46 + path.move(to: NSPoint(x: bounds.maxX, y: bounds.minY)) 47 + path.line(to: NSPoint(x: bounds.maxX, y: bounds.maxY)) 48 + path.lineWidth = 1 49 + 50 + NSColor.aquaSeparatorColor.setStroke() 51 + path.stroke() 52 + } 53 + 54 + override open func mouseDown(with event: NSEvent) { 55 + activeDrag = Drag(initialLocation: convert(event.locationInWindow, from: nil)) 56 + } 57 + 58 + override open func mouseDragged(with event: NSEvent) { 59 + guard var drag = activeDrag else { return } 60 + let location = convert(event.locationInWindow, from: nil) 61 + drag.delta = location.x - drag.initialLocation.x 62 + activeDrag = drag 63 + sendAction(action, to: target) 64 + } 65 + 66 + override open func cursorUpdate(with event: NSEvent) { 67 + NSCursor.columnResize(directions: [.all]).set() 68 + } 69 + 70 + override open func acceptsFirstMouse(for event: NSEvent?) -> Bool { 71 + true 72 + } 73 + }
+20
Sources/AquaKit/Controls and Views/AquaSplitView.swift
··· 1 + import AppKit 2 + 3 + open class AquaSplitView: NSSplitView { 4 + override open var dividerColor: NSColor { 5 + .aquaSeparatorColor 6 + } 7 + 8 + public override init(frame frameRect: NSRect) { 9 + super.init(frame: frameRect) 10 + dividerStyle = .thin 11 + } 12 + 13 + override open func viewDidChangeEffectiveAppearance() { 14 + setNeedsDisplay(bounds) 15 + } 16 + 17 + public required init?(coder: NSCoder) { 18 + fatalError("init(coder:) has not been implemented") 19 + } 20 + }
+25
Sources/AquaKit/General Purpose/ContainerViewController.swift
··· 1 + import AppKit 2 + 3 + open class ContainerViewController: NSViewController { 4 + public override init(nibName nibNameOrNil: NSNib.Name?, bundle nibBundleOrNil: Bundle?) { 5 + super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) 6 + } 7 + 8 + public required init?(coder: NSCoder) { 9 + fatalError("init(coder:) has not been implemented") 10 + } 11 + 12 + open var contentViewController: NSViewController? { 13 + didSet { 14 + oldValue?.removeFromParent() 15 + if let contentViewController { 16 + let contentView = contentViewController.view 17 + contentView.translatesAutoresizingMaskIntoConstraints = false 18 + 19 + view.addSubview(contentView) 20 + NSLayoutConstraint.activate(contentView.constraintsAnchoring(toLayoutGuide: view.safeAreaLayoutGuide)) 21 + addChild(contentViewController) 22 + } 23 + } 24 + } 25 + }
+17
Sources/AquaKit/General Purpose/LayoutGuideProtocol.swift
··· 1 + import AppKit 2 + 3 + public protocol LayoutGuideProtocol { 4 + var leadingAnchor: NSLayoutXAxisAnchor { get } 5 + var trailingAnchor: NSLayoutXAxisAnchor { get } 6 + var leftAnchor: NSLayoutXAxisAnchor { get } 7 + var rightAnchor: NSLayoutXAxisAnchor { get } 8 + var topAnchor: NSLayoutYAxisAnchor { get } 9 + var bottomAnchor: NSLayoutYAxisAnchor { get } 10 + var widthAnchor: NSLayoutDimension { get } 11 + var heightAnchor: NSLayoutDimension { get } 12 + var centerXAnchor: NSLayoutXAxisAnchor { get } 13 + var centerYAnchor: NSLayoutYAxisAnchor { get } 14 + } 15 + 16 + extension NSLayoutGuide: LayoutGuideProtocol {} 17 + extension NSView: @MainActor LayoutGuideProtocol {}
+7
Sources/AquaKit/General Purpose/NSAppearance+DarkMode.swift
··· 1 + import AppKit 2 + 3 + extension NSAppearance { 4 + public var isDarkAqua: Bool { 5 + bestMatch(from: [.darkAqua, .aqua]) == .darkAqua 6 + } 7 + }
+33
Sources/AquaKit/General Purpose/NSColor+Aqua.swift
··· 1 + import AppKit 2 + 3 + extension NSColor { 4 + public var isGrayscale: Bool { 5 + let converted = self.usingColorSpace(.displayP3)! 6 + return converted.redComponent == converted.greenComponent && converted.greenComponent == converted.blueComponent 7 + } 8 + 9 + public static var graphiteColor: NSColor { 10 + #colorLiteral(red: 0.4922081232, green: 0.5492551327, blue: 0.632959187, alpha: 1) 11 + } 12 + 13 + public static var pinstripes: Self { 14 + let stripeThickness = 1.5 15 + let patternSize = CGSize(width: stripeThickness, height: stripeThickness * 3) 16 + let image = NSImage( 17 + size: patternSize, 18 + flipped: false, 19 + drawingHandler: { _ in 20 + guard let context = NSGraphicsContext.current?.cgContext else { return false } 21 + context.setFillColor(NSColor.separatorColor.withAlphaComponent(0.02).cgColor) 22 + context.fill(CGRect(x: 0, y: stripeThickness, width: stripeThickness, height: stripeThickness)) 23 + return true 24 + } 25 + ) 26 + 27 + return Self(patternImage: image) 28 + } 29 + 30 + public static var aquaSeparatorColor: NSColor { 31 + .separatorColor.withAlphaComponent(0.22) 32 + } 33 + }
+22
Sources/AquaKit/General Purpose/NSColor+DarkMode.swift
··· 1 + import AppKit 2 + 3 + extension NSColor { 4 + @MainActor 5 + public convenience init(light: @escaping @autoclosure () -> NSColor, dark: @escaping @autoclosure () -> NSColor) { 6 + self.init(name: nil) { appearance in 7 + if appearance.isDarkAqua { dark() } else { light() } 8 + } 9 + } 10 + 11 + public func modify(_ fun: @escaping (NSAppearance, NSColor) -> NSColor) -> NSColor { 12 + return Self.init(name: nil) { appearance in 13 + return fun(appearance, self) 14 + } 15 + } 16 + 17 + public func modifyDark(_ fun: @escaping (NSColor) -> NSColor) -> NSColor { 18 + return modify { appearance, color in 19 + if appearance.isDarkAqua { fun(color) } else { color } 20 + } 21 + } 22 + }
+12
Sources/AquaKit/General Purpose/NSView+Anchoring.swift
··· 1 + import AppKit 2 + 3 + extension NSView { 4 + public func constraintsAnchoring(toLayoutGuide guide: LayoutGuideProtocol) -> [NSLayoutConstraint] { 5 + [ 6 + leadingAnchor.constraint(equalTo: guide.leadingAnchor), 7 + trailingAnchor.constraint(equalTo: guide.trailingAnchor), 8 + topAnchor.constraint(equalTo: guide.topAnchor), 9 + bottomAnchor.constraint(equalTo: guide.bottomAnchor), 10 + ] 11 + } 12 + }
+16
Sources/AquaKit/General Purpose/Split View Layout/DefaultSizedSplitViewController.swift
··· 1 + import AppKit 2 + 3 + open class DefaultSizedSplitViewController: NSSplitViewController { 4 + public override init(nibName nibNameOrNil: NSNib.Name?, bundle nibBundleOrNil: Bundle?) { 5 + super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) 6 + } 7 + 8 + public required init?(coder: NSCoder) { 9 + fatalError("init(coder:) has not been implemented") 10 + } 11 + 12 + override open func viewDidAppear() { 13 + super.viewDidAppear() 14 + applyPreferredDividerPositions() 15 + } 16 + }
+33
Sources/AquaKit/General Purpose/Split View Layout/NSSplitViewController+PreferredDividerPosition.swift
··· 1 + import AppKit 2 + 3 + extension NSSplitViewController { 4 + private var hasInitialisedSplitLayoutsKey: String? { 5 + self.splitView.autosaveName.map { name in 6 + "HasAppliedPreferredDividerPositions[\(name)]" 7 + } 8 + } 9 + 10 + public func applyPreferredDividerPositionsIfNeeded() { 11 + guard let key = hasInitialisedSplitLayoutsKey else { return } 12 + let defaults = UserDefaults.standard 13 + if !defaults.bool(forKey: key) { 14 + applyPreferredDividerPositions() 15 + defaults.set(true, forKey: key) 16 + } 17 + } 18 + 19 + public func applyPreferredDividerPositions() { 20 + var dividerIndex = 0 21 + var runningPosition = 0.0 22 + 23 + let totalExtent = splitView.isVertical ? view.frame.width : view.frame.height 24 + for item in splitViewItems.dropLast() { 25 + if item.preferredThicknessFraction >= 0 { 26 + runningPosition += totalExtent * item.preferredThicknessFraction 27 + splitView.setPosition(runningPosition, ofDividerAt: dividerIndex) 28 + } 29 + 30 + dividerIndex += 1 31 + } 32 + } 33 + }
+41
Sources/AquaKit/General Purpose/WindowStateSentinelView.swift
··· 1 + import AppKit 2 + 3 + open class WindowStateSentinelView: NSView { 4 + public static let notificationNames: [NSNotification.Name] = [ 5 + NSWindow.didBecomeKeyNotification, 6 + NSWindow.didResignKeyNotification, 7 + NSWindow.didBecomeMainNotification, 8 + NSWindow.didResignMainNotification, 9 + ] 10 + 11 + public override init(frame frameRect: NSRect) { 12 + super.init(frame: frameRect) 13 + } 14 + 15 + public required init?(coder: NSCoder) { 16 + fatalError("init(coder:) has not been implemented") 17 + } 18 + 19 + override open func viewWillMove(toWindow newWindow: NSWindow?) { 20 + for name in Self.notificationNames { 21 + NotificationCenter.default.removeObserver(self, name: name, object: window) 22 + } 23 + } 24 + 25 + override open func viewDidMoveToWindow() { 26 + guard let window else { return } 27 + for name in Self.notificationNames { 28 + NotificationCenter.default.addObserver( 29 + self, 30 + selector: #selector(windowStateDidChange(_:)), 31 + name: name, 32 + object: window 33 + ) 34 + } 35 + } 36 + 37 + @objc private func windowStateDidChange(_ notification: Notification) { 38 + guard let superview else { return } 39 + superview.setNeedsDisplay(superview.bounds) 40 + } 41 + }
+13
Sources/AquaKit/Source Lists/NSSplitViewItem+SourceList.swift
··· 1 + import AppKit 2 + 3 + extension NSSplitViewItem { 4 + public convenience init(sourceListWithViewController viewController: NSViewController) { 5 + self.init(viewController: viewController) 6 + holdingPriority = NSLayoutConstraint.Priority(260) 7 + preferredThicknessFraction = 0.15 8 + canCollapse = true 9 + isSpringLoaded = true 10 + minimumThickness = 140.0 11 + maximumThickness = -1.0 12 + } 13 + }
+87
Sources/AquaKit/Source Lists/SourceListBottomBarViewController.swift
··· 1 + import AppKit 2 + 3 + open class SourceListBottomBarViewController: NSSplitViewItemAccessoryViewController { 4 + public let resizeHandle = AquaResizeHandle() 5 + 6 + public override init(nibName nibNameOrNil: NSNib.Name?, bundle nibBundleOrNil: Bundle?) { 7 + super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) 8 + } 9 + 10 + public required init?(coder: NSCoder) { 11 + fatalError("init(coder:) has not been implemented") 12 + } 13 + 14 + private class View: NSView { 15 + override init(frame frameRect: NSRect) { 16 + super.init(frame: frameRect) 17 + addSubview(WindowStateSentinelView()) 18 + } 19 + 20 + required init?(coder: NSCoder) { 21 + fatalError("init(coder:) has not been implemented") 22 + } 23 + 24 + override func draw(_ dirtyRect: NSRect) { 25 + NSGraphicsContext.saveGraphicsState() 26 + 27 + var alpha = 1.0 28 + if let window, !window.isKeyWindow { 29 + alpha *= 0.6 30 + } 31 + if effectiveAppearance.isDarkAqua { 32 + alpha *= 0.3 33 + } 34 + 35 + NSGraphicsContext.current?.cgContext.setAlpha(alpha) 36 + 37 + let color1 = NSColor( 38 + light: NSColor(white: 0.98, alpha: 1), 39 + dark: NSColor(white: 0.98, alpha: 1), 40 + ) 41 + let color2 = NSColor( 42 + light: NSColor(white: 0.96, alpha: 1), 43 + dark: NSColor.graphiteColor.highlight(withLevel: 0.7)! 44 + ) 45 + let color3 = NSColor( 46 + light: NSColor(white: 0.92, alpha: 1), 47 + dark: NSColor.graphiteColor.shadow(withLevel: 0.2)! 48 + ) 49 + 50 + let gradient = NSGradient( 51 + colorsAndLocations: (color1, 0), 52 + (color2, 0.48), 53 + (color3, 0.52), 54 + ) 55 + 56 + gradient?.draw(in: bounds, angle: 270) 57 + NSGraphicsContext.restoreGraphicsState() 58 + 59 + let borderPath = NSBezierPath() 60 + borderPath.move(to: NSPoint(x: bounds.minX, y: bounds.maxY)) 61 + borderPath.line(to: NSPoint(x: bounds.maxX, y: bounds.maxY)) 62 + borderPath.lineWidth = 1 63 + NSColor.aquaSeparatorColor.setStroke() 64 + borderPath.stroke() 65 + } 66 + } 67 + 68 + override open func loadView() { 69 + view = View() 70 + } 71 + 72 + override open func viewDidLoad() { 73 + super.viewDidLoad() 74 + 75 + resizeHandle.translatesAutoresizingMaskIntoConstraints = false 76 + view.addSubview(resizeHandle) 77 + 78 + NSLayoutConstraint.activate([ 79 + resizeHandle.centerYAnchor.constraint(equalTo: view.centerYAnchor), 80 + resizeHandle.rightAnchor.constraint(equalTo: view.safeAreaLayoutGuide.rightAnchor), 81 + resizeHandle.heightAnchor.constraint(equalTo: view.safeAreaLayoutGuide.heightAnchor, constant: -16), 82 + ]) 83 + 84 + automaticallyAppliesContentInsets = false 85 + view.heightAnchor.constraint(equalToConstant: 30).isActive = true 86 + } 87 + }
+33
Sources/AquaKit/Table Headers/AquaTableHeaderCell.swift
··· 1 + import AppKit 2 + 3 + open class AquaTableHeaderCell: NSTableHeaderCell { 4 + public static var backgroundFillGradient: NSGradient! { 5 + let primary = NSColor( 6 + light: .white, 7 + dark: .graphiteColor.shadow(withLevel: 0.4)!.withAlphaComponent(0.5) 8 + ) 9 + let secondary = NSColor( 10 + light: NSColor(white: 0.9, alpha: 1), 11 + dark: .graphiteColor.shadow(withLevel: 0.7)!.withAlphaComponent(0.5) 12 + ) 13 + return NSGradient(colors: [ 14 + primary, secondary, primary, 15 + ]) 16 + } 17 + 18 + override open func draw(withFrame cellFrame: NSRect, in controlView: NSView) { 19 + Self.backgroundFillGradient.draw(in: cellFrame, angle: -90) 20 + let insetFrame = cellFrame.insetBy(dx: 0, dy: 3) 21 + drawInterior(withFrame: insetFrame, in: controlView) 22 + let dividerPath = NSBezierPath() 23 + dividerPath.move(to: NSPoint(x: insetFrame.maxX, y: insetFrame.minY)) 24 + dividerPath.line(to: NSPoint(x: insetFrame.maxX, y: insetFrame.maxY)) 25 + dividerPath.lineWidth = 1 26 + NSColor.aquaSeparatorColor.setStroke() 27 + dividerPath.stroke() 28 + if state == .on { 29 + NSColor.black.withAlphaComponent(0.07).setFill() 30 + cellFrame.fill() 31 + } 32 + } 33 + }
+31
Sources/AquaKit/Table Headers/AquaTableHeaderView.swift
··· 1 + import AppKit 2 + 3 + open class AquaTableHeaderView: NSTableHeaderView { 4 + public override init(frame frameRect: NSRect) { 5 + var frameRect = frameRect 6 + frameRect.size.height = 20 7 + super.init(frame: frameRect) 8 + addSubview(WindowStateSentinelView()) 9 + } 10 + 11 + public required init?(coder: NSCoder) { 12 + fatalError("init(coder:) has not been implemented") 13 + } 14 + 15 + override open func draw(_ dirtyRect: NSRect) { 16 + NSGraphicsContext.saveGraphicsState() 17 + if let window, !window.isKeyWindow { 18 + NSGraphicsContext.current?.cgContext.setAlpha(0.4) 19 + } 20 + super.draw(dirtyRect) 21 + NSGraphicsContext.restoreGraphicsState() 22 + 23 + NSColor.aquaSeparatorColor.setStroke() 24 + 25 + let borderPath = NSBezierPath() 26 + borderPath.move(to: NSPoint(x: bounds.minX, y: bounds.maxY)) 27 + borderPath.line(to: NSPoint(x: bounds.maxX, y: bounds.maxY)) 28 + borderPath.lineWidth = 2 29 + borderPath.stroke() 30 + } 31 + }
+38
Sources/AquaKit/Windows/AquaTitlebarBackgroundView.swift
··· 1 + import AppKit 2 + 3 + open class AquaTitlebarBackgroundView: NSView { 4 + public override init(frame frameRect: NSRect) { 5 + super.init(frame: frameRect) 6 + addSubview(WindowStateSentinelView()) 7 + } 8 + 9 + public required init?(coder: NSCoder) { 10 + fatalError("init(coder:) has not been implemented") 11 + } 12 + 13 + private var gradient: NSGradient? { 14 + let color1 = NSColor(light: #colorLiteral(red: 0.9262896776, green: 0.9262896776, blue: 0.9262896776, alpha: 1), dark: .underPageBackgroundColor.highlight(withLevel: 0.3)!.withAlphaComponent(0.2)) 15 + let color2 = NSColor(light: #colorLiteral(red: 0.8432617784, green: 0.8432616591, blue: 0.8432617784, alpha: 1), dark: .underPageBackgroundColor.withAlphaComponent(0.2)) 16 + return NSGradient(colors: [color1, color2])! 17 + } 18 + 19 + private var dividerPath: NSBezierPath { 20 + let dividerPath = NSBezierPath() 21 + dividerPath.move(to: bounds.origin) 22 + dividerPath.line(to: NSPoint(x: bounds.maxX, y: bounds.minY + 1)) 23 + dividerPath.lineWidth = 1 24 + return dividerPath 25 + } 26 + 27 + override open func draw(_ dirtyRect: NSRect) { 28 + gradient!.draw(in: bounds, angle: 270) 29 + 30 + NSColor.aquaSeparatorColor.setStroke() 31 + dividerPath.stroke() 32 + 33 + if let window, !window.isKeyWindow { 34 + NSColor.pinstripes.setFill() 35 + bounds.fill() 36 + } 37 + } 38 + }
+60
Sources/AquaKit/Windows/AquaToolbarToggleButton.swift
··· 1 + import AppKit 2 + 3 + open class AquaToolbarToggleButton: AquaWindowControlButton { 4 + public override init(frame frameRect: NSRect) { 5 + super.init(frame: frameRect) 6 + NSLayoutConstraint.activate([ 7 + widthAnchor.constraint(equalToConstant: 30), 8 + heightAnchor.constraint(equalToConstant: Self.windowControlButtonHeight), 9 + ]) 10 + } 11 + 12 + public required init?(coder: NSCoder) { 13 + fatalError("init(coder:) has not been implemented") 14 + } 15 + 16 + override open func draw(_ dirtyRect: NSRect) { 17 + NSGraphicsContext.saveGraphicsState() 18 + 19 + if let window, !window.isKeyWindow { 20 + NSGraphicsContext.current?.cgContext.setAlpha( 21 + effectiveAppearance.isDarkAqua ? 0.3 : 0.4 22 + ) 23 + } 24 + 25 + let lozengeRect = bounds.insetBy(dx: 0, dy: 2) 26 + let lozenge = NSBezierPath(roundedRect: lozengeRect, xRadius: 6, yRadius: 6) 27 + lozenge.lineWidth = 1.0 28 + NSColor.gray.shadow(withLevel: 0.5)!.setStroke() 29 + lozenge.stroke() 30 + 31 + func shadow(_ color: NSColor) -> NSColor { 32 + color.shadow(withLevel: 0.2)! 33 + } 34 + 35 + let gradient = NSGradient( 36 + colorsAndLocations: ( 37 + #colorLiteral(red: 0.7792062163, green: 0.7657493949, blue: 0.7566991448, alpha: 1).modifyDark(shadow(_:)), 0.0 38 + ), 39 + (#colorLiteral(red: 0.9971552491, green: 0.9922525287, blue: 0.9795755744, alpha: 1).modifyDark(shadow(_:)), 1.0) 40 + )! 41 + gradient.draw(in: lozenge, angle: 270) 42 + 43 + do { 44 + let shinePath = NSBezierPath( 45 + roundedRect: lozengeRect.insetBy(dx: bounds.width * 0.02, dy: bounds.width * 0.02), 46 + xRadius: 6, 47 + yRadius: 6 48 + ) 49 + let shineGradient = NSGradient(colorsAndLocations: (.white, 0.0), (.clear, 0.4))! 50 + shineGradient.draw(in: shinePath, angle: 270) 51 + } 52 + 53 + if isHighlighted { 54 + NSColor.gray.withAlphaComponent(0.4).setFill() 55 + lozenge.fill() 56 + } 57 + 58 + NSGraphicsContext.restoreGraphicsState() 59 + } 60 + }
+168
Sources/AquaKit/Windows/AquaTrafficLightButton.swift
··· 1 + import AppKit 2 + 3 + @MainActor @objc public protocol AquaWindowControlAreaEvents { 4 + @objc func mouseEnteredControlArea(_: AquaTrafficLightButton) 5 + @objc func mouseExitedControlArea(_: AquaTrafficLightButton) 6 + } 7 + 8 + open class AquaTrafficLightButton: AquaWindowControlButton { 9 + public enum Kind { 10 + case close 11 + case miniaturize 12 + case zoom 13 + } 14 + 15 + public let kind: Kind 16 + private let imageView: NSImageView 17 + 18 + public var isHovered: Bool = false { 19 + didSet { 20 + imageView.isHidden = !isHovered 21 + setNeedsDisplay(bounds) 22 + } 23 + } 24 + 25 + public required init(kind: Kind, frame: NSRect) { 26 + self.kind = kind 27 + self.imageView = NSImageView(image: kind.image) 28 + super.init(frame: frame) 29 + 30 + NSLayoutConstraint.activate([ 31 + widthAnchor.constraint(equalToConstant: Self.windowControlButtonHeight), 32 + heightAnchor.constraint(equalToConstant: Self.windowControlButtonHeight), 33 + ]) 34 + 35 + imageView.imageScaling = .scaleProportionallyUpOrDown 36 + imageView.symbolConfiguration = NSImage.SymbolConfiguration(pointSize: 8, weight: .black) 37 + imageView.contentTintColor = NSColor.black.withAlphaComponent(0.7) 38 + imageView.isHidden = !isHovered 39 + imageView.imageAlignment = .alignCenter 40 + imageView.translatesAutoresizingMaskIntoConstraints = false 41 + imageView.image = NSImage(systemSymbolName: kind.systemSymbolName, accessibilityDescription: nil) 42 + 43 + addSubview(imageView) 44 + 45 + additionalSafeAreaInsets = NSEdgeInsets(top: 4, left: 4, bottom: 4, right: 4) 46 + NSLayoutConstraint.activate([ 47 + imageView.centerXAnchor.constraint(equalTo: centerXAnchor), 48 + imageView.centerYAnchor.constraint(equalTo: centerYAnchor), 49 + ]) 50 + 51 + let trackingAreaOptions: NSTrackingArea.Options = [ 52 + .activeAlways, 53 + .mouseEnteredAndExited, 54 + .assumeInside, 55 + .inVisibleRect, 56 + ] 57 + 58 + let trackingArea = NSTrackingArea(rect: .zero, options: trackingAreaOptions, owner: self) 59 + addTrackingArea(trackingArea) 60 + } 61 + 62 + public required init?(coder: NSCoder) { 63 + fatalError("init(coder:) has not been implemented") 64 + } 65 + 66 + override open func draw(_ dirtyRect: NSRect) { 67 + NSGraphicsContext.saveGraphicsState() 68 + 69 + var color = kind.baseColor 70 + if NSColor.controlAccentColor.isGrayscale { 71 + color = .graphiteColor 72 + } 73 + if isHighlighted { 74 + color = color.shadow(withLevel: 0.4)! 75 + } 76 + if let window, !window.isKeyWindow { 77 + color = .graphiteColor.highlight(withLevel: 0.8)! 78 + NSGraphicsContext.current?.cgContext.setAlpha(0.4) 79 + } 80 + 81 + let strokeWidth = 1.0 82 + let bounds = bounds 83 + let ellipse = NSBezierPath(ovalIn: bounds) 84 + 85 + NSGraphicsContext.saveGraphicsState() 86 + ellipse.setClip() 87 + do { 88 + ellipse.lineWidth = strokeWidth 89 + color.shadow(withLevel: 0.4)!.setFill() 90 + ellipse.fill() 91 + } 92 + 93 + do { 94 + let bottomGlowGradient = NSGradient( 95 + colorsAndLocations: (.white, 0.0), 96 + (.white.withAlphaComponent(0.8), 0.2), 97 + (.clear, 0.5) 98 + )! 99 + 100 + let gradientRect = bounds.insetBy(dx: 0, dy: 0) 101 + bottomGlowGradient.draw(in: gradientRect, relativeCenterPosition: NSPoint(x: 0, y: -0.7)) 102 + } 103 + 104 + do { 105 + let centerGlowGradient = NSGradient( 106 + colorsAndLocations: (color.withAlphaComponent(0.7), 0.0), 107 + (color.withAlphaComponent(0.5), 0.5), 108 + (color.withAlphaComponent(0.1), 1), 109 + )! 110 + 111 + centerGlowGradient.draw(in: bounds, relativeCenterPosition: .zero) 112 + } 113 + 114 + do { 115 + let innerEllipse = NSBezierPath(ovalIn: bounds.insetBy(dx: bounds.width * 0.05, dy: bounds.width * 0.05)) 116 + let innerEllipseGradient = NSGradient(colorsAndLocations: (.white.withAlphaComponent(0.7), 0.0), (.clear, 0.4))! 117 + innerEllipseGradient.draw(in: innerEllipse, angle: 270) 118 + } 119 + NSGraphicsContext.restoreGraphicsState() 120 + 121 + color.shadow(withLevel: 0.7)!.setStroke() 122 + ellipse.stroke() 123 + 124 + NSGraphicsContext.restoreGraphicsState() 125 + } 126 + 127 + @objc public func setDocumentEdited(_ edited: Bool) {} 128 + @objc public func mouseEnteredOrExited() {} 129 + 130 + override open func acceptsFirstMouse(for event: NSEvent?) -> Bool { 131 + return true 132 + } 133 + 134 + override open func mouseEntered(with event: NSEvent) { 135 + super.mouseEntered(with: event) 136 + NSApp.sendAction(#selector(AquaWindowControlAreaEvents.mouseEnteredControlArea(_:)), to: nil, from: self) 137 + } 138 + 139 + override open func mouseExited(with event: NSEvent) { 140 + super.mouseExited(with: event) 141 + NSApp.sendAction(#selector(AquaWindowControlAreaEvents.mouseExitedControlArea(_:)), to: nil, from: self) 142 + } 143 + } 144 + 145 + extension AquaTrafficLightButton.Kind { 146 + public var systemSymbolName: String { 147 + switch self { 148 + case .close: "xmark" 149 + case .miniaturize: "minus" 150 + case .zoom: "plus" 151 + } 152 + } 153 + 154 + public var image: NSImage { 155 + NSImage(systemSymbolName: systemSymbolName, accessibilityDescription: nil)! 156 + } 157 + 158 + @MainActor public var baseColor: NSColor { 159 + let unblended = 160 + switch self { 161 + case .close: #colorLiteral(red: 0.9998044372, green: 0.3607223034, blue: 0.3726101518, alpha: 1) 162 + case .miniaturize: #colorLiteral(red: 0.9804074168, green: 0.7845029235, blue: 0, alpha: 1) 163 + case .zoom: #colorLiteral(red: 0.5023847222, green: 0.7764200568, blue: 0.07384926826, alpha: 1) 164 + } 165 + 166 + return NSColor(light: unblended, dark: unblended.blended(withFraction: 0.5, of: .black)!) 167 + } 168 + }
+70
Sources/AquaKit/Windows/AquaWindow.swift
··· 1 + import AppKit 2 + 3 + open class AquaWindow: NSWindow { 4 + private var _showsToolbarButton: Bool = false 5 + override open var showsToolbarButton: Bool { 6 + get { _showsToolbarButton } 7 + set { _showsToolbarButton = newValue } 8 + } 9 + 10 + public override init( 11 + contentRect: NSRect, 12 + styleMask style: NSWindow.StyleMask, 13 + backing backingStoreType: NSWindow.BackingStoreType, 14 + defer flag: Bool 15 + ) { 16 + super.init(contentRect: contentRect, styleMask: style, backing: backingStoreType, defer: flag) 17 + isMovableByWindowBackground = true 18 + } 19 + 20 + public static override func standardWindowButton(_ b: NSWindow.ButtonType, for styleMask: NSWindow.StyleMask) -> NSButton? { 21 + guard let original = super.standardWindowButton(b, for: styleMask) else { 22 + if b == .toolbarButton { 23 + let toolbarButton = AquaToolbarToggleButton(frame: NSRect(x: 0, y: 0, width: 30, height: 10)) 24 + toolbarButton.action = #selector(toggleToolbarShown(_:)) 25 + return toolbarButton 26 + } 27 + 28 + return nil 29 + } 30 + 31 + let frame = original.frame 32 + 33 + switch b { 34 + case .closeButton: 35 + let button = AquaTrafficLightButton(kind: .close, frame: frame) 36 + button.action = #selector(close) 37 + return button 38 + case .miniaturizeButton: 39 + let button = AquaTrafficLightButton(kind: .miniaturize, frame: frame) 40 + button.action = #selector(miniaturize(_:)) 41 + return button 42 + case .zoomButton: 43 + let button = AquaTrafficLightButton(kind: .zoom, frame: frame) 44 + button.action = #selector(zoom(_:)) 45 + return button 46 + default: return original 47 + } 48 + } 49 + 50 + private var trafficLightButtons: [AquaTrafficLightButton] { 51 + let types: [ButtonType] = [.closeButton, .miniaturizeButton, .zoomButton] 52 + return types.compactMap { b in 53 + standardWindowButton(b) as? AquaTrafficLightButton 54 + } 55 + } 56 + } 57 + 58 + @MainActor extension AquaWindow: AquaWindowControlAreaEvents { 59 + public func mouseEnteredControlArea(_: AquaTrafficLightButton) { 60 + for button in trafficLightButtons { 61 + button.isHovered = true 62 + } 63 + } 64 + 65 + public func mouseExitedControlArea(_: AquaTrafficLightButton) { 66 + for button in trafficLightButtons { 67 + button.isHovered = false 68 + } 69 + } 70 + }
+27
Sources/AquaKit/Windows/AquaWindowContainerViewController.swift
··· 1 + import AppKit 2 + 3 + open class AquaWindowContainerViewController: ContainerViewController { 4 + public override init(nibName nibNameOrNil: NSNib.Name?, bundle nibBundleOrNil: Bundle?) { 5 + super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) 6 + } 7 + 8 + public required init?(coder: NSCoder) { 9 + fatalError("init(coder:) has not been implemented") 10 + } 11 + 12 + override open func viewDidLoad() { 13 + super.viewDidLoad() 14 + 15 + let titlebarBackgroundView = AquaTitlebarBackgroundView() 16 + titlebarBackgroundView.translatesAutoresizingMaskIntoConstraints = false 17 + 18 + view.addSubview(titlebarBackgroundView) 19 + 20 + NSLayoutConstraint.activate([ 21 + titlebarBackgroundView.leftAnchor.constraint(equalTo: view.leftAnchor), 22 + titlebarBackgroundView.rightAnchor.constraint(equalTo: view.rightAnchor), 23 + titlebarBackgroundView.topAnchor.constraint(equalTo: view.topAnchor), 24 + titlebarBackgroundView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), 25 + ]) 26 + } 27 + }
+35
Sources/AquaKit/Windows/AquaWindowControlButton.swift
··· 1 + import AppKit 2 + 3 + open class AquaWindowControlButton: NSButton { 4 + public static let windowControlButtonTopOffset: CGFloat = 9.0 5 + public static let windowControlButtonHeight: CGFloat = 14.0 6 + 7 + public var constraint: NSLayoutConstraint? 8 + override open var isFlipped: Bool { false } 9 + 10 + public override init(frame frameRect: NSRect) { 11 + super.init(frame: frameRect) 12 + 13 + addSubview(WindowStateSentinelView()) 14 + 15 + title = "" 16 + wantsLayer = true 17 + isBordered = false 18 + 19 + translatesAutoresizingMaskIntoConstraints = false 20 + } 21 + 22 + public required init?(coder: NSCoder) { 23 + fatalError("init(coder:) has not been implemented") 24 + } 25 + 26 + override open func viewDidMoveToSuperview() { 27 + super.viewDidMoveToSuperview() 28 + 29 + constraint?.isActive = false 30 + 31 + guard let superview else { return } 32 + constraint = topAnchor.constraint(equalTo: superview.topAnchor, constant: Self.windowControlButtonTopOffset) 33 + constraint?.isActive = true 34 + } 35 + }