diff --git a/docs/DECISIONS.md b/docs/DECISIONS.md
index 6eb087a..cd5dfb0 100644
--- a/docs/DECISIONS.md
+++ b/docs/DECISIONS.md
@@ -5,6 +5,40 @@ they can be judged and reversed later. Detail lives in RUST.md (and IRIS.md
for iris API changes); this file is only the summary. Newest first. Items
marked **DEFERRED** are ones the agent chose not to decide alone.
+## 2026-09-07 (platform fonts, not bundled ones)
+
+- **Iris's own decision, carried out as directed**: removed the 3.6 MB of
+ bundled Noto Sans/Noto Sans Mono TTFs from `iris-core` and load text
+ from the platform's own font collection instead (`fontique`'s system
+ discovery, already on by default). Matches what the Compose app does --
+ it takes body text from `FontFamily.Default` and code text from
+ `FontFamily.Monospace`, both platform-resolved, and ships no text font
+ of its own. Rejected alternative (the one this pass had left open
+ 2026-09-06): subsetting the bundled Noto Sans to Latin/common
+ punctuation instead of removing it outright, which would have kept
+ identical rendering across devices for a smaller (not zero) size cost;
+ Iris chose to match Compose instead.
+- `.so` **-3,748,136 bytes** (11,193,608 -> 7,445,472), matching the
+ original 3.6 MB estimate. Fallback still lands on the platform's own
+ 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.
+
## 2026-09-06 (how a tool call looks, P1b)
- **A card that never got a result says "no result", in yellow, and it is
diff --git a/docs/IRIS.md b/docs/IRIS.md
index 2f39bfe..f3eab5d 100644
--- a/docs/IRIS.md
+++ b/docs/IRIS.md
@@ -8,6 +8,27 @@ capability that moved. Small and trivial changes do not go here.
An entry gives the date, what changed, why, and a short before/after where
it helps judge the change without the session that made it. Newest first.
+## 2026-09-07: `TextData` no longer bundles a font
+
+Iris's call: "remove the font for now; just match what compose does."
+`TextData::default()` used to embed six Noto Sans/Noto Sans Mono `.ttf`s
+(3.6 MB, `include_bytes!`) and register them ahead of the platform's own
+fonts in the `SansSerif`/`Monospace` fallback lists. That registration is
+gone; `TextData::default()`'s signature is unchanged, but what it produces
+now depends entirely on `fontique`'s platform discovery (already on by
+default, previously shadowed) -- Roboto/Roboto Flex on Android, whatever
+the desktop's fontconfig resolves on Linux. No caller-visible type or
+method changed, but every consumer of `iris-core` text now renders with
+whatever the host platform's fonts are, not a fixed bundled face -- worth
+knowing if you were relying on pixel-identical text across devices.
+`.so` shrank by 3.75 MB. One real gap surfaced by the switch: this
+fontique version's Android backend never resolves the `Monospace`
+generic family (a fontique ordering bug, not new in this change), so
+`Family::Monospace` text falls through to the same face as
+`SansSerif` on Android rather than a true monospaced one -- still
+visible, not blank, just not monospaced. docs/RUST.md's "Platform fonts
+(2026-09-07)" has the full account.
+
## 2026-09-07: a headless harness, replayed touch, and physical-pixel desktop layout
Layer 1 and 2 of docs/RUST.md's "Three test layers".
diff --git a/docs/RUST.md b/docs/RUST.md
index 652438f..db6e13c 100644
--- a/docs/RUST.md
+++ b/docs/RUST.md
@@ -110,6 +110,7 @@ override, reusing the incremental `target/`), and are the raw file sizes
| + `codegen-units = 1` | 15,185,204 | 13,052,736 | -566,008 |
| + `opt-level = "s"` | 13,326,076 | 11,193,608 | -1,859,128 |
| + `opt-level = "z"` (not adopted, see below) | 12,507,276 | 10,374,808 | -818,800 |
+| + platform fonts, no bundled Noto (2026-09-07) | 9,577,940 | 7,445,472 | -3,748,136 |
Adopted: `strip = true`, `lto = "fat"`, `codegen-units = 1`, `opt-level = "s"`.
Baseline to final: `libmain.so` **18,546,488 -> 11,193,608 bytes (-39.7%)**,
@@ -181,7 +182,99 @@ rather than a fixed-codepoint one -- a real behaviour change (text in a
language outside the subset would fall back to tofu or a missing glyph) and
out of scope for a size-only pass. Left as a follow-up, flagged for Iris:
subsetting would plausibly save 1-2 MB but changes what scripts render
-correctly, which is a product decision.
+correctly, which is a product decision. **Superseded 2026-09-07**: Iris
+decided to remove the embedding outright rather than subset it -- see
+"Platform fonts (2026-09-07)" below.
+
+### Platform fonts (2026-09-07)
+
+Iris's verdict on the open question above: **"remove the font for now;
+just match what compose does."** The Compose app takes its body text from
+`FontFamily.Default` (platform Roboto on Android) and its code/tool-output
+text from `FontFamily.Monospace`, and ships no text font of its own --
+only its committed Nerd Fonts icon subset (`app/build-icon-font.sh`) for
+~100 fixed glyphs, a different case (a small, known, closed set of
+codepoints, unlike arbitrary transcript text). iris had no equivalent icon
+font to keep; it draws no icons through a font today, so there was nothing
+parallel to preserve.
+
+**What changed**: `TextData::register_bundled_fonts` and the six
+`include_bytes!` Noto Sans/Noto Sans Mono constants are gone from
+`iris/core/src/primitive/text.rs`, along with the `.ttf`s themselves and
+their `OFL.txt` (`iris/core/assets/fonts/`, now removed -- nothing else in
+the tree referenced the licence file). `TextData::default` now does
+nothing but `FontContext::new()`, which was already discovering the
+platform's fonts underneath the bundled ones -- `fontique` 0.11.1's
+`CollectionOptions::system_fonts` defaults to `true`, and both platforms
+this crate ships on have a real backend behind it: `backend/fontconfig.rs`
+on Linux (this VM's desktop has a full Noto install, confirmed with
+`fc-match sans-serif`/`fc-match monospace`), `backend/android.rs` on
+Android (parses `/system/fonts` and `/system/etc/fonts.xml`, mapping
+`SansSerif`/`SystemUi` to `["Roboto Flex", "Roboto", "Noto Sans"]` and
+`Monospace` to `["monospace"]` -- see the fallback finding below for why
+that last one does not actually resolve on this fontique version). So
+removing the bundled registration did not need a replacement call; the
+platform path was already live, just shadowed.
+
+**Fallback, and the unknown-glyph state (UI_RULES: design it, don't let
+it default to blank)**. `parley`'s shaper sets both an explicit family
+list *and* a script/locale-keyed fallback chain per run
+(`parley-0.11.1/src/shape/mod.rs`'s `query.set_families`/
+`query.set_fallbacks`), so a codepoint the resolved family lacks still
+gets a query against fontique's fallback map before giving up. Checked
+with a throwaway example (`iris/examples/font_check.rs`, deleted after
+use -- not part of the crate) shaping `"ðŸŽ‰ðŸ”¥â–¸â–¾â–²ä½ å¥½"` headless on the
+desktop at `run-headless.sh`: the emoji and CJK characters drew as
+visible **tofu boxes** (the platform's own missing-glyph box, not blank
+space), and the chevron marks (U+25B8/25BE/25B4, the ones
+`transcript-ui/src/tool.rs`'s `CLOSED_MARK`/`OPEN_MARK`/`UP_MARK` draw)
+shaped as real triangles. So the failure mode this crate now depends on
+is "the platform's own tofu," which is the correct unknown-glyph state
+per UI_RULES, not "nothing drawn." `tool.rs`'s doc comment on those marks
+is updated to say this is now a bet on the platform's coverage rather
+than a checked fact about a bundled `cmap`.
+
+**One real gap, found on this checkout's emulator, not the desktop**:
+`fonts: 208 families found, default=Some("Roboto Flex") mono=None` in the
+startup log (`FontDiagnostics`, read via `adb logcat` after installing the
+`force-gles` debug build -- the emulator's default Vulkan backend has no
+adapter here, a pre-existing, documented condition unrelated to this
+change, and aborts with `Could not get adapter!` without that feature).
+`mono=None` means fontique's Android backend never resolves the
+`Monospace` generic family at all on this system image: reading
+`fontique-0.11.1/src/backend/android.rs`, `DEFAULT_GENERIC_FAMILIES`'s
+`["monospace"]` is looked up against `name_map` *before* the `fonts.xml`
+parse that would register a family literally named `"monospace"` runs --
+so even though this AVD's `/system/etc/fonts.xml` does declare
+`DroidSansMono.ttf`,
+fontique's own ordering means that declaration is registered too late to
+be found by the generic-family lookup, on every Android device this
+fontique version runs on, not just this AVD. The visible effect is not
+blank text -- `Family::Monospace`'s explicit-family list comes up empty,
+but the script-based fallback chain (independent of the generic-family
+list) still resolves a real font, the same one `SansSerif` gets -- so
+code blocks and the tool-card chevrons render, just without a genuinely
+monospaced face. Compose does not have this gap: `FontFamily.Monospace`
+resolves through Android's own `Typeface.MONOSPACE` constant, a different
+and unconditionally-populated path that fontique does not use. Left as a
+follow-up rather than fixed here, since a fix means either patching
+around fontique's Android backend or pinning `Family::Named("Droid Sans
+Mono")` (fragile: an OEM-specific font name, not guaranteed across real
+devices) -- out of the scope Iris gave this pass ("remove the font,
+match Compose"), and a real product-visible difference worth her knowing
+about rather than silently living with.
+
+**Verified**: `cargo test -p transcript-fixture` (6 tests, all headless
+layers) and `cargo clippy -p iris-core --all-targets` both clean;
+`./run-headless.sh phone --phone --shot` (the bench-fixture transcript,
+bold/italic/monospace code fences all shaping correctly) and
+`./run-headless.sh tabs` (the desktop example with several distinct text
+styles, per this task's "negative case" check) both look right by eye;
+the emulator's own bench run (`run-bench.sh`, `force-gles` debug build)
+completed a full fling/stream/type/keyboard cycle with text visibly
+drawn throughout and no crash. `.so` size: see the APK size table's final
+row, **11,193,608 -> 7,445,472 bytes**, a 3,748,136-byte drop matching
+the 3.6 MB estimate almost exactly.
**naga/wgpu backend features: investigated, not trimmed, because the
trim would not change the binary.** `iris/core/Cargo.toml` and
@@ -279,14 +372,22 @@ closes it.
recent log; a debug button copies it; Dev Updater reads it), write
the decision in docs/DECISIONS.md, build it.
- [x] APK size: release profile tuned (`42af780`), -35% APK, -40% .so;
- see "APK size (2026-09-07)". **Open question for Iris**: 3.6 MB of the
- remaining 11.2 MB .so is six embedded Noto Sans TTFs
- (`iris/core/src/primitive/text.rs`). Loading the platform's own fonts
- instead (fontique's system collection: Roboto/Noto on Android, the
- desktop's own on Linux) removes them and makes text match what the
- rest of her phone shows (UI_RULES "show what the reader already sees
- elsewhere"), at the cost of the app no longer looking identical on
- every device. Not done without her verdict.
+ see "APK size (2026-09-07)". **Iris's verdict, 2026-09-07: "remove the
+ font for now; just match what compose does."** Done same day -- the six
+ bundled Noto Sans TTFs are gone, text now loads from
+ `fontique`'s platform collection (`FontContext::new()`'s default
+ `CollectionOptions::system_fonts`), and `.so` dropped by 3,748,136 bytes
+ (11,193,608 -> 7,445,472), matching the 3.6 MB estimate almost exactly.
+ See "APK size" table's final row and "Platform fonts (2026-09-07)"
+ below for the fallback behaviour and one real gap it surfaced: this
+ fontique version's Android backend never resolves the `Monospace`
+ generic family at all (`mono=None` in the startup diagnostic, measured
+ on this checkout's emulator) -- code/tool-card text still renders (the
+ script fallback chain still lands on a real face, never blank), just
+ not in a genuinely monospaced one. Compose does not have this gap; it
+ resolves `FontFamily.Monospace` through Android's own Typeface
+ constant rather than through fontique. Flagged as a follow-up, not
+ fixed here -- out of the scope Iris gave.
- [ ] Scroll clamped at both ends, and Compose's impulse velocity
estimator with min/max fling velocity (docs/IRIS_TODO.md, 2026-09-07
later). After the culling fix lands (same file).
diff --git a/iris/core/assets/fonts/NotoSans-Bold.ttf b/iris/core/assets/fonts/NotoSans-Bold.ttf
deleted file mode 100644
index aae7546..0000000
Binary files a/iris/core/assets/fonts/NotoSans-Bold.ttf and /dev/null differ
diff --git a/iris/core/assets/fonts/NotoSans-BoldItalic.ttf b/iris/core/assets/fonts/NotoSans-BoldItalic.ttf
deleted file mode 100644
index 6f685b2..0000000
Binary files a/iris/core/assets/fonts/NotoSans-BoldItalic.ttf and /dev/null differ
diff --git a/iris/core/assets/fonts/NotoSans-Italic.ttf b/iris/core/assets/fonts/NotoSans-Italic.ttf
deleted file mode 100644
index 7f53133..0000000
Binary files a/iris/core/assets/fonts/NotoSans-Italic.ttf and /dev/null differ
diff --git a/iris/core/assets/fonts/NotoSans-Regular.ttf b/iris/core/assets/fonts/NotoSans-Regular.ttf
deleted file mode 100644
index f27f4ff..0000000
Binary files a/iris/core/assets/fonts/NotoSans-Regular.ttf and /dev/null differ
diff --git a/iris/core/assets/fonts/NotoSansMono-Bold.ttf b/iris/core/assets/fonts/NotoSansMono-Bold.ttf
deleted file mode 100644
index 9218a79..0000000
Binary files a/iris/core/assets/fonts/NotoSansMono-Bold.ttf and /dev/null differ
diff --git a/iris/core/assets/fonts/NotoSansMono-Regular.ttf b/iris/core/assets/fonts/NotoSansMono-Regular.ttf
deleted file mode 100644
index 159ca4b..0000000
Binary files a/iris/core/assets/fonts/NotoSansMono-Regular.ttf and /dev/null differ
diff --git a/iris/core/assets/fonts/OFL.txt b/iris/core/assets/fonts/OFL.txt
deleted file mode 100644
index 261eeb9..0000000
--- a/iris/core/assets/fonts/OFL.txt
+++ /dev/null
@@ -1,201 +0,0 @@
- Apache License
- Version 2.0, January 2004
- http://www.apache.org/licenses/
-
- TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
-
- 1. Definitions.
-
- "License" shall mean the terms and conditions for use, reproduction,
- and distribution as defined by Sections 1 through 9 of this document.
-
- "Licensor" shall mean the copyright owner or entity authorized by
- the copyright owner that is granting the License.
-
- "Legal Entity" shall mean the union of the acting entity and all
- other entities that control, are controlled by, or are under common
- control with that entity. For the purposes of this definition,
- "control" means (i) the power, direct or indirect, to cause the
- direction or management of such entity, whether by contract or
- otherwise, or (ii) ownership of fifty percent (50%) or more of the
- outstanding shares, or (iii) beneficial ownership of such entity.
-
- "You" (or "Your") shall mean an individual or Legal Entity
- exercising permissions granted by this License.
-
- "Source" form shall mean the preferred form for making modifications,
- including but not limited to software source code, documentation
- source, and configuration files.
-
- "Object" form shall mean any form resulting from mechanical
- transformation or translation of a Source form, including but
- not limited to compiled object code, generated documentation,
- and conversions to other media types.
-
- "Work" shall mean the work of authorship, whether in Source or
- Object form, made available under the License, as indicated by a
- copyright notice that is included in or attached to the work
- (an example is provided in the Appendix below).
-
- "Derivative Works" shall mean any work, whether in Source or Object
- form, that is based on (or derived from) the Work and for which the
- editorial revisions, annotations, elaborations, or other modifications
- represent, as a whole, an original work of authorship. For the purposes
- of this License, Derivative Works shall not include works that remain
- separable from, or merely link (or bind by name) to the interfaces of,
- the Work and Derivative Works thereof.
-
- "Contribution" shall mean any work of authorship, including
- the original version of the Work and any modifications or additions
- to that Work or Derivative Works thereof, that is intentionally
- submitted to Licensor for inclusion in the Work by the copyright owner
- or by an individual or Legal Entity authorized to submit on behalf of
- the copyright owner. For the purposes of this definition, "submitted"
- means any form of electronic, verbal, or written communication sent
- to the Licensor or its representatives, including but not limited to
- communication on electronic mailing lists, source code control systems,
- and issue tracking systems that are managed by, or on behalf of, the
- Licensor for the purpose of discussing and improving the Work, but
- excluding communication that is conspicuously marked or otherwise
- designated in writing by the copyright owner as "Not a Contribution."
-
- "Contributor" shall mean Licensor and any individual or Legal Entity
- on behalf of whom a Contribution has been received by Licensor and
- subsequently incorporated within the Work.
-
- 2. Grant of Copyright License. Subject to the terms and conditions of
- this License, each Contributor hereby grants to You a perpetual,
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
- copyright license to reproduce, prepare Derivative Works of,
- publicly display, publicly perform, sublicense, and distribute the
- Work and such Derivative Works in Source or Object form.
-
- 3. Grant of Patent License. Subject to the terms and conditions of
- this License, each Contributor hereby grants to You a perpetual,
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
- (except as stated in this section) patent license to make, have made,
- use, offer to sell, sell, import, and otherwise transfer the Work,
- where such license applies only to those patent claims licensable
- by such Contributor that are necessarily infringed by their
- Contribution(s) alone or by combination of their Contribution(s)
- with the Work to which such Contribution(s) was submitted. If You
- institute patent litigation against any entity (including a
- cross-claim or counterclaim in a lawsuit) alleging that the Work
- or a Contribution incorporated within the Work constitutes direct
- or contributory patent infringement, then any patent licenses
- granted to You under this License for that Work shall terminate
- as of the date such litigation is filed.
-
- 4. Redistribution. You may reproduce and distribute copies of the
- Work or Derivative Works thereof in any medium, with or without
- modifications, and in Source or Object form, provided that You
- meet the following conditions:
-
- (a) You must give any other recipients of the Work or
- Derivative Works a copy of this License; and
-
- (b) You must cause any modified files to carry prominent notices
- stating that You changed the files; and
-
- (c) You must retain, in the Source form of any Derivative Works
- that You distribute, all copyright, patent, trademark, and
- attribution notices from the Source form of the Work,
- excluding those notices that do not pertain to any part of
- the Derivative Works; and
-
- (d) If the Work includes a "NOTICE" text file as part of its
- distribution, then any Derivative Works that You distribute must
- include a readable copy of the attribution notices contained
- within such NOTICE file, excluding those notices that do not
- pertain to any part of the Derivative Works, in at least one
- of the following places: within a NOTICE text file distributed
- as part of the Derivative Works; within the Source form or
- documentation, if provided along with the Derivative Works; or,
- within a display generated by the Derivative Works, if and
- wherever such third-party notices normally appear. The contents
- of the NOTICE file are for informational purposes only and
- do not modify the License. You may add Your own attribution
- notices within Derivative Works that You distribute, alongside
- or as an addendum to the NOTICE text from the Work, provided
- that such additional attribution notices cannot be construed
- as modifying the License.
-
- You may add Your own copyright statement to Your modifications and
- may provide additional or different license terms and conditions
- for use, reproduction, or distribution of Your modifications, or
- for any such Derivative Works as a whole, provided Your use,
- reproduction, and distribution of the Work otherwise complies with
- the conditions stated in this License.
-
- 5. Submission of Contributions. Unless You explicitly state otherwise,
- any Contribution intentionally submitted for inclusion in the Work
- by You to the Licensor shall be under the terms and conditions of
- this License, without any additional terms or conditions.
- Notwithstanding the above, nothing herein shall supersede or modify
- the terms of any separate license agreement you may have executed
- with Licensor regarding such Contributions.
-
- 6. Trademarks. This License does not grant permission to use the trade
- names, trademarks, service marks, or product names of the Licensor,
- except as required for reasonable and customary use in describing the
- origin of the Work and reproducing the content of the NOTICE file.
-
- 7. Disclaimer of Warranty. Unless required by applicable law or
- agreed to in writing, Licensor provides the Work (and each
- Contributor provides its Contributions) on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
- implied, including, without limitation, any warranties or conditions
- of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
- PARTICULAR PURPOSE. You are solely responsible for determining the
- appropriateness of using or redistributing the Work and assume any
- risks associated with Your exercise of permissions under this License.
-
- 8. Limitation of Liability. In no event and under no legal theory,
- whether in tort (including negligence), contract, or otherwise,
- unless required by applicable law (such as deliberate and grossly
- negligent acts) or agreed to in writing, shall any Contributor be
- liable to You for damages, including any direct, indirect, special,
- incidental, or consequential damages of any character arising as a
- result of this License or out of the use or inability to use the
- Work (including but not limited to damages for loss of goodwill,
- work stoppage, computer failure or malfunction, or any and all
- other commercial damages or losses), even if such Contributor
- has been advised of the possibility of such damages.
-
- 9. Accepting Warranty or Additional Liability. While redistributing
- the Work or Derivative Works thereof, You may choose to offer,
- and charge a fee for, acceptance of support, warranty, indemnity,
- or other liability obligations and/or rights consistent with this
- License. However, in accepting such obligations, You may act only
- on Your own behalf and on Your sole responsibility, not on behalf
- of any other Contributor, and only if You agree to indemnify,
- defend, and hold each Contributor harmless for any liability
- incurred by, or claims asserted against, such Contributor by reason
- of your accepting any such warranty or additional liability.
-
- END OF TERMS AND CONDITIONS
-
- APPENDIX: How to apply the Apache License to your work.
-
- To apply the Apache License to your work, attach the following
- boilerplate notice, with the fields enclosed by brackets "[]"
- replaced with your own identifying information. (Don't include
- the brackets!) The text should be enclosed in the appropriate
- comment syntax for the file format. We also recommend that a
- file or class name and description of purpose be included on the
- same "printed page" as the copyright notice for easier
- identification within third-party archives.
-
- Copyright [yyyy] [name of copyright owner]
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
diff --git a/iris/core/src/primitive/text.rs b/iris/core/src/primitive/text.rs
index 62dbf69..4810c19 100644
--- a/iris/core/src/primitive/text.rs
+++ b/iris/core/src/primitive/text.rs
@@ -2,34 +2,14 @@ use crate::{Align, GlyphAtlas, GlyphKey, PlacedGlyph, RegionAlign, Textures, UiC
use parley::{
Alignment, AlignmentOptions, FontContext, FontFamily, FontFamilyName, FontStyle, FontWeight,
GenericFamily, Layout, LayoutContext, LineHeight, PositionedLayoutItem, StyleProperty,
- fontique::{Blob, FamilyId},
};
use std::ops::Range;
-use std::sync::Arc;
use swash::{
FontRef,
scale::{Render, ScaleContext, Source, StrikeWith},
zeno::{Format, Vector},
};
-/// Bundled fonts, registered over the system collection rather than relied
-/// on alone -- see `TextData::register_bundled_fonts`'s doc comment for
-/// why. Static weight/style cuts, not a variable font: parley/fontique
-/// resolve a variable font's weight axis by picking normalized coordinates
-/// on whatever single face registers for the family, and a phone whose
-/// system "Roboto" is actually the variable "Roboto Flex" is exactly the
-/// device class this sidesteps, rather than depends on working correctly.
-/// Noto Sans, OFL-licensed (`assets/fonts/OFL.txt`), chosen for coverage
-/// breadth (a transcript's content is not known in advance) over a
-/// smaller-footprint alternative -- see the doc comment for the size this
-/// added.
-const NOTO_SANS_REGULAR: &[u8] = include_bytes!("../../assets/fonts/NotoSans-Regular.ttf");
-const NOTO_SANS_BOLD: &[u8] = include_bytes!("../../assets/fonts/NotoSans-Bold.ttf");
-const NOTO_SANS_ITALIC: &[u8] = include_bytes!("../../assets/fonts/NotoSans-Italic.ttf");
-const NOTO_SANS_BOLD_ITALIC: &[u8] = include_bytes!("../../assets/fonts/NotoSans-BoldItalic.ttf");
-const NOTO_SANS_MONO_REGULAR: &[u8] = include_bytes!("../../assets/fonts/NotoSansMono-Regular.ttf");
-const NOTO_SANS_MONO_BOLD: &[u8] = include_bytes!("../../assets/fonts/NotoSansMono-Bold.ttf");
-
/// What starting up found about text rendering, for the on-screen
/// Diagnostics page and the one startup log line (RUST.md's P0 box, "log
/// once at startup ... the number of font families found, the default
@@ -80,91 +60,31 @@ pub struct TextData {
}
impl Default for TextData {
+ /// Text comes entirely from the platform's own font collection --
+ /// `FontContext::new()` builds a `fontique::Collection` with
+ /// `CollectionOptions::system_fonts` on by default, which is real
+ /// discovery on both targets this crate ships on: Android's backend
+ /// parses `/system/fonts` and `/system/etc/fonts.xml` and maps
+ /// `SansSerif`/`SystemUi` to `["Roboto Flex", "Roboto", "Noto Sans"]`
+ /// and `Monospace` to the platform's `"monospace"` alias; the desktop
+ /// build's backend is fontconfig. No font is bundled or registered
+ /// here -- see DECISIONS.md's 2026-09-07 entry for why (matching what
+ /// the Compose app does: it takes body/monospace text from
+ /// `FontFamily.Default`/`FontFamily.Monospace`, i.e. Android's Roboto
+ /// 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 data = Self {
+ Self {
font_cx: FontContext::new(),
layout_cx: LayoutContext::new(),
scale_cx: ScaleContext::new(),
atlas: GlyphAtlas::default(),
density: 1.0,
- };
- data.register_bundled_fonts();
- data
+ }
}
}
impl TextData {
- /// Registers Noto Sans (regular/bold/italic/bold-italic) and Noto Sans
- /// Mono (regular/bold) as static faces, and puts them **first** in the
- /// `SansSerif`/`Monospace` generic-family fallback lists -- ahead of,
- /// not instead of, whatever the platform already found, so a script
- /// Noto Sans lacks (CJK, emoji, ...) still falls through to the system
- /// font the same as before this existed.
- ///
- /// Exists because text rendering must not depend on the platform's own
- /// font enumeration succeeding or resolving weight/style the way this
- /// crate assumes: RUST.md's P0 box found bold spans on a real phone
- /// rendering as blank gaps of the correct advance width (the glyph
- /// simply wasn't rasterised -- `TextData::place`'s `None` arm), while
- /// the emulator's system fonts happened to resolve every style. A
- /// bundled, static-per-style family removes fontique's Android font
- /// scan (`fontique::backend::android::SystemFonts::new`, which parses
- /// `/system/fonts` and `/system/etc/fonts.xml`) from the path a glyph
- /// has to survive to reach the screen at all.
- ///
- /// Cost: six static `.ttf`s, ~3.6 MB uncompressed
- /// (`iris/core/assets/fonts/`), landing in the APK compressed --
- /// `build-apk.sh`'s own output is what says the delivered number, not
- /// this comment.
- fn register_bundled_fonts(&mut self) {
- fn register(cx: &mut FontContext, bytes: &'static [u8]) -> Option {
- let blob = Blob::new(Arc::new(bytes));
- cx.collection
- .register_fonts(blob, None)
- .into_iter()
- .map(|(id, _)| id)
- .next()
- }
- let sans_id = register(&mut self.font_cx, NOTO_SANS_REGULAR);
- register(&mut self.font_cx, NOTO_SANS_BOLD);
- register(&mut self.font_cx, NOTO_SANS_ITALIC);
- register(&mut self.font_cx, NOTO_SANS_BOLD_ITALIC);
- let mono_id = register(&mut self.font_cx, NOTO_SANS_MONO_REGULAR);
- register(&mut self.font_cx, NOTO_SANS_MONO_BOLD);
-
- if let Some(sans_id) = sans_id {
- let existing: Vec<_> = self
- .font_cx
- .collection
- .generic_families(GenericFamily::SansSerif)
- .collect();
- self.font_cx.collection.set_generic_families(
- GenericFamily::SansSerif,
- std::iter::once(sans_id).chain(existing),
- );
- let existing: Vec<_> = self
- .font_cx
- .collection
- .generic_families(GenericFamily::SystemUi)
- .collect();
- self.font_cx.collection.set_generic_families(
- GenericFamily::SystemUi,
- std::iter::once(sans_id).chain(existing),
- );
- }
- if let Some(mono_id) = mono_id {
- let existing: Vec<_> = self
- .font_cx
- .collection
- .generic_families(GenericFamily::Monospace)
- .collect();
- self.font_cx.collection.set_generic_families(
- GenericFamily::Monospace,
- std::iter::once(mono_id).chain(existing),
- );
- }
- }
-
/// Builds the startup report -- see `FontDiagnostics`. Queries the
/// collection directly (`fontique::Query`) rather than shaping a real
/// string, since all that's needed is which family each axis lands on.
diff --git a/iris/transcript-ui/src/tool.rs b/iris/transcript-ui/src/tool.rs
index 5c5d58a..550df77 100644
--- a/iris/transcript-ui/src/tool.rs
+++ b/iris/transcript-ui/src/tool.rs
@@ -110,13 +110,19 @@ const _: () = assert!(OUTPUT_LINES > 0 && OUTPUT_BYTES > 0);
/// The mark that says a card opens, always drawn from the **monospace**
/// face.
///
-/// Not a style choice: `NotoSans-Regular.ttf`, which every other string
-/// here is set in, has no glyph at U+25B8/U+25BE/U+25B4 at all, while
-/// `NotoSansMono-Regular.ttf` does -- read out of both bundled `cmap`s on
-/// 2026-09-06. A missing glyph is the failure nobody who wrote the code
-/// ever sees, so the face that has the glyph is named at the one place the
-/// character is written. IRIS_TODO's "a drawn chevron" has the real fix,
-/// which needs a line primitive iris does not have.
+/// Not a style choice, historically: with the bundled Noto Sans this crate
+/// used to embed, the sans face every other string here is set in had no
+/// glyph at U+25B8/U+25BE/U+25B4 at all, while the bundled monospace face
+/// did. As of 2026-09-07 iris takes both faces from the platform's own
+/// collection instead (DECISIONS.md's 2026-09-07 entry, matching what
+/// Compose ships), so this is no longer a checked fact about a specific
+/// font's `cmap` -- it is a bet that whatever the platform resolves for
+/// `Family::Monospace` covers these three codepoints, same as it was
+/// before. If a platform's monospace face turns out not to, the fallback
+/// chain still applies (see `TextData::default`'s doc comment) and the
+/// glyph should still land, just not necessarily monospaced. IRIS_TODO's
+/// "a drawn chevron" has the real fix, which needs a line primitive iris
+/// does not have.
const CLOSED_MARK: &str = "\u{25b8}";
const OPEN_MARK: &str = "\u{25be}";
const UP_MARK: &str = "\u{25b4}";