TheNoiseClock/TheNoiseClock/Views/Clock/Components/DigitView.swift

186 lines
6.4 KiB
Swift

//
// DigitView.swift
// TheNoiseClock
//
// Created by Matt Bruce on 9/9/25.
//
import SwiftUI
/// Component for displaying a single digit with fixed width and glow effects
struct DigitView: View {
@Environment(\.sizeCategory) private var sizeCategory
let digit: String
let fontName: FontFamily
let weight: Font.Weight
let design: Font.Design
let opacity: Double
let digitColor: Color
let glowIntensity: Double
@Binding var fontSize: CGFloat
@State private var lastCalculatedSize: CGSize = .zero
@State private var isCalculating: Bool = false
init(digit: String,
fontName: FontFamily,
weight: Font.Weight = .regular,
design: Font.Design = .default,
digitColor: Color = .black,
opacity: Double = 1,
glowIntensity: Double = 0,
fontSize: Binding<CGFloat>) {
DebugLogger.log("DigitView: init called with fontName=\(fontName), weight=\(weight), design=\(design)", category: .general)
self.digit = (digit.count == 1 && "0123456789".contains(digit)) ? digit : "0"
self.fontName = fontName
self.weight = weight
self.design = design
self.opacity = opacity
self.digitColor = digitColor
self.glowIntensity = glowIntensity
self._fontSize = fontSize
}
var body: some View {
GeometryReader { geometry in
ZStack {
glowText
.position(x: geometry.size.width / 2, y: geometry.size.height / 2)
mainText
.position(x: geometry.size.width / 2, y: geometry.size.height / 2)
}
}
}
private var glowRadius: CGFloat {
ColorUtils.glowRadius(intensity: glowIntensity)
}
private var glowOpacity: Double {
ColorUtils.glowOpacity(intensity: glowIntensity) * opacity
}
private var glowText: some View {
text
.foregroundColor(digitColor)
.blur(radius: glowRadius)
.opacity(glowOpacity)
}
private var mainText: some View {
text
.foregroundColor(digitColor)
.opacity(opacity)
}
private var text: some View {
GeometryReader { geometry in
Text(digit)
.font(FontUtils.createFont(name: fontName,
weight: weight,
design: design,
size: fontSize))
.frame(maxWidth: .infinity, maxHeight: .infinity)
.multilineTextAlignment(.center)
.minimumScaleFactor(0.1)
.lineSpacing(0)
.padding(.vertical, 0)
.padding(.horizontal, 0)
.baselineOffset(0)
.onAppear {
calculateOptimalFontSize(for: geometry.size)
}
.onChange(of: geometry.size) { _, newSize in
calculateOptimalFontSize(for: newSize)
}
.onChange(of: sizeCategory) { _, _ in
calculateOptimalFontSize(for: geometry.size)
}
.onChange(of: fontName) { _, _ in
DebugLogger.log("DigitView: fontName changed to \(fontName)", category: .general)
calculateOptimalFontSize(for: geometry.size)
}
.onChange(of: weight) { _, _ in
DebugLogger.log("DigitView: weight changed to \(weight)", category: .general)
calculateOptimalFontSize(for: geometry.size)
}
.onChange(of: design) { _, _ in
DebugLogger.log("DigitView: design changed to \(design)", category: .general)
calculateOptimalFontSize(for: geometry.size)
}
}
}
private func calculateOptimalFontSize(for size: CGSize) {
// Prevent multiple calculations for the same size
guard size != lastCalculatedSize && !isCalculating else { return }
// Prevent multiple updates per frame
guard !isCalculating else { return }
isCalculating = true
let optimalSize = FontUtils.calculateOptimalFontSize(digit: digit,
fontName: fontName,
weight: weight,
design: design,
for: size)
// Only update if the size is significantly different to prevent micro-adjustments
fontSize = optimalSize
lastCalculatedSize = size
// Reset calculation flag after a brief delay to allow for frame completion
DispatchQueue.main.asyncAfter(deadline: .now() + 0.016) { // ~60fps
isCalculating = false
}
}
}
// MARK: - Preview
#Preview {
@Previewable @State var sharedFontSize: CGFloat = 2000
@Previewable @State var fontName: FontFamily = .arial
@Previewable @State var weight: Font.Weight = .heavy
@Previewable @State var design: Font.Design = .rounded
@Previewable @State var glowIntensity: Double = 0.6
HStack {
DigitView(digit: "8",
fontName: fontName,
weight: weight,
design: design,
glowIntensity: glowIntensity,
fontSize: $sharedFontSize)
.border(Color.black)
DigitView(digit: "1",
fontName: fontName,
weight: weight,
design: design,
glowIntensity: glowIntensity,
fontSize: $sharedFontSize)
.border(Color.black)
Text(":")
.font(.system(size: sharedFontSize))
.border(Color.black)
DigitView(digit: "0",
fontName: fontName,
weight: weight,
design: design,
glowIntensity: glowIntensity,
fontSize: $sharedFontSize)
.border(Color.black)
DigitView(digit: "5",
fontName: fontName,
weight: weight,
design: design,
glowIntensity: glowIntensity,
fontSize: $sharedFontSize)
.border(Color.black)
}
}