// // CLSystemTokens.swift // Crush // // Created by Leon on 2025/7/11. // import UIKit // MARK: - Convenience functions extension UIColor { static var c: SystemColor { SystemColor() } } extension UIFont { static var t: SystemFont { SystemFont() } } @dynamicMemberLookup struct SystemColor { subscript(dynamicMember key: String) -> UIColor { guard let token = CLEnumSystemToken.allCases.first(where: { "\($0)" == key }) else { return .black } return CLSystemToken.color(token: token) ?? .red } func systemTokenValue(for key: String) -> String? { CLEnumSystemToken.allCases.first(where: { "\($0)" == key })?.rawValue } } @dynamicMemberLookup struct SystemFont { subscript(dynamicMember key: String) -> UIFont { guard let token = CLEnumSystemToken.allCases.first(where: { "\($0)" == key }) else { return .systemFont(ofSize: 14) } return CLSystemToken.font(token: token) } func systemTokenValue(for key: String) -> String? { CLEnumSystemToken.allCases.first(where: { "\($0)" == key })?.rawValue } } // MARK: - CLEnumSystemToken: Enums and Constants enum CLEnumSystemToken: String, CaseIterable { init?(caseName: String) { self = CLEnumSystemToken.allCases.first(where: { "\($0)" == caseName }) ?? .cpn } case cpn = "color.primary.normal" case cph = "color.primary.hover" case cpp = "color.primary.press" case cpd = "color.primary.disabled" case cpvn = "color.primary.variant.normal" case cpvh = "color.primary.variant.hover" case cpvp = "color.primary.variant.press" case cpvd = "color.primary.variant.disabled" case cpgn = "color.primary.gradient.normal" case cpgh = "color.primary.gradient.hover" case cpgp = "color.primary.gradient.press" case cpgd = "color.primary.gradient.disabled" case cin = "color.important.normal" case cih = "color.important.hover" case cip = "color.important.press" case cid = "color.important.disabled" case civn = "color.important.variant.normal" case civh = "color.important.variant.hover" case civp = "color.important.variant.press" case civd = "color.important.variant.disabled" case cign = "color.important.gradient.normal" case cigh = "color.important.gradient.hover" case cigp = "color.important.gradient.press" case ciopn = "color.important.onpic.normal" case cposn = "color.positive.normal" case cposh = "color.positive.hover" case cposp = "color.positive.press" case cposd = "color.positive.disabled" case cposvn = "color.positive.variant.normal" case cposvh = "color.positive.variant.hover" case cposvp = "color.positive.variant.press" case cposvd = "color.positive.variant.disabled" case cposgn = "color.positive.gradient.normal" case cposopn = "color.positive.onpic.normal" case cwn = "color.warning.normal" case cwh = "color.warning.hover" case cwp = "color.warning.press" case cwd = "color.warning.disabled" case cwvn = "color.warning.variant.normal" case cwvh = "color.warning.variant.hover" case cwvp = "color.warning.variant.press" case cwvd = "color.warning.variant.disabled" case cwgn = "color.warning.gradient.normal" case cwopn = "color.warning.onpic.normal" case cen = "color.emphasis.normal" case ceh = "color.emphasis.hover" case cep = "color.emphasis.press" case ced = "color.emphasis.disabled" case cevn = "color.emphasis.variant.normal" case cevh = "color.emphasis.variant.hover" case cevp = "color.emphasis.variant.press" case cevd = "color.emphasis.variant.disabled" case cegn = "color.emphasis.grandient.normal" case cegh = "color.emphasis.grandient.hover" case cegp = "color.emphasis.grandient.press" case cegd = "color.emphasis.grandient.disabled" case ceopn = "color.emphasis.onpic.normal" case cbd = "color.background.default" case cbs = "color.background.specialmap" case cbdi = "color.background.district" case csbn = "color.surface.base.normal" case csbh = "color.surface.base.hover" case csbp = "color.surface.base.press" case csbsn = "color.surface.base.specialmap.normal" case csbsh = "color.surface.base.specialmap.hover" case csbsp = "color.surface.base.specialmap.press" case csbsd = "color.surface.base.specialmap.disabled" case csfn = "color.surface.float.normal" case csfh = "color.surface.float.hover" case csfp = "color.surface.float.press" case cstn = "color.surface.top.normal" case csth = "color.surface.top.hover" case cstp = "color.surface.top.press" case cstd = "color.surface.top.disabled" case csdn = "color.surface.district.normal" case csdh = "color.surface.district.hover" case csdp = "color.surface.district.press" case csdd = "color.surface.district.disabled" case csnn = "color.surface.nest.normal" case csnh = "color.surface.nest.hover" case csnp = "color.surface.nest.press" case csnd = "color.surface.nest.disabled" case csen = "color.surface.element.normal" case cseh = "color.surface.element.hover" case csep = "color.surface.element.press" case csed = "color.surface.element.disabled" case csedn = "color.surface.element.dark.normal" case csedh = "color.surface.element.dark.hover" case csedp = "color.surface.element.dark.press" case csedd = "color.surface.element.dark.disabled" case cseln = "color.surface.element.light.normal" case cselh = "color.surface.element.light.hover" case cselp = "color.surface.element.light.press" case cseld = "color.surface.element.light.disabled" case cswn = "color.surface.white.normal" case cswh = "color.surface.white.hover" case cswp = "color.surface.white.press" case cswd = "color.surface.white.disabled" case csbn2 = "color.surface.black.normal" case con = "color.outline.normal" case coh = "color.outline.hover" case cop = "color.outline.press" case cod = "color.outline.disabled" case copr = "color.overlay.primary" case cogr = "color.overlay.gradient" case codr = "color.overlay.dark" /// graident case cob = "color.overlay.background" case cobase = "color.overlay.base" /// vip gradient case ccvn = "color.context.vip.normal" case ctpn = "color.txt.primary.normal" case ctph = "color.txt.primary.hover" case ctpp = "color.txt.primary.press" case ctpd = "color.txt.primary.disabled" case ctpsn = "color.txt.primary.specialmap.normal" case ctpsh = "color.txt.primary.specialmap.hover" case ctpsp = "color.txt.primary.specialmap.press" case ctpsd = "color.txt.primary.specialmap.disabled" case ctsn = "color.txt.secondary.normal" case ctsh = "color.txt.secondary.hover" case ctsp = "color.txt.secondary.press" case ctsd = "color.txt.secondary.disabled" case cttn = "color.txt.tertiary.normal" case ctg = "color.txt.grass" case ctd = "color.txt.disabled" case td = "txt.display" case tdxl = "txt.display.xl" case tdl = "txt.display.l" case tdm = "txt.display.m" case tds = "txt.display.s" case thl = "txt.headline.l" case thm = "txt.headline.m" case ths = "txt.headline.s" case ttl = "txt.title.l" case ttm = "txt.title.m" case tts = "txt.title.s" case tbsl = "txt.bodySemibold.l" case tbsm = "txt.bodySemibold.m" case tbss = "txt.bodySemibold.s" case tbl = "txt.body.l" case tbm = "txt.body.m" case tbim = "txt.body.italic.m" case tbs = "txt.body.s" case tll = "txt.label.l" case tlm = "txt.label.m" case tls = "txt.label.s" case tndxl = "txt.numDisplay.xl" case tndl = "txt.numDisplay.l" case tndm = "txt.numDisplay.m" case tnds = "txt.numDisplay.s" case tnmxl = "txt.numMonotype.xl" case tnml = "txt.numMonotype.l" case tnmm = "txt.numMonotype.m" case tnms = "txt.numMonotype.s" case tnmxs = "txt.numMonotype.xs" case shs = "shadow.s" case shm = "shadow.m" case shl = "shadow.l" case rxs = "radius.xs" case rs = "radius.s" case rm = "radius.m" case rl = "radius.l" case rxl = "radius.xl" case rxxl = "radius.xxl" case rr = "radius.round" case rp = "radius.pill" case bd = "border.divider" case bs = "border.s" case bm = "border.m" case bl = "border.l" } func KeyForSystemTokenFrom(_ token: CLEnumSystemToken) -> String { return token.rawValue } // MARK: - EPShadow struct EPShadow { var color: UIColor? var opacity: Float = 0 var offset: CGSize = .zero var radius: CGFloat = 0 } // MARK: - EPGradient struct EPGradient { var direction: CGPoint = .zero var firstColor: UIColor? var secondColor: UIColor? var thirdColor : UIColor? public func toImage(size: CGSize) -> UIImage? { guard let firstColor = firstColor?.cgColor, let secondColor = secondColor?.cgColor else { return nil } // 根据 direction 计算 startPoint 和 endPoint let startPoint: CGPoint let endPoint: CGPoint switch direction { case CGPoint(x: 0, y: -1): // 从上到下 startPoint = CGPoint(x: 0.5, y: 0) endPoint = CGPoint(x: 0.5, y: 1) case CGPoint(x: 1, y: -1): // 从左上到右下 startPoint = CGPoint(x: 0, y: 0) endPoint = CGPoint(x: 1, y: 1) default: // 如果 direction 不匹配已知方向,返回 nil 或使用默认方向 startPoint = CGPoint(x: 0, y: 0.5) endPoint = CGPoint(x: 1, y: 0.5) // 默认水平渐变 } var colors = [firstColor, secondColor] if(colors.count == 3){ if let third = thirdColor?.cgColor{ colors = [firstColor, secondColor, third] } } // 调用 gradientImageWithSize 生成图像 return UIImage.gradientImageWithSize( size: size, colors: colors, startPoint: startPoint, endPoint: endPoint ) } public func colors() -> [UIColor]{ var colors = [UIColor]() if let first = firstColor{ colors.append(first) } if let second = secondColor{ colors.append(second) } if let third = thirdColor{ colors.append(third) } return colors } } // MARK: - EPTypography struct EPTypography { var font: UIFont? var lineHeight: CGFloat = 0 } // MARK: - EPBaseObject class EPBaseObject { static func subGetColorByTokenValues(_ values: [String]) -> UIColor? { guard !values.isEmpty else { preconditionFailure("Invalid color values: empty array") return nil } guard let hex = values.first, hex.hasPrefix("#") else { print("❌ Invalid color format for values: \(values)") return nil } let hexString = hex.replacingOccurrences(of: "#", with: "") var rgb: UInt64 = 0 Scanner(string: hexString).scanHexInt64(&rgb) let red = CGFloat((rgb >> 16) & 0xFF) / 255.0 let green = CGFloat((rgb >> 8) & 0xFF) / 255.0 let blue = CGFloat(rgb & 0xFF) / 255.0 let alpha: CGFloat = values.count > 1 ? CGFloat(Float(values.last!) ?? 1.0) : 1.0 return UIColor(red: red, green: green, blue: blue, alpha: alpha) } } // MARK: - CLSystemToken class CLSystemToken { static let baseTokens = CLBaseTokens.shared // MARK: - Color Methods static func color(token: CLEnumSystemToken) -> UIColor? { let key = KeyForSystemTokenFrom(token) let values = baseTokens.getTokensByKey(key) return EPBaseObject.subGetColorByTokenValues(values) } static func darkColor(token: CLEnumSystemToken) -> UIColor? { let key = KeyForSystemTokenFrom(token) let values = baseTokens.getDarkTokens(key) return EPBaseObject.subGetColorByTokenValues(values) } // MARK: - String Method static func string(token: CLEnumSystemToken) -> String? { let key = KeyForSystemTokenFrom(token) return baseTokens.getTokenByKey(key) } // MARK: - Float Method static func float(token: CLEnumSystemToken) -> CGFloat { let key = KeyForSystemTokenFrom(token) let value = baseTokens.getTokenByKey(key) guard let floatValue = Float(value) else { print("❌ Invalid float format for key: \(key)") return 0 } return CGFloat(floatValue) } // MARK: - Radius Method static func radius(token: CLEnumSystemToken) -> CGFloat { let key = KeyForSystemTokenFrom(token) let value = baseTokens.getTokenByKey(key) guard let radius = Float(value) else { print("❌ Invalid radius format for key: \(key)") return 0 } // print("🎨 radius: \(key) \(radius)") // DLog 替换为 print return CGFloat(radius) } // MARK: - Border Method static func border(token: CLEnumSystemToken) -> CGFloat { let key = KeyForSystemTokenFrom(token) let value = baseTokens.getTokenByKey(key) guard let border = Float(value) else { print("❌ Invalid border format for key: \(key)") return 0 } // print("🎨 border: \(key) \(border)") // DLog 替换为 print return CGFloat(border) } // MARK: - Shadow Method static func shadow(token: CLEnumSystemToken) -> EPShadow { let key = KeyForSystemTokenFrom(token) let values = baseTokens.getTokensByKey(key) guard values.count == 4, values.first?.contains(EPConfigShadowPrefix) == true else { assertionFailure("Wrong format for shadow token: \(key)") return EPShadow() } var shadow = EPShadow() if let colorStr = values.first?.replacingOccurrences(of: EPConfigShadowPrefix, with: "") { let color = EPBaseObject.subGetColorByTokenValues([colorStr]) shadow.color = color } if let opacity = Float(values[1]) { shadow.opacity = opacity } let offXy = values[2].components(separatedBy: "&") if offXy.count == 2, let x = Float(offXy[0]), let y = Float(offXy[1]) { shadow.offset = CGSize(width: CGFloat(x), height: CGFloat(y)) } if let radius = Float(values[3]) { shadow.radius = CGFloat(radius) } return shadow } // MARK: - Gradient Method static func gradient(token: CLEnumSystemToken) -> EPGradient { // if token == .colorContextVipNormal { // assertionFailure("Special style rule, not used in this unified rule, please use another method") // return gradient(token: .colorPrimaryGradientNormal) // } let key = KeyForSystemTokenFrom(token) let values = baseTokens.getTokensByKey(key) if values.count <= 2 { // || values.first?.contains(EPConfigGradientPrefix) != true var gradient = EPGradient() gradient.direction = CGPoint(x: 1, y: 0) gradient.firstColor = EPBaseObject.subGetColorByTokenValues(values) gradient.secondColor = gradient.firstColor return gradient } // 方向+ 2个颜色,或者3个颜色 guard values.count == 3 || values.count == 4 else { assertionFailure("Wrong format for gradient token: \(key)") return EPGradient() } var gradient = EPGradient() if let directionStr = values.first { switch directionStr { case "LTR": gradient.direction = CGPoint(x: 1, y: 0) case "TTB": gradient.direction = CGPoint(x: 0, y: 1) case "LTTRB": gradient.direction = CGPoint(x: 1, y: 1) default: assertionFailure("Not handled direction: \(directionStr)") gradient.direction = .zero } } let color1Str = values[1].replacingOccurrences(of: EPConfigGradientPrefix, with: "") let color1Array = color1Str.components(separatedBy: "&") gradient.firstColor = EPBaseObject.subGetColorByTokenValues(color1Array) let color2Str = values[2].replacingOccurrences(of: EPConfigGradientPrefix, with: "") let color2Array = color2Str.components(separatedBy: "&") gradient.secondColor = EPBaseObject.subGetColorByTokenValues(color2Array) if(values.count == 4){ let color3Str = values[3].replacingOccurrences(of: EPConfigGradientPrefix, with: "") let color3Array = color3Str.components(separatedBy: "&") gradient.thirdColor = EPBaseObject.subGetColorByTokenValues(color3Array) } return gradient } // MARK: - Typography Method static func typography(token: CLEnumSystemToken) -> EPTypography { let key = KeyForSystemTokenFrom(token) let values = baseTokens.getTokensByKey(key) guard values.count == 4 else { assertionFailure("Wrong format for typography token: \(key)") return EPTypography() } var typography = EPTypography() var fontName = values[0] guard let fontSize = Int(values[1]), let weight = Int(values[2]), let lineHeight = Int(values[3]) else { assertionFailure("Invalid typography values for token: \(key)") return EPTypography() } if !fontName.contains("-Italic") { switch weight { case 400: fontName += "-Regular" case 500: fontName += "-Medium" case 600: fontName += "-SemiBold" case 700: fontName += "-Bold" default: assertionFailure("Not handled font weight: \(weight)") } } typography.font = UIFont(name: fontName, size: CGFloat(fontSize)) typography.lineHeight = CGFloat(lineHeight) return typography } // MARK: - Font Method static func font(token: CLEnumSystemToken) -> UIFont { let key = KeyForSystemTokenFrom(token) let values = baseTokens.getTokensByKey(key) guard values.count == 4 else { assertionFailure("Wrong format for font token: \(key)") return UIFont.systemFont(ofSize: 14) } var fontName = values[0] guard let fontSize = Int(values[1]), let weight = Int(values[2]) else { assertionFailure("Invalid font values for token: \(key)") return UIFont.systemFont(ofSize: 14) } if(fontName .isNotBlank){ fontName = fontName.removingAllWhitespace } if !fontName.contains("-Italic") { switch weight { case 400: fontName += "-Regular" case 500: fontName += "-Medium" case 600: fontName += "-SemiBold" case 700: fontName += "-Bold" default: assertionFailure("Not handled font weight: \(weight)") } } return UIFont(name: fontName, size: CGFloat(fontSize)) ?? UIFont.systemFont(ofSize: 14) } }