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
@@ -38,9 +38,10 @@ dependency runs one way.
|
|||||||
`--recurse-submodules` or run `git submodule update --init`.
|
`--recurse-submodules` or run `git submodule update --init`.
|
||||||
- `docs/` — design and working documents.
|
- `docs/` — design and working documents.
|
||||||
|
|
||||||
Nerd Font icons are a committed subset. `iris/core/build-icon-font.sh`
|
Nerd Font icons are an app-owned committed subset. `app/build-icon-font.sh`
|
||||||
produces `iris/core/assets/fonts/nerd_icons.ttf`; its codepoints must match
|
produces `app/assets/fonts/nerd_icons.ttf`; its codepoints must match
|
||||||
`iris/core/src/icon.rs`. Body and monospace fonts come from the platform.
|
`app/src/ui/icon.rs`. The app registers it with Iris at startup. Body and
|
||||||
|
monospace fonts come from the platform; Iris ships no font assets.
|
||||||
|
|
||||||
## Checking work
|
## Checking work
|
||||||
|
|
||||||
|
|||||||
Generated
+1
@@ -180,6 +180,7 @@ dependencies = [
|
|||||||
"pulldown-cmark",
|
"pulldown-cmark",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"swash",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"tokio",
|
"tokio",
|
||||||
"ureq",
|
"ureq",
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ force-gles = ["screens", "iris/force-gles"]
|
|||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tempfile = "3"
|
tempfile = "3"
|
||||||
tokio = { version = "1.53.1", features = ["rt", "time"] }
|
tokio = { version = "1.53.1", features = ["rt", "time"] }
|
||||||
|
swash = "0.2.10"
|
||||||
|
|
||||||
# APK builds select these profiles explicitly.
|
# APK builds select these profiles explicitly.
|
||||||
[profile.android-release]
|
[profile.android-release]
|
||||||
|
|||||||
File renamed without changes.
File renamed without changes.
@@ -1,7 +1,7 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# Rebuilds iris/core/assets/fonts/nerd_icons.ttf.
|
# Rebuilds app/assets/fonts/nerd_icons.ttf.
|
||||||
#
|
#
|
||||||
# iris draws its icons as glyphs in a Nerd Fonts subset it ships, rather
|
# 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
|
# than as ordinary Unicode out of whatever the platform resolved. Unicode's
|
||||||
# own geometric shapes are what this replaced: `tool.rs` set its disclosure
|
# own geometric shapes are what this replaced: `tool.rs` set its disclosure
|
||||||
# mark with U+25B8/25BE/25B4, and once iris stopped bundling fonts
|
# mark with U+25B8/25BE/25B4, and once iris stopped bundling fonts
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
#
|
#
|
||||||
# The whole symbols font is 3 MB for the handful below, so what is
|
# 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
|
# 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
|
# `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
|
# the Rust that this script did not subset is a glyph that silently isn't
|
||||||
# there), then run this and commit the result.
|
# there), then run this and commit the result.
|
||||||
#
|
#
|
||||||
@@ -114,6 +114,7 @@ fn battery_line(samples: &[i32]) -> String {
|
|||||||
|
|
||||||
impl BenchClient {
|
impl BenchClient {
|
||||||
pub(super) fn new(mut ui_state: AndroidUiState, rsc: &mut StdRsc<Self>) -> Self {
|
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 content = WidgetPtr::new().add(rsc);
|
||||||
let loading = placeholder(rsc, "Loading fixture...");
|
let loading = placeholder(rsc, "Loading fixture...");
|
||||||
content(rsc).set(loading);
|
content(rsc).set(loading);
|
||||||
@@ -146,7 +147,7 @@ impl BenchClient {
|
|||||||
let font = rsc.ui.text.font_diagnostics();
|
let font = rsc.ui.text.font_diagnostics();
|
||||||
log::info!(
|
log::info!(
|
||||||
"iris fonts: {} families found, default={:?} mono={:?}, resolved regular={:?} \
|
"iris fonts: {} families found, default={:?} mono={:?}, resolved regular={:?} \
|
||||||
bold={:?} italic={:?} mono={:?}, icons={:?}",
|
bold={:?} italic={:?} mono={:?}",
|
||||||
font.families_found,
|
font.families_found,
|
||||||
font.default_family,
|
font.default_family,
|
||||||
font.default_mono_family,
|
font.default_mono_family,
|
||||||
@@ -154,7 +155,6 @@ impl BenchClient {
|
|||||||
font.bold_resolved,
|
font.bold_resolved,
|
||||||
font.italic_resolved,
|
font.italic_resolved,
|
||||||
font.mono_resolved,
|
font.mono_resolved,
|
||||||
font.icon_family,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
let mut client = Self {
|
let mut client = Self {
|
||||||
|
|||||||
@@ -91,6 +91,7 @@ fn frame_report_controls(rsc: &mut StdRsc<TranscriptClient>) -> WeakWidget {
|
|||||||
|
|
||||||
impl TranscriptClient {
|
impl TranscriptClient {
|
||||||
pub(super) fn new(mut ui_state: AndroidUiState, rsc: &mut StdRsc<Self>) -> Self {
|
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 content = WidgetPtr::new().add(rsc);
|
||||||
let loading = placeholder(rsc, "Loading sessions...");
|
let loading = placeholder(rsc, "Loading sessions...");
|
||||||
content(rsc).set(loading);
|
content(rsc).set(loading);
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ struct Client {
|
|||||||
|
|
||||||
impl DesktopAppState for Client {
|
impl DesktopAppState for Client {
|
||||||
fn new(mut ui_state: DesktopUiState, rsc: &mut StdRsc<Self>) -> Self {
|
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| {
|
let (server, ca_pem) = super::startup::load_startup_config().unwrap_or_else(|e| {
|
||||||
eprintln!("desktop-app: {e}");
|
eprintln!("desktop-app: {e}");
|
||||||
process::exit(2);
|
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.
|
// Keep the 1.9 MB fixture out of ordinary APKs.
|
||||||
#[cfg(feature = "fixture")]
|
#[cfg(feature = "fixture")]
|
||||||
pub mod fixture;
|
pub mod fixture;
|
||||||
|
mod icon;
|
||||||
pub mod markdown;
|
pub mod markdown;
|
||||||
pub mod row;
|
pub mod row;
|
||||||
pub(crate) mod tap;
|
pub(crate) mod tap;
|
||||||
@@ -13,6 +14,16 @@ use iris::prelude::*;
|
|||||||
use std::{mem, rc::Rc};
|
use std::{mem, rc::Rc};
|
||||||
use theme::Theme;
|
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 {
|
pub struct TranscriptScreen {
|
||||||
/// The transcript's own `LazySpan` -- the layout *and* the scroll
|
/// The transcript's own `LazySpan` -- the layout *and* the scroll
|
||||||
/// position, since a lazy span owns a `ScrollController` of its own
|
/// position, since a lazy span owns a `ScrollController` of its own
|
||||||
@@ -241,6 +252,7 @@ pub fn build_tree<Rsc: HasEvents>(
|
|||||||
where
|
where
|
||||||
Rsc::State: FocusHost + OpenUrl,
|
Rsc::State: FocusHost + OpenUrl,
|
||||||
{
|
{
|
||||||
|
register_fonts(rsc.ui_mut());
|
||||||
let theme = Rc::new(Theme::new(&mut rsc.ui_mut().paints));
|
let theme = Rc::new(Theme::new(&mut rsc.ui_mut().paints));
|
||||||
let list = LazySpan::new(Dir::DOWN, Pin::End).add(rsc);
|
let list = LazySpan::new(Dir::DOWN, Pin::End).add(rsc);
|
||||||
list.controller(
|
list.controller(
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
use crate::client::text_cap::{VERBATIM_BYTES, VERBATIM_LINES, cut, show_all_label};
|
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::tool_summary::{ToolInput, parse_tool_input};
|
||||||
use crate::client::transcript_fold::{ToolState, TranscriptItem};
|
use crate::client::transcript_fold::{ToolState, TranscriptItem};
|
||||||
|
use crate::ui::icon;
|
||||||
use crate::ui::markdown::highlight_into;
|
use crate::ui::markdown::highlight_into;
|
||||||
use crate::ui::tap::{hold_edge, on_tap};
|
use crate::ui::tap::{hold_edge, on_tap};
|
||||||
use crate::ui::theme::Theme;
|
use crate::ui::theme::Theme;
|
||||||
|
|||||||
+2
-2
@@ -247,8 +247,8 @@ is a unit test with exactly those names in it.
|
|||||||
|
|
||||||
### 12. Icons
|
### 12. Icons
|
||||||
|
|
||||||
Add these to `iris/core/src/icon.rs` **and**
|
Add these to `app/src/ui/icon.rs` **and**
|
||||||
`iris/core/build-icon-font.sh`, then rerun the script and commit its output:
|
`app/build-icon-font.sh`, then rerun the script and commit its output:
|
||||||
`md-folder` U+F024B (the header button and
|
`md-folder` U+F024B (the header button and
|
||||||
directory rows), `md-plus` U+F0415, `md-pencil` U+F03EB,
|
directory rows), `md-plus` U+F0415, `md-pencil` U+F03EB,
|
||||||
`md-content_save` U+F0193, `md-file_outline` U+F0224. The folder and the plus
|
`md-content_save` U+F0193, `md-file_outline` U+F0224. The folder and the plus
|
||||||
|
|||||||
@@ -813,6 +813,13 @@ widget changed. Winit's user-event proxy and Android's posted callback are
|
|||||||
private implementations of that wake; applications do not define platform
|
private implementations of that wake; applications do not define platform
|
||||||
event types or manually request redraws.
|
event types or manually request redraws.
|
||||||
|
|
||||||
|
**Iris ships no fonts.** Applications register their own font bytes on `Ui`
|
||||||
|
before the first text shape and select them through the same `Family` used by
|
||||||
|
text widgets. `Family::Icons` is a semantic application-configured role, not a
|
||||||
|
particular icon set owned by the framework. ai-app owns its Nerd Fonts subset,
|
||||||
|
its codepoints, its license and the script that rebuilds it; body and monospace
|
||||||
|
families continue to come from the platform.
|
||||||
|
|
||||||
`cargo-iris` is an installable Cargo subcommand, rather than a script callers
|
`cargo-iris` is an installable Cargo subcommand, rather than a script callers
|
||||||
must find inside an Iris checkout. `cargo iris apk` builds the Rust `cdylib`
|
must find inside an Iris checkout. `cargo iris apk` builds the Rust `cdylib`
|
||||||
with cargo-ndk and packages it directly with the installed Android SDK tools:
|
with cargo-ndk and packages it directly with the installed Android SDK tools:
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
pub const OPEN: &str = "\u{F035D}";
|
|
||||||
|
|
||||||
pub const CLOSED: &str = "\u{F035F}";
|
|
||||||
|
|
||||||
pub const COLLAPSE: &str = "\u{F0360}";
|
|
||||||
@@ -19,7 +19,6 @@ mod render;
|
|||||||
mod ui;
|
mod ui;
|
||||||
mod widget;
|
mod widget;
|
||||||
|
|
||||||
pub mod icon;
|
|
||||||
pub mod util;
|
pub mod util;
|
||||||
|
|
||||||
pub use attr::*;
|
pub use attr::*;
|
||||||
|
|||||||
+106
-69
@@ -2,18 +2,15 @@ use crate::{Align, GlyphAtlas, GlyphKey, PaintId, PlacedGlyph, RegionAlign, Text
|
|||||||
use parley::{
|
use parley::{
|
||||||
Alignment, AlignmentOptions, FontContext, FontFamily, FontFamilyName, FontStyle, FontWeight,
|
Alignment, AlignmentOptions, FontContext, FontFamily, FontFamilyName, FontStyle, FontWeight,
|
||||||
GenericFamily, Layout, LayoutContext, LineHeight, PositionedLayoutItem, StyleProperty,
|
GenericFamily, Layout, LayoutContext, LineHeight, PositionedLayoutItem, StyleProperty,
|
||||||
fontique::Blob,
|
fontique::{Blob, FontInfoOverride},
|
||||||
};
|
};
|
||||||
use std::ops::Range;
|
use std::{collections::HashMap, fmt, ops::Range, sync::Arc};
|
||||||
use std::sync::Arc;
|
|
||||||
use swash::{
|
use swash::{
|
||||||
FontRef,
|
FontRef,
|
||||||
scale::{Render, ScaleContext, Source, StrikeWith},
|
scale::{Render, ScaleContext, Source, StrikeWith},
|
||||||
zeno::{Format, Vector},
|
zeno::{Format, Vector},
|
||||||
};
|
};
|
||||||
|
|
||||||
const NERD_ICONS: &[u8] = include_bytes!("../../assets/fonts/nerd_icons.ttf");
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct FontDiagnostics {
|
pub struct FontDiagnostics {
|
||||||
pub families_found: usize,
|
pub families_found: usize,
|
||||||
@@ -23,13 +20,42 @@ pub struct FontDiagnostics {
|
|||||||
pub bold_resolved: Option<String>,
|
pub bold_resolved: Option<String>,
|
||||||
pub italic_resolved: Option<String>,
|
pub italic_resolved: Option<String>,
|
||||||
pub mono_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>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub enum FontRegistrationError {
|
||||||
|
UnsupportedFamily(Family),
|
||||||
|
AlreadyRegistered(Family),
|
||||||
|
TextAlreadyShaped,
|
||||||
|
InvalidFont(Family),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for FontRegistrationError {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::UnsupportedFamily(family) => write!(
|
||||||
|
f,
|
||||||
|
"cannot register font data for {family:?}; use Family::Icons or Family::Named"
|
||||||
|
),
|
||||||
|
Self::AlreadyRegistered(family) => {
|
||||||
|
write!(f, "font data is already registered for {family:?}")
|
||||||
|
}
|
||||||
|
Self::TextAlreadyShaped => write!(
|
||||||
|
f,
|
||||||
|
"cannot register font data after text has been shaped; register application fonts before the first draw"
|
||||||
|
),
|
||||||
|
Self::InvalidFont(family) => {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"font data registered for {family:?} contains no usable fonts"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for FontRegistrationError {}
|
||||||
|
|
||||||
pub struct TextData {
|
pub struct TextData {
|
||||||
pub font_cx: FontContext,
|
pub font_cx: FontContext,
|
||||||
pub layout_cx: LayoutContext<PaintId>,
|
pub layout_cx: LayoutContext<PaintId>,
|
||||||
@@ -46,41 +72,28 @@ pub struct TextData {
|
|||||||
/// truth would mean carrying a `Painter` (or output size) into every
|
/// truth would mean carrying a `Painter` (or output size) into every
|
||||||
/// input handler for the sake of one field.
|
/// input handler for the sake of one field.
|
||||||
pub density: f32,
|
pub density: f32,
|
||||||
/// The family name [`NERD_ICONS`] registered under, which is what
|
registered_families: HashMap<Family, String>,
|
||||||
/// [`Family::Icons`] resolves to. `None` only if registering the
|
next_registered_family: u64,
|
||||||
/// bundled font failed, which is a broken build rather than a
|
shaping_started: bool,
|
||||||
/// platform difference -- said in the startup diagnostics rather than
|
|
||||||
/// silently drawn as tofu.
|
|
||||||
pub icon_family: Option<String>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for TextData {
|
impl Default for TextData {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
let mut font_cx = FontContext::new();
|
let mut font_cx = FontContext::new();
|
||||||
patch_android_monospace(&mut font_cx);
|
patch_android_monospace(&mut font_cx);
|
||||||
let icon_family = register_icon_font(&mut font_cx);
|
|
||||||
Self {
|
Self {
|
||||||
font_cx,
|
font_cx,
|
||||||
layout_cx: LayoutContext::new(),
|
layout_cx: LayoutContext::new(),
|
||||||
scale_cx: ScaleContext::new(),
|
scale_cx: ScaleContext::new(),
|
||||||
atlas: GlyphAtlas::default(),
|
atlas: GlyphAtlas::default(),
|
||||||
density: 1.0,
|
density: 1.0,
|
||||||
icon_family,
|
registered_families: HashMap::new(),
|
||||||
|
next_registered_family: 0,
|
||||||
|
shaping_started: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// So this reads `fonts.xml` itself (already on-device, already the
|
/// So this reads `fonts.xml` itself (already on-device, already the
|
||||||
/// authority Compose's own `Typeface.MONOSPACE` resolves through) for the
|
/// authority Compose's own `Typeface.MONOSPACE` resolves through) for the
|
||||||
/// filename that declaration names, then finds which of fontique's
|
/// filename that declaration names, then finds which of fontique's
|
||||||
@@ -154,16 +167,56 @@ fn android_monospace_font_filename() -> Option<String> {
|
|||||||
fn patch_android_monospace(_font_cx: &mut FontContext) {}
|
fn patch_android_monospace(_font_cx: &mut FontContext) {}
|
||||||
|
|
||||||
impl TextData {
|
impl TextData {
|
||||||
|
pub(crate) fn register_font(
|
||||||
|
&mut self,
|
||||||
|
family: Family,
|
||||||
|
data: impl AsRef<[u8]> + Send + Sync + 'static,
|
||||||
|
) -> Result<(), FontRegistrationError> {
|
||||||
|
if !matches!(family, Family::Icons | Family::Named(_)) {
|
||||||
|
return Err(FontRegistrationError::UnsupportedFamily(family));
|
||||||
|
}
|
||||||
|
if self.shaping_started {
|
||||||
|
return Err(FontRegistrationError::TextAlreadyShaped);
|
||||||
|
}
|
||||||
|
if self.registered_families.contains_key(&family) {
|
||||||
|
return Err(FontRegistrationError::AlreadyRegistered(family));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parley selects fonts by family rather than by a face handle. Give
|
||||||
|
// application data a private family name so selecting it cannot find
|
||||||
|
// a system font that happens to carry the same embedded metadata.
|
||||||
|
let private_name = format!("__iris_registered_font_{}__", self.next_registered_family);
|
||||||
|
let fonts = self.font_cx.collection.register_fonts(
|
||||||
|
Blob::new(Arc::new(data)),
|
||||||
|
Some(FontInfoOverride {
|
||||||
|
family_name: Some(&private_name),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
if fonts.is_empty() {
|
||||||
|
return Err(FontRegistrationError::InvalidFont(family));
|
||||||
|
}
|
||||||
|
self.next_registered_family += 1;
|
||||||
|
self.registered_families.insert(family, private_name);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn is_font_registered(&self, family: &Family) -> bool {
|
||||||
|
self.registered_families.contains_key(family)
|
||||||
|
}
|
||||||
|
|
||||||
/// Cloned rather than borrowed because the caller needs it while the
|
/// Cloned rather than borrowed because the caller needs it while the
|
||||||
/// layout builder holds `&mut self` -- a `String` per shaped icon run,
|
/// layout builder holds `&mut self` -- a `String` per shaped registered
|
||||||
/// paid only when the layout is rebuilt.
|
/// run, paid only when the layout is rebuilt.
|
||||||
pub fn resolve_family(&self, family: &Family) -> Family {
|
pub fn resolve_family(&self, family: &Family) -> Family {
|
||||||
|
if let Some(name) = self.registered_families.get(family) {
|
||||||
|
return Family::Named(name.clone());
|
||||||
|
}
|
||||||
match family {
|
match family {
|
||||||
Family::Icons => self
|
Family::Icons => panic!(
|
||||||
.icon_family
|
"Family::Icons has no font; register application font data with Ui::register_font before the first draw"
|
||||||
.clone()
|
),
|
||||||
.map_or(Family::Icons, Family::Named),
|
_ => family.clone(),
|
||||||
other => other.clone(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -236,7 +289,6 @@ impl TextData {
|
|||||||
bold_resolved,
|
bold_resolved,
|
||||||
italic_resolved,
|
italic_resolved,
|
||||||
mono_resolved,
|
mono_resolved,
|
||||||
icon_family: self.icon_family.clone(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -339,15 +391,13 @@ impl TextData {
|
|||||||
|
|
||||||
/// Which family to ask for. Kept as an owned name rather than parley's
|
/// Which family to ask for. Kept as an owned name rather than parley's
|
||||||
/// borrowed `FontFamily<'_>` so that a widget can hold one without a lifetime.
|
/// borrowed `FontFamily<'_>` so that a widget can hold one without a lifetime.
|
||||||
#[derive(Clone, PartialEq)]
|
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
||||||
pub enum Family {
|
pub enum Family {
|
||||||
SansSerif,
|
SansSerif,
|
||||||
Serif,
|
Serif,
|
||||||
Monospace,
|
Monospace,
|
||||||
/// The bundled icon font -- see [`crate::icon`] for what is in it.
|
/// The icon font supplied by the application through
|
||||||
/// Named as an intention rather than as a font name because only
|
/// [`crate::Ui::register_font`].
|
||||||
/// [`TextData`] knows what the file registered as; it resolves this
|
|
||||||
/// during shaping ([`TextData::resolve_family`]).
|
|
||||||
Icons,
|
Icons,
|
||||||
Named(String),
|
Named(String),
|
||||||
}
|
}
|
||||||
@@ -507,6 +557,7 @@ impl TextBuffer {
|
|||||||
width: Option<f32>,
|
width: Option<f32>,
|
||||||
density: f32,
|
density: f32,
|
||||||
) {
|
) {
|
||||||
|
data.shaping_started = true;
|
||||||
if self.shaped.as_ref() == Some(&(attrs.clone(), width, density)) {
|
if self.shaped.as_ref() == Some(&(attrs.clone(), width, density)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -586,38 +637,24 @@ pub struct RenderedText {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::icon;
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn every_icon_is_in_the_bundled_font() {
|
fn invalid_font_data_is_reported() {
|
||||||
let font = FontRef::from_index(NERD_ICONS, 0).expect("the bundled icon font parses");
|
let mut data = TextData::default();
|
||||||
let charmap = font.charmap();
|
assert_eq!(
|
||||||
for (name, glyph) in [
|
data.register_font(Family::Icons, b"not a font" as &'static [u8]),
|
||||||
("OPEN", icon::OPEN),
|
Err(FontRegistrationError::InvalidFont(Family::Icons))
|
||||||
("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
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn the_icon_family_registers_and_resolves() {
|
fn registering_after_shaping_is_reported() {
|
||||||
let data = TextData::default();
|
let mut data = TextData::default();
|
||||||
let family = data.resolve_family(&Family::Icons);
|
let mut buffer = TextBuffer::new("ordinary platform text");
|
||||||
assert!(
|
buffer.shape(&mut data, &TextAttrs::default(), None, 1.0);
|
||||||
matches!(family, Family::Named(_)),
|
assert_eq!(
|
||||||
"the bundled icon font did not register: {:?}",
|
data.register_font(Family::Icons, b"not a font" as &'static [u8]),
|
||||||
data.icon_family
|
Err(FontRegistrationError::TextAlreadyShaped)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -66,6 +66,24 @@ pub struct Ui {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Ui {
|
impl Ui {
|
||||||
|
/// Register application-owned font data for a semantic or named family.
|
||||||
|
///
|
||||||
|
/// This must happen before the first text shape. Existing text layouts
|
||||||
|
/// cache their resolved faces, so accepting a later registration would
|
||||||
|
/// leave already-shaped widgets displaying the old result.
|
||||||
|
#[track_caller]
|
||||||
|
pub fn register_font(
|
||||||
|
&mut self,
|
||||||
|
family: crate::Family,
|
||||||
|
data: impl AsRef<[u8]> + Send + Sync + 'static,
|
||||||
|
) -> Result<(), crate::FontRegistrationError> {
|
||||||
|
self.data.text.register_font(family, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_font_registered(&self, family: &crate::Family) -> bool {
|
||||||
|
self.data.text.is_font_registered(family)
|
||||||
|
}
|
||||||
|
|
||||||
/// A read-only handle to the retained result of the last completed frame.
|
/// A read-only handle to the retained result of the last completed frame.
|
||||||
/// The handle is owned so a caller may keep its read guard while mutating
|
/// The handle is owned so a caller may keep its read guard while mutating
|
||||||
/// unrelated resources on the `Rsc` that owns this `Ui`.
|
/// unrelated resources on the `Rsc` that owns this `Ui`.
|
||||||
|
|||||||
@@ -321,7 +321,6 @@ impl AndroidRenderer {
|
|||||||
mono={default_mono_family:?}\n\
|
mono={default_mono_family:?}\n\
|
||||||
fonts resolved: regular={regular:?} bold={bold:?} italic={italic:?} \
|
fonts resolved: regular={regular:?} bold={bold:?} italic={italic:?} \
|
||||||
mono={mono:?}\n\
|
mono={mono:?}\n\
|
||||||
icon font: {icons:?}\n\
|
|
||||||
wgpu errors since surface creation:\n {errors_text}\n\n\
|
wgpu errors since surface creation:\n {errors_text}\n\n\
|
||||||
{frame_report}",
|
{frame_report}",
|
||||||
name = self.adapter_name,
|
name = self.adapter_name,
|
||||||
@@ -338,7 +337,6 @@ impl AndroidRenderer {
|
|||||||
bold = font.bold_resolved,
|
bold = font.bold_resolved,
|
||||||
italic = font.italic_resolved,
|
italic = font.italic_resolved,
|
||||||
mono = font.mono_resolved,
|
mono = font.mono_resolved,
|
||||||
icons = font.icon_family,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user