Skip to content

Commit fa6d364

Browse files
authored
Introduce dynamic text styles and various font modifiers (#186)
* Implement dynamic text styles (and various font-related modifiers) * Fix AppKitBackend tests (broke due to changed default font size) * Update Font.monospaced(_:) modifier with default parameter value * Fix AppKitBackend post-rebase and revert CounterApp * Fix UIKitBackend compilation error (post TextEditor rebase)
1 parent 7582a12 commit fa6d364

File tree

18 files changed

+790
-159
lines changed

18 files changed

+790
-159
lines changed

Examples/Sources/CounterExample/CounterApp.swift

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,18 +13,16 @@ 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+
HStack(spacing: 20) {
17+
Button("-") {
18+
count -= 1
19+
}
20+
Text("Count: \(count)")
21+
Button("+") {
22+
count += 1
2523
}
26-
.padding()
2724
}
25+
.padding()
2826
}
2927
}
3028
.defaultSize(width: 400, height: 200)

Sources/AppKitBackend/AppKitBackend.swift

Lines changed: 37 additions & 33 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.
@@ -323,13 +324,9 @@ public final class AppKitBackend: AppBackend {
323324

324325
public func computeRootEnvironment(defaultEnvironment: EnvironmentValues) -> EnvironmentValues {
325326
let isDark = UserDefaults.standard.string(forKey: "AppleInterfaceStyle") == "Dark"
326-
let font = Font.system(
327-
size: Int(NSFont.systemFont(ofSize: 0.0).pointSize.rounded(.awayFromZero))
328-
)
329327
return
330328
defaultEnvironment
331329
.with(\.colorScheme, isDark ? .dark : .light)
332-
.with(\.font, font)
333330
}
334331

335332
public func setRootEnvironmentChangeHandler(to action: @escaping () -> Void) {
@@ -752,8 +749,9 @@ public final class AppKitBackend: AppBackend {
752749
textField.isEnabled = environment.isEnabled
753750
textField.placeholderString = placeholder
754751
textField.appearance = environment.colorScheme.nsAppearance
755-
if textField.font != Self.font(for: environment) {
756-
textField.font = Self.font(for: environment)
752+
let resolvedFont = environment.resolvedFont
753+
if textField.font != Self.font(for: resolvedFont) {
754+
textField.font = Self.font(for: resolvedFont)
757755
}
758756
textField.onEdit = { textField in
759757
onChange(textField.stringValue)
@@ -806,8 +804,9 @@ public final class AppKitBackend: AppBackend {
806804
textEditor.onEdit = { textView in
807805
onChange(self.getContent(ofTextEditor: textView))
808806
}
809-
if textEditor.font != Self.font(for: environment) {
810-
textEditor.font = Self.font(for: environment)
807+
let resolvedFont = environment.resolvedFont
808+
if textEditor.font != Self.font(for: resolvedFont) {
809+
textEditor.font = Self.font(for: resolvedFont)
811810
}
812811
textEditor.appearance = environment.colorScheme.nsAppearance
813812
textEditor.isEditable = environment.isEnabled
@@ -1112,50 +1111,55 @@ public final class AppKitBackend: AppBackend {
11121111
case .trailing:
11131112
.right
11141113
}
1114+
1115+
let resolvedFont = environment.resolvedFont
1116+
1117+
// This is definitely what these properties were intended for
1118+
paragraphStyle.minimumLineHeight = CGFloat(resolvedFont.lineHeight)
1119+
paragraphStyle.maximumLineHeight = CGFloat(resolvedFont.lineHeight)
1120+
paragraphStyle.lineSpacing = 0
1121+
11151122
return [
11161123
.foregroundColor: environment.suggestedForegroundColor.nsColor,
1117-
.font: font(for: environment),
1124+
.font: font(for: resolvedFont),
11181125
.paragraphStyle: paragraphStyle,
11191126
]
11201127
}
11211128

1122-
private static func font(for environment: EnvironmentValues) -> NSFont {
1123-
switch environment.font {
1124-
case .system(let size, let weight, let design):
1125-
switch design {
1126-
case .default, .none:
1127-
NSFont.systemFont(
1128-
ofSize: CGFloat(size), weight: weight.map(Self.weight(for:)) ?? .regular
1129-
)
1129+
private static func font(for font: Font.Resolved) -> NSFont {
1130+
let size = CGFloat(font.pointSize)
1131+
let weight = weight(for: font.weight)
1132+
switch font.identifier.kind {
1133+
case .system:
1134+
switch font.design {
1135+
case .default:
1136+
return NSFont.systemFont(ofSize: size, weight: weight)
11301137
case .monospaced:
1131-
NSFont.monospacedSystemFont(
1132-
ofSize: CGFloat(size),
1133-
weight: weight.map(Self.weight(for:)) ?? .regular
1134-
)
1138+
return NSFont.monospacedSystemFont(ofSize: size, weight: weight)
11351139
}
11361140
}
11371141
}
11381142

11391143
private static func weight(for weight: Font.Weight) -> NSFont.Weight {
11401144
switch weight {
1141-
case .black:
1142-
.black
1143-
case .bold:
1144-
.bold
1145-
case .heavy:
1146-
.heavy
1145+
case .thin:
1146+
.thin
1147+
case .ultraLight:
1148+
.ultraLight
11471149
case .light:
11481150
.light
1149-
case .medium:
1150-
.medium
11511151
case .regular:
11521152
.regular
1153+
case .medium:
1154+
.medium
11531155
case .semibold:
11541156
.semibold
1155-
case .thin:
1156-
.thin
1157-
case .ultraLight:
1158-
.ultraLight
1157+
case .bold:
1158+
.bold
1159+
case .black:
1160+
.black
1161+
case .heavy:
1162+
.heavy
11591163
}
11601164
}
11611165

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

@@ -1433,35 +1434,36 @@ public final class Gtk3Backend: AppBackend {
14331434
) -> [CSSProperty] {
14341435
var properties: [CSSProperty] = []
14351436
properties.append(.foregroundColor(environment.suggestedForegroundColor.gtkColor))
1436-
switch environment.font {
1437-
case .system(let size, let weight, let design):
1438-
properties.append(.fontSize(size))
1437+
let font = environment.resolvedFont
1438+
switch font.identifier.kind {
1439+
case .system:
1440+
properties.append(.fontSize(font.pointSize))
14391441
let weightNumber =
1440-
switch weight {
1441-
case .thin:
1442-
100
1442+
switch font.weight {
14431443
case .ultraLight:
1444+
100
1445+
case .thin:
14441446
200
14451447
case .light:
14461448
300
1447-
case .regular, .none:
1449+
case .regular:
14481450
400
14491451
case .medium:
14501452
500
14511453
case .semibold:
14521454
600
14531455
case .bold:
14541456
700
1455-
case .black:
1456-
900
14571457
case .heavy:
1458+
800
1459+
case .black:
14581460
900
14591461
}
14601462
properties.append(.fontWeight(weightNumber))
1461-
switch design {
1463+
switch font.design {
14621464
case .monospaced:
14631465
properties.append(.fontFamily("monospace"))
1464-
case .default, .none:
1466+
case .default:
14651467
break
14661468
}
14671469
}

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

@@ -1480,35 +1481,40 @@ public final class GtkBackend: AppBackend {
14801481
) -> [CSSProperty] {
14811482
var properties: [CSSProperty] = []
14821483
properties.append(.foregroundColor(environment.suggestedForegroundColor.gtkColor))
1483-
switch environment.font {
1484-
case .system(let size, let weight, let design):
1485-
properties.append(.fontSize(size))
1484+
let font = environment.resolvedFont
1485+
switch font.identifier.kind {
1486+
case .system:
1487+
properties.append(.fontSize(font.pointSize))
1488+
// For some reason I had to tweak these a bit to make them match
1489+
// up with AppKit's font weights. I didn't have to do that for
1490+
// Gtk3Backend (which matches SwiftUI's text layout and rendering
1491+
// remarkbly well).
14861492
let weightNumber =
1487-
switch weight {
1488-
case .thin:
1489-
100
1493+
switch font.weight {
14901494
case .ultraLight:
14911495
200
1492-
case .light:
1496+
case .thin:
14931497
300
1494-
case .regular, .none:
1498+
case .light:
14951499
400
1496-
case .medium:
1500+
case .regular:
14971501
500
1498-
case .semibold:
1502+
case .medium:
14991503
600
1504+
case .semibold:
1505+
700
15001506
case .bold:
15011507
700
1502-
case .black:
1503-
900
15041508
case .heavy:
1509+
800
1510+
case .black:
15051511
900
15061512
}
15071513
properties.append(.fontWeight(weightNumber))
1508-
switch design {
1514+
switch font.design {
15091515
case .monospaced:
15101516
properties.append(.fontFamily("monospace"))
1511-
case .default, .none:
1517+
case .default:
15121518
break
15131519
}
15141520
}

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(
@@ -668,6 +683,13 @@ public protocol AppBackend {
668683
}
669684

670685
extension AppBackend {
686+
@Sendable
687+
public func resolveTextStyle(
688+
_ textStyle: Font.TextStyle
689+
) -> Font.TextStyle.Resolved {
690+
textStyle.resolve(for: deviceClass)
691+
}
692+
671693
public func tag(widget: Widget, as tag: String) {
672694
// This is only really to assist contributors when debugging backends,
673695
// 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)