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:
1 parent
84a13e806b
commit
7e4e26a335
2 files changed
+122
-16
No files matched your search
+22
-15
@@ -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
|
||||
desktop) rather than blank space, so the UI_RULES unknown-glyph rule
|
||||
still holds.
|
||||
- **Flagging for Iris, not blocking on it**: this fontique version's
|
||||
Android backend never resolves the `Monospace` generic family at all
|
||||
(confirmed on this checkout's emulator, `mono=None` in the startup
|
||||
diagnostic) -- a pre-existing ordering bug in fontique's own
|
||||
`fonts.xml` parsing, not something this change introduced, but this
|
||||
change is what stopped masking it (the bundled mono font used to be
|
||||
registered ahead of the broken platform lookup, so it always won).
|
||||
Effect: code blocks and the tool-card chevron marks fall through to
|
||||
the same face as body text on Android instead of a genuinely
|
||||
monospaced one -- still visible, not blank, just not monospaced.
|
||||
Compose does not share this gap; it resolves `FontFamily.Monospace`
|
||||
through Android's own `Typeface.MONOSPACE`, not through fontique.
|
||||
docs/RUST.md's "Platform fonts (2026-09-07)" has the full account and
|
||||
why a fix (an OEM-specific named-family pin, or patching around
|
||||
fontique) is left as a follow-up rather than done in this pass.
|
||||
- **Gap found, then closed same day**: this fontique version's Android
|
||||
backend never resolved the `Monospace` generic family at all (confirmed
|
||||
on this checkout's emulator, `mono=None` in the startup diagnostic) --
|
||||
two pre-existing bugs in fontique's own `fonts.xml` parsing stacked (an
|
||||
ordering bug, and a `<family name="monospace">` declaration whose
|
||||
`<font>` children the backend's parser never reads), not something this
|
||||
change introduced, but this change is what stopped masking it (the
|
||||
bundled mono font used to be registered ahead of the broken platform
|
||||
lookup, so it always won). Checked `linebender/parley`'s `main` branch
|
||||
on GitHub: neither bug is fixed there, so there was no newer release to
|
||||
bump to. Fixed instead in `iris-core` itself
|
||||
(`TextData::patch_android_monospace`, Android-only): reads
|
||||
`/system/etc/fonts.xml`'s own `"monospace"` declaration for the font
|
||||
filename it names, then registers whichever of fontique's actually-
|
||||
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)
|
||||
|
||||
|
||||
@@ -74,8 +74,10 @@ impl Default for TextData {
|
||||
/// and its platform monospace face, and ships no text font of its own,
|
||||
/// only its committed Nerd Fonts icon subset for fixed glyphs).
|
||||
fn default() -> Self {
|
||||
let mut font_cx = FontContext::new();
|
||||
patch_android_monospace(&mut font_cx);
|
||||
Self {
|
||||
font_cx: FontContext::new(),
|
||||
font_cx,
|
||||
layout_cx: LayoutContext::new(),
|
||||
scale_cx: ScaleContext::new(),
|
||||
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 {
|
||||
/// Builds the startup report -- see `FontDiagnostics`. Queries the
|
||||
/// collection directly (`fontique::Query`) rather than shaping a real
|
||||
|
||||
Reference in new issue
Block a user