RaTeX-CMP

July 28, 2026 · View on GitHub

English Version | 中文版本

License Kotlin

✨ RaTeX-CMP is a math formula rendering project for multi-platform UI scenarios, built with Kotlin Multiplatform and Compose Multiplatform. Its core rendering capability is powered by RaTeX.

It allows the same formula rendering engine to be reused across Android, iOS, JVM Desktop, JS, and Wasm, making it easier to integrate consistent mathematical typesetting and display into Compose Multiplatform applications.

This repository is maintained as an independent project. It can continue evolving as a library while also serving as a sample project and integration reference.

🌍 Supported Platforms

PlatformArchitectures / TargetsNotes
Androidarm64-v8a, armeabi-v7a, x86_64, x86x86 is currently untested
iOSiPhone / SimulatorIntegrated via Kotlin Multiplatform Framework
JVM DesktopWindows x86_64, macOS x86_64 / arm64, Linux x86_64 / arm64Desktop native libraries are built and published based on what the current machine supports
Webjs(IR), wasmJsBoth targets share webMain and load WASM through the ratex-wasm npm package

📷 Screenshots

Android iOS JVM Desktop
android.png ios.png desktop.png

🚀 Usage

1. Add repositories

If you are using Maven Central, make sure your project repositories include:

repositories {
    mavenCentral()
}

If you want to validate locally first, you can also use:

repositories {
    mavenLocal()
    mavenCentral()
}

2. Add the dependency

The current KMP main library coordinates are:

implementation("io.github.darriousliu:ratex:0.1.14")

In a Kotlin Multiplatform project, you would typically add it to commonMain:

kotlin {
    sourceSets {
        commonMain.dependencies {
            implementation("io.github.darriousliu:ratex:0.1.14")
        }
    }
}

If you want to run on JVM Desktop, you also need to add the native runtime dependency for the current platform:

kotlin {
    sourceSets {
        jvmMain.dependencies {
            implementation("io.github.darriousliu:ratex:0.1.14")
            runtimeOnly("io.github.darriousliu:ratex-native-darwin-aarch64:0.1.14")
        }
    }
}

Available Desktop native coordinates:

  • io.github.darriousliu:ratex-native-darwin-aarch64
  • io.github.darriousliu:ratex-native-darwin-x86-64
  • io.github.darriousliu:ratex-native-linux-aarch64
  • io.github.darriousliu:ratex-native-linux-x86-64
  • io.github.darriousliu:ratex-native-windows-x86-64

In this repository, Desktop native libraries are published as separate submodules; the sample app automatically selects the matching runtime dependency for the current host platform.

3. Use the Compose component

3.1 Basic usage

The simplest usage is to pass a LaTeX string directly:

import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.sp
import io.ratex.compose.RaTeX

@Composable
fun FormulaSample(modifier: Modifier = Modifier) {
    RaTeX(
        latex = """\frac{-b \pm \sqrt{b^2 - 4ac}}{2a}""",
        modifier = modifier,
        fontSize = 28.sp,
        displayMode = true,
        color = Color(0xFF1565C0),
    )
}

If you want inline math, set displayMode = false:

RaTeX(
    latex = """e^{i\pi}+1=0""",
    fontSize = 20.sp,
    displayMode = false,
)

If you want the formula color to follow the current Material text color, you can omit color and the composable will use LocalContentColor.current.

3.2 Reuse parsed results

If you want to parse first and reuse the resulting DisplayList in multiple places, you can use rememberRaTeXDisplayList:

import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.material3.MaterialTheme
import androidx.compose.ui.unit.sp
import io.ratex.compose.RaTeX
import io.ratex.compose.rememberRaTeXDisplayList

@Composable
fun ParsedFormulaSample(latex: String) {
    val parseResult by rememberRaTeXDisplayList(
        latex = latex,
        displayMode = true,
        color = MaterialTheme.colorScheme.primary,
    )

    RaTeX(
        displayList = parseResult?.getOrNull(),
        fontSize = 28.sp,
    )
}

3.3 Use Text with inlineContent

To mix an inline formula into Text, parse it synchronously with rememberBlockingRaTeXDisplayList, then create InlineTextContent from the measured result:

import androidx.compose.foundation.text.InlineTextContent
import androidx.compose.foundation.text.appendInlineContent
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.Placeholder
import androidx.compose.ui.text.PlaceholderVerticalAlign
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.unit.sp
import io.ratex.compose.RaTeX
import io.ratex.compose.rememberBlockingRaTeXDisplayList
import io.ratex.measure

@Composable
fun InlineFormulaText() {
    val formula = """E = mc^2"""
    val formulaId = "energy"
    val formulaFontSize = 18.sp
    val density = LocalDensity.current
    val parseResult = rememberBlockingRaTeXDisplayList(
        latex = formula,
        displayMode = false,
    )
    val displayList = parseResult.getOrNull()
    val fontSizePx = with(density) { formulaFontSize.toPx() }
    val measured = remember(displayList, fontSizePx) {
        displayList?.measure(fontSizePx)
    }
    val placeholderWidth = with(density) {
        (measured?.widthPx ?: fontSizePx).toSp()
    }
    val placeholderHeight = with(density) {
        (measured?.totalHeightPx ?: fontSizePx).toSp()
    }

    val inlineContent = mapOf(
        formulaId to InlineTextContent(
            placeholder = Placeholder(
                width = placeholderWidth,
                height = placeholderHeight,
                placeholderVerticalAlign = PlaceholderVerticalAlign.TextCenter,
            ),
        ) {
            RaTeX(
                displayList = displayList,
                fontSize = formulaFontSize,
            )
        },
    )

    Text(
        text = buildAnnotatedString {
            append("The mass–energy equation ")
            appendInlineContent(formulaId, formula)
            append(" describes the relationship between mass and energy.")
        },
        inlineContent = inlineContent,
    )
}

Note: JS/Wasm browser targets must explicitly call RaTeXEngine.initialize() from a coroutine before any parsing. After initialization, rememberRaTeXDisplayList / RaTeX(latex = ...) load fonts and parse asynchronously. Before using rememberBlockingRaTeXDisplayList, also preload fonts asynchronously with RaTeXFontLoader.ensureLoaded(); the synchronous helper is therefore intended mainly for Android, iOS, and JVM Desktop.

🧭 Repository Overview

  • library: core library module
  • desktop-native/*: publishing modules for JVM Desktop native libraries
  • example: shared sample module, including the Desktop, JS/Wasm entry points and the new RaTeXShowcasePage
  • androidApp: Android sample app
  • iosApp: iOS sample project
  • build-logic: shared Gradle convention plugins for Desktop native publishing
  • external/RaTeX: upstream RaTeX submodule

🛠️ Local Development

1. Clone the repository and initialize submodules

git clone https://github.com/darriousliu/RaTeX-CMP.git
cd RaTeX-CMP
git submodule update --init --recursive

If you have already cloned the repository but have not initialized submodules, you can simply run the last command.

2. Prepare the base environment

Recommended tools:

  • JDK 17
  • Android Studio or IntelliJ IDEA
  • Rust toolchain
  • Bash environment
  • Android SDK; Android NDK is also required if you want to build Android native libraries
  • Xcode; required only when developing for iOS on macOS

Depending on the platform, you may also need:

  • Android: cargo-ndk
  • Desktop full-platform native packaging: cargo-zigbuild and zig

3. Install the required Rust targets

Before running or building for a platform for the first time, it is recommended to install the required Rust targets for that platform.

Android:

rustup target add aarch64-linux-android armv7-linux-androideabi x86_64-linux-android i686-linux-android

iOS:

rustup target add aarch64-apple-ios aarch64-apple-ios-sim x86_64-apple-ios

JVM Desktop:

  • When building only for the current host platform, you usually do not need to run rustup target add manually
  • When running bash prepare-jvm-rust.sh --all, the script automatically selects all targets supported by the current machine and runs rustup target add as needed
  • For example, on arm64 macOS, it builds darwin-aarch64, darwin-x86-64, linux-aarch64, and linux-x86-64, but does not attempt windows-x86-64

4. Prepare local artifacts

If this is your first time running the project, or if you changed the underlying Rust code, you will usually need to prepare the corresponding local artifacts first.

Android:

bash prepare-android-rust.sh

iOS:

bash prepare-ios-rust.sh

JVM Desktop:

bash prepare-jvm-rust.sh

Prepare all Desktop Rust artifacts supported by the current machine:

bash prepare-jvm-rust.sh --all

If you are only working on the Kotlin / Compose layer and usable artifacts already exist in the repository, run these commands only when needed instead of every time.

5. Run and verify

JVM Desktop sample:

./gradlew :example:run

On Windows:

.\gradlew.bat :example:run

Android debug package:

./gradlew :androidApp:assembleDebug

On Windows:

.\gradlew.bat :androidApp:assembleDebug

For iOS development, it is recommended to open iosApp/iosApp.xcodeproj on macOS for debugging.

Web sample:

./gradlew :example:jsBrowserDevelopmentRun
./gradlew :example:wasmJsBrowserDevelopmentRun

6. Development suggestions

  • Prefer completing Compose UI, Kotlin API, and sample project work within this repository first
  • Prefer keeping Desktop native publishing changes inside build-logic
  • Enter external/RaTeX only when you need to coordinate with the lower-level engine
  • After updating the submodule, remember to regenerate the corresponding platform artifacts
  • When committing changes, distinguish carefully between changes in this project and changes in the submodule

🚀 Common Commands

Initialize submodules:

git submodule update --init --recursive

Run the Desktop sample:

./gradlew :example:run

Build the Android sample:

./gradlew :androidApp:assembleDebug

Run the Web sample:

./gradlew :example:jsBrowserDevelopmentRun
./gradlew :example:wasmJsBrowserDevelopmentRun

Prepare Android Rust artifacts:

bash prepare-android-rust.sh

Prepare iOS Rust artifacts:

bash prepare-ios-rust.sh

Prepare Desktop Rust artifacts:

bash prepare-jvm-rust.sh

Prepare all Desktop Rust artifacts supported by the current machine:

bash prepare-jvm-rust.sh --all

Publish the library to Maven Central:

Make sure your publishing credentials and signing configuration are ready before publishing.

Publish all artifacts supported by the current machine to Maven Local:

./gradlew publishToMavenLocal

This publishes:

  • the KMP main library from :library
  • Desktop native submodules supported by the current machine

This is also the recommended local verification path. If it succeeds, both the main library and the Desktop native libraries supported by the current machine will be published to your local Maven repository.

For example:

  • On arm64 macOS, it also publishes ratex-native-darwin-aarch64, ratex-native-darwin-x86-64, ratex-native-linux-aarch64, and ratex-native-linux-x86-64
  • On Linux, it also publishes ratex-native-linux-aarch64 and ratex-native-linux-x86-64
  • On Windows, it also publishes ratex-native-windows-x86-64

Publish all artifacts supported by the current machine to Maven Central:

./gradlew publishAndReleaseToMavenCentral

Publish only the KMP main library:

./gradlew :library:publishKotlinMultiplatformPublicationToMavenCentralRepository

Publish all JVM Desktop native libraries supported by the current machine:

./gradlew publishSupportedDesktopNativePublicationsToMavenCentralRepository

Publish all JVM Desktop native libraries supported by the current machine to Maven Local:

./gradlew publishSupportedDesktopNativePublicationsToMavenLocal

This task automatically:

  • runs the publish tasks for native submodules supported by the current machine
  • automatically calls the matching prepare-jvm-rust.sh <target> inside each submodule
  • verifies that Desktop native artifacts supported by the current machine were generated successfully

These Desktop native submodules share the same precompiled script plugin from build-logic; each module only declares its target platform, file name, artifactId, and supported host OS set.

For example:

  • On arm64 macOS, it publishes darwin-aarch64, darwin-x86-64, linux-aarch64, and linux-x86-64
  • On Linux, it publishes linux-aarch64 and linux-x86-64
  • On Windows, it publishes windows-x86-64

🙏 Acknowledgements

Thanks to the RaTeX project for providing the core capabilities and open-source foundation.

Thanks also to Kotlin Multiplatform, Compose Multiplatform, Rust, and the related open-source communities for making this cross-platform project possible and helping it continue to evolve in a more unified way across platforms.