Skip to content
re-ovo edited this page Oct 28, 2026 · 1 revision

LaTeX

Module: com.github.re-ovo.bettertext:latex, package me.rerere.bettertext.latex. It includes the text module.

A Kotlin port of the KaTeX 0.19.1 parser and HTML builder that draws on an Android Canvas with the bundled KaTeX fonts. It supports KaTeX's functions, environments, macros and symbols, and chemical equations through its mhchem extension (\ce, \pu). See the KaTeX documentation for the supported commands.

Like KaTeX in a browser, a long inline formula breaks after top-level relations (=, <, …) and binary operators (+, -, …), and at \allowbreak, when it doesn't fit the width.

Standalone formulas

// Inline style, wraps when it doesn't fit.
Latex("e^{i\\pi} + 1 = 0", fontSize = 16.sp, color = MaterialTheme.colorScheme.onSurface)

// Display style, centered.
Latex("\\int_0^\\infty e^{-x^2}\\,dx = \\frac{\\sqrt{\\pi}}{2}", displayMode = true)

// Display style, scrolls horizontally when wider than the available width.
LatexBlock("\\begin{pmatrix} a & b \\\\ c & d \\end{pmatrix}", fontSize = 18.sp)

Use raw strings to avoid doubling backslashes:

LatexBlock("""\sum_{k=1}^n k = \frac{n(n+1)}{2} \tag{1}""")
Parameter Meaning
fontSize Size of the surrounding text. The formula is drawn scale times larger.
scale Defaults to DefaultMathScale (1.21), KaTeX's ratio of math to text.
color Default color; \color and \textcolor override it.
displayMode Latex only; LatexBlock is always in display mode.
macros User macros, e.g. mapOf("\\RR" to "\\mathbb{R}").

Latex and LatexBlock report their first and last baselines, so a formula lines up with text in a Row using Modifier.alignByBaseline().

Display formulas place \tag and numbered environments' tags at the right edge (LatexBlock spans the full width so the tag sits at the edge).

Settings

For anything beyond display mode and macros, lay out the formula yourself with LatexSettings and pass the layout:

val layout = rememberLatexLayout(
    """\RR^n \ni x \mapsto \|x\|""",
    LatexSettings(macros = mapOf("\\RR" to "\\mathbb{R}")),
)
Latex(layout, fontSize = 16.sp)
data class LatexSettings(
    val displayMode: Boolean = false,        // display style ($$...$$) instead of inline ($...$)
    val macros: Map<String, String> = emptyMap(),
    val maxSize: Float = Float.POSITIVE_INFINITY, // cap for user-specified sizes, in em
    val maxExpand: Int = 1000,               // limit on macro expansions, guards against loops
    val minRuleThickness: Float = 0f,        // minimum thickness of fraction lines, in em
    val leqno: Boolean = false,              // equation tags on the left
    val fleqn: Boolean = false,              // left-align display math
    val colorIsTextColor: Boolean = false,   // make \color behave like \textcolor
    val errorColor: String = "#cc0000",      // CSS color for the source of a formula that failed
)

Errors

Parsing never throws. A formula that fails to parse is shown as its source text in errorColor, and the layout's error holds the LatexParseException, with rawMessage and position (the offset in the source, or -1):

val layout = rememberLatexLayout(source)
Latex(layout)
layout.error?.let { Text("Error: ${it.message}", color = Color.Red) }

Laying out ahead of time

LatexLayout.create parses and lays out a formula outside composition. It's safe to call off the main thread:

val layout = withContext(Dispatchers.Default) {
    LatexLayout.create(source, LatexSettings(displayMode = true), context)
}

The overload without a Context measures glyphs approximately, without the fonts. It's meant for JVM tests, not for drawing.

A LatexLayout exposes its size in em of the font size it's drawn at (width, height, depth), its chunkCount and each chunk's metrics, source, displayMode and error.

Formulas in BetterText

LatexInline is a DrawnInline (see BetterText): each unbreakable chunk of the formula is a segment, so it wraps with the text, and it's drawn scale times the surrounding font size in the surrounding color.

In composition, use rememberLatexInline:

@Composable
fun Energy() {
    val emc2 = rememberLatexInline("E = mc^2")
    val inlineContent = remember(emc2) { mapOf("emc2" to emc2) }
    val text = remember {
        buildAnnotatedString {
            append("Mass and energy are related by ")
            appendInline("emc2", "\$E = mc^2\$")
            append(", where c is the speed of light.")
        }
    }
    BetterText(text, inlineContent = inlineContent, style = MaterialTheme.typography.bodyLarge)
}

The alt text ($E = mc^2$ here) is what the formula copies as when it's selected.

Outside composition, as in Markdown's inlineMath component, use LatexInline.cached:

val inline = LatexInline.cached(formula, context)

It caches up to 256 layouts by source and settings, so text that streams in doesn't lay out its formulas again, and shares one painter among all formulas, so draw them on the main thread.

For a custom scale, construct one directly:

val painter = rememberLatexPainter()
val inline = remember(source, painter) {
    LatexInline(LatexLayout.create(source, LatexSettings(), context), painter, scale = 1.1f)
}

Hosts with their own layout

LatexPainter draws chunks of a layout on an Android canvas, for text layouts other than BetterText. It isn't thread-safe; use one per drawing thread.

val painter = LatexPainter(context)
val em = textSizePx * DefaultMathScale
var x = left
for (i in 0 until layout.chunkCount) {
    painter.drawChunk(canvas, layout, i, x, baseline, em, color)
    x += layout.chunkWidth(i) * em
}

LatexChunk(layout, index, …) is the composable equivalent: it draws one chunk and reports its baseline as FirstBaseline, for hosts that use one placeholder per chunk. layout.isLineBreakBefore(i) tells whether a chunk must start a new line (after \\ or \newline).

Chemistry

mhchem 3.3.0 is built in:

Latex("""\ce{CO2 + C -> 2 CO}""")
Latex("""\ce{Hg^2+ ->[I-] HgI2 ->[I-] [Hg^{II}I4]^2-}""")
Latex("""\pu{123 kJ/mol}""")

Known deviations from KaTeX

  • Each environment numbers its equations from 1; KaTeX counts across the whole formula.
  • The extra space in a top-level \\[size] is dropped.
  • \reflectbox doesn't mirror its content.
  • \pmb is approximated by drawing an offset copy.
  • No strict mode or trust checks.
  • \includegraphics and the CD environment are not supported.
  • A tall tag makes its array row taller; KaTeX lets it overflow.

Clone this wiki locally