You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

177 lines
5.7 KiB

// Copyright © 2015 Abhishek Banthia
import AppKit
4 years ago
import Foundation
extension CGRect {
static func center(of layer: CALayer) -> CGPoint {
let parentSize = layer.frame.size
return CGPoint(x: parentSize.width / 2, y: parentSize.height / 2)
}
4 years ago
static func center(of parent: NSView) -> CGPoint {
let parentSize = parent.frame.size
return CGPoint(x: parentSize.width / 2, y: parentSize.height / 6)
}
}
extension String {
func size(with fontSize: CGFloat) -> CGSize {
let attr: [NSAttributedString.Key: Any] = [NSAttributedString.Key.font: NSFont.systemFont(ofSize: fontSize)]
let size = NSString(string: self).size(withAttributes: attr)
return size
}
}
4 years ago
private class HideAnimationDelegate: NSObject, CAAnimationDelegate {
private weak var view: NSView?
fileprivate init(view: NSView) {
self.view = view
}
4 years ago
fileprivate static func delegate(forView NSView: NSView) -> CAAnimationDelegate {
return HideAnimationDelegate(view: NSView)
}
4 years ago
fileprivate func animationDidStart(_: CAAnimation) {
view?.layer?.opacity = 0.0
}
4 years ago
func animationDidStop(_: CAAnimation, finished _: Bool) {
view?.removeFromSuperview()
view = nil
}
}
func hideAnimation(view: NSView, style: Style) {
let anim = CABasicAnimation(keyPath: "opacity")
let timing = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear)
anim.timingFunction = timing
let currentLayerTime = view.layer?.convertTime(CACurrentMediaTime(), from: nil)
anim.beginTime = currentLayerTime! + CFTimeInterval(style.fadeInOutDelay)
anim.duration = CFTimeInterval(style.fadeInOutDuration)
anim.fromValue = 1.0
anim.toValue = 0.0
anim.isRemovedOnCompletion = false
anim.delegate = HideAnimationDelegate.delegate(forView: view)
view.layer?.add(anim, forKey: "hide animation")
}
public protocol Style {
4 years ago
var fontSize: CGFloat { get }
var horizontalMargin: CGFloat { get }
var verticalMargin: CGFloat { get }
var cornerRadius: CGFloat { get }
var font: NSFont { get }
var backgroundColor: NSColor { get }
var foregroundColor: NSColor { get }
var fadeInOutDuration: CGFloat { get }
var fadeInOutDelay: CGFloat { get }
var labelOriginWithMargin: CGPoint { get }
var activitySize: CGSize { get }
}
4 years ago
public extension Style {
var labelOriginWithMargin: CGPoint {
return CGPoint(x: horizontalMargin, y: verticalMargin)
}
4 years ago
var fontSize: CGFloat { return 12 }
var font: NSFont {
if let avenirFont = NSFont(name: "Avenir-Light", size: fontSize) {
return avenirFont
}
return NSFont.systemFont(ofSize: fontSize)
}
4 years ago
var horizontalMargin: CGFloat { return 10 }
var verticalMargin: CGFloat { return 5 }
var cornerRadius: CGFloat { return 8 }
var backgroundColor: NSColor { return .black }
var foregroundColor: NSColor { return .white }
var activitySize: CGSize { return CGSize(width: 100, height: 100) }
var fadeInOutDuration: CGFloat { return 1.0 }
var fadeInOutDelay: CGFloat { return 1.0 }
}
public struct DefaultStyle: Style {
public static let shared = DefaultStyle()
}
4 years ago
private enum ToastKeys {
static var ActiveToast = "TSToastActiveToastKey"
}
class ToastView: NSView {
private let message: String
private let labelSize: CGSize
private let style: Style
init(message: String) {
self.message = message
4 years ago
style = DefaultStyle()
labelSize = message.size(with: style.fontSize)
let size = CGSize(
4 years ago
width: labelSize.width + style.horizontalMargin * 2,
height: labelSize.height + style.verticalMargin * 2
)
let rect = CGRect(origin: .zero, size: size)
super.init(frame: rect)
wantsLayer = true
setAccessibility("ToastView")
}
4 years ago
@available(*, unavailable)
required init?(coder _: NSCoder) { fatalError() }
override func viewDidMoveToSuperview() {
super.viewDidMoveToSuperview()
if superview != nil {
configure()
setAccessibility("ToastView")
}
}
4 years ago
private func configure() {
frame = superview?.bounds ?? NSRect.zero
let rect = CGRect(origin: style.labelOriginWithMargin, size: labelSize)
let sizeWithMargin = CGSize(
4 years ago
width: rect.width + style.horizontalMargin * 2,
height: rect.height + style.verticalMargin * 2
)
let rectWithMargin = CGRect(
origin: .zero, // position is manipulated later anyways
size: sizeWithMargin
)
// outside Container
let container = CALayer()
container.frame = rectWithMargin
container.position = CGRect.center(of: superview!)
container.backgroundColor = style.backgroundColor.cgColor
container.cornerRadius = style.cornerRadius
layer?.addSublayer(container)
// inside TextLayer
let text = CATextLayer()
text.frame = rect
text.position = CGRect.center(of: container)
text.string = message
text.font = NSFont.systemFont(ofSize: style.fontSize)
text.fontSize = style.fontSize
text.alignmentMode = .center
text.foregroundColor = style.foregroundColor.cgColor
text.backgroundColor = style.backgroundColor.cgColor
text.contentsScale = layer?.contentsScale ?? 0 // For Retina Display
container.addSublayer(text)
}
}
4 years ago
public extension NSView {
func makeToast(_ message: String) {
let toast = ToastView(message: message)
toast.setAccessibilityEnabled(true)
toast.setAccessibilityRole(.sheet)
4 years ago
addSubview(toast)
hideAnimation(view: toast, style: DefaultStyle.shared)
}
}