Sources/AquaKit/Controls and Views/AquaDragHandle.swift
···55import AppKit
6677/// This is an abstract superclass that draws nothing but handles drag functionality.
88+/// Drag information is propagated via the target/action pattern.
89open class AquaDragHandle: NSControl {
910 public struct Drag {
1011 public var initialLocation: NSPoint = .zero
+3
Sources/AquaKit/Controls and Views/AquaSplitViewResizeHandle.swift
···4455import AppKit
6677+/// This can be added to an `NSSplitViewItemAccessoryViewController`’s view.
88+///
99+/// Uses the target/action pattern to propagate drag events; a ``AquaSplitViewResizeCoordinator`` can serve as a target.
710open class AquaSplitViewResizeHandle: AquaDragHandle {
811 public override init(frame frameRect: NSRect) {
912 super.init(frame: frameRect)
+1
Sources/AquaKit/Controls and Views/AquaWindowResizeHandle.swift
···4455import AppKit
6677+/// This can be added to a window’s bottom. See ``AquaDragHandle`` for usage.
78class AquaWindowResizeHandle: AquaDragHandle {
89 var cornerInset: CGFloat = 12
910 var lineSpacing: CGFloat = 2
+4-2
Sources/AquaKit/Controls and Views/SplitViewResizeCoordinator.swift
Sources/AquaKit/Controls and Views/AquaSplitViewResizeCoordinator.swift
···4455import AppKit
6677-open class SplitViewResizeCoordinator: NSObject {
77+/// A suitable `target` for an ``AquaSplitViewResizeHandle``.
88+open class AquaSplitViewResizeCoordinator: NSObject {
89 public var splitView: NSSplitView
910 public var dividerIndex: Int = 0
1011···1213 self.splitView = splitView
1314 }
14151616+ /// Use this as the `action` for an ``AquaSplitViewResizeHandle``.
1517 @MainActor
1616- @objc public func resize(_ sender: AquaSplitViewResizeHandle) {
1818+ @objc open func resize(_ sender: AquaDragHandle) {
1719 guard let drag = sender.activeDrag else { return }
1820 let subview = splitView.arrangedSubviews[dividerIndex]
1921 let currentPosition = splitView.isVertical ? subview.frame.maxX : subview.frame.maxY
···4455import AppKit
6677+/// A general-purpose view controller that owns a ``contentViewController``, whose view it lays out within its safe area layout guide.
88+///
99+/// 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.
710open class ContainerViewController: NSViewController {
811 public override init(nibName nibNameOrNil: NSNib.Name?, bundle nibBundleOrNil: Bundle?) {
912 super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
···4455import AppKit
6677+/// This view will call `NSView.setNeedsDisplay(_:)` on its superview when the enclosing window’s key/main states change.
78open class WindowStateSentinelView: NSView {
88- public static let notificationNames: [NSNotification.Name] = [
99+ private static let notificationNames: [NSNotification.Name] = [
910 NSWindow.didBecomeKeyNotification,
1011 NSWindow.didResignKeyNotification,
1112 NSWindow.didBecomeMainNotification,
···33// SPDX-License-Identifier: MIT
4455import AppKit
66-import Foundation
7677+/// This is used when working with an `NSScroller` directly without an `NSClipView`, as in ``AquaWebViewController``.
88public struct AquaScrollState {
99 public var scrollPosition: NSPoint
1010 public var contentSize: NSSize
+16-4
Sources/AquaKit/Scroll Views/AquaScrollView.swift
···55import AppKit
66import AquaKit
7788+/// 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.
89open class AquaScrollView: NSScrollView {
99- private let cornerView = AquaScrollCornerView()
1010+ /// By default, this is set to an instance of ``AquaScrollCornerView``.
1111+ public var cornerView: NSView? {
1212+ didSet {
1313+ oldValue?.removeFromSuperview()
1414+ if let cornerView { addSubview(cornerView) }
1515+ }
1616+ }
10171118 override init(frame frameRect: NSRect) {
1219 super.init(frame: frameRect)
2020+1321 self.verticalScroller = AquaScroller(orientation: .vertical)
1422 self.horizontalScroller = AquaScroller(orientation: .horizontal)
2323+2424+ let cornerView = AquaScrollCornerView()
1525 addSubview(cornerView)
2626+2727+ self.cornerView = cornerView
1628 }
17291830 required public init?(coder: NSCoder) {
···2335 super.tile()
2436 guard scrollerStyle == .legacy, hasHorizontalScroller, hasVerticalScroller, let verticalScroller, let horizontalScroller, !verticalScroller.isHidden, !horizontalScroller.isHidden
2537 else {
2626- cornerView.isHidden = true
3838+ cornerView?.isHidden = true
2739 return
2840 }
29413030- cornerView.isHidden = false
3131- cornerView.frame = NSRect(
4242+ cornerView?.isHidden = false
4343+ cornerView?.frame = NSRect(
3244 x: verticalScroller.frame.minX,
3345 y: horizontalScroller.frame.minY,
3446 width: verticalScroller.frame.width,
+1
Sources/AquaKit/Scroll Views/AquaScroller.swift
···55import AppKit
66import AquaKit
7788+/// The Aqua-styled scroller control that we lost in Mac OS X 10.7 (Lion).
89public class AquaScroller: NSScroller {
910 public let orientation: Axis
1011
···4455import AppKit
6677+/// An Aqua-styled bottom bar suitable for placement in a source list.
88+///
99+/// This has a built-in ``AquaSplitViewResizeHandle``, but you are responsible for hooking this up to the split view using an ``AquaSplitViewResizeCoordinator``.
710open class SourceListBottomBarViewController: NSSplitViewItemAccessoryViewController {
811 public let resizeHandle = AquaSplitViewResizeHandle()
912
···55import AppKit
66import WebKit
7788+/// Wraps a `WKWebView` together with a vertical and horizontal ``AquaScroller`` whose states are bound to the web view.
99+///
1010+/// 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.
811open class AquaWebViewController: NSViewController {
912 public let webView = WKWebView()
1013 public let horizontalScroller = AquaScroller(orientation: .horizontal)
···11+// SPDX-FileCopyrightText: 2026 Jon Sterling
22+//
33+// SPDX-License-Identifier: MIT
44+55+import AppKit
66+77+public protocol AquaTrafficLightButtonProtocol: NSButton {
88+ var systemSymbolName: String? { get set }
99+ var baseColor: NSColor { get set }
1010+ var isHovered: Bool { get set }
1111+}
+40-12
Sources/AquaKit/Windows/AquaWindow.swift
···4455import AppKit
6677+/// An `NSWindow` subclass with Aqua-styled traffic light buttons, and restored ``showsToolbarButton`` functionality.
88+///
99+/// 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``.
710open class AquaWindow: NSWindow {
1111+1212+ /// Defaults to ``AquaTrafficLightButton``.
1313+ open class var trafficLightButtonClass: AquaTrafficLightButtonProtocol.Type { AquaTrafficLightButton.self }
1414+1515+ /// Defaults to ``AquaWindowToolbarButton``
1616+ open class var toolbarButtonClass: NSButton.Type { AquaWindowToolbarButton.self }
1717+1818+ open class var windowCloseButtonBaseColor: NSColor { #colorLiteral(red: 0.9998044372, green: 0.3607223034, blue: 0.3726101518, alpha: 1) }
1919+ open class var windowMiniaturizeButtonBaseColor: NSColor { #colorLiteral(red: 0.9804074168, green: 0.7845029235, blue: 0, alpha: 1) }
2020+ open class var windowZoomButtonBaseColor: NSColor { #colorLiteral(red: 0.5023847222, green: 0.7764200568, blue: 0.07384926826, alpha: 1) }
2121+2222+ /// Reference dimensions for bottom bars.
823 public static var bottomBarThickness: CGFloat { 30.0 }
9241025 private var _showsToolbarButton: Bool = false
2626+2727+ /// 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.
1128 override open var showsToolbarButton: Bool {
1229 get { _showsToolbarButton }
1330 set { _showsToolbarButton = newValue }
1431 }
15321616- public override init(
1717- contentRect: NSRect,
1818- styleMask style: NSWindow.StyleMask,
1919- backing backingStoreType: NSWindow.BackingStoreType,
2020- defer flag: Bool
2121- ) {
3333+3434+ public override init(contentRect: NSRect, styleMask style: NSWindow.StyleMask, backing backingStoreType: NSWindow.BackingStoreType, defer flag: Bool) {
2235 super.init(contentRect: contentRect, styleMask: style, backing: backingStoreType, defer: flag)
2336 isMovableByWindowBackground = true
2437 }
···2639 public static override func standardWindowButton(_ b: NSWindow.ButtonType, for styleMask: NSWindow.StyleMask) -> NSButton? {
2740 guard let original = super.standardWindowButton(b, for: styleMask) else {
2841 if b == .toolbarButton {
2929- let toolbarButton = AquaToolbarToggleButton(frame: NSRect(x: 0, y: 0, width: 30, height: 10))
4242+ let toolbarButton = Self.toolbarButtonClass.init(frame: NSRect(x: 0, y: 0, width: 30, height: 10))
3043 toolbarButton.action = #selector(toggleToolbarShown(_:))
3144 return toolbarButton
3245 }
···38513952 switch b {
4053 case .closeButton:
4141- let button = AquaTrafficLightButton(kind: .close, frame: frame)
5454+ let button = Self.trafficLightButtonClass.init(frame: frame)
5555+ button.baseColor = Self.windowCloseButtonBaseColor.graphiteShadedInDarkMode
5656+ button.systemSymbolName = "xmark"
4257 button.action = #selector(close)
4358 return button
4459 case .miniaturizeButton:
4545- let button = AquaTrafficLightButton(kind: .miniaturize, frame: frame)
6060+ let button = Self.trafficLightButtonClass.init(frame: frame)
6161+ button.baseColor = Self.windowMiniaturizeButtonBaseColor.graphiteShadedInDarkMode
6262+ button.systemSymbolName = "minus"
4663 button.action = #selector(miniaturize(_:))
4764 return button
4865 case .zoomButton:
4949- let button = AquaTrafficLightButton(kind: .zoom, frame: frame)
6666+ let button = Self.trafficLightButtonClass.init(frame: frame)
6767+ button.baseColor = Self.windowZoomButtonBaseColor.graphiteShadedInDarkMode
6868+ button.systemSymbolName = "plus"
5069 button.action = #selector(zoom(_:))
5170 return button
5271 default: return original
5372 }
5473 }
55745656- private var trafficLightButtons: [AquaTrafficLightButton] {
7575+ private var trafficLightButtons: [AquaTrafficLightButtonProtocol] {
5776 let types: [ButtonType] = [.closeButton, .miniaturizeButton, .zoomButton]
5877 return types.compactMap { b in
5959- standardWindowButton(b) as? AquaTrafficLightButton
7878+ standardWindowButton(b) as? AquaTrafficLightButtonProtocol
6079 }
6180 }
6281}
···7493 }
7594 }
7695}
9696+9797+9898+extension NSColor {
9999+ fileprivate var graphiteShadedInDarkMode: NSColor {
100100+ modifyDark {
101101+ $0.blended(withFraction: 0.6, of: .graphiteColor.shadow(withLevel: 0.9)!)!
102102+ }
103103+ }
104104+}
···4455import AppKit
6677+/// An Aqua-styled bottom bar view controller.
88+///
99+/// 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.
710open class AquaWindowBottomBarViewController: ContainerViewController {
811 class View: NSView {
912 override init(frame frameRect: NSRect) {
···4455import AppKit
6677+/// Draws an Aqua-styled unified titlebar/toolbar background outside the window’s safe area layout guide.
88+///
99+/// See ``ContainerViewController`` for how to integrate your own view hierarchy underneath this.
710open class AquaWindowContainerViewController: ContainerViewController {
811 public override init(nibName nibNameOrNil: NSNib.Name?, bundle nibBundleOrNil: Bundle?) {
912 super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)