Repository navigation
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.
// 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).
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
)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) }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.
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)
}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).
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}""")- Each environment numbers its equations from 1; KaTeX counts across the whole formula.
- The extra space in a top-level
\\[size]is dropped. -
\reflectboxdoesn't mirror its content. -
\pmbis approximated by drawing an offset copy. - No strict mode or trust checks.
-
\includegraphicsand theCDenvironment are not supported. - A tall tag makes its array row taller; KaTeX lets it overflow.