iris: load application-owned fonts
This commit is contained in:
1 parent
f7e7950908
commit
bdc9c914c4
19 files changed
+189
-87
No files matched your search
Generated
+1
@@ -180,6 +180,7 @@ dependencies = [
|
||||
"pulldown-cmark",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"swash",
|
||||
"tempfile",
|
||||
"tokio",
|
||||
"ureq",
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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.
Executable
+56
@@ -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"
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,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;
|
||||
|
||||
Reference in new issue
Block a user