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