Add `badge` circle style (#7)
Fixes #3 Co-authored-by: Sindre Sorhus <sindresorhus@gmail.com>
This commit is contained in:
parent
cc5918267a
commit
c4766cb481
|
@ -12,14 +12,26 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
|
|||
func applicationDidFinishLaunching(_ notification: Notification) {
|
||||
borrowIconFrom(app: "Photos")
|
||||
|
||||
var lastStyleWasBar = true
|
||||
let styles: [DockProgress.ProgressStyle] = [
|
||||
.bar,
|
||||
.circle(radius: 58, color: .systemPink),
|
||||
.badge(color: .systemBlue, badgeValue: { Int(DockProgress.progressValue * 12) })
|
||||
]
|
||||
|
||||
var stylesIterator = styles.makeIterator()
|
||||
let _ = stylesIterator.next()
|
||||
|
||||
Timer.scheduledTimer(withTimeInterval: 0.02, repeats: true) { _ in
|
||||
DockProgress.progressValue += 0.01
|
||||
|
||||
if DockProgress.progressValue > 1 {
|
||||
DockProgress.progressValue = 0
|
||||
DockProgress.style = lastStyleWasBar ? .circle(radius: 58, color: .systemPink) : .bar
|
||||
lastStyleWasBar = !lastStyleWasBar
|
||||
if let style = stylesIterator.next() {
|
||||
DockProgress.progressValue = 0
|
||||
DockProgress.style = style
|
||||
} else {
|
||||
// Reset iterator when all is looped
|
||||
stylesIterator = styles.makeIterator()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -39,6 +39,7 @@ public final class DockProgress {
|
|||
case bar
|
||||
/// TODO: Make `color` optional when https://github.com/apple/swift-evolution/blob/master/proposals/0155-normalize-enum-case-representation.md is shipping in Swift
|
||||
case circle(radius: Double, color: NSColor)
|
||||
case badge(color: NSColor, badgeValue: () -> Int)
|
||||
case custom(drawHandler: (_ rect: CGRect) -> Void)
|
||||
}
|
||||
|
||||
|
@ -65,6 +66,8 @@ public final class DockProgress {
|
|||
self.drawProgressBar(dstRect)
|
||||
case let .circle(radius, color):
|
||||
self.drawProgressCircle(dstRect, radius: radius, color: color)
|
||||
case let .badge(color, badgeValue):
|
||||
self.drawProgressBadge(dstRect, color: color, badgeLabel: badgeValue())
|
||||
case let .custom(drawingHandler):
|
||||
drawingHandler(dstRect)
|
||||
}
|
||||
|
@ -104,4 +107,84 @@ public final class DockProgress {
|
|||
progressCircle.progress = progressValue
|
||||
progressCircle.render(in: cgContext)
|
||||
}
|
||||
|
||||
private static func drawProgressBadge(_ dstRect: CGRect, color: NSColor, badgeLabel: Int) {
|
||||
guard let cgContext = NSGraphicsContext.current?.cgContext else {
|
||||
return
|
||||
}
|
||||
|
||||
let radius = dstRect.width / 4.8
|
||||
let newCenter = CGPoint(x: dstRect.maxX - radius - 4, y: dstRect.minY + radius + 4)
|
||||
|
||||
// Background
|
||||
let badge = ProgressCircleShapeLayer(radius: Double(radius), center: newCenter)
|
||||
badge.fillColor = CGColor(red: 0.94, green: 0.96, blue: 1, alpha: 1)
|
||||
badge.shadowColor = .black
|
||||
badge.shadowOpacity = 0.3
|
||||
badge.masksToBounds = false
|
||||
badge.shadowOffset = CGSize(width: -1, height: 1)
|
||||
badge.shadowPath = badge.path
|
||||
|
||||
// Progress circle
|
||||
let lineWidth: CGFloat = 6
|
||||
let innerRadius = radius - lineWidth / 2
|
||||
let progressCircle = ProgressCircleShapeLayer(radius: Double(innerRadius), center: newCenter)
|
||||
progressCircle.strokeColor = color.cgColor
|
||||
progressCircle.lineWidth = lineWidth
|
||||
progressCircle.lineCap = .butt
|
||||
progressCircle.progress = progressValue
|
||||
|
||||
// Label
|
||||
let dimension = badge.bounds.height - 5
|
||||
let rect = CGRect(origin: progressCircle.bounds.origin, size: CGSize(width: dimension, height: dimension))
|
||||
let textLayer = VerticallyCenteredTextLayer(frame: rect, center: newCenter)
|
||||
let badgeText = kiloShortStringFromInt(number: badgeLabel)
|
||||
textLayer.foregroundColor = CGColor(red: 0.23, green: 0.23, blue: 0.24, alpha: 1)
|
||||
textLayer.string = badgeText
|
||||
textLayer.fontSize = scaledBadgeFontSize(text: badgeText)
|
||||
textLayer.font = NSFont.helveticaNeueBold
|
||||
textLayer.alignmentMode = .center
|
||||
textLayer.truncationMode = .end
|
||||
|
||||
badge.addSublayer(textLayer)
|
||||
badge.addSublayer(progressCircle)
|
||||
badge.render(in: cgContext)
|
||||
}
|
||||
|
||||
/**
|
||||
```
|
||||
999 => 999
|
||||
1000 => 1K
|
||||
1100 => 1K
|
||||
2000 => 2K
|
||||
10000 => 9K+
|
||||
```
|
||||
*/
|
||||
private static func kiloShortStringFromInt(number: Int) -> String {
|
||||
let sign = number.signum()
|
||||
let absNumber = abs(number)
|
||||
|
||||
if absNumber < 1000 {
|
||||
return "\(number)"
|
||||
} else if absNumber < 10000 {
|
||||
return "\(sign * Int(absNumber / 1000))k"
|
||||
} else {
|
||||
return "\(sign * 9)k+"
|
||||
}
|
||||
}
|
||||
|
||||
private static func scaledBadgeFontSize(text: String) -> CGFloat {
|
||||
switch text.count {
|
||||
case 1:
|
||||
return 30
|
||||
case 2:
|
||||
return 23
|
||||
case 3:
|
||||
return 19
|
||||
case 4:
|
||||
return 15
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -41,8 +41,12 @@ final class ProgressCircleShapeLayer: CAShapeLayer {
|
|||
self.init()
|
||||
fillColor = nil
|
||||
lineCap = .round
|
||||
path = NSBezierPath.progressCircle(radius: radius, center: center).cgPath
|
||||
position = center
|
||||
strokeEnd = 0
|
||||
|
||||
let cgPath = NSBezierPath.progressCircle(radius: radius, center: center).cgPath
|
||||
path = cgPath
|
||||
bounds = cgPath.boundingBox
|
||||
}
|
||||
|
||||
var progress: Double {
|
||||
|
@ -55,13 +59,15 @@ final class ProgressCircleShapeLayer: CAShapeLayer {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
extension NSColor {
|
||||
func with(alpha: Double) -> NSColor {
|
||||
return withAlphaComponent(CGFloat(alpha))
|
||||
}
|
||||
}
|
||||
|
||||
extension NSFont {
|
||||
static let helveticaNeueBold = NSFont(name: "HelveticaNeue-Bold", size: 0)
|
||||
}
|
||||
|
||||
extension CGRect {
|
||||
var center: CGPoint {
|
||||
|
@ -106,3 +112,24 @@ extension NSBezierPath {
|
|||
self.init(roundedRect: rect, xRadius: cornerRadius, yRadius: cornerRadius)
|
||||
}
|
||||
}
|
||||
|
||||
/// 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()
|
||||
}
|
||||
}
|
||||
|
|
14
readme.md
14
readme.md
|
@ -97,6 +97,20 @@ DockProgress.style = .circle(radius: 55, color: .systemBlue)
|
|||
|
||||
Make sure to set a `radius` that matches your app icon.
|
||||
|
||||
### Badge
|
||||
|
||||

|
||||
|
||||
```swift
|
||||
import DockProgress
|
||||
|
||||
DockProgress.style = .badge(color: .systemBlue, badgeValue: { getDownloadCount() })
|
||||
```
|
||||
|
||||
Large `badgeValue` numbers will be written in kilo short notation, for example, `1000` → `1k`.
|
||||
|
||||
Note: The `badgeValue` is not meant to be used as a numeric percentage. It's for things like count of downloads, number of files being converted, etc.
|
||||
|
||||
|
||||
## Related
|
||||
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 157 KiB |
Loading…
Reference in New Issue