more ui jumpiness

This commit is contained in:
Ian Gulliver
2026-10-17 15:17:56 -07:00
parent c8a7220c9b
commit c807060539
2 changed files with 96 additions and 6 deletions
+1 -1
View File
File diff suppressed because one or more lines are too long
+95 -5
View File
@@ -250,14 +250,104 @@ struct PreBox: View {
struct Spinner: View { struct Spinner: View {
var color: Color var color: Color
private static let frames = Array("⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏") @Environment(\.font) private var font
@Environment(\.fontResolutionContext) private var fontContext
var body: some View { var body: some View {
TimelineView(.periodic(from: .now, by: 0.08)) { context in Text("⠋")
let i = Int(context.date.timeIntervalSinceReferenceDate / 0.08) % Self.frames.count .hidden()
Text(String(Self.frames[i])) .overlay {
.foregroundStyle(color) 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()!
} }
} }