579 lines
19 KiB
Swift
579 lines
19 KiB
Swift
//
|
||
// 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)
|
||
}
|
||
}
|