Files
2022-04-25 07:28:13 +00:00

467 lines
15 KiB
Swift

//
// Switch.swift
// PrivadoVPN
//
// Created by Murad Shabanov on 02.08.2021.
// Copyright © 2021 Privado LLC. All rights reserved.
//
import Foundation
public typealias SwitchValueChange = (_ value: Bool) -> Void
open class BaseControl: NSControl {
// MARK: - Property
open var valueChange: SwitchValueChange?
open var isOn: Bool = false
}
public class Switch: BaseControl {
public override var wantsUpdateLayer: Bool {
return true
}
// MARK: - Properties
//// corner radius of thumbnail in case of square
//// It has no effect if shape is rounded
public var thumbCornerRadius: CGFloat = 0 {
didSet {
self.layoutThumbLayer(for: layer!.bounds)
}
}
public override var isOn: Bool {
didSet {
self.stateDidChange()
self.updateLayer()
}
}
/// Width of the border... it can have any `float` value
public var borderWidth: CGFloat = 0 {
didSet {
self.trackLayer.borderWidth = borderWidth
self.updateLayer()
}
}
public var borderColor: NSColor? {
didSet { self.setBorderColor() }
}
public var onBorderColor: NSColor = .white {
didSet { self.setBorderColor() }
}
public var offBorderColor: NSColor = .white {
didSet { self.setBorderColor() }
}
public var textColor: NSColor? {
didSet {
(self.offContentLayer as? CATextLayer)?.foregroundColor = self.textColor?.cgColor
(self.onContentLayer as? CATextLayer)?.foregroundColor = self.textColor?.cgColor
}
}
public var onTextColor: NSColor = .white {
didSet {
(self.onContentLayer as? CATextLayer)?.foregroundColor = self.onTextColor.cgColor
}
}
public var offTextColor: NSColor = .white {
didSet {
(self.offContentLayer as? CATextLayer)?.foregroundColor = self.offTextColor.cgColor
}
}
public var trackTopBottomPadding: CGFloat = 0 {
didSet {
self.updateLayer()
}
}
public var contentLeadingTrailingPadding: CGFloat = 0 {
didSet {
self.updateLayer()
}
}
//// Distance of `thumb` from track layer
public var thumbRadiusPadding: CGFloat = 0.5 {
didSet {
self.layoutThumbLayer(for: self.layer!.bounds)
}
}
public var onTintColor = NSColor(red: 0, green: 122/255, blue: 1, alpha: 1) {
didSet {
self.trackLayer.backgroundColor = self.getBackgroundColor()
self.needsDisplay = true
}
}
public var offTintColor: NSColor = .white {
didSet {
self.trackLayer.backgroundColor = self.getBackgroundColor()
self.needsDisplay = true
}
}
public var thumbTintColor: NSColor? {
didSet { self.setThumbColor() }
}
public var onThumbTintColor: NSColor = .white {
didSet { self.setThumbColor() }
}
public var offThumbTintColor: NSColor = .white {
didSet { self.setThumbColor() }
}
public var onText: String? {
didSet {
self.addOnTextLayerIfNeeded()
(self.onContentLayer as? CATextLayer)?.string = self.onText
}
}
public var offText: String? {
didSet {
self.addOffTextLayerIfNeeded()
(self.offContentLayer as? CATextLayer)?.string = self.offText
}
}
public var onThumbImage: NSImage? {
didSet {
self.thumbLayer.contents = self.onThumbImage?.cgImage
}
}
public var offThumbImage: NSImage? {
didSet {
self.thumbLayer.contents = self.offThumbImage?.cgImage
}
}
public var thumbImage: NSImage? {
didSet {
self.thumbLayer.contents = self.thumbImage?.cgImage
}
}
public var onImage: NSImage? {
didSet {
self.addOnImageLayerIfNeeded()
self.onContentLayer?.contents = self.onImage?.cgImage
}
}
public var offImage: NSImage? {
didSet {
self.addOffImageLayerIfNeeded()
self.offContentLayer?.contents = self.offImage?.cgImage
}
}
public override var intrinsicContentSize: CGSize {
return CGSize(width: 52, height: 31)
}
// MARK: - Layers
//// `trackLayer`:- is main track layer
//// `innerLayer`:- over track layer
//// `thumLayer` :- it is used for thumb
fileprivate lazy var trackLayer = CALayer()
fileprivate lazy var innerLayer = CALayer()
fileprivate lazy var thumbLayer: CALayer = {
let layer = CALayer()
layer.shadowColor = NSColor.black.cgColor
layer.shadowRadius = 2
layer.shadowOpacity = 0.4
layer.shadowOffset = CGSize(width: 0.75, height: 2)
layer.contentsGravity = .resizeAspect
return layer
}()
private lazy var contentsLayer = CALayer()
private var onContentLayer: CALayer? {
willSet {
self.onContentLayer?.removeFromSuperlayer()
}
didSet {
self.layoutOnContentLayer(for: self.layer!.bounds)
}
}
private var offContentLayer: CALayer? {
willSet {
self.offContentLayer?.removeFromSuperlayer()
}
didSet {
self.layoutOffContentLayer(for: layer!.bounds)
}
}
// MARK: - Initializers
convenience init() {
self.init(frame: .zero)
self.frame.size = intrinsicContentSize
}
override init(frame: CGRect) {
super.init(frame: frame)
self.controlDidLoad()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.controlDidLoad()
}
// MARK: - Common Init
fileprivate func controlDidLoad() {
wantsLayer = true
layer!.shadowOffset = .zero
layer!.shadowOpacity = 0.3
layer!.shadowRadius = 5
layer!.backgroundColor = .clear
layer!.addSublayer(self.trackLayer)
layer!.addSublayer(self.innerLayer)
layer!.addSublayer(self.contentsLayer)
layer!.addSublayer(self.thumbLayer)
self.trackLayer.backgroundColor = self.getBackgroundColor()
self.setBorderColor()
self.trackLayer.borderWidth = self.borderWidth
self.innerLayer.backgroundColor = NSColor.white.cgColor
self.contentsLayer.masksToBounds = true
self.setThumbColor()
self.updateLayer()
}
// MARK: - layoutSubviews
public override func updateLayer() {
super.updateLayer()
self.layoutTrackLayer(for: self.layer!.bounds)
self.layoutThumbLayer(for: self.layer!.bounds)
self.contentsLayer.frame = self.layer!.bounds
self.layoutOffContentLayer(for: self.layer!.bounds)
self.layoutOnContentLayer(for: self.layer!.bounds)
}
fileprivate func layoutTrackLayer(for bounds: CGRect) {
self.trackLayer.frame = bounds.insetBy(dx: trackTopBottomPadding, dy: trackTopBottomPadding)
(self.trackLayer.cornerRadius = self.trackLayer.bounds.height / 2)
}
fileprivate func layoutInnerLayer(for bounds: CGRect) {
let inset = self.borderWidth + self.trackTopBottomPadding
let isInnerHidden = self.isOn
self.innerLayer.frame = isInnerHidden
? CGRect(origin: self.trackLayer.position, size: .zero)
: bounds.insetBy(dx: inset, dy: inset)
self.innerLayer.cornerRadius = isInnerHidden ? 0 : bounds.height / 2 - inset
}
fileprivate func layoutThumbLayer(for bounds: CGRect) {
let size = getThumbSize()
let origin = getThumbOrigin(for: size.width)
self.thumbLayer.frame = CGRect(origin: origin, size: size)
if let thumb = thumbImage {
self.onThumbImage = thumb
self.offThumbImage = thumb
}
self.thumbLayer.contents = self.isOn ? self.onThumbImage?.cgImage : self.offThumbImage?.cgImage
self.thumbLayer.cornerRadius = size.height / 2
}
private func layoutOffContentLayer(for bounds: CGRect) {
let size = self.getContentLayerSize(for: self.offContentLayer)
let y = bounds.midY - size.height / 2
let leading = (bounds.maxX - (self.contentLeadingTrailingPadding + self.borderWidth + self.getThumbSize().width))/2 - size.width/2
let x = !self.isOn ? bounds.width - size.width - leading : bounds.width
let origin = CGPoint(x: x, y: y)
self.offContentLayer?.frame = CGRect(origin: origin, size: size)
bounds.height < 50 ? ((self.offContentLayer as? CATextLayer)?.fontSize = 12) : ((self.offContentLayer as? CATextLayer)?.fontSize = bounds.height * 0.2)
}
private func layoutOnContentLayer(for bounds: CGRect) {
let size = self.getContentLayerSize(for: self.onContentLayer)
let y = bounds.midY - size.height / 2
let leading = (bounds.maxX - (self.contentLeadingTrailingPadding + self.borderWidth + self.getThumbSize().width))/2 - size.width/2
let x = self.isOn ? leading : -bounds.width / 2
let origin = CGPoint(x: x, y: y)
self.onContentLayer?.frame = CGRect(origin: origin, size: size)
self.onContentLayer?.contentsCenter = CGRect(origin: origin, size: size)
bounds.height < 50 ? ((self.onContentLayer as? CATextLayer)?.fontSize = 12) : ((self.onContentLayer as? CATextLayer)?.fontSize = bounds.height * 0.2)
}
fileprivate func stateDidChange() {
self.trackLayer.backgroundColor = self.getBackgroundColor()
self.trackLayer.borderWidth = self.borderWidth
self.thumbLayer.contents = self.isOn ? self.onThumbImage?.cgImage : self.offThumbImage?.cgImage
self.setThumbColor()
self.valueChange?(isOn)
}
public func setOn(_ on: Bool, animated: Bool) {
CATransaction.begin()
CATransaction.setDisableActions(!animated)
self.isOn = on
self.updateLayer()
self.valueChange?(isOn)
if let selector = self.action {
NSApp.sendAction(selector, to: self.target, from: self)
}
CATransaction.commit()
}
public override func mouseDown(with event: NSEvent) {
super.mouseDown(with: event)
self.mouseDown()
}
fileprivate func mouseDown() {
self.isOn.toggle()
self.stateDidChange()
if let selector = self.action {
NSApp.sendAction(selector, to: self.target, from: self)
}
self.updateLayer()
}
// MARK: - Layout Helper
fileprivate func setBorderColor() {
if let borderClor = self.borderColor {
self.trackLayer.borderColor = borderClor.cgColor
} else {
self.trackLayer.borderColor = (self.isOn ? self.onBorderColor : self.offBorderColor).cgColor
}
}
private func setThumbColor() {
if let thumbColor = self.thumbTintColor {
self.thumbLayer.backgroundColor = thumbColor.cgColor
} else {
self.thumbLayer.backgroundColor = (self.isOn ? self.onThumbTintColor : self.offThumbTintColor).cgColor
}
}
final func getBackgroundColor() -> CGColor {
return (self.isOn ? self.onTintColor : self.offTintColor).cgColor
}
fileprivate func getThumbSize() -> CGSize {
let height = bounds.height - 2 * (self.borderWidth + self.thumbRadiusPadding)
let width = height
return CGSize(width: width, height: height)
}
final func getThumbOrigin(for width: CGFloat) -> CGPoint {
let inset = self.borderWidth + self.thumbRadiusPadding
let x = self.isOn ? bounds.width - width - inset : inset
return CGPoint(x: x, y: inset)
}
final func getContentLayerSize(for layer: CALayer?) -> CGSize {
let inset = 2 * (self.borderWidth + self.trackTopBottomPadding)
let diameter = bounds.height - inset - self.getThumbSize().height/2
if let textLayer = layer as? CATextLayer {
return textLayer.preferredFrameSize()
}
return CGSize(width: diameter, height: diameter)
}
// MARK: - Content Layers
private func addOffTextLayerIfNeeded() {
guard self.offText != nil else {
self.offContentLayer = nil
return
}
let textLayer = CATextLayer()
textLayer.alignmentMode = .center
textLayer.fontSize = bounds.height * 0.2
textLayer.font = NSFont.systemFont(ofSize: 10, weight: .bold)
textLayer.foregroundColor = (textColor == nil) ? offTextColor.cgColor : textColor?.cgColor
if let backingScale = NSScreen.main?.backingScaleFactor {
textLayer.contentsScale = backingScale
}
self.contentsLayer.addSublayer(textLayer)
self.offContentLayer = textLayer
}
private func addOnTextLayerIfNeeded() {
guard self.onText != nil else {
self.onContentLayer = nil
return
}
let textLayer = CATextLayer()
textLayer.alignmentMode = .center
textLayer.fontSize = bounds.height * 0.2
textLayer.font = NSFont.systemFont(ofSize: 10, weight: .bold)
textLayer.foregroundColor = (self.textColor == nil) ? self.onTextColor.cgColor : self.textColor?.cgColor
if let backingScale = NSScreen.main?.backingScaleFactor {
textLayer.contentsScale = backingScale
}
self.contentsLayer.addSublayer(textLayer)
self.onContentLayer = textLayer
}
private func addOnImageLayerIfNeeded() {
guard self.onImage != nil else {
self.onContentLayer = nil
return
}
let imageLayer = CALayer()
imageLayer.contentsGravity = .center
if let backingScale = NSScreen.main?.backingScaleFactor {
imageLayer.contentsScale = backingScale
}
self.contentsLayer.addSublayer(imageLayer)
self.onContentLayer = imageLayer
}
private func addOffImageLayerIfNeeded() {
guard self.offImage != nil else {
self.offContentLayer = nil
return
}
let imageLayer = CALayer()
imageLayer.contentsGravity = .resizeAspect
if let backingScale = NSScreen.main?.backingScaleFactor {
imageLayer.contentsScale = backingScale
}
self.contentsLayer.addSublayer(imageLayer)
self.offContentLayer = imageLayer
}
}