iris: resolve fontique's Android monospace generic family ourselves

fontique 0.11.1's Android backend never resolves GenericFamily::Monospace
(mono=None in the startup diagnostic, RUST.md's 2026-09-07 "Platform
fonts" gap): DEFAULT_GENERIC_FAMILIES looks up "monospace" against
name_map before fonts.xml is parsed into it, and even after parsing,
AOSP's fonts.xml names it with a <family name="monospace"> element whose
<font> children the backend's own parser never reads (a TODO left in
place) -- so the name gets a FamilyId with no font data behind it, and
family_by_name("monospace") comes back empty too. Confirmed still present
on linebender/parley's main branch, so there is no newer release to bump
to.

TextData::patch_android_monospace (Android-only, called from
TextData::default) reads fonts.xml's own "monospace" declaration for the
font filename it names, then finds which of fontique's actually-scanned
families owns a font file with that name and registers it as the
Monospace generic directly -- the same authority Compose's
Typeface.MONOSPACE resolves through, without pinning an OEM-specific
family name. Verified on this checkout's emulator:
mono=Some("Droid Sans Mono") in the startup log, and a screenshot showing
the bench-fixture's code block and tool-card values in a visibly
monospaced face beside sans body/heading text. Desktop's fontconfig
backend is unaffected.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
irisandClaude Fable 5.1 committed 2026-09-07 16:25:27 -04:00
1 parent 84a13e806b
commit 7e4e26a335
2 files changed
+122 -16

No files matched your search

+22 -15
View File
@@ -23,21 +23,28 @@ marked **DEFERRED** are ones the agent chose not to decide alone.
tofu for a codepoint no resolved face has (checked with CJK + emoji on tofu for a codepoint no resolved face has (checked with CJK + emoji on
desktop) rather than blank space, so the UI_RULES unknown-glyph rule desktop) rather than blank space, so the UI_RULES unknown-glyph rule
still holds. still holds.
- **Flagging for Iris, not blocking on it**: this fontique version's - **Gap found, then closed same day**: this fontique version's Android
Android backend never resolves the `Monospace` generic family at all backend never resolved the `Monospace` generic family at all (confirmed
(confirmed on this checkout's emulator, `mono=None` in the startup on this checkout's emulator, `mono=None` in the startup diagnostic) --
diagnostic) -- a pre-existing ordering bug in fontique's own two pre-existing bugs in fontique's own `fonts.xml` parsing stacked (an
`fonts.xml` parsing, not something this change introduced, but this ordering bug, and a `<family name="monospace">` declaration whose
change is what stopped masking it (the bundled mono font used to be `<font>` children the backend's parser never reads), not something this
registered ahead of the broken platform lookup, so it always won). change introduced, but this change is what stopped masking it (the
Effect: code blocks and the tool-card chevron marks fall through to bundled mono font used to be registered ahead of the broken platform
the same face as body text on Android instead of a genuinely lookup, so it always won). Checked `linebender/parley`'s `main` branch
monospaced one -- still visible, not blank, just not monospaced. on GitHub: neither bug is fixed there, so there was no newer release to
Compose does not share this gap; it resolves `FontFamily.Monospace` bump to. Fixed instead in `iris-core` itself
through Android's own `Typeface.MONOSPACE`, not through fontique. (`TextData::patch_android_monospace`, Android-only): reads
docs/RUST.md's "Platform fonts (2026-09-07)" has the full account and `/system/etc/fonts.xml`'s own `"monospace"` declaration for the font
why a fix (an OEM-specific named-family pin, or patching around filename it names, then registers whichever of fontique's actually-
fontique) is left as a follow-up rather than done in this pass. scanned families owns that file as the `Monospace` generic -- the same
authority Compose's `Typeface.MONOSPACE` resolves through, without
pinning an OEM-specific family name. Verified on this checkout's
emulator: `mono=Some("Droid Sans Mono")`, and a screenshot showing the
bench-fixture's code block and tool-card values in a visibly monospaced
face beside sans body text; the desktop `fontconfig` backend is
unaffected (still resolves monospace correctly, confirmed unchanged).
docs/RUST.md's "Platform fonts (2026-09-07)" has the full account.
## 2026-09-07 (how a phone log reaches Iris) ## 2026-09-07 (how a phone log reaches Iris)
+100 -1
View File
@@ -74,8 +74,10 @@ impl Default for TextData {
/// and its platform monospace face, and ships no text font of its own, /// and its platform monospace face, and ships no text font of its own,
/// only its committed Nerd Fonts icon subset for fixed glyphs). /// only its committed Nerd Fonts icon subset for fixed glyphs).
fn default() -> Self { fn default() -> Self {
let mut font_cx = FontContext::new();
patch_android_monospace(&mut font_cx);
Self { Self {
font_cx: FontContext::new(), font_cx,
layout_cx: LayoutContext::new(), layout_cx: LayoutContext::new(),
scale_cx: ScaleContext::new(), scale_cx: ScaleContext::new(),
atlas: GlyphAtlas::default(), atlas: GlyphAtlas::default(),
@@ -84,6 +86,103 @@ impl Default for TextData {
} }
} }
/// Works around `fontique` 0.11.1's Android backend never resolving
/// `GenericFamily::Monospace` (confirmed against
/// `fontique-0.11.1/src/backend/android.rs`'s `SystemFonts::new`, and still
/// present on `linebender/parley`'s `main` as of 2026-09-07, so there is no
/// released fix to bump to yet -- see DECISIONS.md's 2026-09-07 entry,
/// "Platform fonts," for the full account). Two bugs stack, not one:
/// `DEFAULT_GENERIC_FAMILIES` looks up the name `"monospace"` *before*
/// `fonts.xml` is parsed into that same name map, and even after parsing,
/// AOSP's `fonts.xml` names it with a `<family name="monospace">` element
/// (not an `<alias>`) whose `<font>` children the backend's own parser
/// does not read (a `TODO` in that match arm) -- so the name gets a
/// `FamilyId` with no font data behind it, and `family_by_name("monospace")`
/// comes back empty too. Confirmed on this checkout's emulator: `adb pull
/// /system/etc/fonts.xml` shows
/// `<family name="monospace"><font weight="400"
/// style="normal">DroidSansMono.ttf</font></family>` with no matching
/// alias.
///
/// So this reads `fonts.xml` itself (already on-device, already the
/// authority Compose's own `Typeface.MONOSPACE` resolves through) for the
/// filename that declaration names, then finds which of fontique's
/// *actually* scanned families (from `/system/fonts`, which do carry real
/// font data, just under whatever name the font's own metadata gives it --
/// "Droid Sans Mono" here, but that name is never hardcoded) owns a font
/// file with that name, and registers that family as the `Monospace`
/// generic the way the backend itself would have if its parser had reified
/// the declaration. A no-op if the family is somehow already resolved
/// (future fontique) or nothing matches (no `fonts.xml`, e.g. a headless
/// test, or a device that names it some other way).
#[cfg(target_os = "android")]
fn patch_android_monospace(font_cx: &mut FontContext) {
use parley::fontique::SourceKind;
let already_resolved = font_cx
.collection
.generic_families(GenericFamily::Monospace)
.next()
.is_some();
if already_resolved {
return;
}
let Some(target_file) = android_monospace_font_filename() else {
return;
};
let names: Vec<String> = font_cx
.collection
.family_names()
.map(str::to_string)
.collect();
for name in names {
let Some(id) = font_cx.collection.family_id(&name) else {
continue;
};
let Some(info) = font_cx.collection.family(id) else {
continue;
};
let Some(font) = info.default_font() else {
continue;
};
let SourceKind::Path(path) = font.source().kind() else {
continue;
};
if path.file_name().and_then(|f| f.to_str()) == Some(target_file.as_str()) {
font_cx
.collection
.append_generic_families(GenericFamily::Monospace, std::iter::once(id));
return;
}
}
}
/// Reads the font filename `fonts.xml` names for its `"monospace"` family
/// (e.g. `"DroidSansMono.ttf"`), by plain substring search rather than a
/// real XML parser -- a new dependency for one well-known, stable AOSP file
/// whose structure fontique itself already parses with a full parser one
/// module over. Not a general XML reader; assumes the file has exactly one
/// `<family name="monospace">` element with at least one `<font>` child,
/// which is the format on every AOSP `fonts.xml` this was checked against.
#[cfg(target_os = "android")]
fn android_monospace_font_filename() -> Option<String> {
let android_root = std::env::var("ANDROID_ROOT").unwrap_or_else(|_| "/system".to_string());
let xml =
std::fs::read_to_string(std::path::Path::new(&android_root).join("etc/fonts.xml")).ok()?;
let family_start = xml.find("<family name=\"monospace\">")?;
let block = &xml[family_start..];
let block = &block[..block.find("</family>")?];
let font_tag = block.find("<font")?;
let after_tag = &block[font_tag..];
let content_start = after_tag.find('>')? + 1;
let content = &after_tag[content_start..];
let filename = content[..content.find('<')?].trim();
(!filename.is_empty()).then(|| filename.to_string())
}
#[cfg(not(target_os = "android"))]
fn patch_android_monospace(_font_cx: &mut FontContext) {}
impl TextData { impl TextData {
/// Builds the startup report -- see `FontDiagnostics`. Queries the /// Builds the startup report -- see `FontDiagnostics`. Queries the
/// collection directly (`fontique::Query`) rather than shaping a real /// collection directly (`fontique::Query`) rather than shaping a real