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 5 public let orientation: Orientation 6 6 7 7 public override var floatValue: Float { 8 - didSet { 9 - self.setNeedsDisplay(bounds) 10 - } 8 + didSet { setNeedsDisplay(bounds) } 9 + } 10 + 11 + public override var doubleValue: Double { 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 + 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 + 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 - let slotLength = axis.mainLength(of: bounds) - Self.slotInset * 2 52 - let mainSize = knobProportion * slotLength 53 - let mainOrigin = Self.slotInset + doubleValue * (slotLength - mainSize) 54 82 return axis.rect( 55 - mainOrigin: mainOrigin, 83 + mainOrigin: knobMainOrigin(for: doubleValue), 56 84 crossOrigin: crossOrigin, 57 - mainSize: mainSize, 85 + mainSize: knobLength, 58 86 crossSize: crossSize 59 87 ) 60 88 default: ··· 62 90 } 63 91 } 64 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 + 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 - 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 - 203 + 159 204 let backEdge: CGRectEdge = 160 - switch axis { 161 - case .vertical: .maxYEdge 162 - case .horizontal: .maxXEdge 163 - } 205 + switch axis { 206 + case .vertical: .maxYEdge 207 + case .horizontal: .maxXEdge 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 - 194 238 195 239 // trailing cross edge glow 196 240 do { ··· 265 309 } 266 310 } 267 311 268 - fileprivate extension Orientation { 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 - var transform: CGAffineTransform { 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 - var crossGradientAngle: CGFloat { 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 - var mainGradientAngle: CGFloat { 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 - 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 } 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 } 297 341 298 - func inset(_ rect: NSRect, main: CGFloat, cross: CGFloat) -> NSRect { 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 - func rect(mainOrigin: CGFloat, crossOrigin: CGFloat, mainSize: CGFloat, crossSize: CGFloat) -> NSRect { 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 - func borderLines(in bounds: NSRect) -> [(NSPoint, NSPoint)] { 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)),