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
Fix further problems with AquaScroller logic
jonmsterling.com
3 weeks ago
64bd7a12
ab3ea543
+70
-26
1 changed file
expand all
collapse all
unified
split
Sources
AquaKit
Scroll Views
AquaScroller.swift
+70
-26
Sources/AquaKit/Scroll Views/AquaScroller.swift
···
5
5
public let orientation: Orientation
6
6
7
7
public override var floatValue: Float {
8
8
-
didSet {
9
9
-
self.setNeedsDisplay(bounds)
10
10
-
}
8
8
+
didSet { setNeedsDisplay(bounds) }
9
9
+
}
10
10
+
11
11
+
public override var doubleValue: Double {
12
12
+
didSet { setNeedsDisplay(bounds) }
11
13
}
12
14
13
15
public required init(orientation: Orientation) {
···
40
42
}
41
43
42
44
private static let slotInset = 4.0
45
45
+
46
46
+
/// The length of the knob slot along the main axis, excluding insets at both ends.
47
47
+
private var slotLength: CGFloat {
48
48
+
orientation.mainLength(of: bounds) - Self.slotInset * 2
49
49
+
}
50
50
+
51
51
+
private static let minimumKnobLength: CGFloat = 20.0
52
52
+
53
53
+
/// The length of the knob along the main axis, proportional to the visible content.
54
54
+
private var knobLength: CGFloat {
55
55
+
max(knobProportion * slotLength, Self.minimumKnobLength)
56
56
+
}
57
57
+
58
58
+
/// The maximum distance the knob's origin can travel within the slot.
59
59
+
private var knobTravel: CGFloat {
60
60
+
slotLength - knobLength
61
61
+
}
62
62
+
63
63
+
/// Maps a scroll fraction (`0`–`1`) to the knob's main-axis origin in view coordinates.
64
64
+
private func knobMainOrigin(for fraction: Double) -> CGFloat {
65
65
+
Self.slotInset + fraction * knobTravel
66
66
+
}
67
67
+
68
68
+
/// Inverse of ``knobMainOrigin(for:)``: maps a main-axis origin back to a scroll fraction, clamped to `0`–`1`.
69
69
+
private func fraction(forKnobMainOrigin origin: CGFloat) -> Double {
70
70
+
guard knobTravel > 0 else { return 0 }
71
71
+
return min(max((origin - Self.slotInset) / knobTravel, 0), 1)
72
72
+
}
73
73
+
43
74
public override func rect(for partCode: NSScroller.Part) -> NSRect {
44
75
let axis = orientation
45
76
switch partCode {
···
48
79
case .knob:
49
80
let crossOrigin: CGFloat = 2
50
81
let crossSize = axis.crossLength(of: bounds) - 4
51
51
-
let slotLength = axis.mainLength(of: bounds) - Self.slotInset * 2
52
52
-
let mainSize = knobProportion * slotLength
53
53
-
let mainOrigin = Self.slotInset + doubleValue * (slotLength - mainSize)
54
82
return axis.rect(
55
55
-
mainOrigin: mainOrigin,
83
83
+
mainOrigin: knobMainOrigin(for: doubleValue),
56
84
crossOrigin: crossOrigin,
57
57
-
mainSize: mainSize,
85
85
+
mainSize: knobLength,
58
86
crossSize: crossSize
59
87
)
60
88
default:
···
62
90
}
63
91
}
64
92
93
93
+
public override func trackKnob(with event: NSEvent) {
94
94
+
let axis = orientation
95
95
+
let mouseDown = convert(event.locationInWindow, from: nil)
96
96
+
let grabOffset = mouseDown[axis] - knobMainOrigin(for: doubleValue)
97
97
+
98
98
+
guard knobTravel > 0 else { return }
99
99
+
100
100
+
var keepTracking = true
101
101
+
while keepTracking {
102
102
+
guard let nextEvent = window?.nextEvent(matching: [.leftMouseDragged, .leftMouseUp]) else { break }
103
103
+
let point = convert(nextEvent.locationInWindow, from: nil)
104
104
+
doubleValue = fraction(forKnobMainOrigin: point[axis] - grabOffset)
105
105
+
sendAction(action, to: target)
106
106
+
keepTracking = nextEvent.type == .leftMouseDragged
107
107
+
}
108
108
+
}
109
109
+
65
110
var wavePatternImage: NSImage {
66
111
let cross: CGFloat = 5
67
112
let main: CGFloat = 15
···
135
180
} else {
136
181
baseColor = NSColor.graphiteColor.highlight(withLevel: 0.5)!
137
182
}
138
138
-
183
183
+
139
184
if effectiveAppearance.isDarkAqua {
140
185
baseColor = baseColor.blended(withFraction: 0.3, of: .graphiteColor.shadow(withLevel: 0.8)!)!
141
186
}
···
155
200
case .vertical: .minYEdge
156
201
case .horizontal: .minXEdge
157
202
}
158
158
-
203
203
+
159
204
let backEdge: CGRectEdge =
160
160
-
switch axis {
161
161
-
case .vertical: .maxYEdge
162
162
-
case .horizontal: .maxXEdge
163
163
-
}
205
205
+
switch axis {
206
206
+
case .vertical: .maxYEdge
207
207
+
case .horizontal: .maxXEdge
208
208
+
}
164
209
165
210
let distance = min(scrollerWidth / 1.5, axis.mainLength(of: knobRect) / 3.0)
166
211
let gradient = NSGradient(colors: [baseColor.shadow(withLevel: 0.6)!.withAlphaComponent(0.5), .clear])
···
190
235
shineGradient.draw(in: shinePath, angle: axis.crossGradientAngle)
191
236
NSGraphicsContext.restoreGraphicsState()
192
237
}
193
193
-
194
238
195
239
// trailing cross edge glow
196
240
do {
···
265
309
}
266
310
}
267
311
268
268
-
fileprivate extension Orientation {
312
312
+
extension Orientation {
269
313
/// The affine transform that maps between screen space and canonical (vertical)
270
314
/// coordinate space. For vertical this is the identity; for horizontal it swaps
271
315
/// the x and y axes. This transform is its own inverse.
272
272
-
var transform: CGAffineTransform {
316
316
+
fileprivate var transform: CGAffineTransform {
273
317
switch self {
274
318
case .vertical: return .identity
275
319
case .horizontal: return CGAffineTransform(a: 0, b: 1, c: 1, d: 0, tx: 0, ty: 0)
276
320
}
277
321
}
278
322
279
279
-
var crossGradientAngle: CGFloat {
323
323
+
fileprivate var crossGradientAngle: CGFloat {
280
324
switch self {
281
325
case .vertical: return 0
282
326
case .horizontal: return 90
283
327
}
284
328
}
285
329
286
286
-
var mainGradientAngle: CGFloat {
330
330
+
fileprivate var mainGradientAngle: CGFloat {
287
331
switch self {
288
332
case .vertical: return 90
289
333
case .horizontal: return 0
290
334
}
291
335
}
292
336
293
293
-
func mainLength(of rect: NSRect) -> CGFloat { rect.applying(transform).height }
294
294
-
func crossLength(of rect: NSRect) -> CGFloat { rect.applying(transform).width }
295
295
-
func mainOrigin(of rect: NSRect) -> CGFloat { rect.applying(transform).origin.y }
296
296
-
func crossOrigin(of rect: NSRect) -> CGFloat { rect.applying(transform).origin.x }
337
337
+
fileprivate func mainLength(of rect: NSRect) -> CGFloat { rect.applying(transform).height }
338
338
+
fileprivate func crossLength(of rect: NSRect) -> CGFloat { rect.applying(transform).width }
339
339
+
fileprivate func mainOrigin(of rect: NSRect) -> CGFloat { rect.applying(transform).origin.y }
340
340
+
fileprivate func crossOrigin(of rect: NSRect) -> CGFloat { rect.applying(transform).origin.x }
297
341
298
298
-
func inset(_ rect: NSRect, main: CGFloat, cross: CGFloat) -> NSRect {
342
342
+
fileprivate func inset(_ rect: NSRect, main: CGFloat, cross: CGFloat) -> NSRect {
299
343
rect.applying(transform).insetBy(dx: cross, dy: main).applying(transform)
300
344
}
301
345
302
346
/// Constructs an `NSRect` from main-axis and cross-axis components, mapping them
303
347
/// to `x`/`y`/`width`/`height` according to the orientation.
304
304
-
func rect(mainOrigin: CGFloat, crossOrigin: CGFloat, mainSize: CGFloat, crossSize: CGFloat) -> NSRect {
348
348
+
fileprivate func rect(mainOrigin: CGFloat, crossOrigin: CGFloat, mainSize: CGFloat, crossSize: CGFloat) -> NSRect {
305
349
NSRect(x: crossOrigin, y: mainOrigin, width: crossSize, height: mainSize).applying(transform)
306
350
}
307
351
308
352
/// Returns line segments for stroking the two edges that run along the main axis
309
353
/// (i.e. the left/right edges for vertical, or the top/bottom edges for horizontal).
310
310
-
func borderLines(in bounds: NSRect) -> [(NSPoint, NSPoint)] {
354
354
+
fileprivate func borderLines(in bounds: NSRect) -> [(NSPoint, NSPoint)] {
311
355
let b = bounds.applying(transform)
312
356
return [
313
357
(NSPoint(x: b.minX, y: b.minY).applying(transform), NSPoint(x: b.minX, y: b.maxY).applying(transform)),