Skip to content
Open
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
1 change: 1 addition & 0 deletions apps/mobile/app.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,7 @@ const config: ExpoConfig = {
`webcredentials:${variant.relyingParty}`,
],
entitlements: {
"com.apple.developer.user-fonts": ["app-usage"],
"keychain-access-groups": [`$(AppIdentifierPrefix)${variant.iosBundleIdentifier}`],
},
infoPlist: {
Expand Down
2 changes: 2 additions & 0 deletions apps/mobile/global.css
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,12 @@
/* ─── Custom utilities ──────────────────────────────────────────────── */
@utility font-t3-medium {
font-family: var(--font-medium);
font-weight: 500;
}

@utility font-t3-bold {
font-family: var(--font-bold);
font-weight: 700;
}

@utility font-t3-extrabold {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import android.view.inputmethod.InputConnection
import android.view.inputmethod.InputConnectionWrapper
import android.view.inputmethod.InputMethodManager
import android.widget.EditText
import com.facebook.react.common.assets.ReactFontManager
import expo.modules.kotlin.AppContext
import expo.modules.kotlin.viewevent.EventDispatcher
import expo.modules.kotlin.views.ExpoView
Expand Down Expand Up @@ -270,11 +271,12 @@ class T3ComposerEditorView(context: Context, appContext: AppContext) : ExpoView(
}

fun setFontFamily(fontFamily: String) {
editor.typeface = if (fontFamily.contains("Mono", ignoreCase = true)) {
Typeface.MONOSPACE
} else {
Typeface.DEFAULT
}
editor.typeface = ReactFontManager.getInstance().getTypeface(
fontFamily,
Typeface.NORMAL,
context.assets,
)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
applyLineHeight()
editor.applyPlaceholder()
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1060,9 +1060,21 @@ public final class T3ComposerEditorView: ExpoView, UITextViewDelegate, UITextDro
return nil
}

private func resolvedComposerFont() -> UIFont {
if let font = UIFont(name: fontFamily, size: fontSize) {
return font
}
// Expo registers aliases through the family-name lookup, not UIFont(name:).
for name in UIFont.fontNames(forFamilyName: fontFamily) {
if let font = UIFont(name: name, size: fontSize) {
return font
}
}
return UIFont.systemFont(ofSize: fontSize)
}

private func baseAttributes() -> [NSAttributedString.Key: Any] {
let font = UIFont(name: fontFamily, size: fontSize)
?? UIFont.systemFont(ofSize: fontSize)
let font = resolvedComposerFont()
let paragraph = NSMutableParagraphStyle()
paragraph.minimumLineHeight = lineHeight
paragraph.maximumLineHeight = lineHeight
Expand All @@ -1074,8 +1086,7 @@ public final class T3ComposerEditorView: ExpoView, UITextViewDelegate, UITextDro
}

private func applyTypography() {
let font = UIFont(name: fontFamily, size: fontSize)
?? UIFont.systemFont(ofSize: fontSize)
let font = resolvedComposerFont()
textView.font = font
restoreBaseTypingAttributes()
placeholderLabel.font = font
Expand Down
100 changes: 100 additions & 0 deletions apps/mobile/modules/t3-native-controls/ios/T3InstalledFonts.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import CoreText
import UIKit

@MainActor
enum T3InstalledFonts {
static func describe(_ font: UIFont, name: String? = nil) -> [String: Any] {
func variant(_ weight: UIFont.Weight) -> String {
let descriptor = UIFontDescriptor(fontAttributes: [
.family: font.familyName,
.traits: [UIFontDescriptor.TraitKey.weight: weight],
])
let candidate = UIFont(descriptor: descriptor, size: font.pointSize)
return candidate.familyName == font.familyName ? candidate.fontName : font.fontName
}
// CoreText reports medium as 0.20 while UIKit requests 0.23.
var emphasisFaces: [String: String] = [:]
for (key, weight) in [("500", UIFont.Weight.medium), ("600", .semibold), ("700", .bold)] {
let name = variant(weight)
guard let candidate = UIFont(name: name, size: font.pointSize),
let traits = candidate.fontDescriptor.object(forKey: .traits) as? [UIFontDescriptor.TraitKey: Any],
let actual = traits[.weight] as? NSNumber,
abs(actual.doubleValue - Double(weight.rawValue)) < 0.04 else { continue }
emphasisFaces[key] = name
}
return [
"name": name ?? font.fontName,
"label": font.familyName,
"regular": font.fontName,
"medium": variant(.medium),
"bold": variant(.bold),
"emphasisFaces": emphasisFaces,
]
}

static func resolve(_ names: [String], completion: @escaping ([[String: Any]]) -> Void) {
guard !names.isEmpty else { completion([]); return }
// Provider fonts must be requested again for each process. Persist names,
// not file URLs: iOS owns the files and the picker grants family access.
let descriptors = names.map { CTFontDescriptorCreateWithAttributes([kCTFontNameAttribute: $0] as CFDictionary) }
CTFontManagerRequestFonts(descriptors as CFArray) { _ in
DispatchQueue.main.async {
completion(names.compactMap { name in
guard let font = UIFont(name: name, size: 16) else { return nil }
return describe(font, name: name)
})
}
}
}
}

@MainActor
final class T3FontPickerPresentation: NSObject, UIFontPickerViewControllerDelegate, UIAdaptivePresentationControllerDelegate {
private let completion: ([String: Any]?) -> Void
private var picker: UIFontPickerViewController?
private var finished = false

init(completion: @escaping ([String: Any]?) -> Void) {
self.completion = completion
}

func present(from presenter: UIViewController, selectedName: String?) {
let configuration = UIFontPickerViewController.Configuration()
configuration.includeFaces = false
let picker = UIFontPickerViewController(configuration: configuration)
if let selectedName, let font = UIFont(name: selectedName, size: 16) {
picker.selectedFontDescriptor = font.fontDescriptor
}
picker.delegate = self
self.picker = picker
presenter.present(picker, animated: true)
picker.presentationController?.delegate = self
}

func fontPickerViewControllerDidPickFont(_ viewController: UIFontPickerViewController) {
guard let descriptor = viewController.selectedFontDescriptor else { dismiss(); return }
let result = T3InstalledFonts.describe(UIFont(descriptor: descriptor, size: 16))
viewController.dismiss(animated: true) { self.finish(result) }
}

func fontPickerViewControllerDidCancel(_ viewController: UIFontPickerViewController) {
dismiss()
}

func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
finish(nil)
}

func dismiss() {
guard let picker else { finish(nil); return }
picker.dismiss(animated: true) { self.finish(nil) }
}

private func finish(_ result: [String: Any]?) {
guard !finished else { return }
finished = true
picker?.delegate = nil
picker = nil
completion(result)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ public final class T3NativeControlsModule: Module {
private let presentationSources = T3PresentationSources()
private var videoPresentation: T3NativeVideoPresentation?
private var filePresentation: T3NativeFilePresentation?
private var fontPresentation: T3FontPickerPresentation?

public func definition() -> ModuleDefinition {
Events("videoPlaybackChange")
Expand Down Expand Up @@ -51,6 +52,29 @@ public final class T3NativeControlsModule: Module {
ViewName("ScrollEdgeBleed")
}

AsyncFunction("pickInstalledFont") { (selectedName: String?, promise: Promise) in
MainActor.assumeIsolated {
guard self.fontPresentation == nil,
let presenter = self.appContext?.utilities?.currentViewController()
else {
promise.reject("ERR_FONT_PICKER", "Cannot present the font picker right now.")
return
}
let presentation = T3FontPickerPresentation { [weak self] result in
self?.fontPresentation = nil
promise.resolve(result)
}
self.fontPresentation = presentation
presentation.present(from: presenter, selectedName: selectedName)
}
}.runOnQueue(.main)

AsyncFunction("resolveInstalledFonts") { (names: [String], promise: Promise) in
MainActor.assumeIsolated {
T3InstalledFonts.resolve(names) { promise.resolve($0) }
}
}.runOnQueue(.main)

AsyncFunction("presentVideo") { (url: URL, title: String, sourceIdentifier: String, identifier: String, promise: Promise) in
try self.presentVideo(
url: url,
Expand Down Expand Up @@ -84,9 +108,11 @@ public final class T3NativeControlsModule: Module {
OnDestroy {
let presentation = self.videoPresentation
let file = self.filePresentation
let font = self.fontPresentation
DispatchQueue.main.async {
presentation?.dismiss()
file?.dismiss()
font?.dismiss()
}
}

Expand Down
12 changes: 6 additions & 6 deletions apps/mobile/src/features/files/FileMarkdownPreview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import { StackActions, useNavigation } from "@react-navigation/native";
import { RefreshControl, ScrollView, View } from "react-native";

import { tryOpenExternalUrl } from "../../lib/openExternalUrl";
import { FONT_FAMILIES } from "../../lib/fontFamilies";
import { resolveNativeMarkdownTypography } from "../../lib/appearancePreferences";
import { useUniwindTheme } from "../../lib/useUniwindTheme";
import {
Expand Down Expand Up @@ -39,8 +38,8 @@ interface MarkdownPreviewStyles {
}

export function useMarkdownPreviewStyles(): MarkdownPreviewStyles {
const { appearance } = useAppearancePreferences();
const nativeMarkdownTypography = resolveNativeMarkdownTypography(appearance.baseFontSize);
const { fonts, replyFontSize, replyEmphasisFontFamily } = useAppearancePreferences();
const nativeMarkdownTypography = resolveNativeMarkdownTypography(replyFontSize);
const theme = useUniwindTheme();
const body = theme["--color-md-body"];
const strong = theme["--color-md-strong"];
Expand All @@ -49,8 +48,8 @@ export function useMarkdownPreviewStyles(): MarkdownPreviewStyles {
const codeBackground = theme["--color-md-code-bg"];
const codeText = theme["--color-md-code-text"];
const horizontalRule = theme["--color-md-hr"];
const regularFontFamily = FONT_FAMILIES.regular;
const boldFontFamily = FONT_FAMILIES.bold;
const regularFontFamily = fonts.reply.regular;
const boldFontFamily = fonts.reply.bold;

return {
nativeTextStyle: {
Expand All @@ -68,9 +67,10 @@ export function useMarkdownPreviewStyles(): MarkdownPreviewStyles {
fontSize: nativeMarkdownTypography.fontSize,
lineHeight: nativeMarkdownTypography.lineHeight,
headingFontSizes: nativeMarkdownTypography.headingFontSizes,
codeFontFamily: fonts.code.regular,
fontFamily: regularFontFamily,
headingFontFamily: boldFontFamily,
boldFontFamily,
boldFontFamily: replyEmphasisFontFamily,
},
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { useSafeAreaInsets } from "react-native-safe-area-context";
import { SettingsScreen } from "./components/SettingsScreen";
import { CodeAppearanceSection } from "./appearance/sections/CodeAppearanceSection";
import { TerminalAppearanceSection } from "./appearance/sections/TerminalAppearanceSection";
import { FontAppearanceSection } from "./appearance/sections/FontAppearanceSection";
import { TextAppearanceSection } from "./appearance/sections/TextAppearanceSection";
import { ThemeAppearanceSection } from "./appearance/sections/ThemeAppearanceSection";

Expand All @@ -26,6 +27,7 @@ export function SettingsAppearanceRouteScreen() {
<ThemeAppearanceSection />
{Platform.OS === "android" ? <AndroidNavigationAppearanceSection /> : null}
<TextAppearanceSection />
<FontAppearanceSection />
<TerminalAppearanceSection />
<CodeAppearanceSection />
</ScrollView>
Expand Down
Loading
Loading