Navigation Menu

Skip to content

Commit

Permalink
Visualize cancelled state (#11)
Browse files Browse the repository at this point in the history
  • Loading branch information
boyvanamstel authored and sindresorhus committed Mar 1, 2019
1 parent 276e1b6 commit 2121abe
Show file tree
Hide file tree
Showing 6 changed files with 101 additions and 24 deletions.
12 changes: 2 additions & 10 deletions Example/AppDelegate.swift
Expand Up @@ -23,14 +23,10 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
}

private func configureManualView() {
manualCircularProgress.onCancelled = {
self.manualCircularProgress.alphaValue = 0.3
}

animateWithRandomColor(
manualCircularProgress,
start: { circularProgress in
circularProgress.alphaValue = 1.0
circularProgress.alphaValue = 1
circularProgress.resetProgress()
},
tick: { circularProgress in
Expand All @@ -40,14 +36,10 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
}

private func configureProgressBasedView() {
progressCircularProgress.onCancelled = {
self.progressCircularProgress.alphaValue = 0.3
}

animateWithRandomColor(
progressCircularProgress,
start: { circularProgress in
circularProgress.alphaValue = 1.0
circularProgress.alphaValue = 1
circularProgress.resetProgress()

let progress = Progress(totalUnitCount: 50)
Expand Down
41 changes: 38 additions & 3 deletions Sources/CircularProgress/CircularProgress.swift
Expand Up @@ -163,9 +163,16 @@ public final class CircularProgress: NSView {
}

override public func updateLayer() {
backgroundCircle.strokeColor = color.with(alpha: 0.5).cgColor
progressCircle.strokeColor = color.cgColor
progressLabel.foregroundColor = color.cgColor
updateColors()
}

private func updateColors() {
let duration = 0.2

backgroundCircle.animate(color: color.with(alpha: 0.5).cgColor, keyPath: #keyPath(CAShapeLayer.strokeColor), duration: duration)
progressCircle.animate(color: color.cgColor, keyPath: #keyPath(CAShapeLayer.strokeColor), duration: duration)

progressLabel.animate(color: color.cgColor, keyPath: #keyPath(CATextLayer.foregroundColor), duration: duration)

cancelButton.textColor = color
cancelButton.backgroundColor = color.with(alpha: 0.1)
Expand All @@ -190,6 +197,8 @@ public final class CircularProgress: NSView {
_isCancelled = false
progressCircle.resetProgress()
progressLabel.string = "0%"

alphaValue = 1
}

/**
Expand Down Expand Up @@ -248,10 +257,36 @@ public final class CircularProgress: NSView {

if newValue {
onCancelled?()
visualizeCancelledStateIfNecessary()
}
}
}

/**
Determines whether to visualize changing into the cancelled state.
*/
public var visualizeCancelledState: Bool = true

/**
Supply the base color to use for displaying the cancelled state.
*/
public var cancelledStateColorHandler: ((NSColor) -> NSColor)?

private func visualizeCancelledStateIfNecessary() {
guard visualizeCancelledState else {
return
}

if let colorHandler = cancelledStateColorHandler {
color = colorHandler(color)
} else {
color = color.desaturating(by: 0.4, brightness: 0.8)
alphaValue = 0.7
}

updateColors()
}

private var trackingArea: NSTrackingArea?

override public func updateTrackingAreas() {
Expand Down
20 changes: 18 additions & 2 deletions Sources/CircularProgress/CustomButton.swift
Expand Up @@ -63,6 +63,14 @@ final class TrackingArea {
}
}

final class AnimationDelegate: NSObject, CAAnimationDelegate {
var didStopHandler: ((Bool) -> Void)?

func animationDidStop(_ animation: CAAnimation, finished flag: Bool) {
didStopHandler?(flag)
}
}

extension CALayer {
// TODO: Find a way to use a strongly-typed KeyPath here.
// TODO: Accept NSColor instead of CGColor.
Expand All @@ -77,8 +85,16 @@ extension CALayer {
animation.duration = duration
animation.fillMode = .forwards
animation.isRemovedOnCompletion = false
add(animation, forKey: keyPath)
setValue(color, forKey: keyPath)
add(animation, forKey: keyPath) { [weak self] _ in
self?.setValue(color, forKey: keyPath)
}
}

func add(_ animation: CAAnimation, forKey key: String?, completion: @escaping ((Bool) -> Void)) {
let animationDelegate = AnimationDelegate()
animationDelegate.didStopHandler = completion
animation.delegate = animationDelegate
add(animation, forKey: key)
}
}

Expand Down
39 changes: 30 additions & 9 deletions Sources/CircularProgress/util.swift
Expand Up @@ -19,9 +19,8 @@ func with<T>(_ item: T, update: (inout T) throws -> Void) rethrows -> T {
return this
}


/// macOS 10.14 polyfill
extension NSColor {
/// macOS 10.14 polyfill
static let controlAccentColorPolyfill: NSColor = {
if #available(macOS 10.14, *) {
return NSColor.controlAccentColor
Expand All @@ -30,6 +29,35 @@ extension NSColor {
return NSColor(red: 0.10, green: 0.47, blue: 0.98, alpha: 1)
}
}()

func with(alpha: Double) -> NSColor {
return withAlphaComponent(CGFloat(alpha))
}

typealias HSBAColor = (hue: Double, saturation: Double, brightness: Double, alpha: Double)
var hsba: HSBAColor {
var hue: CGFloat = 0
var saturation: CGFloat = 0
var brightness: CGFloat = 0
var alpha: CGFloat = 0
let color = usingColorSpace(.deviceRGB) ?? self
color.getHue(&hue, saturation: &saturation, brightness: &brightness, alpha: &alpha)
return HSBAColor(Double(hue), Double(saturation), Double(brightness), Double(alpha))
}

private func colorWithSaturation(ratio: Double, brightness: Double? = nil) -> NSColor {
let color = hsba
return NSColor(
hue: CGFloat(color.hue),
saturation: CGFloat(color.saturation * ratio),
brightness: CGFloat(brightness ?? color.brightness),
alpha: CGFloat(color.alpha)
)
}

func desaturating(by ratio: Double, brightness: Double? = nil) -> NSColor {
return colorWithSaturation(ratio: 1 - ratio, brightness: brightness)
}
}


Expand Down Expand Up @@ -61,13 +89,6 @@ extension CGRect {
}


extension NSColor {
func with(alpha: Double) -> NSColor {
return withAlphaComponent(CGFloat(alpha))
}
}


extension DispatchQueue {
/**
```
Expand Down
13 changes: 13 additions & 0 deletions readme.md
Expand Up @@ -94,6 +94,9 @@ If you use the `.progress` property, you need to opt into the cancel button by s

If you use the `.progressInstance` property, setting a `Progress` object that is [`isCancellable`](https://developer.apple.com/documentation/foundation/progress/1409348-iscancellable), which is the default, automatically enables the cancel button.

<img src="screenshot-desaturate.gif" width="111" align="right">

Per default, the cancelled state is indicated by desaturing the current color and reducing the opacity. You can customize this by implementing the `.cancelledStateColorHandler` callback and returning a color to use for the cancelled state instead. The opacity is not automatically reduced when the callback has been set. To disable the cancelled state visualization entirely, set `.visualizeCancelledState` to `false`.

## API

Expand Down Expand Up @@ -152,6 +155,16 @@ Returns whether the progress has been cancelled.
*/
@IBInspectable private(set) var isCancelled: Bool

/**
Determines whether to visualize changing into the cancelled state.
*/
public var visualizeCancelledState: Bool = true

/**
Supply the base color to use for displaying the cancelled state.
*/
public var cancelledStateColorHandler: ((NSColor) -> NSColor)?

init(frame: CGRect) {}
init?(coder: NSCoder) {}

Expand Down
Binary file added screenshot-desaturate.gif
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit 2121abe

Please sign in to comment.