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

420 lines
14 KiB
Swift

//
// FlatButton.swift
// Privado
//
// Created by Jura on 11/21/19.
// Copyright © 2019 Omicronmedia. All rights reserved.
//
import Foundation
import AppKit
final class FlatButton: NSButton, CALayerDelegate {
/// Restore button state when mouse up.
var momentary = false {
didSet {
self.animateColor(with: self.state)
}
}
/// The animation duration from NSOffState to NSOnState.
var onAnimateDuration: TimeInterval = 0.0
/// The animation duration from NSOnState to NSOffState.
var offAnimateDuration: TimeInterval = 0.0
/// Button's border corner radius.
var cornerRadius: CGFloat = 0.0 {
didSet {
self.layer?.cornerRadius = self.cornerRadius
}
}
/// Button's border width.
var borderWidth: CGFloat = 0.0 {
didSet {
self.layer?.borderWidth = self.borderWidth
}
}
/// Button's spacint between image and title.
var spacing: CGFloat = 0.0 {
didSet {
self.setupImageLayer()
self.setupTitleLayer()
}
}
/// Button's border color when state off.
var borderNormalColor: NSColor? {
didSet {
self.animateColor(with: self.state)
}
}
/// Button's border color when state on.
var borderHighlightColor: NSColor? {
didSet {
self.animateColor(with: self.state)
}
}
/// Button's background color when state off.
var backgroundNormalColor: NSColor? {
didSet {
self.animateColor(with: self.state)
}
}
/// Button's background color when state on.
var backgroundHighlightColor: NSColor? {
didSet {
self.animateColor(with: self.state)
}
}
/// Button's image color when state off.
var imageNormalColor: NSColor?
/// Button's image color when state on.
var imageHighlightColor: NSColor?
/// Button's title color when state off.
var titleNormalColor: NSColor? {
didSet {
self.animateColor(with: self.state)
}
}
/// Button's title color when state on.
var titleHighlightColor: NSColor? {
didSet {
self.animateColor(with: self.state)
}
}
private lazy var imageLayer: CAShapeLayer = {
let layer = CAShapeLayer()
layer.delegate = self
return layer
}()
private lazy var titleLayer: CATextLayer = {
let layer = CATextLayer()
layer.delegate = self
return layer
}()
private var mouseDown = false
// MARK: - Initializers
override init(frame frameRect: NSRect) {
super.init(frame: frameRect)
self.setup()
self.setupImageLayer()
self.setupTitleLayer()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: - Private
private func setup() {
// Setup layer
self.translatesAutoresizingMaskIntoConstraints = false
self.wantsLayer = true
self.layer?.masksToBounds = true
self.layer?.delegate = self
self.layer?.backgroundColor = NSColor.red.cgColor
self.alphaValue = self.isEnabled ? 1.0 : 0.5
}
// swiftlint:disable function_body_length
private func setupImageLayer() {
// Ignore image layer if has no image or imagePosition equal to NSNoImage
guard let image = self.image
, self.imagePosition != .noImage else {
self.imageLayer.removeFromSuperlayer()
return
}
let buttonSize = self.frame.size
let imageSize = image.size
let titleSize: CGSize
if let font = self.font {
titleSize = self.title.size(withAttributes: [.font: font])
} else {
titleSize = self.title.size(withAttributes: nil)
}
var x: CGFloat = 0.0 // Image's origin x
var y: CGFloat = 0.0 // Image's origin y
// Caculate the image's and title's position depends on button's imagePosition and imageHugsTitle property
switch self.imagePosition {
case .noImage:
return
case .imageOnly:
x = (buttonSize.width - imageSize.width) / 2.0
y = (buttonSize.height - imageSize.height) / 2.0
case .imageOverlaps:
x = (buttonSize.width - imageSize.width) / 2.0
y = (buttonSize.height - imageSize.height) / 2.0
case .imageLeading,
.imageLeft:
x = self.imageHugsTitle
? (buttonSize.width - imageSize.width - titleSize.width) / 2.0 - self.spacing
: self.spacing
y = (buttonSize.height - imageSize.height) / 2.0
case .imageTrailing,
.imageRight:
x = self.imageHugsTitle
? (buttonSize.width - imageSize.width + titleSize.width) / 2.0 + self.spacing
: buttonSize.width - imageSize.width - self.spacing
y = (buttonSize.height - imageSize.height) / 2.0
case .imageAbove:
x = (buttonSize.width - imageSize.width) / 2.0
y = self.imageHugsTitle
? (buttonSize.height - imageSize.height - titleSize.height) / 2.0 - self.spacing
: self.spacing
case .imageBelow:
x = (buttonSize.width - imageSize.width) / 2.0
y = self.imageHugsTitle
? (buttonSize.height - imageSize.height + titleSize.height) / 2.0 + self.spacing
: buttonSize.height - imageSize.height - self.spacing
default:
break
}
// Setup image layer
self.imageLayer.frame = self.bounds
let layer = CALayer()
var rect = NSRect(x: round(x), y: round(y), width: imageSize.width, height: imageSize.height)
layer.frame = rect
layer.contents = image.cgImage(forProposedRect: &rect, context: nil, hints: nil)
self.imageLayer.mask = layer
self.imageLayer.backgroundColor = self.state == .on ? self.imageHighlightColor?.cgColor : self.imageNormalColor?.cgColor
self.layer?.addSublayer(self.imageLayer)
}
// swiftlint:enable function_body_length
// swiftlint:disable function_body_length
private func setupTitleLayer() {
// Ignore title layer if has no title or imagePosition equal to NSImageOnly
if self.title.isEmpty || self.imagePosition == .imageOnly {
self.titleLayer.removeFromSuperlayer()
return
}
guard let font = self.font else { return }
let buttonSize = self.frame.size
let imageSize = self.image?.size ?? .zero
let titleSize = self.title.size(withAttributes: [.font: font])
var x: CGFloat = 0.0 // Title's origin x
var y: CGFloat = 0.0 // Title's origin y
// Caculate the image's and title's position depends on button's imagePosition and imageHugsTitle property
switch self.imagePosition {
case .imageOnly:
return
case .noImage:
x = (buttonSize.width - titleSize.width) / 2.0
y = (buttonSize.height - titleSize.height) / 2.0
case .imageOverlaps:
x = (buttonSize.width - titleSize.width) / 2.0
y = (buttonSize.height - titleSize.height) / 2.0
case .imageLeading,
.imageLeft:
x = self.imageHugsTitle
? (buttonSize.width + imageSize.width - titleSize.width) / 2.0 + self.spacing
: buttonSize.width - titleSize.width - self.spacing
y = (buttonSize.height - titleSize.height) / 2.0
case .imageTrailing,
.imageRight:
x = self.imageHugsTitle
? (buttonSize.width - imageSize.width - titleSize.width) / 2.0 - self.spacing
: self.spacing
y = (buttonSize.height - titleSize.height) / 2.0
case .imageAbove:
x = (buttonSize.width - titleSize.width) / 2.0
y = self.imageHugsTitle
? (buttonSize.height + imageSize.height - titleSize.height) / 2.0 + self.spacing
: buttonSize.height - titleSize.height - self.spacing
case .imageBelow:
y = self.imageHugsTitle
? (buttonSize.height - imageSize.height - titleSize.height) / 2.0 - self.spacing
: self.spacing
x = (buttonSize.width - titleSize.width) / 2.0
default:
break
}
// Setup title layer
self.titleLayer.frame = NSRect(x: round(x), y: round(y), width: ceil(titleSize.width), height: ceil(titleSize.height))
self.titleLayer.string = self.title
self.titleLayer.font = font
self.titleLayer.fontSize = font.pointSize
if let scale = NSScreen.main?.backingScaleFactor {
self.titleLayer.contentsScale = scale
}
self.layer?.addSublayer(self.titleLayer)
}
// swiftlint:enable function_body_length
// MARK: - Drawing method
override func draw(_ dirtyRect: NSRect) {
}
// MARK: - Lifecycle
override func layout() {
super.layout()
setupTitleLayer()
setupImageLayer()
}
override func updateLayer() {
super.updateLayer()
// setupTitleLayer()
// setupImageLayer()
}
// MARK: - Animation method
func removeAllAnimations() {
self.layer?.removeAllAnimations()
self.layer?.sublayers?.forEach {
$0.removeAllAnimations()
}
}
func animateColor(with state: NSControl.StateValue) {
self.removeAllAnimations()
let duration = state == .on ? self.onAnimateDuration : self.offAnimateDuration
let borderColor = state == .on ? self.borderHighlightColor : self.borderNormalColor
let backgroundColor = state == .on ? self.backgroundHighlightColor : self.backgroundNormalColor
let titleColor = state == .on ? self.titleHighlightColor : self.titleNormalColor
let imageColor = state == .on ? self.imageHighlightColor : self.imageNormalColor
self.animate(layer: self.layer, color: borderColor, keyPath: "borderColor", duration: duration)
self.animate(layer: self.layer, color: backgroundColor, keyPath: "backgroundColor", duration: duration)
self.animate(layer: self.imageLayer, color: imageColor, keyPath: "backgroundColor", duration: duration)
self.animate(layer: self.titleLayer, color: titleColor, keyPath: "foregroundColor", duration: duration)
}
func animate(layer: CALayer?, color: NSColor?, keyPath: String, duration: TimeInterval) {
guard let currentLayer = layer
, let newColor = color else {
return
}
// swiftlint:disable force_cast
guard let value = currentLayer.value(forKeyPath: keyPath) else { return }
let oldColor = value as! CGColor
// swiftlint:enable force_cast
guard oldColor != newColor.cgColor else { return }
let animation = CABasicAnimation(keyPath: keyPath)
animation.fromValue = currentLayer.value(forKeyPath: keyPath)
animation.toValue = newColor.cgColor
animation.duration = duration
animation.isRemovedOnCompletion = false
currentLayer.setValue(newColor.cgColor, forKey: keyPath)
currentLayer.add(animation, forKey: keyPath)
}
// MARK: - Event Response
override func hitTest(_ point: NSPoint) -> NSView? {
return self.isEnabled
? super.hitTest(point)
: nil
}
override func mouseDown(with event: NSEvent) {
guard self.isEnabled else { return }
self.mouseDown = true
self.state = self.state == .on ? .off : . on
}
override func mouseEntered(with event: NSEvent) {
guard self.mouseDown else { return }
self.state = self.state == .on ? .off : .on
}
override func mouseExited(with event: NSEvent) {
guard self.mouseDown else { return }
self.mouseDown = false
self.state = self.state == .on ? .off : .on
}
override func mouseUp(with event: NSEvent) {
guard self.mouseDown else { return }
self.mouseDown = false
if self.momentary {
self.state = (self.state == .on) ? .off : .on
}
if let selector = self.action {
NSApp.sendAction(selector, to: self.target, from: self)
}
}
// MARK: - Properties
override var frame: NSRect {
didSet {
self.setupTitleLayer()
}
}
override var font: NSFont? {
didSet {
self.setupTitleLayer()
}
}
override var title: String {
didSet {
self.setupTitleLayer()
}
}
override var image: NSImage? {
didSet {
self.setupImageLayer()
}
}
override var state: NSControl.StateValue {
didSet {
self.animateColor(with: self.state)
}
}
override var imagePosition: NSControl.ImagePosition {
didSet {
self.setupImageLayer()
self.setupTitleLayer()
}
}
}