Files
lemana-vpn/app/Sources/LemanaVPN/VPNIcons.swift
T

216 lines
11 KiB
Swift

import AppKit
enum VPNIcon: CaseIterable {
case off, connecting, unlock, connected, reconnect, network, error, setup, unknown, noTime, expired, clock
case connect, disconnect, cancel, importKey, password, copy, logs, refresh, quit, manual, auto, local, vault
var isAction: Bool {
switch self {
case .connect, .disconnect, .cancel, .importKey, .password, .copy, .logs, .refresh, .quit, .manual, .auto, .local, .vault: return true
default: return false
}
}
}
enum VPNIcons {
// Geometry and palettes from the approved artifacts/icon-family-v1/draw_icons.py.
static let shield: CGPath = {
let p = CGMutablePath()
p.move(to: CGPoint(x: 12, y: 2))
p.addCurve(to: CGPoint(x: 20, y: 5), control1: CGPoint(x: 14.6, y: 3.7), control2: CGPoint(x: 17.3, y: 4.7))
p.addLine(to: CGPoint(x: 20, y: 11.2))
p.addCurve(to: CGPoint(x: 12, y: 22), control1: CGPoint(x: 20, y: 16.1), control2: CGPoint(x: 16.7, y: 19.6))
p.addCurve(to: CGPoint(x: 4, y: 11.2), control1: CGPoint(x: 7.3, y: 19.6), control2: CGPoint(x: 4, y: 16.1))
p.addLine(to: CGPoint(x: 4, y: 5))
p.addCurve(to: CGPoint(x: 12, y: 2), control1: CGPoint(x: 6.7, y: 4.7), control2: CGPoint(x: 9.4, y: 3.7))
p.closeSubpath()
return p
}()
struct Palette {
let ink, muted, green, amber, red: NSColor
init(dark: Bool) {
func color(_ hex: UInt32) -> NSColor {
NSColor(srgbRed: CGFloat((hex >> 16) & 255) / 255, green: CGFloat((hex >> 8) & 255) / 255, blue: CGFloat(hex & 255) / 255, alpha: 1)
}
ink = color(dark ? 0xE5EAF0 : 0x27333E)
muted = color(dark ? 0xACB7C3 : 0x657381)
green = color(dark ? 0x78CAA2 : 0x278055)
amber = color(dark ? 0xF0BB68 : 0xA56A13)
red = color(dark ? 0xF58C95 : 0xBE4149)
}
func color(_ urgency: SessionTiming.Urgency) -> NSColor {
switch urgency {
case .normal: return green
case .warning: return amber
case .critical: return red
case .unknown: return muted
}
}
}
static func image(_ icon: VPNIcon, dark: Bool) -> NSImage {
let result = image { draw(icon, in: $0, dark: dark) }
result.isTemplate = icon.isAction || [.off, .connecting, .unlock, .reconnect, .unknown, .noTime].contains(icon)
return result
}
static func sessionImage(style: VPNIndicatorStyle, progress: Double?, urgency: SessionTiming.Urgency, dark: Bool) -> NSImage {
image { drawSession(style: style, progress: progress, urgency: urgency, in: $0, dark: dark) }
}
private static func image(_ draw: @escaping (CGContext) -> Void) -> NSImage {
NSImage(size: NSSize(width: 16, height: 16), flipped: true) { _ in
guard let context = NSGraphicsContext.current?.cgContext else { return false }
context.saveGState()
context.scaleBy(x: 16 / 24, y: 16 / 24)
draw(context)
context.restoreGState()
return true
}
}
static func draw(_ icon: VPNIcon, in context: CGContext, dark: Bool) {
let palette = Palette(dark: dark)
let color: NSColor
switch icon {
case .connected, .clock: color = palette.green
case .network, .setup: color = palette.amber
case .error, .expired: color = palette.red
default: color = palette.ink
}
context.saveGState()
if !icon.isAction { stroke(shield, in: context, color: palette.ink) }
stroke(mark(icon), in: context, color: color)
context.setFillColor(color.cgColor)
if icon == .connecting {
for x in [9.0, 12.0, 15.0] { context.fillEllipse(in: CGRect(x: x - 0.7, y: 11.3, width: 1.4, height: 1.4)) }
} else if icon == .setup || icon == .unknown {
context.fillEllipse(in: CGRect(x: 11.35, y: (icon == .setup ? 15.5 : 15.9) - 0.65, width: 1.3, height: 1.3))
}
context.restoreGState()
}
static func drawSession(style: VPNIndicatorStyle, progress: Double?, urgency: SessionTiming.Urgency, in context: CGContext, dark: Bool) {
guard let value = progress, value.isFinite else {
draw(.noTime, in: context, dark: dark)
return
}
let progress = min(1, max(0, value))
let remaining = 1 - progress
let palette = Palette(dark: dark)
let accent = palette.color(urgency)
if style == .time {
stroke(shield, in: context, color: palette.ink)
stroke(mark(progress == 1 ? .expired : .connected), in: context, color: accent)
return
}
if style == .clock {
stroke(shield, in: context, color: palette.ink)
if progress == 1 {
stroke(mark(.expired), in: context, color: accent)
} else {
let hand = CGMutablePath()
hand.move(to: CGPoint(x: 12, y: 7.5))
hand.addLine(to: CGPoint(x: 12, y: 12))
let angle = progress * .pi * 2
hand.addLine(to: CGPoint(x: 12 + sin(angle) * 4.5, y: 12 - cos(angle) * 4.5))
stroke(hand, in: context, color: accent)
}
return
}
let ring = style == .ring
let shape = ring ? CGPath(ellipseIn: CGRect(x: 3, y: 3, width: 18, height: 18), transform: nil) : shield
let height: CGFloat = ring ? 18 : 20
let bottom: CGFloat = ring ? 21 : 22
context.saveGState()
context.addPath(shape)
context.clip()
context.setFillColor(palette.muted.withAlphaComponent(0.30).cgColor)
context.fill(CGRect(x: 0, y: 0, width: 24, height: 24))
context.setFillColor(accent.withAlphaComponent(0.80).cgColor)
context.fill(CGRect(x: 0, y: bottom - height * remaining, width: 24, height: height * remaining))
context.restoreGState()
stroke(shape, in: context, color: palette.muted.withAlphaComponent(ring ? 0.65 : 0.68))
if remaining > 0 {
context.saveGState()
context.setLineCap(.butt)
context.setLineWidth(1.75)
context.setLineJoin(.round)
context.setStrokeColor(accent.cgColor)
if ring {
context.addArc(center: CGPoint(x: 12, y: 12), radius: 9, startAngle: -.pi / 2, endAngle: -.pi / 2 + remaining * .pi * 2, clockwise: false)
} else {
context.setLineDash(phase: 0, lengths: [remaining * 58.04152102, 58.04152102])
context.addPath(shape)
}
context.strokePath()
context.restoreGState()
}
if progress == 1 { stroke(mark(.expired), in: context, color: palette.ink) }
}
private static func stroke(_ path: CGPath, in context: CGContext, color: NSColor) {
context.setLineWidth(1.75)
context.setLineCap(.round)
context.setLineJoin(.round)
context.setStrokeColor(color.cgColor)
context.addPath(path)
context.strokePath()
}
private static func mark(_ icon: VPNIcon) -> CGPath {
let p = CGMutablePath()
func m(_ x: CGFloat, _ y: CGFloat) { p.move(to: CGPoint(x: x, y: y)) }
func l(_ x: CGFloat, _ y: CGFloat) { p.addLine(to: CGPoint(x: x, y: y)) }
func c(_ x1: CGFloat, _ y1: CGFloat, _ x2: CGFloat, _ y2: CGFloat, _ x: CGFloat, _ y: CGFloat) {
p.addCurve(to: CGPoint(x: x, y: y), control1: CGPoint(x: x1, y: y1), control2: CGPoint(x: x2, y: y2))
}
func circle(_ x: CGFloat, _ y: CGFloat, _ r: CGFloat) { p.addEllipse(in: CGRect(x: x - r, y: y - r, width: 2 * r, height: 2 * r)) }
func rect(_ x: CGFloat, _ y: CGFloat, _ w: CGFloat, _ h: CGFloat, _ r: CGFloat) {
p.addRoundedRect(in: CGRect(x: x, y: y, width: w, height: h), cornerWidth: r, cornerHeight: r)
}
switch icon {
case .off: m(8.7, 8.7); l(15.3, 15.3)
case .connecting: break
case .unlock: circle(10, 10.5, 1.8); m(11.4, 11.9); l(15.4, 15.9); m(13.5, 14); l(15, 12.5)
case .connected: m(7.8, 11.8); l(10.8, 14.7); l(16.2, 9.3)
case .reconnect:
p.addArc(center: CGPoint(x: 12.107508, y: 12.15), radius: 4, startAngle: -0.509224, endAngle: 0.509224, clockwise: true)
m(15.6, 7.3); l(15.6, 10.2); l(12.7, 10.2)
case .network:
m(10, 10); l(9, 10); p.addArc(center: CGPoint(x: 9, y: 12), radius: 2, startAngle: -.pi / 2, endAngle: -.pi * 1.5, clockwise: true); l(10, 14)
m(14, 10); l(15, 10); p.addArc(center: CGPoint(x: 15, y: 12), radius: 2, startAngle: -.pi / 2, endAngle: .pi / 2, clockwise: false); l(14, 14)
case .error: m(9, 9); l(15, 15); m(15, 9); l(9, 15)
case .setup: m(12, 7.8); l(12, 12.3)
case .unknown: m(9.7, 9.2); c(9.8, 6.7, 14.6, 6.7, 14.6, 9.4); c(14.6, 11.3, 12, 11.3, 12, 13)
case .expired: m(9, 8); l(15, 8); m(9, 16); l(15, 16); m(9.7, 8); c(9.7, 10.3, 14.3, 13.7, 14.3, 16); m(14.3, 8); c(14.3, 10.3, 9.7, 13.7, 9.7, 16)
case .noTime: m(9, 12); l(15, 12)
case .clock: m(12, 7.5); l(12, 12); l(15, 13.7)
case .connect:
m(8, 3); l(8, 8); m(16, 3); l(16, 8); m(6, 8); l(18, 8); l(18, 11); c(18, 14.3, 15.3, 17, 12, 17); c(8.7, 17, 6, 14.3, 6, 11); p.closeSubpath(); m(12, 17); l(12, 21)
case .disconnect:
m(8, 2.5); l(8, 6); m(16, 2.5); l(16, 6); m(6, 6); l(18, 6); l(18, 8); c(18, 10.2, 15.3, 11.5, 12, 11.5); c(8.7, 11.5, 6, 10.2, 6, 8); p.closeSubpath()
m(7.5, 15.5); p.addQuadCurve(to: CGPoint(x: 16.5, y: 15.5), control: CGPoint(x: 12, y: 19.5)); m(12, 17.5); l(12, 21.5)
case .cancel: m(6, 6); l(18, 18); m(18, 6); l(6, 18)
case .importKey: m(5, 13); l(5, 20); l(19, 20); l(19, 13); m(12, 3); l(12, 15); m(8, 11); l(12, 15); l(16, 11)
case .password: circle(8, 8, 4); m(11, 11); l(20, 20); m(15, 15); l(17, 13); m(18, 18); l(20, 16)
case .copy: m(9, 4); l(19, 4); l(19, 15); m(5, 8); l(15, 8); l(15, 21); l(5, 21); p.closeSubpath()
case .logs: m(6, 3); l(14, 3); l(19, 8); l(19, 21); l(6, 21); p.closeSubpath(); m(14, 3); l(14, 8); l(19, 8); m(9, 12); l(16, 12); m(9, 16); l(16, 16)
case .refresh:
p.addArc(center: CGPoint(x: 12.174321, y: 11.291312), radius: 7.2, startAngle: -0.323870, endAngle: -2.413483, clockwise: true)
l(4, 9); m(4, 4); l(4, 9); l(9, 9)
m(5, 15); p.addArc(center: CGPoint(x: 11.825679, y: 12.708688), radius: 7.2, startAngle: 2.817723, endAngle: 0.728110, clockwise: true)
l(20, 15); m(20, 20); l(20, 15); l(15, 15)
case .quit: m(11, 3); l(4, 3); l(4, 21); l(11, 21); m(10, 12); l(21, 12); m(17, 8); l(21, 12); l(17, 16)
case .manual: rect(3, 6, 18, 12, 2); m(6.5, 10); l(7, 10); m(11.5, 10); l(12, 10); m(16.5, 10); l(17, 10); m(7, 14); l(17, 14)
case .auto: m(13, 3); l(6, 13); l(11, 13); l(10, 21); l(18, 10); l(13, 10); p.closeSubpath()
case .local: rect(5, 4, 14, 12, 1.5); m(2.5, 19); l(21.5, 19); l(20, 21); l(4, 21); p.closeSubpath()
case .vault: rect(4.5, 3, 15, 18, 2.5); circle(12, 12, 3.8); m(12, 8.2); l(12, 10); m(15.8, 12); l(14, 12); m(12, 15.8); l(12, 14); m(8.2, 12); l(10, 12)
}
return p
}
}