Skip to content

Fonts

The type a backend is set in: which family each role resolves through, the sizes a family was cut in, and how a string is split into the faces that actually draw it. The faces are U8g2's bitmaps, read from flash a glyph at a time, so nothing is scaled at run time and nothing allocates.

design.md says why a role names a size rather than a face, and why a font id is a hash of the bytes. This page is what each piece does.

FontsThe type a chrome is set in: one family, and the size each role wants.
FamilyA typeface, in ascending size order.
TierOne size of one family: four styles, and the height of a line of it.
FaceA font, resolved: which family it came from, which size, and which style.
HELVETICAThe family every preset is built on.
font_tier!One tier of a family cut in a regular and a bold and nothing else.
font_idFNV-1a over every byte of every style in a tier.
request_familySets the type in family, from the next frame, on every backend.
clear_chosen_familyForgets the choice, so backends keep whatever they were built with.
PieceOne part of a string, and what draws it.
piecesWalks text as the pieces it will actually be drawn as.
advanceWhat one piece moves the pen by.
Re-exportsFont, FontRenderer and the u8g2 faces.

The type a chrome is set in: one family, and the size each role wants.

pub struct Fonts

A role names a size, not a face. Each role asks for a line height, and the family answers with the tier nearest it. Swapping the family keeps the design: it cannot carry the old family's sizes into a chrome that has no room for them. A backend takes one with Backend::with_fonts and changes it while running with Backend::set_family. Fonts is also Default, as Fonts::DEFAULT.

FieldMeaning
xpui_eg::Fonts::familyThe family every role resolves through.
xpui_eg::Fonts::uiThe line height interface text wants.
xpui_eg::Fonts::ui_smallThe line height secondary text wants.
xpui_eg::Fonts::readerThe line height a page of prose wants.

Example — what each role resolves to

Rust
use xpui::host::{FontRole, FontStyle};
use xpui_eg::{Fonts, HELVETICA};
let fonts = Fonts::DEFAULT;
assert!(core::ptr::eq(fonts.family, &HELVETICA));
assert_eq!(fonts.tier(FontRole::Ui).line_height, 30);
assert_eq!(fonts.tier(FontRole::Reader).line_height, 39);
let id = fonts.id(FontRole::Ui);
assert!(fonts.tier_by_id(id).is_some());
let face = fonts.face(id, FontStyle::Bold);
assert_eq!(face.tier.line_height, 30);

The set for the default chrome: a reader held with buttons.

pub const DEFAULT: Fonts = Fonts

Interface text at 30 pixels, secondary at 18, prose at 39. Thirty pixels is 3.5mm of glass at 218 ppi: the size below which a person stops reading a label and starts recognising its shape.

The set for chrome a board has scaled up for a finger.

pub const LARGE: Fonts = Fonts

The set for a small colour panel, with 30-pixel rows.

pub const COMPACT: Fonts = Fonts

The set for a strip, with 24-pixel rows and a 16-pixel hint band.

pub const SMALL: Fonts = Fonts

The smallest chrome, not the smallest type: at 111 ppi, 18 pixels is larger on the glass than 30 pixels on a reader.

The sizes that fit this chrome.

pub const fn for_metrics(metrics: &Metrics) -> Fonts

Chosen by list_row_height, the band body text is painted into:

Row heightPreset
44 and upLARGE
34 to 43DEFAULT
26 to 33COMPACT
below 26SMALL

The same sizes, set in another family.

pub const fn with_family(self, family: &'static Family) -> Fonts

The tier a role resolves to.

pub fn tier(&self, role: FontRole) -> &'static Tier

The id this backend reports for a role: the tier's bytes and the family's name.

pub fn id(&self, role: FontRole) -> FontId

Both halves are needed: a face swapped for different bytes has to move the id, and two families over one tier that differ only in what they fall back to draw the same string differently. Every id moves when the family changes.

The tier an id refers to, or None for one this set never handed out.

pub fn tier_by_id(&self, id: FontId) -> Option<&'static Tier>

The font an id and style resolve to.

pub fn face(&self, id: FontId, style: FontStyle) -> Face

See also: Family, Face, request_family

A typeface, in ascending size order.

pub struct Family

A bitmap family has the sizes it has: "18pt" is one of a handful of tiers the foundry cut. Build one of your own with font_tier! and the u8g2 faces.

FieldMeaning
xpui_eg::Family::nameWhat a picker shows.
xpui_eg::Family::tiersThe sizes it was cut in, ascending by line_height.
xpui_eg::Family::fallbackWhere a glyph this family lacks is looked for.

name is not an identity: the id is the bytes. tiers must be in ascending order, which tier_for relies on. A fallback of None ends the chain, and a character nothing in the chain has draws as a marker box rather than disappearing.

Example — a family of your own

Rust
use xpui::host::FontRole;
use xpui_eg::{Family, Fonts, HELVETICA, Tier, font_tier, u8g2};
const COURIER_18: Tier = font_tier!(
27,
u8g2::u8g2_font_courR18_tf,
u8g2::u8g2_font_courB18_tf
);
static COURIER: Family = Family {
name: "Courier",
tiers: &[COURIER_18],
fallback: Some(&HELVETICA),
};
let fonts = Fonts::DEFAULT.with_family(&COURIER);
assert_eq!(fonts.tier(FontRole::Reader).line_height, 27);
assert_ne!(fonts.id(FontRole::Ui), Fonts::DEFAULT.id(FontRole::Ui));

The tier nearest line_height.

pub fn tier_for(&'static self, line_height: i32) -> &'static Tier

Nearest, not the largest that fits: a chrome asking for 30 on a family cut at 28 and 39 gets the 28. A tie goes to the smaller.

One size of one family: four styles, and the height of a line of it.

pub struct Tier
FieldMeaning
xpui_eg::Tier::idA hash of every byte of every style below.
xpui_eg::Tier::line_heightThe band a line of this tier is painted into.
xpui_eg::Tier::regularThe upright face.
xpui_eg::Tier::boldThe bold face.
xpui_eg::Tier::italicNone when the family was never cut in it — u8g2's Helvetica was not.
xpui_eg::Tier::bold_italicNone when the family was never cut in it; bold is drawn instead.

line_height is the tallest style, not the regular: a bold is often a pixel or two taller, and the framework asks for a line height without saying which style it will draw. Asking for italic from a tier without one gets the regular face.

The face for a style, falling back to the nearest cut that exists.

pub fn face(&self, style: FontStyle) -> &'static FontRenderer

A font, resolved: which family it came from, which size, and which style.

pub struct Face

The family travels with the tier because a missing glyph is looked for in the family's fallback, and a tier on its own does not know which family cut it. Fonts::face makes one.

FieldMeaning
xpui_eg::Face::familyThe family it came from, for the fallback chain.
xpui_eg::Face::tierThe size it was cut in.
xpui_eg::Face::styleWeight and slant.

The renderer that actually paints this style.

pub fn renderer(&self) -> &'static u8g2_fonts::FontRenderer

The family every preset is built on.

pub static HELVETICA: Family = Family

Cut at line heights 14, 18, 21, 30 and 39, each a regular and a bold from u8g2's full 8-bit set, so accented Latin renders as itself. It has no italic and no fallback.

One tier of a family cut in a regular and a bold and nothing else.

macro_rules! font_tier
font_tier!(line_height, RegularFace, BoldFace)

It expands to a Tier whose id is font_id over both faces' bytes, with no italic. Every path in it goes through this crate, so a caller needs no dependency on u8g2-fonts of its own. The line height is declared, not measured: give the taller of the two faces. See the example under Family.

FNV-1a over every byte of every style in a tier.

pub const fn font_id(styles: &[&[u8]]) -> FontId

const, so a tier's id costs nothing at run time. The answer is folded to a positive i32, and never 0, which the framework reads as "no such font".

Rust
use xpui_eg::font_id;
let one = font_id(&[b"one".as_slice()]);
let two = font_id(&[b"two".as_slice()]);
assert_ne!(one, two);
assert!(font_id(&[]).0 > 0);

Sets the type in family, from the next frame, on every backend.

pub fn request_family(family: &'static Family)

A standing choice, not a one-shot: each backend picks it up in its next begin_frame, so a simulator that holds one backend per board keeps it across a switch of board. It lands between frames because a family applied mid-frame would leave a screen measured in one face and painted in another. It asks for a repaint, so a host must be installed.

Rust
use xpui_eg::{Backend, HELVETICA, Palette, clear_chosen_family, request_family};
use xpui_screenshot::Framebuffer;
let backend = Backend::new(Framebuffer::new(480, 800), Palette::INK_IS_ON);
request_family(&HELVETICA);
backend.begin_frame(0);
assert!(core::ptr::eq(backend.fonts().family, &HELVETICA));
clear_chosen_family();

Forgets the choice, so backends keep whatever they were built with.

pub fn clear_chosen_family()

For tests, which share the choice with every other test in their binary.

One part of a string, and what draws it.

pub enum Piece<'a>
VariantMeaning
xpui_eg::Piece::RunDraw text with face.
xpui_eg::Piece::MarkerA character nothing in the chain has, drawn as a box this size.

A Run's text is usually a slice of the string, but an ellipsis no face in the chain has arrives as "...". A Marker carries its width and height.

Walks text as the pieces it will actually be drawn as.

pub fn pieces<'a>(font: Face, text: &'a str, emit: impl FnMut(Piece<'a>))

Measuring and drawing walk this same function, so a layout cannot measure with one face and paint with another. Fallback is per glyph: the face the role resolved to, then the family's fallback chain, then a marker. A string the first face draws whole is one piece.

Rust
use xpui::host::{FontRole, FontStyle};
use xpui_eg::{Fonts, Piece, advance, pieces};
let fonts = Fonts::DEFAULT;
let face = fonts.face(fonts.id(FontRole::Ui), FontStyle::Regular);
let mut runs = 0;
let mut width = 0;
pieces(face, "Wi-Fi…", |piece| {
if let Piece::Run { .. } = piece {
runs += 1;
}
width += advance(piece);
});
assert_eq!(runs, 2, "the ellipsis is drawn as three full stops");
assert!(width > 0);

What one piece moves the pen by.

pub fn advance(piece: Piece<'_>) -> i32

Drawing advances by exactly this after each piece, so a string is as wide as its parts wherever it is asked. A marker advances one pixel past its box.

Re-exported so a caller can assemble a family without a second dependency on u8g2-fonts that could drift to another version.

NameWhat it is
xpui_eg::Fontu8g2-fonts' trait for a face; font_tier! reads each face's bytes through it.
xpui_eg::FontRendereru8g2-fonts' renderer for one face, which a Tier holds per style.
xpui_eg::u8g2Every face u8g2-fonts ships, as u8g2_fonts::fonts: u8g2::u8g2_font_helvR18_tf and the rest.