From c8de54d1c2f195e3669a4cd7355288701045a36d Mon Sep 17 00:00:00 2001 From: davotoula Date: Tue, 9 Jun 2026 19:41:55 +0200 Subject: [PATCH] fix(math): align inline equations to the text baseline --- .../amethyst/ui/components/LatexEquation.kt | 33 +++++++++++++++++-- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/LatexEquation.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/LatexEquation.kt index 2042fb82a1..ea0f43dffc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/LatexEquation.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/LatexEquation.kt @@ -36,6 +36,7 @@ import androidx.compose.ui.graphics.drawscope.drawIntoCanvas import androidx.compose.ui.graphics.nativeCanvas import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.text.rememberTextMeasurer import androidx.compose.ui.unit.TextUnit import androidx.compose.ui.unit.TextUnitType import androidx.compose.ui.unit.sp @@ -91,12 +92,33 @@ fun LatexEquation( return } + // Baseline alignment. The parent FlowRow centers each word vertically, so a + // tight equation bitmap floats above the line — most of a formula's mass sits + // above the baseline, so centering its box lifts the baseline above the text. + // We pad the box so that, once centered, the equation's baseline coincides + // with a surrounding text word's baseline. With text ascent/descent A/Dsc and + // icon total height H and depth-below-baseline Dp, the padded box satisfies + // `baseline-from-center = (A - Dsc) / 2` — the same value a Text box has — when + // pad = (A - Dsc) - H + 2*Dp (on top if positive, on bottom if negative). + val resolvedStyle = LocalTextStyle.current.let { if (fontSize.isUsable()) it else it.copy(fontSize = 16.sp) } + val measurer = rememberTextMeasurer() + val (drawTopPx, boxHeightPx) = + remember(drawable, resolvedStyle) { + val text = measurer.measure("Mg", resolvedStyle) + val ascent = text.firstBaseline + val descent = text.size.height - ascent + val iconHeight = drawable.intrinsicHeight.toFloat() + val iconDepth = drawable.icon().iconDepth.toFloat() + val pad = (ascent - descent) - iconHeight + 2f * iconDepth + maxOf(pad, 0f) to (iconHeight + maxOf(pad, 0f) + maxOf(-pad, 0f)) + } + val widthDp = with(density) { drawable.intrinsicWidth.toDp() } - val heightDp = with(density) { drawable.intrinsicHeight.toDp() } + val boxHeightDp = with(density) { boxHeightPx.toDp() } // Wide display equations can overflow the column; allow them to scroll // horizontally instead of being clipped. - val equationSize = Modifier.size(widthDp, heightDp) + val equationSize = Modifier.size(widthDp, boxHeightDp) val equationModifier = if (displayMode) Modifier.horizontalScroll(rememberScrollState()).then(equationSize) else equationSize // Row keeps leading/trailing punctuation (the `(` and `)` in `($x$)`) hugging @@ -107,8 +129,13 @@ fun LatexEquation( } Canvas(modifier = equationModifier) { drawIntoCanvas { canvas -> + val native = canvas.nativeCanvas + val checkpoint = native.save() + // Position the icon's baseline on the text baseline within the padded box. + native.translate(0f, drawTopPx) drawable.setBounds(0, 0, drawable.intrinsicWidth, drawable.intrinsicHeight) - drawable.draw(canvas.nativeCanvas) + drawable.draw(native) + native.restoreToCount(checkpoint) } } if (trailing.isNotEmpty()) {