2026-10-16 22:23:31 -07:00
|
|
|
import CoreText
|
2026-10-20 19:32:27 -07:00
|
|
|
import Foundation
|
2026-10-17 09:57:28 -07:00
|
|
|
import Synchronization
|
2026-10-20 19:32:27 -07:00
|
|
|
import SwiftData
|
2026-10-16 22:23:31 -07:00
|
|
|
import SwiftUI
|
|
|
|
|
|
2026-10-20 19:32:27 -07:00
|
|
|
@MainActor
|
|
|
|
|
@Observable
|
|
|
|
|
final class FontSize {
|
|
|
|
|
static let shared = FontSize()
|
|
|
|
|
static let range = 9...20
|
|
|
|
|
|
|
|
|
|
private(set) var body = PrefsRecord.defaultFontSize
|
|
|
|
|
|
|
|
|
|
private init() {
|
|
|
|
|
load()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func load() {
|
|
|
|
|
body = record()?.fontSize ?? PrefsRecord.defaultFontSize
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func set(_ size: Int) {
|
|
|
|
|
if let r = record() {
|
|
|
|
|
r.fontSize = size
|
|
|
|
|
r.saved = Date()
|
|
|
|
|
} else {
|
|
|
|
|
persistence.insert(PrefsRecord(fontSize: size))
|
|
|
|
|
}
|
|
|
|
|
saveRecords()
|
|
|
|
|
body = size
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private func record() -> PrefsRecord? {
|
|
|
|
|
fetchRecords(FetchDescriptor<PrefsRecord>(sortBy: [SortDescriptor(\.saved, order: .reverse)])).first
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-16 22:23:31 -07:00
|
|
|
enum Fonts {
|
|
|
|
|
private static func file(_ subset: String, _ italic: Bool, _ weight: CGFloat) -> CTFontDescriptor {
|
2026-10-18 00:31:46 -07:00
|
|
|
let url = Bundle.main.resourceURL!.appending(path: "fonts/atkinson-hyperlegible-mono-\(subset)-wght-\(italic ? "italic" : "normal").woff2")
|
|
|
|
|
let d = (CTFontManagerCreateFontDescriptorsFromURL(url as CFURL) as! [CTFontDescriptor])[0]
|
2026-10-16 22:23:31 -07:00
|
|
|
return CTFontDescriptorCreateCopyWithVariation(d, 0x7767_6874 as CFNumber, weight)
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-18 00:31:46 -07:00
|
|
|
private static func ct(_ size: CGFloat, weight: CGFloat = 400, italic: Bool = false) -> CTFont {
|
2026-10-16 22:23:31 -07:00
|
|
|
let ext = file("latin-ext", italic, weight)
|
|
|
|
|
let d = CTFontDescriptorCreateCopyWithAttributes(file("latin", italic, weight), [kCTFontCascadeListAttribute: [ext]] as CFDictionary)
|
|
|
|
|
return CTFontCreateWithFontDescriptor(d, size, nil)
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-17 09:57:28 -07:00
|
|
|
private static let fonts = Mutex<[String: CTFont]>([:])
|
2026-10-16 23:30:57 -07:00
|
|
|
|
2026-10-20 19:32:27 -07:00
|
|
|
// Call sites give sizes relative to a 13pt body; the user's font size shifts them all.
|
|
|
|
|
@MainActor
|
2026-10-17 09:57:28 -07:00
|
|
|
private static func cached(_ size: CGFloat, _ weight: CGFloat, _ italic: Bool) -> CTFont {
|
2026-10-20 19:32:27 -07:00
|
|
|
let size = size + CGFloat(FontSize.shared.body - 13)
|
2026-10-16 23:30:57 -07:00
|
|
|
let key = "\(size) \(weight) \(italic)"
|
2026-10-17 09:57:28 -07:00
|
|
|
return fonts.withLock { fonts in
|
|
|
|
|
if let f = fonts[key] {
|
|
|
|
|
return f
|
|
|
|
|
}
|
|
|
|
|
let f = ct(size, weight: weight, italic: italic)
|
|
|
|
|
fonts[key] = f
|
2026-10-16 23:30:57 -07:00
|
|
|
return f
|
|
|
|
|
}
|
2026-10-16 22:23:31 -07:00
|
|
|
}
|
2026-10-17 09:15:50 -07:00
|
|
|
|
2026-10-20 19:32:27 -07:00
|
|
|
@MainActor
|
2026-10-17 09:15:50 -07:00
|
|
|
static func font(_ size: CGFloat, weight: CGFloat = 400, italic: Bool = false) -> Font {
|
2026-10-17 09:57:28 -07:00
|
|
|
Font(cached(size, weight, italic))
|
2026-10-17 09:15:50 -07:00
|
|
|
}
|
|
|
|
|
|
2026-10-20 19:32:27 -07:00
|
|
|
@MainActor
|
2026-10-17 09:15:50 -07:00
|
|
|
static func ns(_ size: CGFloat, weight: CGFloat = 400, italic: Bool = false) -> NSFont {
|
2026-10-17 09:57:28 -07:00
|
|
|
cached(size, weight, italic) as NSFont
|
2026-10-17 09:15:50 -07:00
|
|
|
}
|
2026-10-16 22:23:31 -07:00
|
|
|
}
|