Files
openclaw/apps/ios/Sources/Design/OpenClawTypography.swift
Marvin Kaunda 5370f64cf0 improve: native iOS look with stock SwiftUI navigation, forms, chat, and talk visualizer (#99231)
* 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>
2026-07-03 18:17:09 -07:00

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)
}
}