Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
name: CI

on:
push:
branches:
- main
pull_request:
branches:
- '*'
workflow_dispatch:

concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true

jobs:
test:
name: Test All Platforms
runs-on: macos-15
steps:
- uses: actions/checkout@v4
- name: Select Xcode 26.0
run: sudo xcode-select -s /Applications/Xcode_26.0.app
- name: Skip macro validation
run: defaults write com.apple.dt.Xcode IDESkipMacroFingerprintValidation -bool YES
- name: Run tests
run: make test

build-demo:
name: Build TextualDemo
runs-on: macos-15
steps:
- uses: actions/checkout@v4
- name: Select Xcode 26.0
run: sudo xcode-select -s /Applications/Xcode_26.0.app
- name: Skip macro validation
run: defaults write com.apple.dt.Xcode IDESkipMacroFingerprintValidation -bool YES
- name: Build demo
run: make build-demo

backward-compatibility:
name: Backward Compatibility (Xcode 16.4)
runs-on: macos-15
steps:
- uses: actions/checkout@v4
- name: Select Xcode 16.4
run: sudo xcode-select -s /Applications/Xcode_16.4.app
- name: Skip macro validation
run: defaults write com.apple.dt.Xcode IDESkipMacroFingerprintValidation -bool YES
- name: Test macOS
run: make test-macos
27 changes: 27 additions & 0 deletions .github/workflows/format.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
name: Format

on:
push:
branches:
- main

concurrency:
group: format-${{ github.ref }}
cancel-in-progress: true

jobs:
swift_format:
name: swift-format
runs-on: macos-15
steps:
- uses: actions/checkout@v4
- name: Select Xcode 26.0
run: sudo xcode-select -s /Applications/Xcode_26.0.app
- name: Format
run: make format
- uses: stefanzweifel/git-auto-commit-action@v4
with:
commit_message: Run swift-format
branch: main
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
16 changes: 11 additions & 5 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
IOS_VERSION = 18.0
TVOS_VERSION = 18.0
WATCHOS_VERSION = 11.0
VISIONOS_VERSION = 2.0
IOS_VERSION = 26.0
TVOS_VERSION = 26.0
WATCHOS_VERSION = 26.0
VISIONOS_VERSION = 26.0

PLATFORM_IOS = iOS Simulator,id=$(call udid_for,iOS $(IOS_VERSION),iPhone \d\+ Pro [^M])
PLATFORM_MACOS = macOS
Expand Down Expand Up @@ -45,7 +45,13 @@ bundle-prism:
@echo "Bundling Prism.js..."
./Scripts/bundle-prism.sh

.PHONY: format test bundle-prism
build-demo:
@echo "Building TextualDemo for iOS..."
xcodebuild build -workspace Textual.xcworkspace -scheme TextualDemo -destination platform="$(PLATFORM_IOS)" CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO
@echo "Building TextualDemo for macOS..."
xcodebuild build -workspace Textual.xcworkspace -scheme TextualDemo -destination platform="$(PLATFORM_MACOS)" CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO

.PHONY: format test bundle-prism build-demo

define udid_for
$(shell xcrun simctl list devices available '$(1)' | grep '$(2)' | sort -r | head -1 | awk -F '[()]' '{ print $$(NF-3) }')
Expand Down
30 changes: 16 additions & 14 deletions Sources/Textual/Internal/Font/AnyFontProvider+Font.swift
Original file line number Diff line number Diff line change
Expand Up @@ -221,21 +221,23 @@ extension AnyFontProvider {
base: baseProvider, modifier: LeadingFontModifier(leading: leading)
)
)
case "ModifierProvider<ScalePointSizeModifier>":
guard
#available(iOS 26.0, macOS 26.0, tvOS 26.0, watchOS 26.0, visionOS 26.0, *),
let modifierMirror = mirror.descendant("modifier").map(Mirror.init(reflecting:)),
let scaleFactor = modifierMirror.descendant("scaleFactor") as? CGFloat,
let base = mirror.descendant("base", "provider", "base"),
let baseProvider = AnyFontProvider(reflecting: base)
else {
return nil
}
self.init(
FontModifierProvider(
base: baseProvider, modifier: ScaleFontModifier(scaleFactor: scaleFactor)
#if compiler(>=6.2)
case "ModifierProvider<ScalePointSizeModifier>":
guard
#available(iOS 26.0, macOS 26.0, tvOS 26.0, watchOS 26.0, visionOS 26.0, *),
let modifierMirror = mirror.descendant("modifier").map(Mirror.init(reflecting:)),
let scaleFactor = modifierMirror.descendant("scaleFactor") as? CGFloat,
let base = mirror.descendant("base", "provider", "base"),
let baseProvider = AnyFontProvider(reflecting: base)
else {
return nil
}
self.init(
FontModifierProvider(
base: baseProvider, modifier: ScaleFontModifier(scaleFactor: scaleFactor)
)
)
)
#endif
default:
return nil
}
Expand Down
28 changes: 15 additions & 13 deletions Sources/Textual/Internal/Font/FontModifier.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,20 +35,22 @@ struct LeadingFontModifier: FontModifier {
}
}

@available(iOS 26.0, macOS 26.0, tvOS 26.0, watchOS 26.0, visionOS 26.0, *)
struct ScaleFontModifier: FontModifier {
var scaleFactor: CGFloat

func modify(_ font: inout Font) {
font = font.scaled(by: scaleFactor)
}

// NB: Modify both font and size so that measurements match rendering.
// FontProvider.size(in:) must return the actual rendered size.
func modify(_ size: inout CGFloat) {
size *= scaleFactor
#if compiler(>=6.2)
@available(iOS 26.0, macOS 26.0, tvOS 26.0, watchOS 26.0, visionOS 26.0, *)
struct ScaleFontModifier: FontModifier {
var scaleFactor: CGFloat

func modify(_ font: inout Font) {
font = font.scaled(by: scaleFactor)
}

// NB: Modify both font and size so that measurements match rendering.
// FontProvider.size(in:) must return the actual rendered size.
func modify(_ size: inout CGFloat) {
size *= scaleFactor
}
}
}
#endif

// MARK: - Static font modifiers

Expand Down
12 changes: 8 additions & 4 deletions Sources/Textual/Internal/Font/FontScaleModifier.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,15 @@ struct FontScaleModifier: ViewModifier {
}

func body(content: Content) -> some View {
if #available(iOS 26.0, macOS 26.0, tvOS 26.0, watchOS 26.0, visionOS 26.0, *) {
content.font(font.scaled(by: scale))
} else {
#if compiler(>=6.2)
if #available(iOS 26.0, macOS 26.0, tvOS 26.0, watchOS 26.0, visionOS 26.0, *) {
content.font(font.scaled(by: scale))
} else {
content.font(modifiedFont())
}
#else
content.font(modifiedFont())
}
#endif
}

private func modifiedFont() -> Font {
Expand Down
19 changes: 13 additions & 6 deletions Sources/Textual/TextProperty/Font/FontScaleProperty.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,19 @@ public struct FontScaleProperty: TextProperty {

public func apply(in attributes: inout AttributeContainer, environment: TextEnvironmentValues) {
let font = attributes.font ?? environment.font ?? .body
if #available(iOS 26.0, macOS 26.0, tvOS 26.0, watchOS 26.0, visionOS 26.0, *) {
attributes.font = font.scaled(by: scale)
} else if var provider = font.provider() {
provider.scale = scale
attributes.font = provider.resolve(in: environment)
}
#if compiler(>=6.2)
if #available(iOS 26.0, macOS 26.0, tvOS 26.0, watchOS 26.0, visionOS 26.0, *) {
attributes.font = font.scaled(by: scale)
} else if var provider = font.provider() {
provider.scale = scale
attributes.font = provider.resolve(in: environment)
}
#else
if var provider = font.provider() {
provider.scale = scale
attributes.font = provider.resolve(in: environment)
}
#endif
}
}

Expand Down
76 changes: 39 additions & 37 deletions Tests/TextualTests/Internal/Font/FontProviderTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -112,50 +112,52 @@ struct FontProviderTests {
#expect(resolvedFont == .system(size: 17 * scale).bold().monospaced())
}

@available(iOS 26.0, macOS 26.0, tvOS 26.0, watchOS 26.0, visionOS 26.0, *)
@Test func textStyleScaledByResolve() {
// given
let font = Font.body
.scaled(by: 2)
.bold()
.monospaced()
.scaled(by: 3)
let fontProvider = AnyFontProvider(font: font)
let environment = TextEnvironmentValues(dynamicTypeSize: .xxxLarge)
#if compiler(>=6.2)
@available(iOS 26.0, macOS 26.0, tvOS 26.0, watchOS 26.0, visionOS 26.0, *)
@Test func textStyleScaledByResolve() {
// given
let font = Font.body
.scaled(by: 2)
.bold()
.monospaced()
.scaled(by: 3)
let fontProvider = AnyFontProvider(font: font)
let environment = TextEnvironmentValues(dynamicTypeSize: .xxxLarge)

let fontDescriptor = FontDescriptor.preferredFontDescriptor(
withTextStyle: .body,
in: environment
)
let fontDescriptor = FontDescriptor.preferredFontDescriptor(
withTextStyle: .body,
in: environment
)

// when
let size = fontProvider?.size(in: environment)
let resolvedFont = fontProvider?.resolve(in: environment)
// when
let size = fontProvider?.size(in: environment)
let resolvedFont = fontProvider?.resolve(in: environment)

// then
#expect(size == fontDescriptor.pointSize * 2 * 3)
#expect(resolvedFont == font)
}
// then
#expect(size == fontDescriptor.pointSize * 2 * 3)
#expect(resolvedFont == font)
}

@available(iOS 26.0, macOS 26.0, tvOS 26.0, watchOS 26.0, visionOS 26.0, *)
@Test func systemFontScaledByResolve() {
// given
let scale = CGFloat(0.8)
let font = Font.system(size: 17)
.bold()
.scaled(by: scale)
.monospaced()
@available(iOS 26.0, macOS 26.0, tvOS 26.0, watchOS 26.0, visionOS 26.0, *)
@Test func systemFontScaledByResolve() {
// given
let scale = CGFloat(0.8)
let font = Font.system(size: 17)
.bold()
.scaled(by: scale)
.monospaced()

let fontProvider = AnyFontProvider(font: font)
let fontProvider = AnyFontProvider(font: font)

// when
let size = fontProvider?.size(in: .init())
let resolvedFont = fontProvider?.resolve(in: .init())
// when
let size = fontProvider?.size(in: .init())
let resolvedFont = fontProvider?.resolve(in: .init())

// then
#expect(size == 17 * scale)
#expect(resolvedFont == font)
}
// then
#expect(size == 17 * scale)
#expect(resolvedFont == font)
}
#endif

#if canImport(UIKit)
@Test func customFontRelativeToTextStyleScale() {
Expand Down
Loading