Skip to content

text-indent with em / rem / % is incompatible with Compose Multiplatform 1.10.3 #724

@guiyanakuang

Description

@guiyanakuang

Summary

compose-rich-editor is incompatible with Compose Multiplatform 1.10.3 when the input HTML contains a text-indent declaration whose value uses em, rem, or %. Loading such HTML into a RichTextState and rendering it via RichText / RichTextEditor crashes the layout pass with:

java.lang.IllegalStateException: Only Sp can convert to Px
    at androidx.compose.ui.unit.Density.toPx--R2X_6o(Density.kt:114)
    at androidx.compose.ui.text.platform.ParagraphBuilder.textStyleToParagraphStyle(ParagraphBuilder.skiko.kt:551)

After this throws once, the text node's layout cache stays null, so every subsequent draw of the same node also crashes:

java.lang.IllegalStateException: Internal Error: MultiParagraphLayoutCache could not provide TextLayoutResult during the draw phase.

Minimal reproduction

Compose Multiplatform Desktop, compose-plugin 1.10.3, com.mohamedrejeb.richeditor:richeditor-compose:1.0.0-rc14:

@Composable
fun App() {
    val state = rememberRichTextState()
    LaunchedEffect(Unit) {
        // Any of these three triggers the crash:
        state.setHtml("""<p style="text-indent: 2em">Hello</p>""")
        // state.setHtml("""<p style="text-indent: 1.5rem">Hello</p>""")
        // state.setHtml("""<p style="text-indent: 50%">Hello</p>""")
    }
    RichText(state = state, modifier = Modifier.fillMaxSize())
}

Real-world trigger: HTML pasted from Microsoft Word, Google Docs, or many web pages — these commonly emit text-indent: <n>em or text-indent: <n>% on <p> / <div> style attributes.

Why it crashes

parser/html/CssEncoder.kt, parseCssTextSize:

return when (unit) {
    "px"  -> value.sp
    "pt"  -> (value * 1.333f).sp
    "em"  -> value.em             // ← Em
    "rem" -> value.em             // ← Em
    "%"   -> (value / 100f).em    // ← Em
    else  -> TextUnit.Unspecified
}

parseCssTextIndent then feeds that result into TextIndent(textUnit, textUnit):

val textUnit = parseCssTextSize(cssTextIndent)
return if (textUnit.isSpecified) TextIndent(textUnit, textUnit) else null

So a TextIndent(Em, Em) ends up on the ParagraphStyle. Inside Compose Multiplatform 1.10.3, Skia's ParagraphBuilder.textStyleToParagraphStyle converts it via the raw Density.toPx(TextUnit):

// compose-ui-text-desktop:ParagraphBuilder.skiko.kt
textStyle.textIndent?.run {
    with(density) {
        pStyle.textIndent = SkTextIndent(firstLine.toPx(), restLine.toPx())
    }
}
// compose-ui-unit:Density.kt
fun TextUnit.toPx(): Float {
    checkPrecondition(type == TextUnitType.Sp) { \"Only Sp can convert to Px\" }
    return toDp().toPx()
}

Density.toPx(TextUnit) only accepts Sp, so any Em value reaches a hard crash. Other text properties (e.g. lineHeight) are not affected because they go through a helper that resolves Em against the current fontSize before converting; only textIndent uses the raw Density.toPx(TextUnit).

Suggested fix

Two options:

  1. Resolve em / rem / % to sp at parse time for text-indent, using a fixed default base font size (e.g. 16f). The result is stable and never crashes, at the cost of text-indent no longer scaling with the caller's actual fontSize — a fair trade given that CMP's TextIndent is absolute Sp anyway.

  2. Drop unsupported units by returning null from parseCssTextIndent when the parsed unit is not Sp-convertible. The visual indent is silently lost but the editor stays alive.

Workaround

For anyone hitting this before the upstream fix lands, sanitize the HTML before calling setHtml — strip every text-indent declaration from inline styles. Match the property name exactly (e.g. via splitting on ; and comparing the lowercased property) so vendor-prefixed names like mso-text-indent from Word-pasted HTML are left untouched.

Versions

  • compose-rich-editor 1.0.0-rc14 (also reproduces on rc13)
  • Compose Multiplatform 1.10.3 (Skia / JVM Desktop)
  • macOS — failing code path is in commonMain / skikoMain, so all Skia-backed targets are affected

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions