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
21 changes: 16 additions & 5 deletions resources/android/TextRenderer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ import androidx.compose.runtime.Composable
import android.content.Context
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.PlatformTextStyle
import androidx.compose.ui.text.SpanStyle
Expand Down Expand Up @@ -58,6 +60,8 @@ object TextRenderer {
?: nuiThemeDefaultFontFamily(LocalContext.current)
val textDecoration = resolveDecoration(p.getInt("underline"), p.getInt("line_through"))
val letterSpacingEm = p.getFloat("letter_spacing", 0f)
val outlineWidth = p.getFloat("text_outline", 0f)
val density = LocalDensity.current.density
val lineHeight = nuiLineHeightUnit(p.getFloat("line_height_px", 0f), p.getFloat("line_height", 0f), fontSize)
val maxLines = p.getInt("max_lines")
val textAlign = resolveTextAlign(p.getInt("text_align"))
Expand Down Expand Up @@ -87,6 +91,8 @@ object TextRenderer {
style = TextStyle(
fontFamily = fontFamily,
lineHeight = lineHeight,
// `text-outline`: the glyphs' outline alone, in the text colour.
drawStyle = if (outlineWidth > 0f) Stroke(width = outlineWidth * density) else null,
platformStyle = PlatformTextStyle(includeFontPadding = false),
lineHeightStyle = LineHeightStyle(
alignment = LineHeightStyle.Alignment.Center,
Expand All @@ -106,8 +112,9 @@ object TextRenderer {
val lineHeight = nuiLineHeightUnit(p.getFloat("line_height_px", 0f), p.getFloat("line_height", 0f), p.getFloat("font_size", 16f))

val ctx = LocalContext.current
val density = LocalDensity.current.density
val annotated = buildAnnotatedString {
appendTextRuns(node, RunCtx.Root, isDark, ctx)
appendTextRuns(node, RunCtx.Root, isDark, ctx, density)
}

Text(
Expand Down Expand Up @@ -147,21 +154,22 @@ private data class RunCtx(
val italic: Boolean,
val letterSpacingEm: Float,
val textTransform: Int,
val outlineWidth: Float,
val pressCallbackId: Int,
val pressNodeId: Int,
) {
companion object {
// Root defaults — mirror the leaf path (16sp, normal, black, no dark
// override, no custom font, no decoration/letter-spacing/transform).
val Root = RunCtx(16f, 0, 0, "", 0xFF000000.toInt(), 0, false, 0f, 0, 0, 0)
// override, no custom font, no decoration/letter-spacing/transform/outline).
val Root = RunCtx(16f, 0, 0, "", 0xFF000000.toInt(), 0, false, 0f, 0, 0f, 0, 0)
}
}

/**
* Walk a node, emitting one styled span for its own text (leaf, or the leading
* text before nested runs) then recursing into text children.
*/
private fun AnnotatedString.Builder.appendTextRuns(node: NativeUINode, inherited: RunCtx, isDark: Boolean, context: Context) {
private fun AnnotatedString.Builder.appendTextRuns(node: NativeUINode, inherited: RunCtx, isDark: Boolean, context: Context, density: Float) {
val p = node.props

// Resolve this level's effective context: own props over inherited.
Expand All @@ -175,6 +183,7 @@ private fun AnnotatedString.Builder.appendTextRuns(node: NativeUINode, inherited
italic = p.getInt("font_style", if (inherited.italic) 1 else 0) == 1,
letterSpacingEm = p.getFloat("letter_spacing", inherited.letterSpacingEm),
textTransform = p.getInt("text_transform", inherited.textTransform),
outlineWidth = p.getFloat("text_outline", inherited.outlineWidth),
pressCallbackId = if (node.onPress != 0) node.onPress else inherited.pressCallbackId,
pressNodeId = if (node.onPress != 0) node.id else inherited.pressNodeId,
)
Expand All @@ -199,6 +208,8 @@ private fun AnnotatedString.Builder.appendTextRuns(node: NativeUINode, inherited
letterSpacing = if (ctx.letterSpacingEm != 0f) ctx.letterSpacingEm.em else TextUnit.Unspecified,
textDecoration = resolveDecoration(p.getInt("underline"), p.getInt("line_through")),
background = if (effectiveBg != 0) argbToComposeColor(effectiveBg) else Color.Unspecified,
// `text-outline`: the glyphs' outline alone, in the run's colour.
drawStyle = if (ctx.outlineWidth > 0f) Stroke(width = ctx.outlineWidth * density) else null,
)
if (ctx.pressCallbackId != 0) {
// One Clickable per emitted span keeps annotation ranges flat
Expand Down Expand Up @@ -226,7 +237,7 @@ private fun AnnotatedString.Builder.appendTextRuns(node: NativeUINode, inherited
}

node.children.filter { it.type == "text" }.forEach { child ->
appendTextRuns(child, ctx, isDark, context)
appendTextRuns(child, ctx, isDark, context, density)
}
}

Expand Down
19 changes: 19 additions & 0 deletions resources/ios/NativeUIFontResolver.swift
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,25 @@ enum NativeUIFontResolver {
return Font.custom(name, fixedSize: size)
}

/// The Core Text font for a bundled token at `size`, or nil to fall
/// back, for drawing glyph outlines (`text-outline`). With `italic` it's
/// the family's italic face, or for a single-style font the same
/// synthesized oblique `font` gives.
static func ctFont(_ token: String, size: CGFloat, italic: Bool = false) -> CTFont? {
guard let name = postScriptName(for: token) else { return nil }

let upright = CTFontCreateWithName(name as CFString, size, nil)
guard italic else { return upright }

if supportsItalicTrait(name),
let face = CTFontCreateCopyWithSymbolicTraits(upright, size, nil, .italicTrait, .italicTrait) {
return face
}

var matrix = CGAffineTransform(a: 1, b: 0, c: obliqueSkew, d: 1, tx: 0, ty: 0)
return CTFontCreateWithName(name as CFString, size, &matrix)
}

/// Whether an italic request for this token needs a synthesized oblique
/// (custom font resolves, but its family has no real italic face).
static func needsSyntheticOblique(_ token: String) -> Bool {
Expand Down
Loading
Loading