Skip to content

Commit

Permalink
Squircle style
Browse files Browse the repository at this point in the history
Fixes #11
  • Loading branch information
sindresorhus committed Dec 1, 2020
1 parent 89bfdf0 commit 4fd9ae9
Show file tree
Hide file tree
Showing 7 changed files with 314 additions and 158 deletions.
8 changes: 4 additions & 4 deletions DockProgress.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
E3FB30D020EA5EBD009BA1BD /* DockProgress.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = "DockProgress::DockProgress::Product" /* DockProgress.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
E3FEB1AB21DC4F70009C38CA /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = E3FEB1AA21DC4F70009C38CA /* Images.xcassets */; };
OBJ_19 /* DockProgress.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_9 /* DockProgress.swift */; };
OBJ_20 /* util.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_10 /* util.swift */; };
OBJ_20 /* Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_10 /* Utilities.swift */; };
OBJ_27 /* Package.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_6 /* Package.swift */; };
/* End PBXBuildFile section */

Expand Down Expand Up @@ -48,7 +48,7 @@
E3FB30C820EA5DBE009BA1BD /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = "<group>"; };
E3FB30CA20EA5DBE009BA1BD /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
E3FEB1AA21DC4F70009C38CA /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = "<group>"; };
OBJ_10 /* util.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = util.swift; sourceTree = "<group>"; usesTabs = 1; };
OBJ_10 /* Utilities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = Utilities.swift; sourceTree = "<group>"; usesTabs = 1; };
OBJ_6 /* Package.swift */ = {isa = PBXFileReference; explicitFileType = sourcecode.swift; fileEncoding = 4; lineEnding = 0; path = Package.swift; sourceTree = "<group>"; usesTabs = 1; };
OBJ_9 /* DockProgress.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = DockProgress.swift; sourceTree = "<group>"; usesTabs = 1; };
/* End PBXFileReference section */
Expand Down Expand Up @@ -115,7 +115,7 @@
isa = PBXGroup;
children = (
OBJ_9 /* DockProgress.swift */,
OBJ_10 /* util.swift */,
OBJ_10 /* Utilities.swift */,
);
name = DockProgress;
path = Sources/DockProgress;
Expand Down Expand Up @@ -260,7 +260,7 @@
buildActionMask = 0;
files = (
OBJ_19 /* DockProgress.swift in Sources */,
OBJ_20 /* util.swift in Sources */,
OBJ_20 /* Utilities.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down
5 changes: 3 additions & 2 deletions Example/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ final class AppDelegate: NSObject, NSApplicationDelegate {

let styles: [DockProgress.ProgressStyle] = [
.bar,
.circle(radius: 58, color: .systemPink),
.squircle(color: .systemGray),
.circle(radius: 30, color: .white),
.badge(color: .systemBlue, badgeValue: { Int(DockProgress.progress * 12) })
]

Expand All @@ -29,7 +30,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
DockProgress.resetProgress()
DockProgress.style = style
} else {
// Reset iterator when all is looped
// Reset iterator when all is looped.
stylesIterator = styles.makeIterator()
}
}
Expand Down
24 changes: 23 additions & 1 deletion Sources/DockProgress/DockProgress.swift
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ public enum DockProgress {

public enum ProgressStyle {
case bar
case squircle(inset: Double? = nil, color: NSColor = .controlAccentColorPolyfill)
case circle(radius: Double, color: NSColor = .controlAccentColorPolyfill)
case badge(color: NSColor = .controlAccentColorPolyfill, badgeValue: () -> Int)
case custom(drawHandler: (_ rect: CGRect) -> Void)
Expand Down Expand Up @@ -90,6 +91,8 @@ public enum DockProgress {
switch self.style {
case .bar:
self.drawProgressBar(dstRect)
case .squircle(let inset, let color):
self.drawProgressSquircle(dstRect, inset: inset, color: color)
case .circle(let radius, let color):
self.drawProgressCircle(dstRect, radius: radius, color: color)
case .badge(let color, let badgeValue):
Expand Down Expand Up @@ -121,6 +124,26 @@ public enum DockProgress {
roundedRect(barProgress)
}

private static func drawProgressSquircle(_ dstRect: CGRect, inset: Double? = nil, color: NSColor) {
guard let cgContext = NSGraphicsContext.current?.cgContext else {
return
}

let defaultInset: CGFloat = 14.4

var rect = dstRect.insetBy(dx: defaultInset, dy: defaultInset)

if let inset = inset {
rect = rect.insetBy(dx: CGFloat(inset), dy: CGFloat(inset))
}

let progressSquircle = ProgressSquircleShapeLayer(rect: rect)
progressSquircle.strokeColor = color.cgColor
progressSquircle.lineWidth = 5
progressSquircle.progress = progress
progressSquircle.render(in: cgContext)
}

private static func drawProgressCircle(_ dstRect: CGRect, radius: Double, color: NSColor) {
guard let cgContext = NSGraphicsContext.current?.cgContext else {
return
Expand All @@ -129,7 +152,6 @@ public enum DockProgress {
let progressCircle = ProgressCircleShapeLayer(radius: radius, center: dstRect.center)
progressCircle.strokeColor = color.cgColor
progressCircle.lineWidth = 4
progressCircle.cornerRadius = 3
progressCircle.progress = progress
progressCircle.render(in: cgContext)
}
Expand Down
272 changes: 272 additions & 0 deletions Sources/DockProgress/Utilities.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,272 @@
import Cocoa


/**
Convenience function for initializing an object and modifying its properties.
```
let label = with(NSTextField()) {
$0.stringValue = "Foo"
$0.textColor = .systemBlue
view.addSubview($0)
}
```
*/
@discardableResult
func with<T>(_ item: T, update: (inout T) throws -> Void) rethrows -> T {
var this = item
try update(&this)
return this
}


extension NSBezierPath {
/**
Create a path for a superellipse that fits inside the given rect.
*/
static func superellipse(in rect: CGRect, cornerRadius: Double) -> Self {
let minSide = min(rect.width, rect.height)
let radius = min(CGFloat(cornerRadius), minSide / 2)

let topLeft = CGPoint(x: rect.minX, y: rect.minY)
let topRight = CGPoint(x: rect.maxX, y: rect.minY)
let bottomLeft = CGPoint(x: rect.minX, y: rect.maxY)
let bottomRight = CGPoint(x: rect.maxX, y: rect.maxY)

// Top side (clockwise)
let point1 = CGPoint(x: rect.minX + radius, y: rect.minY)
let point2 = CGPoint(x: rect.maxX - radius, y: rect.minY)

// Right side (clockwise)
let point3 = CGPoint(x: rect.maxX, y: rect.minY + radius)
let point4 = CGPoint(x: rect.maxX, y: rect.maxY - radius)

// Bottom side (clockwise)
let point5 = CGPoint(x: rect.maxX - radius, y: rect.maxY)
let point6 = CGPoint(x: rect.minX + radius, y: rect.maxY)

// Left side (clockwise)
let point7 = CGPoint(x: rect.minX, y: rect.maxY - radius)
let point8 = CGPoint(x: rect.minX, y: rect.minY + radius)

let path = self.init()
path.move(to: point1)
path.addLine(to: point2)
path.addCurve(to: point3, controlPoint1: topRight, controlPoint2: topRight)
path.addLine(to: point4)
path.addCurve(to: point5, controlPoint1: bottomRight, controlPoint2: bottomRight)
path.addLine(to: point6)
path.addCurve(to: point7, controlPoint1: bottomLeft, controlPoint2: bottomLeft)
path.addLine(to: point8)
path.addCurve(to: point1, controlPoint1: topLeft, controlPoint2: topLeft)

return path
}

/**
Create a path for a squircle that fits inside the given `rect`.
- Important: The given `rect` must be square.
*/
static func squircle(rect: CGRect) -> Self {
assert(rect.width == rect.height)
return superellipse(in: rect, cornerRadius: Double(rect.width / 2))
}
}


final class ProgressSquircleShapeLayer: CAShapeLayer {
convenience init(rect: CGRect) {
self.init()
fillColor = nil
lineCap = .round
position = .zero
strokeEnd = 0

let cgPath = NSBezierPath
.squircle(rect: rect)
.rotating(byRadians: .pi, centerPoint: rect.center)
.reversed
.cgPath

path = cgPath
bounds = cgPath.boundingBox
}

var progress: Double {
get { Double(strokeEnd) }
set {
// Multiplying by `1.02` ensures that the start and end points meet at the end. Needed because of the round line cap.
strokeEnd = CGFloat(newValue * 1.02)
}
}
}


extension NSBezierPath {
/// For making a circle progress indicator.
static func progressCircle(radius: Double, center: CGPoint) -> Self {
let startAngle: CGFloat = 90
let path = self.init()
path.appendArc(
withCenter: center,
radius: CGFloat(radius),
startAngle: startAngle,
endAngle: startAngle - 360,
clockwise: true
)
return path
}
}


final class ProgressCircleShapeLayer: CAShapeLayer {
convenience init(radius: Double, center: CGPoint) {
self.init()
fillColor = nil
lineCap = .round
position = center
strokeEnd = 0

let cgPath = NSBezierPath.progressCircle(radius: radius, center: center).cgPath
path = cgPath
bounds = cgPath.boundingBox
}

var progress: Double {
get { Double(strokeEnd) }
set {
// Multiplying by `1.02` ensures that the start and end points meet at the end. Needed because of the round line cap.
strokeEnd = CGFloat(newValue * 1.02)
}
}
}


extension NSColor {
func withAlpha(_ alpha: Double) -> NSColor {
withAlphaComponent(CGFloat(alpha))
}
}


extension NSFont {
static let helveticaNeueBold = NSFont(name: "HelveticaNeue-Bold", size: 0)
}


extension CGRect {
var center: CGPoint {
get { CGPoint(x: midX, y: midY) }
set {
origin = CGPoint(
x: newValue.x - (size.width / 2),
y: newValue.y - (size.height / 2)
)
}
}
}


extension NSBezierPath {
/// UIKit polyfill.
var cgPath: CGPath {
let path = CGMutablePath()
var points = [CGPoint](repeating: .zero, count: 3)

for index in 0..<elementCount {
let type = element(at: index, associatedPoints: &points)
switch type {
case .moveTo:
path.move(to: points[0])
case .lineTo:
path.addLine(to: points[0])
case .curveTo:
path.addCurve(to: points[2], control1: points[0], control2: points[1])
case .closePath:
path.closeSubpath()
@unknown default:
assertionFailure("NSBezierPath received a new enum case. Please handle it.")
}
}

return path
}

/// UIKit polyfill.
convenience init(roundedRect rect: CGRect, cornerRadius: CGFloat) {
self.init(roundedRect: rect, xRadius: cornerRadius, yRadius: cornerRadius)
}

/// UIKit polyfill.
func addLine(to point: CGPoint) {
line(to: point)
}

/// UIKit polyfill.
func addCurve(to endPoint: CGPoint, controlPoint1: CGPoint, controlPoint2: CGPoint) {
curve(to: endPoint, controlPoint1: controlPoint1, controlPoint2: controlPoint2)
}
}


extension NSBezierPath {
func copyPath() -> Self {
copy() as! Self
}

func rotationTransform(byRadians radians: Double, centerPoint point: CGPoint) -> AffineTransform {
var transform = AffineTransform()
transform.translate(x: point.x, y: point.y)
transform.rotate(byRadians: CGFloat(radians))
transform.translate(x: -point.x, y: -point.y)
return transform
}

func rotating(byRadians radians: Double, centerPoint point: CGPoint) -> Self {
let path = copyPath()

guard radians != 0 else {
return path
}

let transform = rotationTransform(byRadians: radians, centerPoint: point)
path.transform(using: transform)
return path
}
}


/// Fixes the vertical alignment issue of the `CATextLayer` class.
final class VerticallyCenteredTextLayer: CATextLayer {
convenience init(frame rect: CGRect, center: CGPoint) {
self.init()
frame = rect
frame.center = center
contentsScale = NSScreen.main?.backingScaleFactor ?? 2
}

// From https://stackoverflow.com/a/44055040/6863743
override func draw(in context: CGContext) {
let height = bounds.size.height
let deltaY = ((height - fontSize) / 2 - fontSize / 10) * -1

context.saveGState()
context.translateBy(x: 0, y: deltaY)
super.draw(in: context)
context.restoreGState()
}
}


/// macOS 10.14 polyfill.
extension NSColor {
public static let controlAccentColorPolyfill: NSColor = {
if #available(macOS 10.14, *) {
return NSColor.controlAccentColor
} else {
// swiftlint:disable:next object_literal
return NSColor(red: 0.10, green: 0.47, blue: 0.98, alpha: 1)
}
}()
}
Loading

0 comments on commit 4fd9ae9

Please sign in to comment.