tangled
alpha
login
or
join now
jonmsterling.com
/
AquaKit
4
fork
atom
A collection of user interface components and drawing routines for building tasteful apps using AppKit.
appkit
swift
aqua
ui
mac
4
fork
atom
overview
issues
1
pulls
pipelines
Add AquaDrawer
jonmsterling.com
3 weeks ago
083f060c
130d4999
+655
5 changed files
expand all
collapse all
unified
split
Sources
AquaKit
Drawers
AquaDrawer.swift
AquaDrawerChromeView.swift
AquaDrawerChromeViewController.swift
AquaDrawerFrameView.swift
AquaDrawerPanel.swift
+377
Sources/AquaKit/Drawers/AquaDrawer.swift
···
1
1
+
import AppKit
2
2
+
3
3
+
extension AquaDrawer {
4
4
+
/// This protocol replaces the AppKit `NSDrawerDelegate` protocol.
5
5
+
@objc public protocol Delegate: NSObjectProtocol {
6
6
+
/// This method is invoked on user-initiated attempts to open a drawer.
7
7
+
@objc optional func drawerShouldOpen(_ drawer: AquaDrawer) -> Bool
8
8
+
9
9
+
/// This method is invoked on user-initiated attempts to close a drawer.
10
10
+
@objc optional func drawerShouldClose(_ drawer: AquaDrawer) -> Bool
11
11
+
12
12
+
/// Notifies the delegate that the drawer will open.
13
13
+
@objc optional func drawerWillOpen(_ notification: Notification)
14
14
+
15
15
+
/// Notifies the delegate that the drawer did open.
16
16
+
@objc optional func drawerDidOpen(_ notification: Notification)
17
17
+
18
18
+
/// Notifies the delegate that the drawer will close.
19
19
+
@objc optional func drawerWillClose(_ notification: Notification)
20
20
+
21
21
+
/// Notifies the delegate that the drawer did close.
22
22
+
@objc optional func drawerDidClose(_ notification: Notification)
23
23
+
}
24
24
+
}
25
25
+
26
26
+
/// This is a replacement of the AppKit `NSDrawer` class, but it is not intended to replicate the interface and behaviour exactly. Instead, ``AquaDrawer`` is built on top of `NSWindowController`.
27
27
+
public class AquaDrawer: NSWindowController {
28
28
+
public enum State: Sendable, Equatable {
29
29
+
case closedState
30
30
+
case openingState
31
31
+
case openState(edge: NSRectEdge)
32
32
+
case closingState
33
33
+
}
34
34
+
35
35
+
public static let willOpenNotification: NSNotification.Name = NSNotification.Name("DrawerWillOpen")
36
36
+
public static let didOpenNotification: NSNotification.Name = NSNotification.Name("DrawerDidOpen")
37
37
+
public static let willCloseNotification: NSNotification.Name = NSNotification.Name("DrawerWillClose")
38
38
+
public static let didCloseNotification: NSNotification.Name = NSNotification.Name("DrawerDidClose")
39
39
+
40
40
+
public static let drawerTransitionDuration = 0.25
41
41
+
public static let defaultDrawerExtent: CGFloat = 200.0
42
42
+
public static let drawerOverlap: CGFloat = 14.0
43
43
+
44
44
+
static let notificationDispatchTable: [Notification.Name: Selector] = [
45
45
+
willOpenNotification: #selector(Delegate.drawerWillOpen(_:)),
46
46
+
didOpenNotification: #selector(Delegate.drawerDidOpen(_:)),
47
47
+
willCloseNotification: #selector(Delegate.drawerWillClose(_:)),
48
48
+
didCloseNotification: #selector(Delegate.drawerDidClose(_:))
49
49
+
]
50
50
+
51
51
+
public var delegate: Delegate? {
52
52
+
didSet {
53
53
+
if let oldValue {
54
54
+
for notificationName in Self.notificationDispatchTable.keys {
55
55
+
NotificationCenter.default.removeObserver(
56
56
+
oldValue,
57
57
+
name: notificationName,
58
58
+
object: self
59
59
+
)
60
60
+
}
61
61
+
}
62
62
+
63
63
+
for mapping in Self.notificationDispatchTable {
64
64
+
if let delegate, delegate.responds(to: mapping.value) {
65
65
+
NotificationCenter.default.addObserver(
66
66
+
delegate,
67
67
+
selector: mapping.value,
68
68
+
name: mapping.key,
69
69
+
object: self
70
70
+
)
71
71
+
}
72
72
+
}
73
73
+
}
74
74
+
}
75
75
+
76
76
+
private let drawerChromeViewController: AquaDrawerChromeViewController
77
77
+
78
78
+
/// The view controller for the inside of the drawer.
79
79
+
public var drawerContentViewController: NSViewController? {
80
80
+
get { drawerChromeViewController.contentViewController }
81
81
+
set { drawerChromeViewController.contentViewController = newValue }
82
82
+
}
83
83
+
84
84
+
public private(set) var state: State = .closedState
85
85
+
86
86
+
public var preferredExtents: [NSRectEdge: CGFloat] = [:]
87
87
+
88
88
+
/// This replaces `NSDrawer.minContentSize`.
89
89
+
public var minExtent: CGFloat? = 70.0
90
90
+
91
91
+
/// This replaces `NSDrawer.maxContentSize`.
92
92
+
public var maxExtent: CGFloat? = 200.0
93
93
+
94
94
+
public var leadingOffset: CGFloat = 20.0
95
95
+
public var trailingOffset: CGFloat = 10.0
96
96
+
public var preferredEdge: NSRectEdge = .minX
97
97
+
98
98
+
@IBOutlet public var targetWindow: NSWindow? {
99
99
+
didSet {
100
100
+
if let oldValue {
101
101
+
NotificationCenter.default.removeObserver(self, name: NSWindow.didResizeNotification, object: oldValue)
102
102
+
}
103
103
+
104
104
+
if let targetWindow {
105
105
+
NotificationCenter.default.addObserver(
106
106
+
self,
107
107
+
selector: #selector(parentWindowDidResize(_:)),
108
108
+
name: NSWindow.didResizeNotification,
109
109
+
object: targetWindow
110
110
+
)
111
111
+
}
112
112
+
}
113
113
+
}
114
114
+
115
115
+
public override var acceptsFirstResponder: Bool { true }
116
116
+
117
117
+
/// A drawer acts as its window’s delegate.
118
118
+
public override var window: NSWindow? {
119
119
+
didSet {
120
120
+
oldValue?.delegate = nil
121
121
+
window?.delegate = self
122
122
+
}
123
123
+
}
124
124
+
125
125
+
public convenience init(preferredExtents: [NSRectEdge: CGFloat], preferredEdge: NSRectEdge) {
126
126
+
self.init(panel: nil)
127
127
+
self.preferredExtents = preferredExtents
128
128
+
self.preferredEdge = preferredEdge
129
129
+
}
130
130
+
131
131
+
required init(panel: AquaDrawerPanel?) {
132
132
+
let panel =
133
133
+
panel
134
134
+
?? AquaDrawerPanel(
135
135
+
contentRect: NSRect(origin: .zero, size: .zero),
136
136
+
styleMask: [.resizable, .utilityWindow, .nonactivatingPanel],
137
137
+
backing: .buffered,
138
138
+
defer: false
139
139
+
)
140
140
+
141
141
+
panel.backgroundColor = .clear
142
142
+
panel.becomesKeyOnlyIfNeeded = false
143
143
+
144
144
+
self.drawerChromeViewController = AquaDrawerChromeViewController()
145
145
+
super.init(window: panel)
146
146
+
147
147
+
contentViewController = drawerChromeViewController
148
148
+
panel.initialFirstResponder = drawerChromeViewController.view
149
149
+
}
150
150
+
151
151
+
required init?(coder: NSCoder) {
152
152
+
fatalError("init(coder:) has not been implemented")
153
153
+
}
154
154
+
155
155
+
@MainActor
156
156
+
deinit {
157
157
+
delegate = nil
158
158
+
targetWindow = nil
159
159
+
}
160
160
+
161
161
+
func drawerExtent(along edge: NSRectEdge) -> CGFloat {
162
162
+
let preferred = preferredExtents[edge] ?? Self.defaultDrawerExtent
163
163
+
return preferred.clamped(
164
164
+
lowerBound: minExtent,
165
165
+
upperBound: maxExtent
166
166
+
)
167
167
+
}
168
168
+
169
169
+
func drawerFrame(edge: NSRectEdge, state: State) -> NSRect? {
170
170
+
guard let targetWindow, let (axis, extreme) = edge.split, !state.isTransitioning else { return nil }
171
171
+
let extent = drawerExtent(along: edge)
172
172
+
let parentContentRect = targetWindow.contentRect(forFrameRect: targetWindow.frame)
173
173
+
let coextent = parentContentRect.size[axis.opposite] - (leadingOffset + trailingOffset)
174
174
+
175
175
+
var origin = parentContentRect.origin
176
176
+
177
177
+
switch axis {
178
178
+
case .horizontal: origin.y += trailingOffset
179
179
+
case .vertical: origin.x += leadingOffset
180
180
+
}
181
181
+
182
182
+
let overlapCoefficient: CGFloat =
183
183
+
switch extreme {
184
184
+
case .min: 1
185
185
+
case .max: -1
186
186
+
}
187
187
+
188
188
+
origin[axis] += overlapCoefficient * Self.drawerOverlap
189
189
+
190
190
+
let contentSize = NSSize(extent: extent, coextent: coextent, axis: axis)
191
191
+
var frame = NSWindow.frameRect(
192
192
+
forContentRect: NSRect(origin: origin, size: contentSize),
193
193
+
styleMask: [.resizable]
194
194
+
)
195
195
+
196
196
+
switch extreme {
197
197
+
case .max:
198
198
+
frame.origin[axis] += parentContentRect.size[axis]
199
199
+
if case .closedState = state {
200
200
+
frame.origin[axis] -= frame.size[axis]
201
201
+
}
202
202
+
203
203
+
case .min:
204
204
+
if case .openState = state {
205
205
+
frame.origin[axis] -= frame.size[axis]
206
206
+
}
207
207
+
}
208
208
+
209
209
+
return frame
210
210
+
}
211
211
+
212
212
+
/// Computes the visible edge based on available space, taking into account the ``preferredEdge`` and ``preferredExtents``.
213
213
+
var visibleEdge: NSRectEdge {
214
214
+
guard
215
215
+
let targetWindow,
216
216
+
let screenRect = targetWindow.screen?.visibleFrame
217
217
+
else { return preferredEdge }
218
218
+
219
219
+
let parentRect = targetWindow.frame
220
220
+
221
221
+
for edge in [
222
222
+
preferredEdge, preferredEdge.opposite, preferredEdge.counterclockwiseNextEdge, preferredEdge.clockwiseNextEdge
223
223
+
] {
224
224
+
guard let edge else { continue }
225
225
+
let availableSpace = edge.spaceAvailable(from: parentRect, in: screenRect)
226
226
+
if availableSpace >= drawerExtent(along: edge) {
227
227
+
return edge
228
228
+
}
229
229
+
}
230
230
+
231
231
+
return preferredEdge
232
232
+
}
233
233
+
234
234
+
public func open() {
235
235
+
open(edge: visibleEdge)
236
236
+
}
237
237
+
238
238
+
private func open(edge: NSRectEdge) {
239
239
+
guard
240
240
+
case .closedState = state, let targetWindow, let window,
241
241
+
let startFrame = drawerFrame(edge: edge, state: .closedState),
242
242
+
let endFrame = drawerFrame(edge: edge, state: .openState(edge: edge))
243
243
+
else { return }
244
244
+
245
245
+
drawerChromeViewController.drawerEdge = edge
246
246
+
247
247
+
NotificationCenter.default.post(name: Self.willOpenNotification, object: self)
248
248
+
249
249
+
targetWindow.makeFirstResponder(self)
250
250
+
window.setFrame(startFrame, display: true, animate: false)
251
251
+
targetWindow.addChildWindow(window, ordered: .below)
252
252
+
253
253
+
state = .openingState
254
254
+
255
255
+
resetWindowOrdering()
256
256
+
257
257
+
NSAnimationContext.runAnimationGroup { context in
258
258
+
context.duration = Self.drawerTransitionDuration
259
259
+
window.animator().setFrame(endFrame, display: true)
260
260
+
} completionHandler: { [weak self] in
261
261
+
if let self {
262
262
+
Task { @MainActor in
263
263
+
self.state = .openState(edge: edge)
264
264
+
NotificationCenter.default.post(name: Self.didOpenNotification, object: self)
265
265
+
}
266
266
+
}
267
267
+
}
268
268
+
}
269
269
+
270
270
+
public override func close() {
271
271
+
guard
272
272
+
case .openState(let edge) = state,
273
273
+
let targetWindow, let window,
274
274
+
let frame = drawerFrame(edge: edge, state: .closedState)
275
275
+
else { return }
276
276
+
277
277
+
targetWindow.endEditing(for: nil)
278
278
+
targetWindow.makeFirstResponder(targetWindow)
279
279
+
NotificationCenter.default.post(name: Self.willCloseNotification, object: self)
280
280
+
state = .closingState
281
281
+
282
282
+
NSAnimationContext.runAnimationGroup { context in
283
283
+
context.duration = Self.drawerTransitionDuration
284
284
+
window.animator().setFrame(frame, display: true)
285
285
+
} completionHandler: { [weak self] in
286
286
+
if let self {
287
287
+
Task { @MainActor in
288
288
+
self.window?.orderOut(nil)
289
289
+
self.state = .closedState
290
290
+
NotificationCenter.default.post(name: Self.didCloseNotification, object: self)
291
291
+
}
292
292
+
}
293
293
+
}
294
294
+
}
295
295
+
296
296
+
@objc private func parentWindowDidResize(_ notification: Notification) {
297
297
+
guard case .openState(let edge) = state, let window else { return }
298
298
+
guard let frame = drawerFrame(edge: edge, state: state) else { return }
299
299
+
window.setFrame(frame, display: true)
300
300
+
}
301
301
+
302
302
+
private func resetWindowOrdering() {
303
303
+
guard let targetWindow, let window else { return }
304
304
+
window.order(.above, relativeTo: targetWindow.windowNumber)
305
305
+
window.order(.below, relativeTo: targetWindow.windowNumber)
306
306
+
}
307
307
+
}
308
308
+
309
309
+
extension AquaDrawer {
310
310
+
@IBAction public func open(_ sender: Any?) {
311
311
+
if let shouldOpen = delegate?.drawerShouldOpen?(self), !shouldOpen {
312
312
+
return
313
313
+
}
314
314
+
315
315
+
open()
316
316
+
}
317
317
+
318
318
+
@IBAction public func close(_ sender: Any?) {
319
319
+
if let shouldClose = delegate?.drawerShouldClose?(self), !shouldClose {
320
320
+
return
321
321
+
}
322
322
+
323
323
+
close()
324
324
+
}
325
325
+
326
326
+
@IBAction public func toggle(_ sender: Any?) {
327
327
+
switch state {
328
328
+
case .closedState: open(self)
329
329
+
case .openState: close(self)
330
330
+
case .openingState, .closingState: break
331
331
+
}
332
332
+
}
333
333
+
}
334
334
+
335
335
+
extension AquaDrawer: NSWindowDelegate {
336
336
+
public func windowWillResize(_ sender: NSWindow, to frameSize: NSSize) -> NSSize {
337
337
+
guard case State.openState(edge: let edge) = state, let axis = edge.axis, let parentWindow = sender.parent else {
338
338
+
return frameSize
339
339
+
}
340
340
+
var frameSize = frameSize
341
341
+
342
342
+
let parentFrame = parentWindow.frame
343
343
+
let parentContentRect = parentWindow.contentRect(forFrameRect: parentFrame)
344
344
+
let coextent = parentContentRect.size[axis.opposite] - (leadingOffset + trailingOffset)
345
345
+
346
346
+
frameSize[axis.opposite] = coextent
347
347
+
348
348
+
if let minExtent, frameSize[axis] < minExtent {
349
349
+
return frameSize
350
350
+
}
351
351
+
352
352
+
frameSize[axis].clamp(lowerBound: minExtent, upperBound: maxExtent)
353
353
+
354
354
+
return frameSize
355
355
+
}
356
356
+
357
357
+
public func windowDidEndLiveResize(_ notification: Notification) {
358
358
+
guard case State.openState(edge: let edge) = state, let axis = edge.axis,
359
359
+
let window = notification.object as? NSWindow
360
360
+
else { return }
361
361
+
let frameSize = window.frame.size
362
362
+
if let minExtent, frameSize[axis] < minExtent {
363
363
+
close(nil)
364
364
+
} else {
365
365
+
preferredExtents[edge] = frameSize[axis]
366
366
+
}
367
367
+
}
368
368
+
}
369
369
+
370
370
+
extension AquaDrawer.State {
371
371
+
var isTransitioning: Bool {
372
372
+
switch self {
373
373
+
case .openingState, .closingState: true
374
374
+
default: false
375
375
+
}
376
376
+
}
377
377
+
}
+60
Sources/AquaKit/Drawers/AquaDrawerChromeView.swift
···
1
1
+
import AppKit
2
2
+
3
3
+
final class AquaDrawerChromeView: NSView {
4
4
+
private var maskLayer: CAShapeLayer
5
5
+
var drawerEdge: NSRectEdge? {
6
6
+
didSet {
7
7
+
var insets = NSEdgeInsets()
8
8
+
if let edge = drawerEdge?.opposite {
9
9
+
insets[edge] = AquaDrawer.drawerOverlap
10
10
+
}
11
11
+
additionalSafeAreaInsets = insets
12
12
+
}
13
13
+
}
14
14
+
15
15
+
override init(frame frameRect: NSRect) {
16
16
+
self.maskLayer = CAShapeLayer()
17
17
+
super.init(frame: frameRect)
18
18
+
19
19
+
wantsLayer = true
20
20
+
clipsToBounds = true
21
21
+
layer!.mask = maskLayer
22
22
+
23
23
+
let visualEffectsView = NSVisualEffectView()
24
24
+
visualEffectsView.translatesAutoresizingMaskIntoConstraints = false
25
25
+
visualEffectsView.material = .sheet
26
26
+
visualEffectsView.state = .active
27
27
+
28
28
+
addSubview(visualEffectsView)
29
29
+
NSLayoutConstraint.activate([
30
30
+
visualEffectsView.topAnchor.constraint(equalTo: topAnchor),
31
31
+
visualEffectsView.bottomAnchor.constraint(equalTo: bottomAnchor),
32
32
+
visualEffectsView.leftAnchor.constraint(equalTo: leftAnchor),
33
33
+
visualEffectsView.rightAnchor.constraint(equalTo: rightAnchor)
34
34
+
])
35
35
+
}
36
36
+
37
37
+
required init?(coder: NSCoder) { fatalError() }
38
38
+
39
39
+
override var safeAreaInsets: NSEdgeInsets {
40
40
+
let defaultInset = 10.0
41
41
+
return NSEdgeInsets(
42
42
+
top: defaultInset + additionalSafeAreaInsets.top,
43
43
+
left: defaultInset + additionalSafeAreaInsets.left,
44
44
+
bottom: defaultInset + additionalSafeAreaInsets.bottom,
45
45
+
right: defaultInset + additionalSafeAreaInsets.right
46
46
+
)
47
47
+
}
48
48
+
49
49
+
override func layout() {
50
50
+
super.layout()
51
51
+
52
52
+
maskLayer.frame = bounds
53
53
+
maskLayer.path =
54
54
+
NSBezierPath(
55
55
+
roundedRect: bounds,
56
56
+
xRadius: AquaDrawerFrameView.cornerRadius,
57
57
+
yRadius: AquaDrawerFrameView.cornerRadius
58
58
+
).cgPath
59
59
+
}
60
60
+
}
+50
Sources/AquaKit/Drawers/AquaDrawerChromeViewController.swift
···
1
1
+
import AppKit
2
2
+
3
3
+
class AquaDrawerChromeViewController: NSViewController {
4
4
+
let chromeView = AquaDrawerChromeView()
5
5
+
let frameView = AquaDrawerFrameView()
6
6
+
7
7
+
var drawerEdge: NSRectEdge? {
8
8
+
get { chromeView.drawerEdge }
9
9
+
set { chromeView.drawerEdge = newValue }
10
10
+
}
11
11
+
12
12
+
var contentViewController: NSViewController? {
13
13
+
didSet {
14
14
+
if let oldValue {
15
15
+
oldValue.view.removeFromSuperview()
16
16
+
oldValue.removeFromParent()
17
17
+
}
18
18
+
if let contentViewController {
19
19
+
addChild(contentViewController)
20
20
+
let contentView = contentViewController.view
21
21
+
contentView.translatesAutoresizingMaskIntoConstraints = false
22
22
+
view.addSubview(contentView, positioned: .below, relativeTo: frameView)
23
23
+
NSLayoutConstraint.activate([
24
24
+
contentView.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor),
25
25
+
contentView.rightAnchor.constraint(equalTo: view.safeAreaLayoutGuide.rightAnchor),
26
26
+
contentView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
27
27
+
contentView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor)
28
28
+
])
29
29
+
}
30
30
+
}
31
31
+
}
32
32
+
33
33
+
override func loadView() {
34
34
+
view = chromeView
35
35
+
}
36
36
+
37
37
+
override func viewDidLoad() {
38
38
+
super.viewDidLoad()
39
39
+
40
40
+
frameView.translatesAutoresizingMaskIntoConstraints = false
41
41
+
view.addSubview(frameView)
42
42
+
43
43
+
NSLayoutConstraint.activate([
44
44
+
frameView.leftAnchor.constraint(equalTo: view.leftAnchor),
45
45
+
frameView.rightAnchor.constraint(equalTo: view.rightAnchor),
46
46
+
frameView.topAnchor.constraint(equalTo: view.topAnchor),
47
47
+
frameView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
48
48
+
])
49
49
+
}
50
50
+
}
+163
Sources/AquaKit/Drawers/AquaDrawerFrameView.swift
···
1
1
+
import AppKit
2
2
+
3
3
+
final class AquaDrawerFrameView: NSView {
4
4
+
private var dragStart: NSPoint = .zero
5
5
+
private var activePosition: NSCursor.FrameResizePosition?
6
6
+
7
7
+
static let borderThickness: CGFloat = 6
8
8
+
static var cornerRadius: CGFloat { borderThickness * 2 }
9
9
+
private static var activeAreaThickness: CGFloat { Self.borderThickness * 2 }
10
10
+
11
11
+
override var acceptsFirstResponder: Bool { false }
12
12
+
13
13
+
override init(frame frameRect: NSRect) {
14
14
+
super.init(frame: frameRect)
15
15
+
16
16
+
addTrackingArea(
17
17
+
NSTrackingArea(
18
18
+
rect: bounds,
19
19
+
options: [.activeAlways, .mouseMoved, .inVisibleRect],
20
20
+
owner: self,
21
21
+
userInfo: nil
22
22
+
)
23
23
+
)
24
24
+
}
25
25
+
26
26
+
var strokePath: NSBezierPath {
27
27
+
let inset = Self.borderThickness / 2
28
28
+
let strokeRadius = Self.cornerRadius - inset
29
29
+
30
30
+
let strokeRect = bounds.insetBy(dx: inset, dy: inset)
31
31
+
32
32
+
let path = NSBezierPath(
33
33
+
roundedRect: strokeRect,
34
34
+
xRadius: strokeRadius,
35
35
+
yRadius: strokeRadius
36
36
+
)
37
37
+
38
38
+
path.lineWidth = Self.borderThickness
39
39
+
return path
40
40
+
}
41
41
+
42
42
+
override func draw(_ dirtyRect: NSRect) {
43
43
+
super.draw(dirtyRect)
44
44
+
45
45
+
NSGraphicsContext.saveGraphicsState()
46
46
+
let shadow = NSShadow()
47
47
+
shadow.shadowColor = .black.withAlphaComponent(0.7)
48
48
+
shadow.shadowBlurRadius = 5.0
49
49
+
shadow.set()
50
50
+
51
51
+
NSColor.controlColor.setStroke()
52
52
+
strokePath.stroke()
53
53
+
NSGraphicsContext.restoreGraphicsState()
54
54
+
}
55
55
+
56
56
+
required init?(coder: NSCoder) { fatalError() }
57
57
+
58
58
+
override func hitTest(_ point: NSPoint) -> NSView? {
59
59
+
if frame.insetBy(dx: Self.borderThickness, dy: Self.borderThickness).contains(point) {
60
60
+
return nil
61
61
+
}
62
62
+
return self
63
63
+
}
64
64
+
65
65
+
override func mouseMoved(with event: NSEvent) {
66
66
+
let loc = convert(event.locationInWindow, from: nil)
67
67
+
activePosition = frameResizePosition(at: loc)
68
68
+
updateCursor()
69
69
+
}
70
70
+
71
71
+
override func mouseDown(with event: NSEvent) {
72
72
+
dragStart = convert(event.locationInWindow, from: nil)
73
73
+
74
74
+
if let window {
75
75
+
NotificationCenter.default.post(
76
76
+
name: NSWindow.willStartLiveResizeNotification,
77
77
+
object: window
78
78
+
)
79
79
+
}
80
80
+
}
81
81
+
82
82
+
override func mouseDragged(with event: NSEvent) {
83
83
+
guard let window, let activePosition else { return }
84
84
+
85
85
+
let current = convert(event.locationInWindow, from: nil)
86
86
+
let delta = CGPoint(x: current.x - dragStart.x, y: current.y - dragStart.y)
87
87
+
var frame = window.frame
88
88
+
89
89
+
if activePosition.containsLeft {
90
90
+
frame.size.width -= delta.x
91
91
+
frame.origin.x += delta.x
92
92
+
} else if activePosition.containsRight {
93
93
+
frame.size.width += delta.x
94
94
+
}
95
95
+
96
96
+
if activePosition.containsBottom {
97
97
+
frame.size.height -= delta.y
98
98
+
frame.origin.y += delta.y
99
99
+
} else if activePosition.containsTop {
100
100
+
frame.size.height += delta.y
101
101
+
}
102
102
+
103
103
+
if let constrainedSize = window.delegate?.windowWillResize?(window, to: frame.size) {
104
104
+
let widthDelta = frame.size.width - constrainedSize.width
105
105
+
let heightDelta = frame.size.height - constrainedSize.height
106
106
+
107
107
+
frame.size = constrainedSize
108
108
+
109
109
+
if activePosition.containsLeft { frame.origin.x += widthDelta }
110
110
+
if activePosition.containsBottom { frame.origin.y += heightDelta }
111
111
+
}
112
112
+
113
113
+
window.setFrame(frame, display: true)
114
114
+
}
115
115
+
116
116
+
override func mouseUp(with event: NSEvent) {
117
117
+
if let window {
118
118
+
NotificationCenter.default.post(
119
119
+
name: NSWindow.didEndLiveResizeNotification,
120
120
+
object: window
121
121
+
)
122
122
+
}
123
123
+
}
124
124
+
125
125
+
private func frameResizePosition(at point: NSPoint) -> NSCursor.FrameResizePosition? {
126
126
+
guard let contentView = superview else { return nil }
127
127
+
128
128
+
let left = point.x <= Self.activeAreaThickness
129
129
+
let right = point.x >= contentView.bounds.width - Self.activeAreaThickness
130
130
+
let bottom = point.y <= Self.activeAreaThickness
131
131
+
let top = point.y >= contentView.bounds.height - Self.activeAreaThickness
132
132
+
133
133
+
if top, left { return .topLeft }
134
134
+
if top, right { return .topRight }
135
135
+
if bottom, left { return .bottomLeft }
136
136
+
if bottom, right { return .bottomRight }
137
137
+
if top { return .top }
138
138
+
if bottom { return .bottom }
139
139
+
if left { return .left }
140
140
+
if right { return .right }
141
141
+
142
142
+
return nil
143
143
+
}
144
144
+
145
145
+
var cursor: NSCursor {
146
146
+
if let activePosition {
147
147
+
NSCursor.frameResize(position: activePosition, directions: .all)
148
148
+
} else {
149
149
+
NSCursor.arrow
150
150
+
}
151
151
+
}
152
152
+
153
153
+
private func updateCursor() {
154
154
+
cursor.set()
155
155
+
}
156
156
+
}
157
157
+
158
158
+
extension NSCursor.FrameResizePosition {
159
159
+
fileprivate var containsLeft: Bool { self == .left || self == .topLeft || self == .bottomLeft }
160
160
+
fileprivate var containsRight: Bool { self == .right || self == .topRight || self == .bottomRight }
161
161
+
fileprivate var containsTop: Bool { self == .top || self == .topLeft || self == .topRight }
162
162
+
fileprivate var containsBottom: Bool { self == .bottom || self == .bottomLeft || self == .bottomRight }
163
163
+
}
+5
Sources/AquaKit/Drawers/AquaDrawerPanel.swift
···
1
1
+
import AppKit
2
2
+
3
3
+
open class AquaDrawerPanel: NSPanel {
4
4
+
open override var canBecomeKey: Bool { true }
5
5
+
}