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

Fix further problems with AquaScroller logic

+70 -26
+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 - } 11 } 12 13 public required init(orientation: Orientation) { ··· 40 } 41 42 private static let slotInset = 4.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 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 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 } 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)),