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
|
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)
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in new issue
Block a user