mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-08 17:44:01 +00:00
* improve(ios): adopt native SwiftUI chrome across the app * fix(ios): keep phone chat on the native navigation bar * fix(ios): render licenses screen as native settings rows * fix(ios): complete native interface polish * fix(ios): preserve branded native interface * test(ios): align gateway badge source guard * fix(ios): center onboarding on iPad * fix(ios): remove dead reskin symbols --------- Co-authored-by: Peter Steinberger <steipete@gmail.com>
448 lines
15 KiB
Swift
448 lines
15 KiB
Swift
import CoreText
|
|
import SwiftUI
|
|
import UIKit
|
|
|
|
enum OpenClawType {
|
|
// MARK: - Display — Red Hat Display
|
|
|
|
static var title1: Font {
|
|
scaledDisplay(weight: Display.heavyTitle, size: 34, relativeTo: .largeTitle)
|
|
}
|
|
|
|
static var title2: Font {
|
|
scaledDisplay(weight: Display.heavyTitle, size: 28, relativeTo: .title1)
|
|
}
|
|
|
|
static var title3: Font {
|
|
scaledDisplay(weight: Display.opticalBold, size: 22, relativeTo: .title2)
|
|
}
|
|
|
|
static var title3SemiBold: Font {
|
|
scaledDisplay(weight: Display.opticalSemiBold, size: 22, relativeTo: .title2)
|
|
}
|
|
|
|
static var headline: Font {
|
|
scaledDisplay(weight: Display.opticalSemiBold, size: 17, relativeTo: .headline)
|
|
}
|
|
|
|
static var headlineBold: Font {
|
|
scaledDisplay(weight: Display.opticalBold, size: 17, relativeTo: .headline)
|
|
}
|
|
|
|
// MARK: - Body — Inter
|
|
|
|
static var body: Font {
|
|
scaledBody(weight: Body.regular, size: 17, relativeTo: .body)
|
|
}
|
|
|
|
static var callout: Font {
|
|
scaledBody(weight: Body.regular, size: 16, relativeTo: .callout)
|
|
}
|
|
|
|
static var subhead: Font {
|
|
scaledBody(weight: Body.regular, size: 15, relativeTo: .subheadline)
|
|
}
|
|
|
|
static var subheadMedium: Font {
|
|
scaledBody(weight: Body.medium, size: 15, relativeTo: .subheadline)
|
|
}
|
|
|
|
static var subheadSemiBold: Font {
|
|
scaledDisplay(weight: Display.opticalSemiBold, size: 15, relativeTo: .subheadline)
|
|
}
|
|
|
|
static var subheadBold: Font {
|
|
scaledDisplay(weight: Display.opticalBold, size: 15, relativeTo: .subheadline)
|
|
}
|
|
|
|
static var footnote: Font {
|
|
scaledBody(weight: Body.regular, size: 13, relativeTo: .footnote)
|
|
}
|
|
|
|
static var footnoteMedium: Font {
|
|
scaledBody(weight: Body.medium, size: 13, relativeTo: .footnote)
|
|
}
|
|
|
|
static var footnoteSemiBold: Font {
|
|
scaledBody(weight: Body.semiBold, size: 13, relativeTo: .footnote)
|
|
}
|
|
|
|
static var caption: Font {
|
|
scaledBody(weight: Body.regular, size: 12, relativeTo: .caption1)
|
|
}
|
|
|
|
static var captionMedium: Font {
|
|
scaledBody(weight: Body.medium, size: 12, relativeTo: .caption1)
|
|
}
|
|
|
|
static var captionSemiBold: Font {
|
|
scaledBody(weight: Body.semiBold, size: 12, relativeTo: .caption1)
|
|
}
|
|
|
|
static var captionBold: Font {
|
|
scaledBody(weight: Body.bold, size: 12, relativeTo: .caption1)
|
|
}
|
|
|
|
static func avatar(size: CGFloat) -> Font {
|
|
self.scaledBody(weight: Body.bold, size: size, relativeTo: .caption1)
|
|
}
|
|
|
|
static var caption2: Font {
|
|
scaledBody(weight: Body.regular, size: 11, relativeTo: .caption2)
|
|
}
|
|
|
|
static var caption2Medium: Font {
|
|
scaledBody(weight: Body.medium, size: 11, relativeTo: .caption2)
|
|
}
|
|
|
|
static var caption2SemiBold: Font {
|
|
scaledBody(weight: Body.semiBold, size: 11, relativeTo: .caption2)
|
|
}
|
|
|
|
static var caption2Bold: Font {
|
|
scaledDisplay(weight: Display.opticalBold, size: 11, relativeTo: .caption2)
|
|
}
|
|
|
|
static var title2SemiBold: Font {
|
|
scaledDisplay(weight: Display.opticalSemiBold, size: 28, relativeTo: .title1)
|
|
}
|
|
|
|
// MARK: - Mono — JetBrains Mono
|
|
|
|
static var mono: Font {
|
|
scaledMono(name: Mono.regular, size: 14, relativeTo: .body)
|
|
}
|
|
|
|
static var monoSmall: Font {
|
|
scaledMono(name: Mono.regular, size: 12, relativeTo: .caption1)
|
|
}
|
|
|
|
static var monoSmallMedium: Font {
|
|
scaledMono(name: Mono.medium, size: 12, relativeTo: .caption1)
|
|
}
|
|
|
|
static var monoCaption2: Font {
|
|
scaledMono(name: Mono.regular, size: 11, relativeTo: .caption2)
|
|
}
|
|
|
|
static var monoFootnote: Font {
|
|
scaledMono(name: Mono.regular, size: 13, relativeTo: .footnote)
|
|
}
|
|
|
|
static var monoHeadline: Font {
|
|
scaledMono(name: Mono.medium, size: 17, relativeTo: .headline)
|
|
}
|
|
|
|
/// PostScript names for bundled fonts. Keep aligned with `UIAppFonts` in `project.yml`.
|
|
static let registeredPostScriptNames: [String] = [
|
|
Display.postScriptName,
|
|
Body.postScriptName,
|
|
Body.italicPostScriptName,
|
|
Mono.regular,
|
|
Mono.medium,
|
|
Mono.semiBold,
|
|
]
|
|
|
|
@MainActor
|
|
static func installUIKitAppearance() {
|
|
self.applyUIKitAppearance(self.makeUIKitAppearanceFonts())
|
|
}
|
|
|
|
@MainActor
|
|
static func refreshUIKitAppearance(in windows: [UIWindow]) {
|
|
let fonts = self.makeUIKitAppearanceFonts()
|
|
self.applyUIKitAppearance(fonts)
|
|
for window in windows {
|
|
self.applyUIKitTypography(fonts, to: window)
|
|
}
|
|
}
|
|
|
|
private static func makeUIKitAppearanceFonts() -> UIKitAppearanceFonts {
|
|
UIKitAppearanceFonts(
|
|
inlineNavigationTitleFont: self.scaledDisplayUIFont(
|
|
weight: Display.opticalSemiBold,
|
|
size: 17,
|
|
relativeTo: .headline,
|
|
maximumPointSize: 22),
|
|
largeNavigationTitleFont: self.scaledDisplayUIFont(
|
|
weight: Display.heavyTitle,
|
|
size: 34,
|
|
relativeTo: .largeTitle,
|
|
maximumPointSize: 44),
|
|
tabBarNormalFont: self.scaledBodyUIFont(
|
|
weight: Body.medium,
|
|
size: 11,
|
|
relativeTo: .caption2,
|
|
maximumPointSize: 13),
|
|
tabBarSelectedFont: self.scaledBodyUIFont(
|
|
weight: Body.semiBold,
|
|
size: 11,
|
|
relativeTo: .caption2,
|
|
maximumPointSize: 13),
|
|
segmentedNormalFont: self.scaledBodyUIFont(
|
|
weight: Body.medium,
|
|
size: 13,
|
|
relativeTo: .footnote,
|
|
maximumPointSize: 16),
|
|
segmentedSelectedFont: self.scaledBodyUIFont(
|
|
weight: Body.semiBold,
|
|
size: 13,
|
|
relativeTo: .footnote,
|
|
maximumPointSize: 16),
|
|
barButtonFont: self.scaledBodyUIFont(
|
|
weight: Body.semiBold,
|
|
size: 17,
|
|
relativeTo: .body,
|
|
maximumPointSize: 22),
|
|
disabledBarButtonFont: self.scaledBodyUIFont(
|
|
weight: Body.regular,
|
|
size: 17,
|
|
relativeTo: .body,
|
|
maximumPointSize: 22),
|
|
textInputFont: self.scaledBodyUIFont(
|
|
weight: Body.regular,
|
|
size: 17,
|
|
relativeTo: .body,
|
|
maximumPointSize: 22))
|
|
}
|
|
|
|
@MainActor
|
|
private static func applyUIKitAppearance(_ fonts: UIKitAppearanceFonts) {
|
|
let navigationBar = UINavigationBar.appearance()
|
|
self.applyNavigationBarTitleAttributes(fonts, to: navigationBar)
|
|
|
|
let tabBarItem = UITabBarItem.appearance()
|
|
self.applyTabBarItemAttributes(fonts, to: tabBarItem)
|
|
|
|
let segmentedControl = UISegmentedControl.appearance()
|
|
self.applySegmentedControlAttributes(fonts, to: segmentedControl)
|
|
|
|
let barButtonItem = UIBarButtonItem.appearance()
|
|
self.applyBarButtonItemAttributes(fonts, to: barButtonItem)
|
|
|
|
UITextField.appearance().font = fonts.textInputFont
|
|
UITextView.appearance().font = fonts.textInputFont
|
|
UISearchTextField.appearance().font = fonts.textInputFont
|
|
}
|
|
|
|
@MainActor
|
|
private static func applyUIKitTypography(_ fonts: UIKitAppearanceFonts, to view: UIView) {
|
|
switch view {
|
|
case let navigationBar as UINavigationBar:
|
|
self.applyNavigationBarTitleAttributes(fonts, to: navigationBar)
|
|
for item in navigationBar.items ?? [] {
|
|
self.applyNavigationItemBarButtonAttributes(fonts, to: item)
|
|
}
|
|
if let topItem = navigationBar.topItem {
|
|
self.applyNavigationItemBarButtonAttributes(fonts, to: topItem)
|
|
}
|
|
case let tabBar as UITabBar:
|
|
tabBar.items?.forEach { self.applyTabBarItemAttributes(fonts, to: $0) }
|
|
case let segmentedControl as UISegmentedControl:
|
|
self.applySegmentedControlAttributes(fonts, to: segmentedControl)
|
|
case let toolbar as UIToolbar:
|
|
toolbar.items?.forEach { self.applyBarButtonItemAttributes(fonts, to: $0) }
|
|
case let searchTextField as UISearchTextField:
|
|
searchTextField.font = fonts.textInputFont
|
|
default:
|
|
break
|
|
}
|
|
|
|
for subview in view.subviews {
|
|
self.applyUIKitTypography(fonts, to: subview)
|
|
}
|
|
}
|
|
|
|
@MainActor
|
|
private static func applyNavigationBarTitleAttributes(
|
|
_ fonts: UIKitAppearanceFonts,
|
|
to navigationBar: UINavigationBar)
|
|
{
|
|
var titleAttributes = navigationBar.titleTextAttributes ?? [:]
|
|
titleAttributes[.font] = fonts.inlineNavigationTitleFont
|
|
navigationBar.titleTextAttributes = titleAttributes
|
|
|
|
var largeTitleAttributes = navigationBar.largeTitleTextAttributes ?? [:]
|
|
largeTitleAttributes[.font] = fonts.largeNavigationTitleFont
|
|
navigationBar.largeTitleTextAttributes = largeTitleAttributes
|
|
}
|
|
|
|
@MainActor
|
|
private static func applyTabBarItemAttributes(_ fonts: UIKitAppearanceFonts, to tabBarItem: UITabBarItem) {
|
|
tabBarItem.setTitleTextAttributes([.font: fonts.tabBarNormalFont], for: .normal)
|
|
tabBarItem.setTitleTextAttributes([.font: fonts.tabBarSelectedFont], for: .selected)
|
|
}
|
|
|
|
@MainActor
|
|
private static func applySegmentedControlAttributes(
|
|
_ fonts: UIKitAppearanceFonts,
|
|
to segmentedControl: UISegmentedControl)
|
|
{
|
|
segmentedControl.setTitleTextAttributes([.font: fonts.segmentedNormalFont], for: .normal)
|
|
segmentedControl.setTitleTextAttributes([.font: fonts.segmentedSelectedFont], for: .selected)
|
|
}
|
|
|
|
@MainActor
|
|
private static func applyBarButtonItemAttributes(_ fonts: UIKitAppearanceFonts, to barButtonItem: UIBarButtonItem) {
|
|
barButtonItem.setTitleTextAttributes([.font: fonts.barButtonFont], for: .normal)
|
|
barButtonItem.setTitleTextAttributes([.font: fonts.barButtonFont], for: .highlighted)
|
|
barButtonItem.setTitleTextAttributes([.font: fonts.barButtonFont], for: .selected)
|
|
barButtonItem.setTitleTextAttributes([.font: fonts.disabledBarButtonFont], for: .disabled)
|
|
}
|
|
|
|
@MainActor
|
|
private static func applyNavigationItemBarButtonAttributes(
|
|
_ fonts: UIKitAppearanceFonts,
|
|
to item: UINavigationItem)
|
|
{
|
|
let barButtonItems = [
|
|
item.backBarButtonItem,
|
|
item.leftBarButtonItem,
|
|
item.rightBarButtonItem,
|
|
] + (item.leftBarButtonItems ?? []) + (item.rightBarButtonItems ?? [])
|
|
barButtonItems.compactMap(\.self).forEach { self.applyBarButtonItemAttributes(fonts, to: $0) }
|
|
}
|
|
|
|
private struct UIKitAppearanceFonts {
|
|
let inlineNavigationTitleFont: UIFont
|
|
let largeNavigationTitleFont: UIFont
|
|
let tabBarNormalFont: UIFont
|
|
let tabBarSelectedFont: UIFont
|
|
let segmentedNormalFont: UIFont
|
|
let segmentedSelectedFont: UIFont
|
|
let barButtonFont: UIFont
|
|
let disabledBarButtonFont: UIFont
|
|
let textInputFont: UIFont
|
|
}
|
|
|
|
private enum Display {
|
|
static let postScriptName = "RedHatDisplay-Regular"
|
|
static let opticalSemiBold: CGFloat = 650
|
|
static let opticalBold: CGFloat = 750
|
|
static let heavyTitle: CGFloat = 800
|
|
}
|
|
|
|
private enum Body {
|
|
static let postScriptName = "Inter-Regular"
|
|
static let italicPostScriptName = "Inter-Italic"
|
|
static let regular: CGFloat = 400
|
|
static let medium: CGFloat = 500
|
|
static let semiBold: CGFloat = 600
|
|
static let bold: CGFloat = 700
|
|
}
|
|
|
|
private enum Mono {
|
|
static let regular = "JetBrainsMono-Regular"
|
|
static let medium = "JetBrainsMono-Medium"
|
|
static let semiBold = "JetBrainsMono-SemiBold"
|
|
}
|
|
|
|
private static let fontWeightAxis = NSNumber(value: 2_003_265_652) // "wght"
|
|
private static let opticalSizeAxis = NSNumber(value: 1_869_640_570) // "opsz"
|
|
|
|
private static func scaledDisplay(
|
|
weight: CGFloat,
|
|
size: CGFloat,
|
|
relativeTo textStyle: UIFont.TextStyle) -> Font
|
|
{
|
|
Font(
|
|
self.scaledDisplayUIFont(
|
|
weight: weight,
|
|
size: size,
|
|
relativeTo: textStyle))
|
|
}
|
|
|
|
private static func scaledBody(
|
|
weight: CGFloat,
|
|
size: CGFloat,
|
|
relativeTo textStyle: UIFont.TextStyle) -> Font
|
|
{
|
|
Font(
|
|
self.scaledBodyUIFont(
|
|
weight: weight,
|
|
size: size,
|
|
relativeTo: textStyle))
|
|
}
|
|
|
|
private static func scaledMono(
|
|
name: String,
|
|
size: CGFloat,
|
|
relativeTo textStyle: UIFont.TextStyle) -> Font
|
|
{
|
|
self.scaledFont(name: name, size: size, relativeTo: textStyle)
|
|
}
|
|
|
|
private static func scaledDisplayUIFont(
|
|
weight: CGFloat,
|
|
size: CGFloat,
|
|
relativeTo textStyle: UIFont.TextStyle,
|
|
maximumPointSize: CGFloat? = nil) -> UIFont
|
|
{
|
|
self.scaledVariableUIFont(
|
|
name: Display.postScriptName,
|
|
size: size,
|
|
relativeTo: textStyle,
|
|
maximumPointSize: maximumPointSize,
|
|
variations: [self.fontWeightAxis: weight])
|
|
}
|
|
|
|
private static func scaledBodyUIFont(
|
|
weight: CGFloat,
|
|
size: CGFloat,
|
|
relativeTo textStyle: UIFont.TextStyle,
|
|
maximumPointSize: CGFloat? = nil) -> UIFont
|
|
{
|
|
self.scaledVariableUIFont(
|
|
name: Body.postScriptName,
|
|
size: size,
|
|
relativeTo: textStyle,
|
|
maximumPointSize: maximumPointSize,
|
|
variations: [
|
|
self.fontWeightAxis: weight,
|
|
self.opticalSizeAxis: min(max(size, 14), 32),
|
|
])
|
|
}
|
|
|
|
private static func scaledVariableUIFont(
|
|
name: String,
|
|
size: CGFloat,
|
|
relativeTo textStyle: UIFont.TextStyle,
|
|
maximumPointSize: CGFloat? = nil,
|
|
variations: [NSNumber: CGFloat]) -> UIFont
|
|
{
|
|
guard UIFont(name: name, size: size) != nil else {
|
|
let fallback = UIFont.systemFont(ofSize: size)
|
|
return self.scaledUIFont(fallback, relativeTo: textStyle, maximumPointSize: maximumPointSize)
|
|
}
|
|
|
|
let descriptor = UIFontDescriptor(fontAttributes: [
|
|
.name: name,
|
|
kCTFontVariationAttribute as UIFontDescriptor.AttributeName: variations,
|
|
])
|
|
let base = UIFont(descriptor: descriptor, size: size)
|
|
return self.scaledUIFont(base, relativeTo: textStyle, maximumPointSize: maximumPointSize)
|
|
}
|
|
|
|
private static func scaledFont(
|
|
name: String,
|
|
size: CGFloat,
|
|
relativeTo textStyle: UIFont.TextStyle) -> Font
|
|
{
|
|
let base = UIFont(name: name, size: size) ?? UIFont.systemFont(ofSize: size)
|
|
let scaled = UIFontMetrics(forTextStyle: textStyle).scaledFont(for: base)
|
|
return Font(scaled)
|
|
}
|
|
|
|
private static func scaledUIFont(
|
|
_ base: UIFont,
|
|
relativeTo textStyle: UIFont.TextStyle,
|
|
maximumPointSize: CGFloat?) -> UIFont
|
|
{
|
|
let metrics = UIFontMetrics(forTextStyle: textStyle)
|
|
if let maximumPointSize {
|
|
return metrics.scaledFont(for: base, maximumPointSize: maximumPointSize)
|
|
}
|
|
return metrics.scaledFont(for: base)
|
|
}
|
|
}
|