Map Android Kotlin typography tokens

This commit is contained in:
Meta Vens 2026-07-09 16:18:30 +01:00
commit ab8560e1fb
4 changed files with 108 additions and 21 deletions

View file

@ -181,15 +181,15 @@ class MainActivity : Activity(), SensorEventListener {
setPadding(dp(10), dp(10), dp(10), dp(10))
background = panelBackground(tokens, 6)
val labelText = props.optString("label", "")
if (labelText.isNotBlank()) addView(label(labelText.uppercase(Locale.US), 11f, tokenColor(tokens, "colorAccent", 0xffff8a2a.toInt()), true))
addView(label(props.optString("source", ""), 17f, tokenColor(tokens, "colorText", 0xfff6f1e2.toInt()), false))
if (labelText.isNotBlank()) addView(tokenLabel(labelText.uppercase(Locale.US), tokens, "caption", 11f, "colorAccent", 0xffff8a2a.toInt(), true))
addView(tokenLabel(props.optString("source", ""), tokens, "mono", 17f, "colorText", 0xfff6f1e2.toInt(), false))
}
"panel.dialogue" -> LinearLayout(this).apply {
orientation = LinearLayout.VERTICAL
setPadding(dp(18), dp(18), dp(18), dp(18))
background = panelBackground(tokens, 8)
addView(label(props.optString("speaker", ""), 15f, tokenColor(tokens, "colorAccent", 0xffff8a2a.toInt()), true))
addView(label(props.optString("text", ""), 20f, tokenColor(tokens, "colorText", 0xfff6f1e2.toInt()), false))
addView(tokenLabel(props.optString("speaker", ""), tokens, "title", 15f, "colorAccent", 0xffff8a2a.toInt(), true))
addView(tokenLabel(props.optString("text", ""), tokens, "body", 20f, "colorText", 0xfff6f1e2.toInt(), false))
}
"rail.actions" -> LinearLayout(this).apply {
orientation = LinearLayout.HORIZONTAL
@ -203,7 +203,7 @@ class MainActivity : Activity(), SensorEventListener {
}
}
"surface.slot" -> embeddedSurfaceSlot(node, props, tokens)
"control.button" -> label(props.optString("label", ""), 14f, tokenColor(tokens, "colorText", 0xfff6f1e2.toInt()), true).apply {
"control.button" -> tokenLabel(props.optString("label", ""), tokens, "button", 14f, "colorText", 0xfff6f1e2.toInt(), true).apply {
gravity = Gravity.CENTER
setPadding(dp(14), dp(12), dp(14), dp(12))
background = android.graphics.drawable.GradientDrawable().apply {
@ -219,24 +219,24 @@ class MainActivity : Activity(), SensorEventListener {
orientation = LinearLayout.VERTICAL
setPadding(dp(12), dp(12), dp(12), dp(12))
background = panelBackground(tokens, 4)
addView(label(props.optString("label", "Status"), 12f, tokenColor(tokens, "colorAccent", 0xffff8a2a.toInt()), true))
addView(label(props.optString("stage", ""), 15f, tokenColor(tokens, "colorText", 0xfff6f1e2.toInt()), true))
addView(label(props.optString("detail", ""), 12f, tokenColor(tokens, "colorMuted", 0xffd3bb7f.toInt()), false))
addView(tokenLabel(props.optString("label", "Status"), tokens, "caption", 12f, "colorAccent", 0xffff8a2a.toInt(), true))
addView(tokenLabel(props.optString("stage", ""), tokens, "strong", 15f, "colorText", 0xfff6f1e2.toInt(), true))
addView(tokenLabel(props.optString("detail", ""), tokens, "caption", 12f, "colorMuted", 0xffd3bb7f.toInt(), false))
}
"input.file", "dropzone" -> label(props.optString("label", "Choose File"), 14f, tokenColor(tokens, "colorText", 0xfff6f1e2.toInt()), true).apply {
"input.file", "dropzone" -> tokenLabel(props.optString("label", "Choose File"), tokens, "button", 14f, "colorText", 0xfff6f1e2.toInt(), true).apply {
gravity = Gravity.CENTER
setPadding(dp(14), dp(12), dp(14), dp(12))
background = panelBackground(tokens, 6)
}
"input.number", "input.select", "control.range" -> LinearLayout(this).apply {
orientation = LinearLayout.VERTICAL
addView(label(props.optString("label", ""), 11f, tokenColor(tokens, "colorAccent", 0xffff8a2a.toInt()), true))
addView(label(props.optString("value", ""), 13f, tokenColor(tokens, "colorText", 0xfff6f1e2.toInt()), false).apply {
addView(tokenLabel(props.optString("label", ""), tokens, "caption", 11f, "colorAccent", 0xffff8a2a.toInt(), true))
addView(tokenLabel(props.optString("value", ""), tokens, "body", 13f, "colorText", 0xfff6f1e2.toInt(), false).apply {
setPadding(dp(8), dp(6), dp(8), dp(6))
background = panelBackground(tokens, 2)
})
}
"control.toggle" -> label("${props.optString("label", "Toggle")}: ${if (props.optBoolean("value", false)) "on" else "off"}", 12f, tokenColor(tokens, "colorText", 0xfff6f1e2.toInt()), false).apply {
"control.toggle" -> tokenLabel("${props.optString("label", "Toggle")}: ${if (props.optBoolean("value", false)) "on" else "off"}", tokens, "body", 12f, "colorText", 0xfff6f1e2.toInt(), false).apply {
setPadding(dp(8), dp(6), dp(8), dp(6))
background = panelBackground(tokens, 2)
}
@ -249,7 +249,7 @@ class MainActivity : Activity(), SensorEventListener {
}
"metric" -> LinearLayout(this).apply {
orientation = LinearLayout.VERTICAL
addView(label("${props.optString("label", "Metric")}: ${props.opt("value") ?: ""}", 13f, tokenColor(tokens, "colorText", 0xfff6f1e2.toInt()), true))
addView(tokenLabel("${props.optString("label", "Metric")}: ${props.opt("value") ?: ""}", tokens, "strong", 13f, "colorText", 0xfff6f1e2.toInt(), true))
}
"surface" -> LinearLayout(this).apply {
orientation = LinearLayout.VERTICAL
@ -298,7 +298,7 @@ class MainActivity : Activity(), SensorEventListener {
cornerRadius = dp(6).toFloat()
}
val title = props.optString("title", "")
if (title.isNotBlank()) addView(label(title, 12f, tokenColor(tokens, "colorMuted", 0xff8ba5a3.toInt()), true))
if (title.isNotBlank()) addView(tokenLabel(title, tokens, "caption", 12f, "colorMuted", 0xff8ba5a3.toInt(), true))
forEachChild(children) { addView(renderCultUiNode(it, values, tokens)) }
}
"card", "card.external" -> LinearLayout(this).apply {
@ -310,13 +310,19 @@ class MainActivity : Activity(), SensorEventListener {
cornerRadius = dp(4).toFloat()
}
val title = props.optString("title", "")
if (title.isNotBlank()) addView(label(title, 14f, tokenColor(tokens, "colorText", 0xffe7f1f1.toInt()), true))
if (title.isNotBlank()) addView(tokenLabel(title, tokens, "title", 14f, "colorText", 0xffe7f1f1.toInt(), true))
forEachChild(children) { addView(renderCultUiNode(it, values, tokens)) }
}
"label", "text", "text.title", "text.dialogue" -> {
val bind = props.optString("bind", "")
val text = if (bind.isNotBlank()) values.opt(bind)?.toString() ?: "" else props.optString("text", "")
label(if (kind == "label") text.uppercase(Locale.US) else text, if (kind == "text.title") 18f else 13f, if (kind == "label") tokenColor(tokens, "colorAccent", 0xffffb84f.toInt()) else tokenColor(tokens, "colorText", 0xffe7f1f1.toInt()), kind == "label")
val role = when (kind) {
"text.title" -> "title"
"text.dialogue" -> "body"
"label" -> "caption"
else -> "body"
}
tokenLabel(if (kind == "label") text.uppercase(Locale.US) else text, tokens, role, if (kind == "text.title") 18f else 13f, if (kind == "label") "colorAccent" else "colorText", if (kind == "label") 0xffffb84f.toInt() else 0xffe7f1f1.toInt(), kind == "label" || kind == "text.title")
}
"control.slider" -> CultSliderView(this, sliderValue(props, values), tokenColor(tokens, "colorAccent", 0xffffb84f.toInt())).apply {
layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, dp(24))
@ -438,6 +444,72 @@ class MainActivity : Activity(), SensorEventListener {
return Color.rgb(value.substring(1, 3).toInt(16), value.substring(3, 5).toInt(16), value.substring(5, 7).toInt(16))
}
private fun tokenLabel(text: String, tokens: JSONObject, role: String, fallbackSp: Float, colorName: String, fallbackColor: Int, title: Boolean): TextView =
label(text, tokenTextSize(tokens, role, fallbackSp), tokenColor(tokens, colorName, fallbackColor), title, typefaceForToken(tokens, role, title))
private fun tokenTextSize(tokens: JSONObject, role: String, fallbackSp: Float): Float {
val names = when (role) {
"title" -> arrayOf("textTitleSize", "fontSizeTitle", "fontScaleTitle")
"strong" -> arrayOf("textStrongSize", "fontSizeStrong", "fontScaleStrong")
"caption" -> arrayOf("textCaptionSize", "fontSizeCaption", "fontScaleCaption")
"mono" -> arrayOf("textMonoSize", "fontSizeMono", "fontScaleMono")
"button" -> arrayOf("textButtonSize", "fontSizeButton", "fontScaleButton")
else -> arrayOf("textBodySize", "fontSizeBody", "fontScaleBody", "textScale")
}
for (name in names) {
val value = tokens.opt(name) ?: continue
val parsed = parseTextSizeToken(value, fallbackSp)
if (parsed != null) return parsed
}
return fallbackSp
}
private fun parseTextSizeToken(value: Any, fallbackSp: Float): Float? = when (value) {
is Number -> value.toFloat()
is String -> {
val text = value.trim().lowercase(Locale.US)
when {
text.endsWith("px") -> text.removeSuffix("px").toFloatOrNull()
text.endsWith("sp") -> text.removeSuffix("sp").toFloatOrNull()
text.endsWith("rem") -> text.removeSuffix("rem").toFloatOrNull()?.let { it * 16f }
text.endsWith("em") -> text.removeSuffix("em").toFloatOrNull()?.let { it * fallbackSp }
text.toFloatOrNull() != null -> text.toFloat()
else -> null
}
}
else -> null
}
private fun typefaceForToken(tokens: JSONObject, role: String, bold: Boolean): android.graphics.Typeface {
val family = when (role) {
"title", "strong", "button" -> firstTokenString(tokens, "fontTitle", "fontDisplay", "fontBody")
"caption", "mono" -> firstTokenString(tokens, "fontMono", "fontBody")
else -> firstTokenString(tokens, "fontBody", "fontTitle")
}
val style = if (bold) android.graphics.Typeface.BOLD else android.graphics.Typeface.NORMAL
if (family.isBlank()) return android.graphics.Typeface.create(android.graphics.Typeface.MONOSPACE, style)
val primaryFamily = family.split(",").firstOrNull()?.trim()?.trim('"', '\'').orEmpty()
val generic = family.lowercase(Locale.US)
val fallback = when {
generic.contains("mono") -> android.graphics.Typeface.MONOSPACE
generic.contains("serif") && !generic.contains("sans") -> android.graphics.Typeface.SERIF
else -> android.graphics.Typeface.SANS_SERIF
}
return if (primaryFamily.isBlank()) {
android.graphics.Typeface.create(fallback, style)
} else {
android.graphics.Typeface.create(primaryFamily, style)
}
}
private fun firstTokenString(tokens: JSONObject, vararg names: String): String {
for (name in names) {
val value = tokens.optString(name, "")
if (value.isNotBlank()) return value
}
return ""
}
override fun onDestroy() {
super.onDestroy()
mediaRunning = false
@ -508,14 +580,20 @@ class MainActivity : Activity(), SensorEventListener {
return scroll
}
private fun label(text: String, sp: Float, color: Int, title: Boolean): TextView =
private fun label(
text: String,
sp: Float,
color: Int,
title: Boolean,
typeface: android.graphics.Typeface = android.graphics.Typeface.create(android.graphics.Typeface.MONOSPACE, if (title) android.graphics.Typeface.BOLD else android.graphics.Typeface.NORMAL)
): TextView =
TextView(this).apply {
this.text = text
setTextSize(if (parityFixtureMode) TypedValue.COMPLEX_UNIT_PX else TypedValue.COMPLEX_UNIT_SP, sp)
setTextColor(color)
gravity = Gravity.START
includeFontPadding = true
typeface = android.graphics.Typeface.create(android.graphics.Typeface.MONOSPACE, if (title) android.graphics.Typeface.BOLD else android.graphics.Typeface.NORMAL)
this.typeface = typeface
}
private fun card(text: String): TextView =

View file

@ -814,6 +814,12 @@ Recently cut:
device-edge proofs; plugin projection authority belongs to EveFlutter,
EveUnity, EveElectron, EveTui, or future runtime-specific adapters when those
bodies earn the claim.
- Android Kotlin now maps surface style tokens into its lightweight parity
fixture typography path. `MainActivity.kt` uses token-aware text roles,
parses future text-size/scale tokens, and maps `fontBody`, `fontTitle`, and
`fontMono` to Android typefaces while keeping Kotlin device-edge scope limited
to generic embedded documents, style tokens, and sensor/uplink work. It still
does not claim Sai/Norn/TeX plugin projection acceptance.
- The web reference no longer hardcodes the local provider list in
`web/surface.js`. It loads `web/local-provider-catalog.json`, then opens
advertisement-backed entries for Aetheria, Repixelizer, and Sai through the

View file

@ -573,7 +573,10 @@ The harness tracks every target runtime:
identity, and `node tools/parity/run-parity.mjs` reports
`embeddedDocuments` support for `android-kotlin`. It does not consume plugin
fixtures until EveAndroid grows a plugin projection adapter; Android Flutter
is the Android-side plugin projection acceptance target.
is the Android-side plugin projection acceptance target. Kotlin declares
`styleTokens` only after proving token-aware text roles, text-size parsing,
and `fontBody` / `fontTitle` / `fontMono` typeface mapping in
`MainActivity.kt`.
- Rust / CultMesh: typed document runtime rather than a CultUI renderer.
Required nested-surface evidence: `cargo test -p cultnet-rs
rust_preserves_cultui_embedded_surface_slots_through_typed_document_sync`

View file

@ -1432,13 +1432,13 @@
"graduationTrigger": "Split when Kotlin device edge has generic surface traversal, sensor release flow, and Android-specific CI.",
"requiredFixtures": ["embedded-surface"],
"pluginFixtures": [],
"supportedFeatures": ["cultmesh-dashboard-documents", "sensor-uplink", "embeddedDocuments"],
"supportedFeatures": ["cultmesh-dashboard-documents", "sensor-uplink", "embeddedDocuments", "styleTokens"],
"unsupportedPlugins": [],
"expectedPaths": ["android/app/src/main/java/org/gamecult/eve/MainActivity.kt", "scripts/build-android.ps1"],
"expectedSourceSymbols": [
{
"path": "android/app/src/main/java/org/gamecult/eve/MainActivity.kt",
"contains": ["surface.slot", "embeddedDocuments", "contentDescription"]
"contains": ["surface.slot", "embeddedDocuments", "contentDescription", "tokenLabel", "tokenTextSize", "typefaceForToken", "fontBody", "fontTitle", "fontMono"]
}
],
"capture": {