iris: ship an icon font subset, and delete the drawn mark
Iris asked why `mark` existed at all -- "the font should be working if
it's working for compose and nerd fonts are bundled". It was not: the
Compose app draws its icons from its own committed Nerd Fonts subset,
while iris, which bundles no font since 2026-09-07, was setting the
disclosure mark with bare geometric codepoints (U+25B8/25BE/25B4) out of
whatever face the platform resolved -- an empty box on her phone, a dot
on this VM. The 2026-09-07 note that "iris had no equivalent icon font to
keep" is the gap: it had none because it had never had one.
So iris ships the same kind of subset. iris/core/build-icon-font.sh is
the Compose script with its own GLYPHS list, writing a 992-byte
nerd_icons.ttf with three Material Design glyphs from the Mono face;
iris::icon names the codepoints; Family::Icons is how text asks for them.
The variant names an intention rather than a font name -- only TextData
knows what the file registered as, and it resolves it during shaping --
and it is a named family, never a generic one, so nothing falls back into
it for text and an icon cannot fall back out of it onto a system face
that happens to have the codepoint.
every_icon_is_in_the_bundled_font maps each constant through the shipped
font's charmap, which is the guard the script's "the two lists have to
agree" comment asks for. FontDiagnostics gains icon_family, so a build
whose font failed to register says so instead of drawing tofu; the
emulator reports icons=Some("Symbols Nerd Font Mono").
widget/mark.rs is deleted. It drew one correct triangle, but every
further icon would have been another rasteriser, and an icon as text
takes the size, colour and baseline of the line it sits in for free.
Looked at rather than only compiled: closed and open marks in
run-headless.sh phone --phone either side of a tap, and the collapse
bar's up mark under IRIS_TOOLS_EXPANDED=1.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
341b7a5922
commit
9e301f30c6
17 files changed
+421
-311
No files matched your search
@@ -101,6 +101,18 @@ the **Mono** face, where every glyph is one em square, which is what makes
|
||||
two icon buttons the same width without either being given one — and why
|
||||
`GLYPH_SIZE` is smaller than it looks like it should be.
|
||||
|
||||
**The Rust app does the same, from its own subset**:
|
||||
`iris/core/build-icon-font.sh` -> `iris/core/assets/fonts/nerd_icons.ttf`,
|
||||
with the codepoints named in `iris/core/src/icon.rs` and drawn as text
|
||||
with `Family::Icons`. Same rule about the two lists agreeing (there is a
|
||||
test, `every_icon_is_in_the_bundled_font`), same Mono face, same Material
|
||||
Design family so an icon means the same thing in both apps. Its subset is
|
||||
separate rather than shared because subsetting only what one app draws is
|
||||
the point. This is the **only** font iris bundles — body and monospace
|
||||
text come from the platform (docs/DECISIONS.md, 2026-09-07), and an icon
|
||||
is the opposite case: a small closed set of codepoints no system font is
|
||||
guaranteed to have.
|
||||
|
||||
## Checking your work
|
||||
|
||||
- **Server**: `./run-tests.sh` from the repo root (or `cargo test` from
|
||||
|
||||
@@ -5,6 +5,31 @@ 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-08 (iris ships an icon font, and the drawn mark is deleted)
|
||||
|
||||
- **Directed by Iris.** Her question on seeing `widget::mark`: "why does
|
||||
mark exist? The font should be working if it's working for compose and
|
||||
nerd fonts are bundled." It was not: the Compose app draws its icons
|
||||
from **its own committed Nerd Fonts subset**, while iris was setting
|
||||
the disclosure mark with bare Unicode geometric codepoints
|
||||
(U+25B8/25BE/25B4) out of whatever face the platform resolved -- an
|
||||
empty box on her phone, a dot on this VM. The 2026-09-07 entry below,
|
||||
which said "iris had no equivalent icon font to keep", is what left
|
||||
that gap: iris had no icon font because it had never had one, not
|
||||
because it needed none.
|
||||
- **So iris now bundles the same kind of subset**:
|
||||
`iris/core/build-icon-font.sh` writes
|
||||
`iris/core/assets/fonts/nerd_icons.ttf` (992 bytes, three Material
|
||||
Design glyphs today), `iris::icon` names the codepoints, and
|
||||
`Family::Icons` draws them. This does **not** reopen the platform-fonts
|
||||
decision: body and monospace text still come from the platform, and an
|
||||
icon is the opposite case -- a small, closed, known set of codepoints,
|
||||
which is exactly the division the Compose app already makes.
|
||||
- **`iris::widget::mark` is deleted** (added earlier the same day). It
|
||||
drew a correct triangle, but only a triangle, and every further icon
|
||||
would have been another bespoke rasteriser. An icon as text also takes
|
||||
the size, colour and baseline of the line it sits in for free.
|
||||
|
||||
## 2026-09-08 (the emulator is a GLES machine, and Vulkan is verified elsewhere)
|
||||
|
||||
- **Directed by Iris, carried out here**: "make sure the setup uses GL for
|
||||
|
||||
@@ -1422,3 +1422,47 @@ which is exactly the run nobody needs.
|
||||
`LogRing::try_tail_text`, because a panic raised while the ring's own
|
||||
lock was held would otherwise deadlock the hook and hang the process
|
||||
instead of aborting it.
|
||||
|
||||
## 2026-09-08: iris ships an icon font, and `widget::mark` is gone
|
||||
|
||||
Iris's question -- "why does mark exist? The font should be working if
|
||||
it's working for compose and nerd fonts are bundled" -- and its answer:
|
||||
the Compose app draws icons from its own committed Nerd Fonts subset,
|
||||
while iris was setting the disclosure mark with bare Unicode geometric
|
||||
codepoints out of whatever face the platform resolved. So iris now does
|
||||
what Compose does.
|
||||
|
||||
- **`iris::icon`** (new module): the codepoints iris draws, one constant
|
||||
each -- `OPEN`, `CLOSED`, `COLLAPSE` today. Every one has to have a
|
||||
matching entry in `iris/core/build-icon-font.sh`'s `GLYPHS`, which is
|
||||
what builds the shipped `iris/core/assets/fonts/nerd_icons.ttf` (992
|
||||
bytes, Material Design, Mono face). `every_icon_is_in_the_bundled_font`
|
||||
fails the build if the two lists drift.
|
||||
- **`Family::Icons`** (new variant): how any text asks for that family.
|
||||
Before/after:
|
||||
|
||||
// was
|
||||
mark(if open { Dir::DOWN } else { Dir::RIGHT }, 9.0, MUTED)
|
||||
// now
|
||||
text(if open { icon::OPEN } else { icon::CLOSED }, 9.0, MUTED)
|
||||
.family(Family::Icons)
|
||||
|
||||
It names an intention, not a font name: only `TextData` knows what the
|
||||
bundled file registered as, and it resolves the variant during shaping
|
||||
(`TextData::resolve_family`, also public). A *named* family rather than
|
||||
a generic one, so nothing falls back into it for ordinary text and an
|
||||
icon cannot fall back out of it onto a system face that happens to have
|
||||
the codepoint.
|
||||
- **`iris::widget::mark` is removed** -- added earlier the same day and
|
||||
superseded within it. It drew one correct triangle; every further icon
|
||||
would have been another rasteriser, and an icon as text takes the size,
|
||||
colour and baseline of the line it sits in for free.
|
||||
- **`FontDiagnostics::icon_family`** (new field), in the startup log line
|
||||
and the Diagnostics pane: which family the icons resolved to, so a
|
||||
build whose bundled font failed to register says so instead of drawing
|
||||
tofu.
|
||||
|
||||
This does not reopen the 2026-09-07 platform-fonts decision. Body and
|
||||
monospace text still come from the platform's own collection; an icon is
|
||||
the opposite case, a small closed set of codepoints no system font is
|
||||
guaranteed to have, and it is the same division the Compose app makes.
|
||||
+19
-17
@@ -819,23 +819,25 @@ has the fuller account.
|
||||
nothing on screen says it was cut. Whichever end is cut has to be a
|
||||
choice when this lands: a path is identified by its tail, a command by
|
||||
its head.
|
||||
- [x] **A drawn chevron.** **Done 2026-09-08**: `iris::widget::mark(dir,
|
||||
dp, colour)` rasterises an antialiased triangle into the ordinary
|
||||
texture path and scales it into the box asked for, so no line/path
|
||||
primitive was needed after all. `tool.rs` uses it for all three marks.
|
||||
The original entry, for the record: *the bundled fonts
|
||||
were removed on 2026-09-07 in favour of the platform collection, so the
|
||||
mark is a codepoint the phone's own faces may not have -- Iris's
|
||||
2026-09-08 screenshot shows an empty box where it should be, and the
|
||||
desktop render draws it as a small dot. UI_RULES: "don't rely on
|
||||
characters the platform might not have." iris has rects, text and
|
||||
textures, so the smallest honest fix is a generated texture rather than
|
||||
a new primitive.* `Chevron.kt` draws its own strokes precisely
|
||||
because a chevron from a font is a glyph a system font may not have --
|
||||
and the bundled `NotoSans-Regular.ttf` indeed has no U+25B8/25BE/25B4,
|
||||
while `NotoSansMono-Regular.ttf` does. `tool.rs` sets the mark in the
|
||||
monospace face as a result. A real fix needs a line/path primitive;
|
||||
iris has rects, text and textures only.
|
||||
- [x] **A chevron the platform cannot fail to have.** **Done
|
||||
2026-09-08**, twice. First as `iris::widget::mark(dir, dp, colour)`,
|
||||
which rasterised an antialiased triangle into the ordinary texture path
|
||||
-- correct, but one bespoke shape, and it built a texture *per widget*,
|
||||
which is what crashed the bench (RUST.md's 2026-09-08 evening entry).
|
||||
Then, on Iris's question -- "why does mark exist? The font should be
|
||||
working if it's working for compose and nerd fonts are bundled" -- as
|
||||
what the Compose app has always done: **iris ships its own Nerd Fonts
|
||||
subset** (`iris/core/build-icon-font.sh` -> `iris/core/assets/fonts/
|
||||
nerd_icons.ttf`, 992 bytes, three Material Design glyphs), named in
|
||||
`iris::icon` and drawn with `Family::Icons`. `mark` is deleted. That
|
||||
serves every future icon rather than one triangle, and an icon is text,
|
||||
so it takes the size, colour and baseline of the line it sits in for
|
||||
free. The original entry, for the record: *the bundled fonts were
|
||||
removed on 2026-09-07 in favour of the platform collection, so the mark
|
||||
is a codepoint the phone's own faces may not have -- Iris's 2026-09-08
|
||||
screenshot shows an empty box where it should be, and the desktop render
|
||||
draws it as a small dot. UI_RULES: "don't rely on characters the
|
||||
platform might not have."*
|
||||
- [ ] **A tool card's text is not selectable.** `Selection` is keyed
|
||||
`(RowKey, block index)` and a card has no markdown blocks, so nothing in
|
||||
a card registers. Compose's `SelectionContainer` covers tool output,
|
||||
|
||||
+43
-11
@@ -8143,16 +8143,48 @@ recorded anywhere. It is the crash this code had; the devlog fix above is
|
||||
what will say whether it was hers. Ask for the Runtime tab after the next
|
||||
bench APK.
|
||||
|
||||
### Open: why `mark` exists at all (Iris asked, 2026-09-08)
|
||||
### Why `mark` existed, and what replaced it (Iris asked, 2026-09-08)
|
||||
|
||||
Her question, mid-fix: "why does mark exist? The font should be working if
|
||||
it's working for compose and nerd fonts are bundled." The Compose app
|
||||
draws its chevron from **its own committed Nerd Fonts subset**
|
||||
(`app/build-icon-font.sh`, `NerdIcons.kt`); iris bundles no font at all
|
||||
since 2026-09-07, and `tool.rs` was using bare geometric codepoints
|
||||
(U+25B8/25BE/25B4) out of whatever system face resolved -- which her phone
|
||||
had none for. So the two apps were never doing the same thing, and the
|
||||
2026-09-07 note that "iris had no equivalent icon font to keep" is what
|
||||
left the gap. The alternative to `mark` is to bundle the same ~100-glyph
|
||||
subset in iris and take icons from it, which would serve every future icon
|
||||
rather than one triangle. Not done: it is hers to choose.
|
||||
it's working for compose and nerd fonts are bundled." It was not working,
|
||||
and the two apps were never doing the same thing. The Compose app draws
|
||||
its icons from **its own committed Nerd Fonts subset**
|
||||
(`app/build-icon-font.sh`, `NerdIcons.kt`); iris bundles no font since
|
||||
2026-09-07, and `tool.rs` was setting its mark with bare geometric
|
||||
codepoints (U+25B8/25BE/25B4) out of whatever system face resolved --
|
||||
which her phone had none for. The 2026-09-07 note that "iris had no
|
||||
equivalent icon font to keep" is the gap: iris had none because it had
|
||||
never had one.
|
||||
|
||||
**Done, on her instruction.** iris ships the same kind of subset:
|
||||
|
||||
- `iris/core/build-icon-font.sh` -- a copy of the Compose app's script
|
||||
with its own `GLYPHS` list, writing
|
||||
`iris/core/assets/fonts/nerd_icons.ttf`. **992 bytes** for three
|
||||
Material Design glyphs (`md-menu_down`, `md-menu_right`, `md-menu_up`),
|
||||
from the **Mono** face for the reason the Compose script gives.
|
||||
- `iris::icon` names the codepoints, one constant each, and
|
||||
`iris_core::Family::Icons` is how a `TextBuilder` asks for the family.
|
||||
The name behind it is read back from the collection at registration
|
||||
rather than written down, and resolved during shaping
|
||||
(`TextData::resolve_family`), so nothing outside `TextData` needs to
|
||||
know what the file calls itself. A *named* family, never a generic one:
|
||||
nothing should fall back into it for text, and an icon must not fall
|
||||
back out of it onto a system face that happens to have the codepoint.
|
||||
- `iris::widget::mark` and `src/widget/mark.rs` are **deleted**.
|
||||
`tool.rs`'s three call sites are `disclosure(icon::OPEN | CLOSED |
|
||||
COLLAPSE)`, which is `text(...).family(Family::Icons)`.
|
||||
- `every_icon_is_in_the_bundled_font` is the guard the script's comment
|
||||
asks for: it maps each `icon::` constant through the shipped font's own
|
||||
charmap, so a constant added on one side and not the other fails a test
|
||||
instead of drawing nothing.
|
||||
- The startup font line and the Diagnostics pane now say which family the
|
||||
icons resolved to, so a build whose font failed to register says so
|
||||
rather than drawing tofu. On the emulator:
|
||||
`icons=Some("Symbols Nerd Font Mono")`.
|
||||
|
||||
Looked at, per UI_RULES, rather than only compiled: the closed and open
|
||||
marks in `run-headless.sh phone --phone` (before and after a tap that
|
||||
opens the card), and the collapse bar's up mark in `IRIS_TOOLS_EXPANDED=1
|
||||
run-headless.sh transcript`. All three draw, at the size and alignment the
|
||||
drawn triangle had.
|
||||
@@ -261,7 +261,7 @@ impl AndroidAppState for BenchClient {
|
||||
let font = rsc.ui.text.font_diagnostics();
|
||||
log::info!(
|
||||
"iris fonts: {} families found, default={:?} mono={:?}, resolved regular={:?} \
|
||||
bold={:?} italic={:?} mono={:?}",
|
||||
bold={:?} italic={:?} mono={:?}, icons={:?}",
|
||||
font.families_found,
|
||||
font.default_family,
|
||||
font.default_mono_family,
|
||||
@@ -269,6 +269,7 @@ impl AndroidAppState for BenchClient {
|
||||
font.bold_resolved,
|
||||
font.italic_resolved,
|
||||
font.mono_resolved,
|
||||
font.icon_family,
|
||||
);
|
||||
|
||||
let mut client = Self {
|
||||
|
||||
@@ -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
+62
@@ -0,0 +1,62 @@
|
||||
#!/usr/bin/env bash
|
||||
# Rebuilds iris/core/assets/fonts/nerd_icons.ttf.
|
||||
#
|
||||
# iris 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
|
||||
# `iris/core/src/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 same arrangement as the Compose app's `app/build-icon-font.sh`, which
|
||||
# this is copied from -- including the Mono face and the Material Design
|
||||
# family, so an icon means the same thing in both apps. Copied rather than
|
||||
# shared because most of it is the GLYPHS list, which has to differ: the
|
||||
# point of subsetting is to ship only the codepoints one app draws.
|
||||
set -euo pipefail
|
||||
|
||||
# Codepoint, then the Nerd Fonts glyph name it came from. Material Design
|
||||
# Icons, as in the Compose app.
|
||||
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 -- the same reason the Compose app's script takes it. It is also what
|
||||
# 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"
|
||||
@@ -0,0 +1,39 @@
|
||||
//! The icons iris draws, as codepoints in the Nerd Fonts subset it ships.
|
||||
//!
|
||||
//! **Why a bundled font rather than ordinary Unicode**: the disclosure
|
||||
//! mark used to be U+25B8/25BE/25B4 out of whatever face the platform
|
||||
//! resolved, and once iris stopped bundling fonts (DECISIONS.md,
|
||||
//! 2026-09-07) Iris's phone drew an empty box for them and this VM drew a
|
||||
//! dot. UI_RULES' answer is not to avoid glyphs but to ship them, which is
|
||||
//! also what the Compose app has always done for its icons
|
||||
//! (`app/build-icon-font.sh`, `NerdIcons.kt`) -- the same Material Design
|
||||
//! family, so an icon means the same thing in both apps.
|
||||
//!
|
||||
//! **Why not vector assets or drawn shapes**: an icon beside a line of
|
||||
//! text wants that line's size, colour and baseline, and text gets all
|
||||
//! three for free. This replaced `iris::widget::mark`, which drew the
|
||||
//! triangle into a texture: correct, but one shape, and every further icon
|
||||
//! would have been another bespoke rasteriser.
|
||||
//!
|
||||
//! Each constant here has to have a matching codepoint in
|
||||
//! `iris/core/build-icon-font.sh`'s `GLYPHS`; a codepoint here that the
|
||||
//! script did not subset is a glyph that silently isn't there. The subset
|
||||
//! is the font's **Mono** face, where every glyph is one em wide and one
|
||||
//! em tall, so two icons at one font size are one size without either
|
||||
//! being given one -- and why an icon looks smaller than text at the same
|
||||
//! size, since the glyph is drawn inside that em rather than filling it.
|
||||
//!
|
||||
//! Draw one with [`crate::Family::Icons`]:
|
||||
//!
|
||||
//! ```ignore
|
||||
//! text(icon::OPEN, 12.0, MUTED).family(Family::Icons)
|
||||
//! ```
|
||||
|
||||
/// `md-menu_down` -- a filled triangle pointing down: this card is open.
|
||||
pub const OPEN: &str = "\u{F035D}";
|
||||
|
||||
/// `md-menu_right` -- pointing right: this card opens.
|
||||
pub const CLOSED: &str = "\u{F035F}";
|
||||
|
||||
/// `md-menu_up` -- pointing up: fold this group of cards away again.
|
||||
pub const COLLAPSE: &str = "\u{F0360}";
|
||||
@@ -19,6 +19,7 @@ mod render;
|
||||
mod ui;
|
||||
mod widget;
|
||||
|
||||
pub mod icon;
|
||||
pub mod util;
|
||||
|
||||
pub use attr::*;
|
||||
|
||||
@@ -2,14 +2,27 @@ 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,
|
||||
};
|
||||
use std::ops::Range;
|
||||
use std::sync::Arc;
|
||||
use swash::{
|
||||
FontRef,
|
||||
scale::{Render, ScaleContext, Source, StrikeWith},
|
||||
zeno::{Format, Vector},
|
||||
};
|
||||
|
||||
/// The icon font iris ships: the Nerd Fonts Symbols **Mono** subset built
|
||||
/// by `iris/core/build-icon-font.sh`, holding only the codepoints
|
||||
/// `crate::icon` names (992 bytes for three glyphs today).
|
||||
///
|
||||
/// This is the one font bundled here, and it is not a text font: body and
|
||||
/// monospace text still come from the platform's own collection
|
||||
/// (DECISIONS.md, 2026-09-07). An icon is the opposite case -- a small,
|
||||
/// closed set of codepoints no system font is guaranteed to have -- which
|
||||
/// is the same division the Compose app makes.
|
||||
const NERD_ICONS: &[u8] = include_bytes!("../../assets/fonts/nerd_icons.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
|
||||
@@ -37,6 +50,11 @@ pub struct FontDiagnostics {
|
||||
pub bold_resolved: Option<String>,
|
||||
pub italic_resolved: Option<String>,
|
||||
pub mono_resolved: Option<String>,
|
||||
/// The family the bundled icon font registered under, or `None` if
|
||||
/// registering it failed. Reported rather than assumed: it is the one
|
||||
/// font iris ships, so `None` is a broken build and must not look
|
||||
/// like a device that happens to lack a face.
|
||||
pub icon_family: Option<String>,
|
||||
}
|
||||
|
||||
/// Everything text needs that outlives one string: the font collection, the
|
||||
@@ -57,6 +75,12 @@ pub struct TextData {
|
||||
/// truth would mean carrying a `Painter` (or output size) into every
|
||||
/// input handler for the sake of one field.
|
||||
pub density: f32,
|
||||
/// The family name [`NERD_ICONS`] registered under, which is what
|
||||
/// [`Family::Icons`] resolves to. `None` only if registering the
|
||||
/// bundled font failed, which is a broken build rather than a
|
||||
/// platform difference -- said in the startup diagnostics rather than
|
||||
/// silently drawn as tofu.
|
||||
pub icon_family: Option<String>,
|
||||
}
|
||||
|
||||
impl Default for TextData {
|
||||
@@ -76,16 +100,39 @@ impl Default for TextData {
|
||||
fn default() -> Self {
|
||||
let mut font_cx = FontContext::new();
|
||||
patch_android_monospace(&mut font_cx);
|
||||
let icon_family = register_icon_font(&mut font_cx);
|
||||
Self {
|
||||
font_cx,
|
||||
layout_cx: LayoutContext::new(),
|
||||
scale_cx: ScaleContext::new(),
|
||||
atlas: GlyphAtlas::default(),
|
||||
density: 1.0,
|
||||
icon_family,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Registers the bundled icon font as an ordinary named family and
|
||||
/// answers the name it registered under -- read back from the collection
|
||||
/// rather than written down here, so the name cannot drift from the file
|
||||
/// (`build-icon-font.sh` takes whatever face the Nerd Fonts release
|
||||
/// ships).
|
||||
///
|
||||
/// A *named* family rather than a generic one: nothing should fall back
|
||||
/// to it for ordinary text, and nothing should fall back out of it for an
|
||||
/// icon -- a system face that happens to have one of these codepoints
|
||||
/// would draw somebody else's picture.
|
||||
fn register_icon_font(font_cx: &mut FontContext) -> Option<String> {
|
||||
let blob = Blob::new(Arc::new(NERD_ICONS));
|
||||
let id = font_cx
|
||||
.collection
|
||||
.register_fonts(blob, None)
|
||||
.into_iter()
|
||||
.map(|(id, _)| id)
|
||||
.next()?;
|
||||
font_cx.collection.family_name(id).map(str::to_string)
|
||||
}
|
||||
|
||||
/// 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
|
||||
@@ -184,6 +231,22 @@ fn android_monospace_font_filename() -> Option<String> {
|
||||
fn patch_android_monospace(_font_cx: &mut FontContext) {}
|
||||
|
||||
impl TextData {
|
||||
/// [`Family::Icons`] as the name the bundled font actually registered
|
||||
/// under; everything else unchanged.
|
||||
///
|
||||
/// Cloned rather than borrowed because the caller needs it while the
|
||||
/// layout builder holds `&mut self` -- a `String` per shaped icon run,
|
||||
/// paid only when the layout is rebuilt.
|
||||
pub fn resolve_family(&self, family: &Family) -> Family {
|
||||
match family {
|
||||
Family::Icons => self
|
||||
.icon_family
|
||||
.clone()
|
||||
.map_or(Family::Icons, Family::Named),
|
||||
other => other.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
/// 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.
|
||||
@@ -261,6 +324,7 @@ impl TextData {
|
||||
bold_resolved,
|
||||
italic_resolved,
|
||||
mono_resolved,
|
||||
icon_family: self.icon_family.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -272,6 +336,11 @@ pub enum Family {
|
||||
SansSerif,
|
||||
Serif,
|
||||
Monospace,
|
||||
/// The bundled icon font -- see [`crate::icon`] for what is in it.
|
||||
/// Named as an intention rather than as a font name because only
|
||||
/// [`TextData`] knows what the file registered as; it resolves this
|
||||
/// during shaping ([`TextData::resolve_family`]).
|
||||
Icons,
|
||||
Named(String),
|
||||
}
|
||||
|
||||
@@ -281,6 +350,11 @@ impl Family {
|
||||
Self::SansSerif => FontFamilyName::Generic(GenericFamily::SansSerif),
|
||||
Self::Serif => FontFamilyName::Generic(GenericFamily::Serif),
|
||||
Self::Monospace => FontFamilyName::Generic(GenericFamily::Monospace),
|
||||
// Only reachable if `resolve_family` did not run, which no
|
||||
// shaping path allows -- and sans-serif is the honest answer
|
||||
// for a build whose icon font failed to register: the reader
|
||||
// gets the platform's own tofu rather than a wrong picture.
|
||||
Self::Icons => FontFamilyName::Generic(GenericFamily::SansSerif),
|
||||
Self::Named(name) => FontFamilyName::Named(name.as_str().into()),
|
||||
};
|
||||
FontFamily::Single(name)
|
||||
@@ -479,21 +553,29 @@ impl TextBuffer {
|
||||
if self.shaped.as_ref() == Some(&(attrs.clone(), width, density)) {
|
||||
return;
|
||||
}
|
||||
// Resolved before the builder borrows `data`: `Family::Icons`
|
||||
// names an intention, and the name behind it lives on `TextData`.
|
||||
let base_family = data.resolve_family(&attrs.family);
|
||||
let span_families: Vec<Option<Family>> = self
|
||||
.spans
|
||||
.iter()
|
||||
.map(|span| span.family.as_ref().map(|f| data.resolve_family(f)))
|
||||
.collect();
|
||||
let mut builder = data
|
||||
.layout_cx
|
||||
.ranged_builder(&mut data.font_cx, &self.text, 1.0, true);
|
||||
builder.push_default(StyleProperty::FontFamily(attrs.family.family()));
|
||||
builder.push_default(StyleProperty::FontFamily(base_family.family()));
|
||||
builder.push_default(StyleProperty::FontSize(attrs.font_size * density));
|
||||
builder.push_default(StyleProperty::LineHeight(LineHeight::Absolute(
|
||||
attrs.line_height * density,
|
||||
)));
|
||||
builder.push_default(StyleProperty::Brush(attrs.color));
|
||||
for span in &self.spans {
|
||||
for (span, family) in self.spans.iter().zip(&span_families) {
|
||||
let range = span.range.clone();
|
||||
if let Some(color) = span.color {
|
||||
builder.push(StyleProperty::Brush(color), range.clone());
|
||||
}
|
||||
if let Some(family) = &span.family {
|
||||
if let Some(family) = family {
|
||||
builder.push(StyleProperty::FontFamily(family.family()), range.clone());
|
||||
}
|
||||
if let Some(size) = span.font_size {
|
||||
@@ -647,3 +729,49 @@ impl TextData {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::icon;
|
||||
|
||||
/// Every codepoint `icon` names is actually in the subset the script
|
||||
/// built. This is the failure `build-icon-font.sh`'s own comment warns
|
||||
/// about -- a constant added on one side and not the other is a glyph
|
||||
/// that silently isn't there -- and it is invisible at runtime,
|
||||
/// because a missing glyph draws as nothing rather than as an error.
|
||||
#[test]
|
||||
fn every_icon_is_in_the_bundled_font() {
|
||||
let font = FontRef::from_index(NERD_ICONS, 0).expect("the bundled icon font parses");
|
||||
let charmap = font.charmap();
|
||||
for (name, glyph) in [
|
||||
("OPEN", icon::OPEN),
|
||||
("CLOSED", icon::CLOSED),
|
||||
("COLLAPSE", icon::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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// The font registers, so `Family::Icons` resolves to a real family
|
||||
/// rather than falling through to sans-serif and drawing tofu.
|
||||
#[test]
|
||||
fn the_icon_family_registers_and_resolves() {
|
||||
let data = TextData::default();
|
||||
let family = data.resolve_family(&Family::Icons);
|
||||
assert!(
|
||||
matches!(family, Family::Named(_)),
|
||||
"the bundled icon font did not register: {:?}",
|
||||
data.icon_family
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -357,6 +357,7 @@ impl AndroidRenderer {
|
||||
mono={default_mono_family:?}\n\
|
||||
fonts resolved: regular={regular:?} bold={bold:?} italic={italic:?} \
|
||||
mono={mono:?}\n\
|
||||
icon font: {icons:?}\n\
|
||||
wgpu errors since surface creation:\n {errors_text}\n\n\
|
||||
{frame_report}",
|
||||
name = self.adapter_name,
|
||||
@@ -371,6 +372,7 @@ impl AndroidRenderer {
|
||||
bold = font.bold_resolved,
|
||||
italic = font.italic_resolved,
|
||||
mono = font.mono_resolved,
|
||||
icons = font.icon_family,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -862,8 +862,8 @@ impl<State: AndroidAppState> ViewPeer for IrisViewPeer<State> {
|
||||
//
|
||||
// It replaces clearing them, which threw away the *slot
|
||||
// numbering* as well as the pixels: every `TextureHandle`
|
||||
// a live widget still held -- one per `widget::mark`, so
|
||||
// one per folded card on the transcript screen -- then
|
||||
// a live widget still held -- one per icon or image on
|
||||
// screen, and one per folded card at the time -- then
|
||||
// named a slot nothing recognised, and the next frame
|
||||
// panicked in `image_bind_group` ("texture slot 89 is not
|
||||
// a live standalone image: None"). Re-uploading also keeps
|
||||
|
||||
@@ -1,248 +0,0 @@
|
||||
//! A small filled triangle, drawn rather than typed -- a disclosure
|
||||
//! marker, and the first thing in iris that draws a shape text cannot.
|
||||
//!
|
||||
//! It exists because the alternative was a font codepoint. `tool.rs` used
|
||||
//! U+25B8/25BE/25B4 in whatever face resolved, which worked only while
|
||||
//! iris bundled its own fonts; once text moved to the platform's
|
||||
//! collection (2026-09-07) Iris's phone drew an empty box where the mark
|
||||
//! should be, and this machine drew a dot. UI_RULES: "don't rely on
|
||||
//! characters the platform might not have -- ship the glyph or the asset
|
||||
//! rather than hoping."
|
||||
//!
|
||||
//! Rasterised into iris's ordinary texture path rather than needing a new
|
||||
//! primitive: iris has rects, text and textures, and a triangle is not
|
||||
//! expressible as any number of rects without a staircase edge. One
|
||||
//! oversampled bitmap per *shape* is drawn scaled into the box the caller
|
||||
//! asks for, so the same texture is correct at any density -- which is
|
||||
//! also why it is built at construction, where the density is not known
|
||||
//! yet, and scaled at draw, where it is.
|
||||
//!
|
||||
//! Per shape, not per widget: the bitmap depends only on the direction and
|
||||
//! the colour, so it goes through [`Textures::shared`] and a screen full of
|
||||
//! folded cards draws the three marks it actually has rather than one
|
||||
//! texture, bind group and draw call per card.
|
||||
|
||||
use crate::prelude::*;
|
||||
use image::{Rgba, RgbaImage};
|
||||
use iris_core::SharedTextureKey;
|
||||
|
||||
/// The bitmap's own size. Generous enough that a 12dp mark at density 3
|
||||
/// (36px) is still sampling *down*, which is what keeps the diagonal
|
||||
/// clean; small enough that the handful the program has cost nothing
|
||||
/// (48x48 RGBA is 9 KB).
|
||||
const TEXTURE_PX: u32 = 48;
|
||||
/// Subsamples per pixel per axis when measuring how much of a pixel the
|
||||
/// triangle covers. 4x4 is the point where the edge stops looking stepped
|
||||
/// at these sizes.
|
||||
const SUBSAMPLES: u32 = 4;
|
||||
|
||||
pub struct Mark {
|
||||
handle: TextureHandle,
|
||||
/// The box the triangle is drawn into, in dp -- resolved against the
|
||||
/// density at draw time, so one mark is the same physical size on any
|
||||
/// screen.
|
||||
size_dp: f32,
|
||||
}
|
||||
|
||||
impl Widget for Mark {
|
||||
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||
let px = self.size_dp * painter.density();
|
||||
let size = Vec2::new(px, px);
|
||||
painter.texture_within(&self.handle, size.align(Align::CENTER));
|
||||
Size::abs(size)
|
||||
}
|
||||
|
||||
fn is_size_independent(&self) -> bool {
|
||||
true // its size is its own, not a share of what it was offered
|
||||
}
|
||||
}
|
||||
|
||||
/// A filled triangle `size_dp` across, pointing along `dir`, in `color` --
|
||||
/// what a row uses to say "this opens" and "this is open".
|
||||
///
|
||||
/// `size_dp` is not part of what is rasterised (the bitmap is scaled at
|
||||
/// draw), so two marks differing only in size share one texture.
|
||||
pub fn mark<Rsc: UiRsc>(dir: Dir, size_dp: f32, color: UiColor) -> impl WidgetFn<Rsc, Mark> {
|
||||
move |state| Mark {
|
||||
handle: state
|
||||
.ui_mut()
|
||||
.textures
|
||||
.shared(key(dir, color), || rasterise(dir, color).into()),
|
||||
size_dp,
|
||||
}
|
||||
}
|
||||
|
||||
/// The mark's whole description, packed exactly: the axis and sign of the
|
||||
/// direction and the four colour channels, one byte each, so distinct
|
||||
/// marks are distinct keys and equal ones are equal.
|
||||
fn key(dir: Dir, color: UiColor) -> SharedTextureKey {
|
||||
let axis = match dir.axis {
|
||||
Axis::X => 0u64,
|
||||
Axis::Y => 1,
|
||||
};
|
||||
let sign = match dir.sign {
|
||||
Sign::Neg => 0u64,
|
||||
Sign::Pos => 1,
|
||||
};
|
||||
SharedTextureKey {
|
||||
owner: "mark",
|
||||
id: u64::from_le_bytes([
|
||||
axis as u8, sign as u8, color.r, color.g, color.b, color.a, 0, 0,
|
||||
]),
|
||||
}
|
||||
}
|
||||
|
||||
/// The triangle, as coverage: for each pixel, how much of it the shape
|
||||
/// covers, measured by subsampling rather than by an analytic edge
|
||||
/// function -- one bitmap is built per mark in the whole program, so the
|
||||
/// simple method is the right one.
|
||||
fn rasterise(dir: Dir, color: UiColor) -> RgbaImage {
|
||||
let corners = corners(dir);
|
||||
let n = TEXTURE_PX as f32;
|
||||
let step = 1.0 / SUBSAMPLES as f32;
|
||||
RgbaImage::from_fn(TEXTURE_PX, TEXTURE_PX, |x, y| {
|
||||
let mut inside = 0u32;
|
||||
for sy in 0..SUBSAMPLES {
|
||||
for sx in 0..SUBSAMPLES {
|
||||
let p = Vec2::new(
|
||||
(x as f32 + (sx as f32 + 0.5) * step) / n,
|
||||
(y as f32 + (sy as f32 + 0.5) * step) / n,
|
||||
);
|
||||
if contains(&corners, p) {
|
||||
inside += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
let coverage = inside as f32 / (SUBSAMPLES * SUBSAMPLES) as f32;
|
||||
// Premultiplied is wrong for this pipeline (`Textures::add` takes
|
||||
// ordinary RGBA and the shader samples it straight), so the colour
|
||||
// stays put and only alpha carries the coverage.
|
||||
Rgba([
|
||||
color.r,
|
||||
color.g,
|
||||
color.b,
|
||||
(color.a as f32 * coverage).round() as u8,
|
||||
])
|
||||
})
|
||||
}
|
||||
|
||||
/// The triangle's three corners in unit space, inset a little from the
|
||||
/// bitmap's edge so its own antialiasing is never clipped by the texture
|
||||
/// border, and squat rather than equilateral -- the proportions of the
|
||||
/// disclosure triangles this replaces.
|
||||
fn corners(dir: Dir) -> [Vec2; 3] {
|
||||
const NEAR: f32 = 0.12;
|
||||
const FAR: f32 = 0.88;
|
||||
// Across the direction of travel, the base spans the full width; along
|
||||
// it, the tip is at the far end.
|
||||
let (base, tip) = match dir.sign {
|
||||
Sign::Pos => (NEAR, FAR),
|
||||
Sign::Neg => (FAR, NEAR),
|
||||
};
|
||||
let mid = 0.5;
|
||||
match dir.axis {
|
||||
Axis::Y => [
|
||||
Vec2::new(NEAR, base),
|
||||
Vec2::new(FAR, base),
|
||||
Vec2::new(mid, tip),
|
||||
],
|
||||
Axis::X => [
|
||||
Vec2::new(base, NEAR),
|
||||
Vec2::new(base, FAR),
|
||||
Vec2::new(tip, mid),
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether `p` is inside the triangle, by the sign of the cross product
|
||||
/// against each edge. The corners above are given in a consistent winding
|
||||
/// per direction, so "all three the same sign" is the test -- written as
|
||||
/// "never both signs" so a point exactly on an edge counts as inside
|
||||
/// rather than falling through a crack between two triangles.
|
||||
fn contains(t: &[Vec2; 3], p: Vec2) -> bool {
|
||||
let side = |a: Vec2, b: Vec2| (b.x - a.x) * (p.y - a.y) - (b.y - a.y) * (p.x - a.x);
|
||||
let (d0, d1, d2) = (side(t[0], t[1]), side(t[1], t[2]), side(t[2], t[0]));
|
||||
let neg = d0 < 0.0 || d1 < 0.0 || d2 < 0.0;
|
||||
let pos = d0 > 0.0 || d1 > 0.0 || d2 > 0.0;
|
||||
!(neg && pos)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// The tip points where it was asked to, and the base is at the other
|
||||
/// end -- checked as coverage rather than by eye, because the whole
|
||||
/// reason this widget exists is that nobody looked at the glyph it
|
||||
/// replaces on the device that lacked it.
|
||||
#[test]
|
||||
fn a_mark_points_along_its_direction() {
|
||||
let at = |f: f32| (TEXTURE_PX as f32 * f) as u32;
|
||||
let (mid, near_tip, past_base) = (at(0.5), at(0.6), at(0.02));
|
||||
let (far_tip, far_base) = (at(0.4), at(0.98));
|
||||
// Not the tip pixel itself, which is a point and covers nothing:
|
||||
// a little back from it, where the triangle has width. And just
|
||||
// *outside* the base, which no part of the shape reaches.
|
||||
for (name, dir, tip, base) in [
|
||||
("down", Dir::DOWN, (mid, near_tip), (mid, past_base)),
|
||||
("up", Dir::UP, (mid, far_tip), (mid, far_base)),
|
||||
("right", Dir::RIGHT, (near_tip, mid), (past_base, mid)),
|
||||
("left", Dir::LEFT, (far_tip, mid), (far_base, mid)),
|
||||
] {
|
||||
let img = rasterise(dir, UiColor::WHITE);
|
||||
// Just behind the tip is solid; the same distance past the
|
||||
// base's outer edge is empty. A triangle drawn the wrong way
|
||||
// round passes neither.
|
||||
let inside = img.get_pixel(tip.0, tip.1)[3];
|
||||
let outside = img.get_pixel(base.0, base.1)[3];
|
||||
assert!(
|
||||
inside > 200,
|
||||
"{name}: the pixel at the tip should be covered, alpha={inside}"
|
||||
);
|
||||
assert!(
|
||||
outside < 40,
|
||||
"{name}: the pixel past the base should be clear, alpha={outside}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Two marks of the same shape share a texture and two of different
|
||||
/// shapes do not -- the whole of what [`Textures::shared`] is being
|
||||
/// keyed on, checked here rather than through a widget so it does not
|
||||
/// need a resource tree.
|
||||
#[test]
|
||||
fn a_marks_key_is_its_direction_and_its_colour() {
|
||||
assert_eq!(key(Dir::UP, UiColor::WHITE), key(Dir::UP, UiColor::WHITE));
|
||||
for (name, other) in [
|
||||
("down", Dir::DOWN),
|
||||
("left", Dir::LEFT),
|
||||
("right", Dir::RIGHT),
|
||||
] {
|
||||
assert_ne!(
|
||||
key(Dir::UP, UiColor::WHITE),
|
||||
key(other, UiColor::WHITE),
|
||||
"{name} shares a key with up"
|
||||
);
|
||||
}
|
||||
assert_ne!(
|
||||
key(Dir::UP, UiColor::WHITE),
|
||||
key(Dir::UP, UiColor::new(255, 255, 255, 128)),
|
||||
"two colours differing only in alpha share a key"
|
||||
);
|
||||
}
|
||||
|
||||
/// A corner of the bitmap is never covered, whichever way the mark
|
||||
/// points -- what says the shape is a triangle rather than a filled
|
||||
/// box, and that the inset in `corners` is keeping its antialiasing
|
||||
/// inside the texture.
|
||||
#[test]
|
||||
fn a_mark_leaves_its_corners_clear() {
|
||||
for dir in [Dir::DOWN, Dir::UP, Dir::LEFT, Dir::RIGHT] {
|
||||
let img = rasterise(dir, UiColor::WHITE);
|
||||
let last = TEXTURE_PX - 1;
|
||||
for (x, y) in [(0, 0), (last, 0), (0, last), (last, last)] {
|
||||
assert_eq!(img.get_pixel(x, y)[3], 0, "corner ({x},{y}) is covered");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
mod image;
|
||||
mod list;
|
||||
mod mark;
|
||||
mod mask;
|
||||
mod position;
|
||||
mod ptr;
|
||||
@@ -10,7 +9,6 @@ mod trait_fns;
|
||||
|
||||
pub use image::*;
|
||||
pub use list::*;
|
||||
pub use mark::*;
|
||||
pub use mask::*;
|
||||
pub use position::*;
|
||||
pub use ptr::*;
|
||||
|
||||
@@ -107,27 +107,15 @@ const OUTPUT_BYTES: usize = 4096;
|
||||
/// Checked at compile time, since both are constants.
|
||||
const _: () = assert!(OUTPUT_LINES > 0 && OUTPUT_BYTES > 0);
|
||||
|
||||
/// The mark that says a card opens, always drawn from the **monospace**
|
||||
/// face.
|
||||
/// The size of the disclosure mark, as a font size in dp.
|
||||
///
|
||||
/// 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.
|
||||
/// The disclosure marks, drawn (`iris::widget::mark`) rather than typed.
|
||||
/// They were the codepoints U+25B8/25BE/25B4 until 2026-09-08, which is a
|
||||
/// bet on the platform's fonts having them -- Iris's phone drew an empty
|
||||
/// box and this machine drew a dot once iris stopped bundling its own
|
||||
/// faces.
|
||||
/// The mark is a glyph in the icon font iris ships (`iris::icon`, built by
|
||||
/// `iris/core/build-icon-font.sh`) -- not a codepoint out of whatever the
|
||||
/// platform resolved, which is what U+25B8/25BE/25B4 were until
|
||||
/// 2026-09-08: Iris's phone drew an empty box for them and this VM drew a
|
||||
/// dot once iris stopped bundling faces. The font's Mono face draws each
|
||||
/// glyph inside a full em, so this reads a little larger than the same
|
||||
/// number would as text.
|
||||
const MARK_DP: f32 = 9.0;
|
||||
|
||||
/// Which cards the reader has opened, and which have had their whole
|
||||
@@ -252,6 +240,13 @@ fn text<Rsc>(content: impl Into<String>, size: f32, color: UiColor) -> TextBuild
|
||||
.text_align(Align::LEFT)
|
||||
}
|
||||
|
||||
/// One of `iris::icon`'s disclosure marks, at [`MARK_DP`] in the muted
|
||||
/// colour -- the one place this crate names the icon family, so a second
|
||||
/// icon is a second constant rather than a second way of asking.
|
||||
fn disclosure<Rsc>(glyph: &'static str) -> TextBuilder<Rsc> {
|
||||
text(glyph, MARK_DP, MUTED_COLOR).family(Family::Icons)
|
||||
}
|
||||
|
||||
/// A verbatim block: monospace on the surface everything verbatim in this
|
||||
/// app sits on, not wrapped, panning sideways on a finger.
|
||||
///
|
||||
@@ -452,11 +447,7 @@ where
|
||||
|
||||
let mut header = Span::empty(Dir::RIGHT).gap(dp(GAP_DP));
|
||||
header.push(
|
||||
mark(
|
||||
if open { Dir::DOWN } else { Dir::RIGHT },
|
||||
MARK_DP,
|
||||
MUTED_COLOR,
|
||||
)
|
||||
disclosure(if open { icon::OPEN } else { icon::CLOSED })
|
||||
.add_strong(rsc)
|
||||
.any(),
|
||||
);
|
||||
@@ -617,8 +608,8 @@ where
|
||||
{
|
||||
let strong = WidgetPtr::new().add_strong(rsc);
|
||||
let ptr = strong.weak();
|
||||
let mark = mark(Dir::UP, MARK_DP, MUTED_COLOR)
|
||||
.center()
|
||||
let mark = disclosure(icon::COLLAPSE)
|
||||
.center_text()
|
||||
.width(rest(1))
|
||||
.pad(dp(CARD_PAD_DP))
|
||||
// Anything shown only as a mark still needs a name: this is what
|
||||
|
||||
Reference in new issue
Block a user