186 lines
6.4 KiB
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)
|
|
}
|
|
}
|