more ui jumpiness
This commit is contained in:
@@ -250,14 +250,104 @@ struct PreBox: View {
|
||||
|
||||
struct Spinner: View {
|
||||
var color: Color
|
||||
private static let frames = Array("⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏")
|
||||
@Environment(\.font) private var font
|
||||
@Environment(\.fontResolutionContext) private var fontContext
|
||||
|
||||
var body: some View {
|
||||
TimelineView(.periodic(from: .now, by: 0.08)) { context in
|
||||
let i = Int(context.date.timeIntervalSinceReferenceDate / 0.08) % Self.frames.count
|
||||
Text(String(Self.frames[i]))
|
||||
.foregroundStyle(color)
|
||||
Text("⠋")
|
||||
.hidden()
|
||||
.overlay {
|
||||
SpinnerGlyphs(font: (font ?? .body).resolve(in: fontContext).ctFont, color: color)
|
||||
.allowsHitTesting(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct SpinnerGlyphs: NSViewRepresentable {
|
||||
let font: CTFont
|
||||
let color: Color
|
||||
|
||||
func makeNSView(context: Context) -> SpinnerView {
|
||||
SpinnerView()
|
||||
}
|
||||
|
||||
func updateNSView(_ v: SpinnerView, context: Context) {
|
||||
v.font = font
|
||||
v.color = color.resolve(in: context.environment).cgColor
|
||||
v.needsLayout = true
|
||||
}
|
||||
}
|
||||
|
||||
private final class SpinnerView: NSView {
|
||||
private struct Key: Equatable {
|
||||
let size: CGSize
|
||||
let scale: CGFloat
|
||||
let font: CTFont
|
||||
let color: CGColor
|
||||
}
|
||||
|
||||
private static let frames = Array("⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏")
|
||||
var font: CTFont?
|
||||
var color: CGColor?
|
||||
private let glyph = CALayer()
|
||||
private var drawn: Key?
|
||||
|
||||
override init(frame: NSRect) {
|
||||
super.init(frame: frame)
|
||||
wantsLayer = true
|
||||
layer?.addSublayer(glyph)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) is not supported")
|
||||
}
|
||||
|
||||
override func viewDidChangeBackingProperties() {
|
||||
super.viewDidChangeBackingProperties()
|
||||
needsLayout = true
|
||||
}
|
||||
|
||||
override func layout() {
|
||||
super.layout()
|
||||
guard let font, let color, let window, bounds.width > 0, bounds.height > 0 else {
|
||||
return
|
||||
}
|
||||
let key = Key(size: bounds.size, scale: window.backingScaleFactor, font: font, color: color)
|
||||
if key == drawn {
|
||||
return
|
||||
}
|
||||
drawn = key
|
||||
let images = Self.frames.map { render(String($0), key) }
|
||||
CATransaction.begin()
|
||||
CATransaction.setDisableActions(true)
|
||||
glyph.frame = bounds
|
||||
glyph.contentsScale = key.scale
|
||||
glyph.contents = images[0]
|
||||
let spin = CAKeyframeAnimation(keyPath: "contents")
|
||||
spin.values = images
|
||||
spin.keyTimes = (0...images.count).map { NSNumber(value: Double($0) / Double(images.count)) }
|
||||
spin.calculationMode = .discrete
|
||||
spin.duration = 0.08 * Double(images.count)
|
||||
spin.repeatCount = .infinity
|
||||
spin.isRemovedOnCompletion = false
|
||||
glyph.add(spin, forKey: "spin")
|
||||
CATransaction.commit()
|
||||
}
|
||||
|
||||
private func render(_ s: String, _ key: Key) -> CGImage {
|
||||
let ctx = CGContext(data: nil, width: Int(ceil(key.size.width * key.scale)), height: Int(ceil(key.size.height * key.scale)), bitsPerComponent: 8, bytesPerRow: 0, space: CGColorSpace(name: CGColorSpace.sRGB)!, bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue)!
|
||||
ctx.scaleBy(x: key.scale, y: key.scale)
|
||||
let attrs: [NSAttributedString.Key: Any] = [
|
||||
NSAttributedString.Key(kCTFontAttributeName as String): key.font,
|
||||
NSAttributedString.Key(kCTForegroundColorAttributeName as String): key.color,
|
||||
]
|
||||
let line = CTLineCreateWithAttributedString(NSAttributedString(string: s, attributes: attrs))
|
||||
var ascent: CGFloat = 0
|
||||
var descent: CGFloat = 0
|
||||
let width = CTLineGetTypographicBounds(line, &ascent, &descent, nil)
|
||||
ctx.textPosition = CGPoint(x: (key.size.width - width) / 2, y: (key.size.height - ascent - descent) / 2 + descent)
|
||||
CTLineDraw(line, ctx)
|
||||
return ctx.makeImage()!
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user