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

Working on documentation

Also improving the API slightly.

+117 -65
+1
Sources/AquaKit/Controls and Views/AquaDragHandle.swift
··· 5 5 import AppKit 6 6 7 7 /// This is an abstract superclass that draws nothing but handles drag functionality. 8 + /// Drag information is propagated via the target/action pattern. 8 9 open class AquaDragHandle: NSControl { 9 10 public struct Drag { 10 11 public var initialLocation: NSPoint = .zero
+3
Sources/AquaKit/Controls and Views/AquaSplitViewResizeHandle.swift
··· 4 4 5 5 import AppKit 6 6 7 + /// This can be added to an `NSSplitViewItemAccessoryViewController`’s view. 8 + /// 9 + /// Uses the target/action pattern to propagate drag events; a ``AquaSplitViewResizeCoordinator`` can serve as a target. 7 10 open class AquaSplitViewResizeHandle: AquaDragHandle { 8 11 public override init(frame frameRect: NSRect) { 9 12 super.init(frame: frameRect)
+1
Sources/AquaKit/Controls and Views/AquaWindowResizeHandle.swift
··· 4 4 5 5 import AppKit 6 6 7 + /// This can be added to a window’s bottom. See ``AquaDragHandle`` for usage. 7 8 class AquaWindowResizeHandle: AquaDragHandle { 8 9 var cornerInset: CGFloat = 12 9 10 var lineSpacing: CGFloat = 2
+4 -2
Sources/AquaKit/Controls and Views/SplitViewResizeCoordinator.swift Sources/AquaKit/Controls and Views/AquaSplitViewResizeCoordinator.swift
··· 4 4 5 5 import AppKit 6 6 7 - open class SplitViewResizeCoordinator: NSObject { 7 + /// A suitable `target` for an ``AquaSplitViewResizeHandle``. 8 + open class AquaSplitViewResizeCoordinator: NSObject { 8 9 public var splitView: NSSplitView 9 10 public var dividerIndex: Int = 0 10 11 ··· 12 13 self.splitView = splitView 13 14 } 14 15 16 + /// Use this as the `action` for an ``AquaSplitViewResizeHandle``. 15 17 @MainActor 16 - @objc public func resize(_ sender: AquaSplitViewResizeHandle) { 18 + @objc open func resize(_ sender: AquaDragHandle) { 17 19 guard let drag = sender.activeDrag else { return } 18 20 let subview = splitView.arrangedSubviews[dividerIndex] 19 21 let currentPosition = splitView.isVertical ? subview.frame.maxX : subview.frame.maxY
+3
Sources/AquaKit/General Purpose/ContainerViewController.swift
··· 4 4 5 5 import AppKit 6 6 7 + /// A general-purpose view controller that owns a ``contentViewController``, whose view it lays out within its safe area layout guide. 8 + /// 9 + /// This is useful for creating a view with a bottom bar: just adjust the `additionalSafeAreaInsets` and insert the bottom bar’s view in the appropriate place. 7 10 open class ContainerViewController: NSViewController { 8 11 public override init(nibName nibNameOrNil: NSNib.Name?, bundle nibBundleOrNil: Bundle?) { 9 12 super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
+2 -1
Sources/AquaKit/General Purpose/WindowStateSentinelView.swift
··· 4 4 5 5 import AppKit 6 6 7 + /// This view will call `NSView.setNeedsDisplay(_:)` on its superview when the enclosing window’s key/main states change. 7 8 open class WindowStateSentinelView: NSView { 8 - public static let notificationNames: [NSNotification.Name] = [ 9 + private static let notificationNames: [NSNotification.Name] = [ 9 10 NSWindow.didBecomeKeyNotification, 10 11 NSWindow.didResignKeyNotification, 11 12 NSWindow.didBecomeMainNotification,
+2 -2
Sources/AquaKit/Scroll Views/AquaScrollCornerView.swift
··· 5 5 import AppKit 6 6 import AquaKit 7 7 8 - class AquaScrollCornerView: NSView { 9 - override func draw(_ dirtyRect: NSRect) { 8 + open class AquaScrollCornerView: NSView { 9 + override open func draw(_ dirtyRect: NSRect) { 10 10 NSColor.windowBackgroundColor.setFill() 11 11 bounds.fill() 12 12 }
+1 -1
Sources/AquaKit/Scroll Views/AquaScrollState.swift
··· 3 3 // SPDX-License-Identifier: MIT 4 4 5 5 import AppKit 6 - import Foundation 7 6 7 + /// This is used when working with an `NSScroller` directly without an `NSClipView`, as in ``AquaWebViewController``. 8 8 public struct AquaScrollState { 9 9 public var scrollPosition: NSPoint 10 10 public var contentSize: NSSize
+16 -4
Sources/AquaKit/Scroll Views/AquaScrollView.swift
··· 5 5 import AppKit 6 6 import AquaKit 7 7 8 + /// This subclass sets its horizontal and vertical scrollers to ``AquaScroller`` instances, and draws a view at their intersection when both scrollers are visible. The latter is important because the clipped content would appear behind that square if we did not intervene. 8 9 open class AquaScrollView: NSScrollView { 9 - private let cornerView = AquaScrollCornerView() 10 + /// By default, this is set to an instance of ``AquaScrollCornerView``. 11 + public var cornerView: NSView? { 12 + didSet { 13 + oldValue?.removeFromSuperview() 14 + if let cornerView { addSubview(cornerView) } 15 + } 16 + } 10 17 11 18 override init(frame frameRect: NSRect) { 12 19 super.init(frame: frameRect) 20 + 13 21 self.verticalScroller = AquaScroller(orientation: .vertical) 14 22 self.horizontalScroller = AquaScroller(orientation: .horizontal) 23 + 24 + let cornerView = AquaScrollCornerView() 15 25 addSubview(cornerView) 26 + 27 + self.cornerView = cornerView 16 28 } 17 29 18 30 required public init?(coder: NSCoder) { ··· 23 35 super.tile() 24 36 guard scrollerStyle == .legacy, hasHorizontalScroller, hasVerticalScroller, let verticalScroller, let horizontalScroller, !verticalScroller.isHidden, !horizontalScroller.isHidden 25 37 else { 26 - cornerView.isHidden = true 38 + cornerView?.isHidden = true 27 39 return 28 40 } 29 41 30 - cornerView.isHidden = false 31 - cornerView.frame = NSRect( 42 + cornerView?.isHidden = false 43 + cornerView?.frame = NSRect( 32 44 x: verticalScroller.frame.minX, 33 45 y: horizontalScroller.frame.minY, 34 46 width: verticalScroller.frame.width,
+1
Sources/AquaKit/Scroll Views/AquaScroller.swift
··· 5 5 import AppKit 6 6 import AquaKit 7 7 8 + /// The Aqua-styled scroller control that we lost in Mac OS X 10.7 (Lion). 8 9 public class AquaScroller: NSScroller { 9 10 public let orientation: Axis 10 11
+1
Sources/AquaKit/Source Lists/NSSplitViewItem+SourceList.swift
··· 5 5 import AppKit 6 6 7 7 extension NSSplitViewItem { 8 + /// Default settings for a pre-Tahoe-style source list split view item. 8 9 public convenience init(sourceListWithViewController viewController: NSViewController) { 9 10 self.init(viewController: viewController) 10 11 holdingPriority = NSLayoutConstraint.Priority(260)
+3
Sources/AquaKit/Source Lists/SourceListBottomBarViewController.swift
··· 4 4 5 5 import AppKit 6 6 7 + /// An Aqua-styled bottom bar suitable for placement in a source list. 8 + /// 9 + /// This has a built-in ``AquaSplitViewResizeHandle``, but you are responsible for hooking this up to the split view using an ``AquaSplitViewResizeCoordinator``. 7 10 open class SourceListBottomBarViewController: NSSplitViewItemAccessoryViewController { 8 11 public let resizeHandle = AquaSplitViewResizeHandle() 9 12
+3
Sources/AquaKit/Web Views/AquaWebViewController.swift
··· 5 5 import AppKit 6 6 import WebKit 7 7 8 + /// Wraps a `WKWebView` together with a vertical and horizontal ``AquaScroller`` whose states are bound to the web view. 9 + /// 10 + /// Unlike the deprecated `WebView`, WebKit’s `WKWebView` does not employ an `NSScrollView` in its view hierarchy; instead, scrollers are drawn by WebKit itself and the styling of these scrollers is not natively customisable. As a workaround, we create our own ``AquaScroller``s and bind their state to the web view using JavaScript injection. The scrollers wil lautomatically show and hide depending on whether or not they are needed. 8 11 open class AquaWebViewController: NSViewController { 9 12 public let webView = WKWebView() 10 13 public let horizontalScroller = AquaScroller(orientation: .horizontal)
+1
Sources/AquaKit/Windows/AquaTitlebarBackgroundView.swift
··· 4 4 5 5 import AppKit 6 6 7 + /// Used by ``AquaWindowContainerViewController``. 7 8 open class AquaTitlebarBackgroundView: NSView { 8 9 public override init(frame frameRect: NSRect) { 9 10 super.init(frame: frameRect)
+1 -1
Sources/AquaKit/Windows/AquaToolbarToggleButton.swift Sources/AquaKit/Windows/AquaWindowToolbarButton.swift
··· 4 4 5 5 import AppKit 6 6 7 - open class AquaToolbarToggleButton: AquaWindowControlButton { 7 + open class AquaWindowToolbarButton: AquaWindowControlButton { 8 8 public override init(frame frameRect: NSRect) { 9 9 super.init(frame: frameRect) 10 10 NSLayoutConstraint.activate([
+17 -42
Sources/AquaKit/Windows/AquaTrafficLightButton.swift
··· 9 9 @objc func mouseExitedControlArea(_: AquaTrafficLightButton) 10 10 } 11 11 12 - open class AquaTrafficLightButton: AquaWindowControlButton { 13 - public enum Kind { 14 - case close 15 - case miniaturize 16 - case zoom 12 + open class AquaTrafficLightButton: AquaWindowControlButton, AquaTrafficLightButtonProtocol { 13 + public var isHovered: Bool = false { 14 + didSet { 15 + imageView.isHidden = !isHovered 16 + setNeedsDisplay(bounds) 17 + } 17 18 } 18 19 19 - public let kind: Kind 20 - private let imageView: NSImageView 20 + public var baseColor: NSColor = .graphiteColor { 21 + didSet { 22 + setNeedsDisplay(bounds) 23 + } 24 + } 21 25 22 - public var isHovered: Bool = false { 26 + public var systemSymbolName: String? { 23 27 didSet { 24 - imageView.isHidden = !isHovered 28 + imageView.image = systemSymbolName.flatMap { name in NSImage(systemSymbolName: name, accessibilityDescription: nil) } 25 29 setNeedsDisplay(bounds) 26 30 } 27 31 } 28 32 29 - public required init(kind: Kind, frame: NSRect) { 30 - self.kind = kind 31 - self.imageView = NSImageView(image: kind.image) 33 + private let imageView: NSImageView = NSImageView() 34 + 35 + public override init(frame: NSRect) { 32 36 super.init(frame: frame) 33 37 34 38 NSLayoutConstraint.activate([ ··· 42 46 imageView.isHidden = !isHovered 43 47 imageView.imageAlignment = .alignCenter 44 48 imageView.translatesAutoresizingMaskIntoConstraints = false 45 - imageView.image = NSImage(systemSymbolName: kind.systemSymbolName, accessibilityDescription: nil) 46 49 47 50 addSubview(imageView) 48 51 ··· 70 73 override open func draw(_ dirtyRect: NSRect) { 71 74 NSGraphicsContext.saveGraphicsState() 72 75 73 - var color = kind.baseColor 76 + var color = baseColor 74 77 if NSColor.controlAccentColor.isGrayscale { 75 78 color = .graphiteColor 76 79 } ··· 145 148 NSApp.sendAction(#selector(AquaWindowControlAreaEvents.mouseExitedControlArea(_:)), to: nil, from: self) 146 149 } 147 150 } 148 - 149 - extension AquaTrafficLightButton.Kind { 150 - public var systemSymbolName: String { 151 - switch self { 152 - case .close: "xmark" 153 - case .miniaturize: "minus" 154 - case .zoom: "plus" 155 - } 156 - } 157 - 158 - public var image: NSImage { 159 - NSImage(systemSymbolName: systemSymbolName, accessibilityDescription: nil)! 160 - } 161 - 162 - @MainActor public var baseColor: NSColor { 163 - let unblended = 164 - switch self { 165 - case .close: #colorLiteral(red: 0.9998044372, green: 0.3607223034, blue: 0.3726101518, alpha: 1) 166 - case .miniaturize: #colorLiteral(red: 0.9804074168, green: 0.7845029235, blue: 0, alpha: 1) 167 - case .zoom: #colorLiteral(red: 0.5023847222, green: 0.7764200568, blue: 0.07384926826, alpha: 1) 168 - } 169 - 170 - return NSColor( 171 - light: unblended, 172 - dark: unblended.blended(withFraction: 0.6, of: .graphiteColor.shadow(withLevel: 0.9)!)! 173 - ) 174 - } 175 - }
+11
Sources/AquaKit/Windows/AquaTrafficLightButtonProtocol.swift
··· 1 + // SPDX-FileCopyrightText: 2026 Jon Sterling 2 + // 3 + // SPDX-License-Identifier: MIT 4 + 5 + import AppKit 6 + 7 + public protocol AquaTrafficLightButtonProtocol: NSButton { 8 + var systemSymbolName: String? { get set } 9 + var baseColor: NSColor { get set } 10 + var isHovered: Bool { get set } 11 + }
+40 -12
Sources/AquaKit/Windows/AquaWindow.swift
··· 4 4 5 5 import AppKit 6 6 7 + /// An `NSWindow` subclass with Aqua-styled traffic light buttons, and restored ``showsToolbarButton`` functionality. 8 + /// 9 + /// If you wish to have an Aqua-style window background, set `.fullSizeContentView` in the style mask and set the `contentViewController` to an instance of ``AquaWindowContainerViewController``. 7 10 open class AquaWindow: NSWindow { 11 + 12 + /// Defaults to ``AquaTrafficLightButton``. 13 + open class var trafficLightButtonClass: AquaTrafficLightButtonProtocol.Type { AquaTrafficLightButton.self } 14 + 15 + /// Defaults to ``AquaWindowToolbarButton`` 16 + open class var toolbarButtonClass: NSButton.Type { AquaWindowToolbarButton.self } 17 + 18 + open class var windowCloseButtonBaseColor: NSColor { #colorLiteral(red: 0.9998044372, green: 0.3607223034, blue: 0.3726101518, alpha: 1) } 19 + open class var windowMiniaturizeButtonBaseColor: NSColor { #colorLiteral(red: 0.9804074168, green: 0.7845029235, blue: 0, alpha: 1) } 20 + open class var windowZoomButtonBaseColor: NSColor { #colorLiteral(red: 0.5023847222, green: 0.7764200568, blue: 0.07384926826, alpha: 1) } 21 + 22 + /// Reference dimensions for bottom bars. 8 23 public static var bottomBarThickness: CGFloat { 30.0 } 9 24 10 25 private var _showsToolbarButton: Bool = false 26 + 27 + /// This property is overriden and its functionality restored. When activated, a lozenge-shaped button will appear in the northeastern corner of the window that toggles the toolbar’s visibility. 11 28 override open var showsToolbarButton: Bool { 12 29 get { _showsToolbarButton } 13 30 set { _showsToolbarButton = newValue } 14 31 } 15 32 16 - public override init( 17 - contentRect: NSRect, 18 - styleMask style: NSWindow.StyleMask, 19 - backing backingStoreType: NSWindow.BackingStoreType, 20 - defer flag: Bool 21 - ) { 33 + 34 + public override init(contentRect: NSRect, styleMask style: NSWindow.StyleMask, backing backingStoreType: NSWindow.BackingStoreType, defer flag: Bool) { 22 35 super.init(contentRect: contentRect, styleMask: style, backing: backingStoreType, defer: flag) 23 36 isMovableByWindowBackground = true 24 37 } ··· 26 39 public static override func standardWindowButton(_ b: NSWindow.ButtonType, for styleMask: NSWindow.StyleMask) -> NSButton? { 27 40 guard let original = super.standardWindowButton(b, for: styleMask) else { 28 41 if b == .toolbarButton { 29 - let toolbarButton = AquaToolbarToggleButton(frame: NSRect(x: 0, y: 0, width: 30, height: 10)) 42 + let toolbarButton = Self.toolbarButtonClass.init(frame: NSRect(x: 0, y: 0, width: 30, height: 10)) 30 43 toolbarButton.action = #selector(toggleToolbarShown(_:)) 31 44 return toolbarButton 32 45 } ··· 38 51 39 52 switch b { 40 53 case .closeButton: 41 - let button = AquaTrafficLightButton(kind: .close, frame: frame) 54 + let button = Self.trafficLightButtonClass.init(frame: frame) 55 + button.baseColor = Self.windowCloseButtonBaseColor.graphiteShadedInDarkMode 56 + button.systemSymbolName = "xmark" 42 57 button.action = #selector(close) 43 58 return button 44 59 case .miniaturizeButton: 45 - let button = AquaTrafficLightButton(kind: .miniaturize, frame: frame) 60 + let button = Self.trafficLightButtonClass.init(frame: frame) 61 + button.baseColor = Self.windowMiniaturizeButtonBaseColor.graphiteShadedInDarkMode 62 + button.systemSymbolName = "minus" 46 63 button.action = #selector(miniaturize(_:)) 47 64 return button 48 65 case .zoomButton: 49 - let button = AquaTrafficLightButton(kind: .zoom, frame: frame) 66 + let button = Self.trafficLightButtonClass.init(frame: frame) 67 + button.baseColor = Self.windowZoomButtonBaseColor.graphiteShadedInDarkMode 68 + button.systemSymbolName = "plus" 50 69 button.action = #selector(zoom(_:)) 51 70 return button 52 71 default: return original 53 72 } 54 73 } 55 74 56 - private var trafficLightButtons: [AquaTrafficLightButton] { 75 + private var trafficLightButtons: [AquaTrafficLightButtonProtocol] { 57 76 let types: [ButtonType] = [.closeButton, .miniaturizeButton, .zoomButton] 58 77 return types.compactMap { b in 59 - standardWindowButton(b) as? AquaTrafficLightButton 78 + standardWindowButton(b) as? AquaTrafficLightButtonProtocol 60 79 } 61 80 } 62 81 } ··· 74 93 } 75 94 } 76 95 } 96 + 97 + 98 + extension NSColor { 99 + fileprivate var graphiteShadedInDarkMode: NSColor { 100 + modifyDark { 101 + $0.blended(withFraction: 0.6, of: .graphiteColor.shadow(withLevel: 0.9)!)! 102 + } 103 + } 104 + }
+3
Sources/AquaKit/Windows/AquaWindowBottomBarViewController.swift
··· 4 4 5 5 import AppKit 6 6 7 + /// An Aqua-styled bottom bar view controller. 8 + /// 9 + /// The intended usage is to have a ``ContainerViewController`` whose ``ContainerViewController/contentViewController`` is your main view controller, set the container’s `additionalSafeAreaInsets` to account for the desired height of your bottom bar. Then add the ``AquaWindowBottomBarViewController`` to the container view controller as a child, and lay out its view at the bottom outside the safe area layout guide. 7 10 open class AquaWindowBottomBarViewController: ContainerViewController { 8 11 class View: NSView { 9 12 override init(frame frameRect: NSRect) {
+3
Sources/AquaKit/Windows/AquaWindowContainerViewController.swift
··· 4 4 5 5 import AppKit 6 6 7 + /// Draws an Aqua-styled unified titlebar/toolbar background outside the window’s safe area layout guide. 8 + /// 9 + /// See ``ContainerViewController`` for how to integrate your own view hierarchy underneath this. 7 10 open class AquaWindowContainerViewController: ContainerViewController { 8 11 public override init(nibName nibNameOrNil: NSNib.Name?, bundle nibBundleOrNil: Bundle?) { 9 12 super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)