-
Notifications
You must be signed in to change notification settings - Fork 1
/
JSSwitch.swift
175 lines (151 loc) · 5.45 KB
/
JSSwitch.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
//
// JSSwitch.swift
// JSSwitch
//
// Created by Julien Sagot on 29/05/16.
// Copyright © 2016 Julien Sagot. All rights reserved.
//
import AppKit
public class JSSwitch: NSControl {
// MARK: - Properties
private var pressed = false
private let backgroundLayer = CALayer()
private let knobContainer = CALayer()
private let knobLayer = CALayer()
private let knobShadows = (smallStroke: CALayer(), smallShadow: CALayer(), mediumShadow: CALayer(), bigShadow: CALayer())
// MARK: Computed
public override var wantsUpdateLayer: Bool { return true }
public override var intrinsicContentSize: NSSize {
return CGSize(width: 52, height: 32)
}
private var scaleFactor: CGFloat {
return ceil(frame.size.height / 62) // Hardcoded base height
}
public var tintColor = NSColor(deviceRed: 76/255, green: 217/255, blue: 100/255, alpha: 1.0) {
didSet { needsDisplay = true }
}
public var on = false {
didSet { needsDisplay = true }
}
// MARK: - Initializers
public override init(frame: CGRect) {
super.init(frame: frame)
setupLayers()
}
required public init?(coder: NSCoder) {
super.init(coder: coder)
setupLayers()
}
// MARK: - Layers Setup
private func setupLayers() {
wantsLayer = true
layer?.masksToBounds = false
layerContentsRedrawPolicy = .onSetNeedsDisplay
// Background
setupBackgroundLayer()
layer?.addSublayer(backgroundLayer)
// Knob
setupKnobLayers()
layer?.addSublayer(knobContainer)
}
// MARK: Background Layer
private func setupBackgroundLayer() {
backgroundLayer.frame = bounds
backgroundLayer.autoresizingMask = [.layerWidthSizable, .layerHeightSizable]
}
// MARK: Knob
private func setupKnobLayers() {
setupKnobContainerLayer()
setupKnobLayer()
setupKnobLayerShadows()
knobContainer.addSublayer(knobLayer)
knobContainer.insertSublayer(knobShadows.smallStroke, below: knobLayer)
knobContainer.insertSublayer(knobShadows.smallShadow, below: knobShadows.smallStroke)
knobContainer.insertSublayer(knobShadows.mediumShadow, below: knobShadows.smallShadow)
knobContainer.insertSublayer(knobShadows.bigShadow, below: knobShadows.mediumShadow)
}
private func setupKnobContainerLayer() {
knobContainer.frame = knobFrameForState(on: false, pressed: false)
}
private func setupKnobLayer() {
knobLayer.autoresizingMask = [.layerWidthSizable]
knobLayer.backgroundColor = NSColor.white.cgColor
knobLayer.frame = knobContainer.bounds
knobLayer.cornerRadius = ceil(knobContainer.bounds.height / 2)
}
private func setupKnobLayerShadows() {
let effectScale = scaleFactor
// Small Stroke
let smallStroke = knobShadows.smallStroke
smallStroke.frame = knobLayer.frame.insetBy(dx: -1, dy: -1)
smallStroke.autoresizingMask = [.layerWidthSizable]
smallStroke.backgroundColor = NSColor.black.withAlphaComponent(0.06).cgColor
smallStroke.cornerRadius = ceil(smallStroke.bounds.height / 2)
let smallShadow = knobShadows.smallShadow
smallShadow.frame = knobLayer.frame.insetBy(dx: 2, dy: 2)
smallShadow.autoresizingMask = [.layerWidthSizable]
smallShadow.cornerRadius = ceil(smallShadow.bounds.height / 2)
smallShadow.backgroundColor = NSColor.red.cgColor
smallShadow.shadowColor = NSColor.black.cgColor
smallShadow.shadowOffset = CGSize(width: 0, height: -3 * effectScale)
smallShadow.shadowOpacity = 0.12
smallShadow.shadowRadius = 2.0 * effectScale
let mediumShadow = knobShadows.mediumShadow
mediumShadow.frame = smallShadow.frame
mediumShadow.autoresizingMask = [.layerWidthSizable]
mediumShadow.cornerRadius = smallShadow.cornerRadius
mediumShadow.backgroundColor = NSColor.red.cgColor
mediumShadow.shadowColor = NSColor.black.cgColor
mediumShadow.shadowOffset = CGSize(width: 0, height: -9 * effectScale)
mediumShadow.shadowOpacity = 0.16
mediumShadow.shadowRadius = 6.0 * effectScale
let bigShadow = knobShadows.bigShadow
bigShadow.frame = smallShadow.frame
bigShadow.autoresizingMask = [.layerWidthSizable]
bigShadow.cornerRadius = smallShadow.cornerRadius
bigShadow.backgroundColor = NSColor.red.cgColor
bigShadow.shadowColor = NSColor.black.cgColor
bigShadow.shadowOffset = CGSize(width: 0, height: -9 * effectScale)
bigShadow.shadowOpacity = 0.06
bigShadow.shadowRadius = 0.5 * effectScale
}
// MARK: - Drawing
public override func updateLayer() {
// Background
backgroundLayer.cornerRadius = ceil(bounds.height / 2)
backgroundLayer.borderWidth = on ? ceil(bounds.height) : 3.0 * scaleFactor
backgroundLayer.borderColor = on ? tintColor.cgColor : NSColor.black.withAlphaComponent(0.09).cgColor
// Knob
knobContainer.frame = knobFrameForState(on: on, pressed: pressed)
knobLayer.cornerRadius = ceil(knobContainer.bounds.height / 2)
}
// MARK: - Helpers
private func knobFrameForState(on: Bool, pressed: Bool) -> CGRect {
let borderWidth = 3.0 * scaleFactor
var origin: CGPoint
var size: CGSize {
if pressed {
return CGSize(
width: ceil(bounds.width * 0.69) - (2 * borderWidth),
height: bounds.height - (2 * borderWidth)
)
}
return CGSize(width: bounds.height - (2 * borderWidth), height: bounds.height - (2 * borderWidth))
}
if on {
origin = CGPoint(x: bounds.width - size.width - borderWidth, y: borderWidth)
} else {
origin = CGPoint(x: borderWidth, y: borderWidth)
}
return CGRect(origin: origin, size: size)
}
// MARK: - Events
public override func mouseDown(with theEvent: NSEvent) {
pressed = true
needsDisplay = true
}
public override func mouseUp(with theEvent: NSEvent) {
pressed = false
on = !on
}
}