Skip to content

fontique: on Android, `sans-serif` and `system-ui` resolve to Roboto Flex instead of Roboto

Generated from repros/fontique-android-roboto-flex-generic/README.md when the site was built at 7aa23e2.

crates/niso-core/src/lib.rs:11-26 (android_font_context, used at lib.rs:164) builds its own fontique Collection on Android and resets GenericFamily::SansSerif and GenericFamily::SystemUi to just “Roboto”.

fontique hardcodes the Android generic families in fontique/src/backend/android.rs:22-39 (DEFAULT_GENERIC_FAMILIES), unchanged since linebender/parley#28 (2024-04):

// TODO: Use actual generic families here, where available, when fonts.xml is properly parsed.
(GenericFamily::SansSerif, &["Roboto Flex", "Roboto", "Noto Sans"]),
(GenericFamily::SystemUi, &["Roboto Flex", "Roboto", "Noto Sans"]),

Same lines at niso’s fork rev 00cbfc6ae4dc26e530f1e05c11097e3e149b8b9a and on main 746fdcf0d53fab04c5da1e2d7bde89018cbbb2b4.

Android’s own definition is /system/etc/fonts.xml: the first family (“first font is default”) is <family name="sans-serif"> backed by Roboto-Regular.ttf (the variable Roboto, family name “Roboto”); Roboto Flex is a separate, opt-in family <family name="roboto-flex"> backed by RobotoFlex-Regular.ttf (AOSP frameworks/base/data/fonts/fonts.xml, main, lines 38 and 292). Typeface.DEFAULT/SANS_SERIF, and so Compose’s FontFamily.Default/FontFamily.SansSerif and BasicText, resolve to that sans-serif family, i.e. Roboto. On any device that ships RobotoFlex-Regular.ttf, fontique instead picks Roboto Flex: a different design with its own metrics (advances, ascent/descent) and an opsz axis, so text no longer matches native text in the same box. fontique does parse fonts.xml, but only for aliases and language/script fallback; it never maps the sans-serif family to its files.

  • Expected: first family of SansSerif and SystemUi is “Roboto” (whatever fonts.xml’s sans-serif is).
  • Actual (from the code above; not run here): ["Roboto Flex", "Roboto", "Noto Sans"] on devices with Roboto Flex.

This cannot run here: fontique’s Android backend is compiled only for target_os = "android", and this machine has no Android device, emulator or NDK. On the host the program exits 2 with a message. It was type-checked with cargo check --target aarch64-linux-android against the fork rev and against parley main.

On a machine with the NDK and a device or emulator (API 34+ images ship Roboto Flex):

Terminal window
rustup target add aarch64-linux-android # x86_64-linux-android for an x86_64 emulator
cargo install cargo-ndk
export ANDROID_NDK_HOME=/path/to/ndk
cargo ndk -t arm64-v8a build --release
adb push target/aarch64-linux-android/release/fontique-android-roboto-flex-generic /data/local/tmp/
adb shell /data/local/tmp/fontique-android-roboto-flex-generic; echo "exit=$?"
adb shell grep -n -A1 'family name=\"sans-serif\"' /system/etc/fonts.xml

Expected output while the bug reproduces (on a device with Roboto Flex):

Roboto Flex installed: true
SansSerif: expected first family "Roboto" (fonts.xml sans-serif), actual ["Roboto Flex", "Roboto", "Noto Sans"]
SystemUi: expected first family "Roboto" (fonts.xml sans-serif), actual ["Roboto Flex", "Roboto", "Noto Sans"]
exit=1

Not runnable here; the hardcoded list is identical on linebender/parley main 746fdcf (read in source), so it should reproduce there too.

None found. Searched linebender/parley issues and PRs (all 944) for Roboto, Android, generic, fonts.xml. Related: #355 (open, “fontique: generic families (Fedora/KDE)”, Linux), #884 (open, Android CJK fallback without postScriptName), #337 (open, Android kana fallback), #30 (open, generic-family-specific fallback).

Title: fontique (Android): sans-serif/system-ui resolve to Roboto Flex, but Android’s default is Roboto

Body:

fontique/src/backend/android.rs hardcodes SansSerif and SystemUi as ["Roboto Flex", "Roboto", "Noto Sans"]. On Android, sans-serif is the first family in /system/etc/fonts.xml (Roboto-Regular.ttf, family “Roboto”), which is what Typeface.DEFAULT, TextView and Compose use; Roboto Flex is the separate roboto-flex family. On devices that ship RobotoFlex-Regular.ttf, parley text therefore uses a different font, with different metrics, from native text.

use fontique::{Collection, CollectionOptions, GenericFamily};
fn main() {
let mut c = Collection::new(CollectionOptions { shared: false, system_fonts: true });
let ids: Vec<_> = c.generic_families(GenericFamily::SansSerif).collect();
let names: Vec<_> = ids.iter().filter_map(|&id| c.family_name(id).map(str::to_owned)).collect();
println!("{names:?}"); // ["Roboto Flex", "Roboto", "Noto Sans"]; expected "Roboto" first
}

Suggested fix: put “Roboto” ahead of “Roboto Flex” (or drop Roboto Flex), or better, resolve the generics from fonts.xml: map the first <family name="sans-serif"> (and serif, monospace, …) to the families of the files it lists, as the existing TODO says.