Skip to content

Commit 1675de3

Browse files
committed
Implement dynamic text styles (and various font-related modifiers)
1 parent 42f2ae8 commit 1675de3

File tree

18 files changed

+796
-157
lines changed

18 files changed

+796
-157
lines changed

Examples/Sources/CounterExample/CounterApp.swift

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,18 +13,18 @@ struct CounterApp: App {
1313
var body: some Scene {
1414
WindowGroup("CounterExample: \(count)") {
1515
#hotReloadable {
16-
VStack {
17-
HStack(spacing: 20) {
18-
Button("-") {
19-
count -= 1
20-
}
21-
Text("Count: \(count)")
22-
Button("+") {
23-
count += 1
24-
}
16+
VStack(alignment: .leading, spacing: 1) {
17+
ForEach(Font.TextStyle.allCases) { style in
18+
Text("This is \(style)")
19+
.font(.system(style))
2520
}
26-
.padding()
2721
}
22+
// VStack(alignment: .leading, spacing: 1) {
23+
// ForEach(Font.Weight.allCases) { weight in
24+
// Text("This is \(weight) text")
25+
// .fontWeight(weight)
26+
// }
27+
// }
2828
}
2929
}
3030
.defaultSize(width: 400, height: 200)

Package.resolved

Lines changed: 12 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Sources/AppKitBackend/AppKitBackend.swift

Lines changed: 31 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ public final class AppKitBackend: AppBackend {
2424
public let requiresImageUpdateOnScaleFactorChange = false
2525
public let menuImplementationStyle = MenuImplementationStyle.dynamicPopover
2626
public let canRevealFiles = true
27+
public let deviceClass = DeviceClass.desktop
2728

2829
public var scrollBarWidth: Int {
2930
// We assume that all scrollers have their controlSize set to `.regular` by default.
@@ -247,13 +248,9 @@ public final class AppKitBackend: AppBackend {
247248

248249
public func computeRootEnvironment(defaultEnvironment: EnvironmentValues) -> EnvironmentValues {
249250
let isDark = UserDefaults.standard.string(forKey: "AppleInterfaceStyle") == "Dark"
250-
let font = Font.system(
251-
size: Int(NSFont.systemFont(ofSize: 0.0).pointSize.rounded(.awayFromZero))
252-
)
253251
return
254252
defaultEnvironment
255253
.with(\.colorScheme, isDark ? .dark : .light)
256-
.with(\.font, font)
257254
}
258255

259256
public func setRootEnvironmentChangeHandler(to action: @escaping () -> Void) {
@@ -981,50 +978,55 @@ public final class AppKitBackend: AppBackend {
981978
case .trailing:
982979
.right
983980
}
981+
982+
let resolvedFont = environment.resolvedFont
983+
984+
// This is definitely what these properties were intended for
985+
paragraphStyle.minimumLineHeight = CGFloat(resolvedFont.lineHeight)
986+
paragraphStyle.maximumLineHeight = CGFloat(resolvedFont.lineHeight)
987+
paragraphStyle.lineSpacing = 0
988+
984989
return [
985990
.foregroundColor: environment.suggestedForegroundColor.nsColor,
986-
.font: font(for: environment),
991+
.font: font(for: resolvedFont),
987992
.paragraphStyle: paragraphStyle,
988993
]
989994
}
990995

991-
private static func font(for environment: EnvironmentValues) -> NSFont {
992-
switch environment.font {
993-
case .system(let size, let weight, let design):
994-
switch design {
995-
case .default, .none:
996-
NSFont.systemFont(
997-
ofSize: CGFloat(size), weight: weight.map(Self.weight(for:)) ?? .regular
998-
)
996+
private static func font(for font: Font.Resolved) -> NSFont {
997+
let size = CGFloat(font.pointSize)
998+
let weight = weight(for: font.weight)
999+
switch font.identifier.kind {
1000+
case .system:
1001+
switch font.design {
1002+
case .default:
1003+
return NSFont.systemFont(ofSize: size, weight: weight)
9991004
case .monospaced:
1000-
NSFont.monospacedSystemFont(
1001-
ofSize: CGFloat(size),
1002-
weight: weight.map(Self.weight(for:)) ?? .regular
1003-
)
1005+
return NSFont.monospacedSystemFont(ofSize: size, weight: weight)
10041006
}
10051007
}
10061008
}
10071009

10081010
private static func weight(for weight: Font.Weight) -> NSFont.Weight {
10091011
switch weight {
1010-
case .black:
1011-
.black
1012-
case .bold:
1013-
.bold
1014-
case .heavy:
1015-
.heavy
1012+
case .thin:
1013+
.thin
1014+
case .ultraLight:
1015+
.ultraLight
10161016
case .light:
10171017
.light
1018-
case .medium:
1019-
.medium
10201018
case .regular:
10211019
.regular
1020+
case .medium:
1021+
.medium
10221022
case .semibold:
10231023
.semibold
1024-
case .thin:
1025-
.thin
1026-
case .ultraLight:
1027-
.ultraLight
1024+
case .bold:
1025+
.bold
1026+
case .black:
1027+
.black
1028+
case .heavy:
1029+
.heavy
10281030
}
10291031
}
10301032

Sources/Gtk/Utility/CSS/CSSProperty.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ public struct CSSProperty: Equatable {
5858
CSSProperty(key: "min-height", value: "\(height)px")
5959
}
6060

61-
public static func fontSize(_ size: Int) -> CSSProperty {
61+
public static func fontSize(_ size: Double) -> CSSProperty {
6262
CSSProperty(key: "font-size", value: "\(size)px")
6363
}
6464

Sources/Gtk3/Utility/CSS/CSSProperty.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ public struct CSSProperty: Equatable {
5858
CSSProperty(key: "min-height", value: "\(height)px")
5959
}
6060

61-
public static func fontSize(_ size: Int) -> CSSProperty {
61+
public static func fontSize(_ size: Double) -> CSSProperty {
6262
CSSProperty(key: "font-size", value: "\(size)px")
6363
}
6464

Sources/Gtk3Backend/Gtk3Backend.swift

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ public final class Gtk3Backend: AppBackend {
3636
public let requiresImageUpdateOnScaleFactorChange = true
3737
public let menuImplementationStyle = MenuImplementationStyle.dynamicPopover
3838
public let canRevealFiles = true
39+
public let deviceClass = DeviceClass.desktop
3940

4041
var gtkApp: Application
4142

@@ -1398,35 +1399,36 @@ public final class Gtk3Backend: AppBackend {
13981399
) -> [CSSProperty] {
13991400
var properties: [CSSProperty] = []
14001401
properties.append(.foregroundColor(environment.suggestedForegroundColor.gtkColor))
1401-
switch environment.font {
1402-
case .system(let size, let weight, let design):
1403-
properties.append(.fontSize(size))
1402+
let font = environment.resolvedFont
1403+
switch font.identifier.kind {
1404+
case .system:
1405+
properties.append(.fontSize(font.pointSize))
14041406
let weightNumber =
1405-
switch weight {
1406-
case .thin:
1407-
100
1407+
switch font.weight {
14081408
case .ultraLight:
1409+
100
1410+
case .thin:
14091411
200
14101412
case .light:
14111413
300
1412-
case .regular, .none:
1414+
case .regular:
14131415
400
14141416
case .medium:
14151417
500
14161418
case .semibold:
14171419
600
14181420
case .bold:
14191421
700
1420-
case .black:
1421-
900
14221422
case .heavy:
1423+
800
1424+
case .black:
14231425
900
14241426
}
14251427
properties.append(.fontWeight(weightNumber))
1426-
switch design {
1428+
switch font.design {
14271429
case .monospaced:
14281430
properties.append(.fontFamily("monospace"))
1429-
case .default, .none:
1431+
case .default:
14301432
break
14311433
}
14321434
}

Sources/GtkBackend/GtkBackend.swift

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ public final class GtkBackend: AppBackend {
3535
public let requiresImageUpdateOnScaleFactorChange = false
3636
public let menuImplementationStyle = MenuImplementationStyle.dynamicPopover
3737
public let canRevealFiles = true
38+
public let deviceClass = DeviceClass.desktop
3839

3940
var gtkApp: Application
4041

@@ -1449,35 +1450,40 @@ public final class GtkBackend: AppBackend {
14491450
) -> [CSSProperty] {
14501451
var properties: [CSSProperty] = []
14511452
properties.append(.foregroundColor(environment.suggestedForegroundColor.gtkColor))
1452-
switch environment.font {
1453-
case .system(let size, let weight, let design):
1454-
properties.append(.fontSize(size))
1453+
let font = environment.resolvedFont
1454+
switch font.identifier.kind {
1455+
case .system:
1456+
properties.append(.fontSize(font.pointSize))
1457+
// For some reason I had to tweak these a bit to make them match
1458+
// up with AppKit's font weights. I didn't have to do that for
1459+
// Gtk3Backend (which matches SwiftUI's text layout and rendering
1460+
// remarkbly well).
14551461
let weightNumber =
1456-
switch weight {
1457-
case .thin:
1458-
100
1462+
switch font.weight {
14591463
case .ultraLight:
14601464
200
1461-
case .light:
1465+
case .thin:
14621466
300
1463-
case .regular, .none:
1467+
case .light:
14641468
400
1465-
case .medium:
1469+
case .regular:
14661470
500
1467-
case .semibold:
1471+
case .medium:
14681472
600
1473+
case .semibold:
1474+
700
14691475
case .bold:
14701476
700
1471-
case .black:
1472-
900
14731477
case .heavy:
1478+
800
1479+
case .black:
14741480
900
14751481
}
14761482
properties.append(.fontWeight(weightNumber))
1477-
switch design {
1483+
switch font.design {
14781484
case .monospaced:
14791485
properties.append(.fontFamily("monospace"))
1480-
case .default, .none:
1486+
case .default:
14811487
break
14821488
}
14831489
}

Sources/SwiftCrossUI/Backend/AppBackend.swift

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,10 @@ public protocol AppBackend {
8888
/// are called.
8989
var menuImplementationStyle: MenuImplementationStyle { get }
9090

91+
/// The class of device that the backend is currently running on. Used to
92+
/// determine text sizing and other adaptive properties.
93+
var deviceClass: DeviceClass { get }
94+
9195
/// Whether the backend can reveal files in the system file manager or not.
9296
/// Mobile backends generally can't.
9397
var canRevealFiles: Bool { get }
@@ -180,6 +184,17 @@ public protocol AppBackend {
180184
/// may or may not override the previous handler.
181185
func setRootEnvironmentChangeHandler(to action: @escaping () -> Void)
182186

187+
/// Resolves the given text style to concrete font properties.
188+
///
189+
/// This method doesn't take ``EnvironmentValues`` because its result
190+
/// should be consistent when given the same text style twice. Font modifiers
191+
/// take effect later in the font resolution process.
192+
///
193+
/// A default implementation is provided. It uses the backend's reported
194+
/// device class and looks up the text style in a lookup table derived
195+
/// from Apple's typography guidelines. See ``TextStyle/resolve(for:)``.
196+
@Sendable func resolveTextStyle(_ textStyle: Font.TextStyle) -> Font.TextStyle.Resolved
197+
183198
/// Computes a window's environment based off the root environment. This may involve
184199
/// updating ``EnvironmentValues/windowScaleFactor`` etc.
185200
func computeWindowEnvironment(
@@ -642,6 +657,13 @@ public protocol AppBackend {
642657
}
643658

644659
extension AppBackend {
660+
@Sendable
661+
public func resolveTextStyle(
662+
_ textStyle: Font.TextStyle
663+
) -> Font.TextStyle.Resolved {
664+
textStyle.resolve(for: deviceClass)
665+
}
666+
645667
public func tag(widget: Widget, as tag: String) {
646668
// This is only really to assist contributors when debugging backends,
647669
// so it's safe enough to have a no-op default implementation.

Sources/SwiftCrossUI/Environment/EnvironmentValues.swift

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,35 @@ public struct EnvironmentValues {
1313
/// The current stack spacing. Inherited by ``ForEach`` and ``Group`` so
1414
/// that they can be used without affecting layout.
1515
public var layoutSpacing: Int
16+
1617
/// The current font.
1718
public var font: Font
19+
/// A font overlay storing font modifications. If these conflict with the
20+
/// font's internal overlay, these win.
21+
///
22+
/// We keep this separate overlay for modifiers because we want modifiers to
23+
/// be persisted even if the developer sets a custom font further down the
24+
/// view hierarchy.
25+
var fontOverlay: Font.Overlay
26+
27+
/// A font resolution context derived from the current environment.
28+
///
29+
/// Essentially just a subset of the environment.
30+
public var fontResolutionContext: Font.Context {
31+
Font.Context(
32+
overlay: fontOverlay,
33+
deviceClass: backend.deviceClass,
34+
resolveTextStyle: backend.resolveTextStyle(_:)
35+
)
36+
}
37+
38+
/// The current font resolved to a form suitable for rendering. Just a
39+
/// helper method for our own backends. We haven't made this public because
40+
/// it would be weird to have two pretty equivalent ways of resolving fonts.
41+
package var resolvedFont: Font.Resolved {
42+
font.resolve(in: fontResolutionContext)
43+
}
44+
1845
/// How lines should be aligned relative to each other when line wrapped.
1946
public var multilineTextAlignment: HorizontalAlignment
2047

@@ -158,7 +185,8 @@ public struct EnvironmentValues {
158185
layoutAlignment = .center
159186
layoutSpacing = 10
160187
foregroundColor = nil
161-
font = .system(size: 12)
188+
font = .body
189+
fontOverlay = Font.Overlay()
162190
multilineTextAlignment = .leading
163191
colorScheme = .light
164192
windowScaleFactor = 1

0 commit comments

Comments
 (0)