iris: load application-owned fonts

This commit is contained in:
iris committed 2026-09-11 14:14:41 -04:00
1 parent f7e7950908
commit bdc9c914c4
19 files changed
+189 -87

No files matched your search

+1
View File
@@ -180,6 +180,7 @@ dependencies = [
"pulldown-cmark",
"serde",
"serde_json",
"swash",
"tempfile",
"tokio",
"ureq",
+1
View File
@@ -56,6 +56,7 @@ force-gles = ["screens", "iris/force-gles"]
[dev-dependencies]
tempfile = "3"
tokio = { version = "1.53.1", features = ["rt", "time"] }
swash = "0.2.10"
# APK builds select these profiles explicitly.
[profile.android-release]
+21
View File
@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2014 Ryan L McIntyre
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Binary file not shown.
+56
View File
@@ -0,0 +1,56 @@
#!/usr/bin/env bash
# Rebuilds app/assets/fonts/nerd_icons.ttf.
#
# ai-app draws its icons as glyphs in a Nerd Fonts subset it ships, rather
# than as ordinary Unicode out of whatever the platform resolved. Unicode's
# own geometric shapes are what this replaced: `tool.rs` set its disclosure
# mark with U+25B8/25BE/25B4, and once iris stopped bundling fonts
# (2026-09-07) Iris's phone drew an empty box for them and this VM drew a
# dot. UI_RULES: "don't rely on characters the platform might not have --
# ship the glyph or the asset rather than hoping."
#
# The whole symbols font is 3 MB for the handful below, so what is
# committed is a subset. Add a codepoint to GLYPHS *and* to
# `app/src/ui/icon.rs` (the two lists have to agree -- a codepoint in
# the Rust that this script did not subset is a glyph that silently isn't
# there), then run this and commit the result.
#
# Needs python3 and network access; fontTools is fetched into a temporary
# venv, so nothing has to be installed on the machine.
#
# The Mono face makes icon metrics stable; the Material Design family keeps
# their meanings conventional.
set -euo pipefail
GLYPHS=(
U+F035D # md-menu_down -- a card that is open
U+F035F # md-menu_right -- a card that opens
U+F0360 # md-menu_up -- collapse this group again
)
url=https://github.com/ryanoasis/nerd-fonts/releases/latest/download/NerdFontsSymbolsOnly.zip
here="$(cd "$(dirname "$0")" && pwd)"
out="$here/assets/fonts/nerd_icons.ttf"
work="$(mktemp -d)"
trap 'rm -rf "$work"' EXIT
echo "Fetching $url"
curl -fsSL -o "$work/nf.zip" "$url"
python3 -c 'import sys,zipfile; zipfile.ZipFile(sys.argv[1]).extractall(sys.argv[2])' "$work/nf.zip" "$work"
python3 -m venv "$work/venv"
"$work/venv/bin/pip" -q install fonttools
unicodes="$(IFS=,; echo "${GLYPHS[*]}")"
mkdir -p "$(dirname "$out")"
# The Mono face, where every glyph is one em wide and one em tall, so two
# icons at the same font size are the same size without either being given
# one, which makes an icon's box predictable beside a line of text.
"$work/venv/bin/pyftsubset" "$work/SymbolsNerdFontMono-Regular.ttf" \
--unicodes="$unicodes" \
--layout-features= \
--drop-tables+=DSIG \
--output-file="$out"
cp "$work/LICENSE" "$here/assets/fonts/NERD_FONTS_LICENSE.txt"
echo "Wrote $out ($(stat -c %s "$out") bytes) with ${#GLYPHS[@]} glyphs"
+2 -2
View File
@@ -114,6 +114,7 @@ fn battery_line(samples: &[i32]) -> String {
impl BenchClient {
pub(super) fn new(mut ui_state: AndroidUiState, rsc: &mut StdRsc<Self>) -> Self {
crate::ui::register_fonts(&mut rsc.ui);
let content = WidgetPtr::new().add(rsc);
let loading = placeholder(rsc, "Loading fixture...");
content(rsc).set(loading);
@@ -146,7 +147,7 @@ impl BenchClient {
let font = rsc.ui.text.font_diagnostics();
log::info!(
"iris fonts: {} families found, default={:?} mono={:?}, resolved regular={:?} \
bold={:?} italic={:?} mono={:?}, icons={:?}",
bold={:?} italic={:?} mono={:?}",
font.families_found,
font.default_family,
font.default_mono_family,
@@ -154,7 +155,6 @@ impl BenchClient {
font.bold_resolved,
font.italic_resolved,
font.mono_resolved,
font.icon_family,
);
let mut client = Self {
+1
View File
@@ -91,6 +91,7 @@ fn frame_report_controls(rsc: &mut StdRsc<TranscriptClient>) -> WeakWidget {
impl TranscriptClient {
pub(super) fn new(mut ui_state: AndroidUiState, rsc: &mut StdRsc<Self>) -> Self {
crate::ui::register_fonts(&mut rsc.ui);
let content = WidgetPtr::new().add(rsc);
let loading = placeholder(rsc, "Loading sessions...");
content(rsc).set(loading);
+1
View File
@@ -57,6 +57,7 @@ struct Client {
impl DesktopAppState for Client {
fn new(mut ui_state: DesktopUiState, rsc: &mut StdRsc<Self>) -> Self {
crate::ui::register_fonts(&mut rsc.ui);
let (server, ca_pem) = super::startup::load_startup_config().unwrap_or_else(|e| {
eprintln!("desktop-app: {e}");
process::exit(2);
+30
View File
@@ -0,0 +1,30 @@
pub const OPEN: &str = "\u{F035D}";
pub const CLOSED: &str = "\u{F035F}";
pub const COLLAPSE: &str = "\u{F0360}";
#[cfg(test)]
mod tests {
use super::*;
use swash::FontRef;
#[test]
fn every_icon_is_in_the_application_font() {
let font = FontRef::from_index(super::super::ICON_FONT, 0)
.expect("the application icon font parses");
let charmap = font.charmap();
for (name, glyph) in [("OPEN", OPEN), ("CLOSED", CLOSED), ("COLLAPSE", COLLAPSE)] {
let mut chars = glyph.chars();
let ch = chars.next().expect("an icon is one character");
assert!(chars.next().is_none(), "{name} is more than one character");
assert_ne!(
charmap.map(ch),
0,
"{name} (U+{:04X}) is not in nerd_icons.ttf -- add it to \
build-icon-font.sh's GLYPHS and rerun the script",
ch as u32
);
}
}
}
+12
View File
@@ -2,6 +2,7 @@ pub mod composer;
// Keep the 1.9 MB fixture out of ordinary APKs.
#[cfg(feature = "fixture")]
pub mod fixture;
mod icon;
pub mod markdown;
pub mod row;
pub(crate) mod tap;
@@ -13,6 +14,16 @@ use iris::prelude::*;
use std::{mem, rc::Rc};
use theme::Theme;
const ICON_FONT: &[u8] = include_bytes!("../../assets/fonts/nerd_icons.ttf");
pub(crate) fn register_fonts(ui: &mut Ui) {
if ui.is_font_registered(&Family::Icons) {
return;
}
ui.register_font(Family::Icons, ICON_FONT)
.expect("the ai-app icon font must register before text is drawn");
}
pub struct TranscriptScreen {
/// The transcript's own `LazySpan` -- the layout *and* the scroll
/// position, since a lazy span owns a `ScrollController` of its own
@@ -241,6 +252,7 @@ pub fn build_tree<Rsc: HasEvents>(
where
Rsc::State: FocusHost + OpenUrl,
{
register_fonts(rsc.ui_mut());
let theme = Rc::new(Theme::new(&mut rsc.ui_mut().paints));
let list = LazySpan::new(Dir::DOWN, Pin::End).add(rsc);
list.controller(
+1
View File
@@ -1,6 +1,7 @@
use crate::client::text_cap::{VERBATIM_BYTES, VERBATIM_LINES, cut, show_all_label};
use crate::client::tool_summary::{ToolInput, parse_tool_input};
use crate::client::transcript_fold::{ToolState, TranscriptItem};
use crate::ui::icon;
use crate::ui::markdown::highlight_into;
use crate::ui::tap::{hold_edge, on_tap};
use crate::ui::theme::Theme;