Text and fonts
Shaped labels, font weights and families, bidirectional layout, font fallback, color emoji, and glyph caching.
ui.label("Status: ready");Builders
use zaxis::{Color, Text};
ui.add(Text::new("Build output\nCompleted")
.size(18.0)
.color(Color::gray(210))
.wrap(true));| Method | Default / behavior |
|---|---|
new(impl Into<String>) | Own the text string |
size(f32) | Style::font_size; effective size clamped to 1–256 logical pixels; prefer typography(role) or ui.heading / title / small to stay on the theme scale |
weight(FontWeight) | TextStyle::weight, else the weight of the typography role (Regular unless the theme sets one); see Font weights |
color(Color) | Style::text_color |
muted() | Use Style::muted_text; an explicit color takes precedence |
wrap(bool) | true; wrap at the available layout width |
monospace() / family(TextFamily) | Proportional; the code font, see Monospace text |
tabular_numbers(bool) | false; equal-width digits, see Tabular figures |
tab_size(u16) | 8; width of a tab in cells (space advances) |
Text is passive: no pointer capture or keyboard focus. Its Response contains its
measured rectangle and hover state. ## is displayed literally. For text the user can
select and copy, links, or mixed styles inside a line, see
Hyperlink and selectable text.
ui.muted("No selection") is the short form of ui.add(Text::new("No selection").muted()).
Text without an explicit color also uses muted text inside a disabled group.
Layout behavior
The engine uses cosmic-text advanced shaping, including ligatures, complex scripts, bidirectional layout, and font fallback. Newlines create new lines. Wrapping uses word boundaries with glyph-level fallback for words wider than the available width. Painting is clipped by the enclosing UI.
wrap(false) measures without a width limit. Content can extend beyond its allocation
bounds, but painting still uses the enclosing content clip. The engine reserves at
least one line even for an empty string. Line height is 1.25 × font_size.
Font weights
FontWeight is a number on the CSS scale (100–900, clamped) with the constants THIN,
EXTRA_LIGHT, LIGHT, REGULAR (400), MEDIUM (500), SEMIBOLD (600), BOLD (700),
EXTRA_BOLD and BLACK.
use zaxis::{FontWeight, Text, TextStyle, TypographyRole};
ui.add(Text::new("Build output").weight(FontWeight::SEMIBOLD));
ui.add(Text::new("Caption").style(TextStyle { weight: Some(FontWeight::MEDIUM), ..Default::default() }));A weight selects a font file. Bold is drawn from real bold outlines with their own advance
widths, never by thickening Regular. The bundled Inter has Regular, Medium, SemiBold and Bold
(bundled-weights feature, on by default). A weight the family has no file for uses the
nearest declared one by the CSS rule: an exact file wins; below 400 lighter files are tried
first, then heavier; above 500 heavier files first, then lighter; 400–500 look up to 500, then
down, then above. FontFamily::resolve(weight) returns the choice. Without
bundled-weights, or in a family declared with only Regular, every weight resolves to Regular
and nothing is faked.
Weight resolution, highest priority first:
| Source | Applies to |
|---|---|
Text::weight | One Text |
TextStyle::weight (Style::text, StyleOverrides::text, Text::style) | Text widgets in the subtree |
Typography::weights by role: small, body, heading, title | Text::typography, ui.heading, ui.title, ui.small; body is the default |
ButtonStyle::font_weight, then Typography::weights.control | Button and its variants |
Typography::weights.selected (falls back to control) | Active tab of ui.tab_bar |
WindowStyle::title_font_weight, TitleBarStyle::font_weight | Window and title bar titles |
TableStyle::header_font_weight | Table column headers |
DisclosureStyle::font_weight (tree.row, collapsing.header) | Tree node names and collapsing headers |
TextEditStyle::font_weight | TextEdit text, placeholder, caret and selection |
Everything defaults to Regular, so a theme looks exactly as before until a weight is set:
let mut theme = Theme::dark();
theme.typography.weights.heading = FontWeight::SEMIBOLD;
theme.typography.weights.small = FontWeight::MEDIUM;Measurement, caret positions, selection and IME placement use the same weighted layout as painting, so a weight change reflows only text that uses it. A weight is a property of a whole string; there is no mixed-weight rich text or range styling.
Replace the font
Context::new() uses the bundled Inter family (OFL). Pass your own with a FontFamily: one
file per weight, the first for Regular.
use zaxis::{FontFamily, FontWeight, RunOptions};
let family = FontFamily::new(include_bytes!("../assets/Brand-Regular.ttf"))
.with_weight(FontWeight::MEDIUM, include_bytes!("../assets/Brand-Medium.ttf"))
.with_weight(FontWeight::BOLD, include_bytes!("../assets/Brand-Bold.ttf"));
zaxis::run_with_options(app, RunOptions::default().with_font_family(family))?;FontFamily::new and with_weight accept anything that is AsRef<[u8]> + Send + Sync + 'static:
include_bytes! data stays in the executable image, a Vec<u8> is kept as given. The declared
weight is authoritative; the font's own weight metadata is ignored. FontFamily::inter() is the
bundled family, for example to add a missing weight. A custom host calls
Context::with_fonts(family); Context::with_font(FontArc) remains for a single regular face.
Set the new context's viewport before rendering. There is no public runtime font setter.
Fallback order for a character the family lacks: the family itself, then installed system
fonts chosen by script (platform tables), then the bundled Noto Color Emoji, then any installed
face. The weight applies to all of them. Color emoji follow every requested weight (the single
file is registered at each weight step). A system font is used at the requested weight only if
it has a file for it; otherwise the layout still succeeds with a nearest face chosen by
cosmic-text, which may differ from the platform's preferred family for that script. Coverage of
scripts depends on the fonts of the host. Disable bundled-emoji to use system fonts for emoji.
Monospace text
Text::monospace() sets a string in the monospace family; Text::family(TextFamily::Monospace)
is the long form, and ui.code(text) / TypographyRole::Code use it with the
Typography::code size (13 px) and TypographyWeights::code. The family is chosen
independently of size, weight and the main font. Precedence is the builder, then
TextStyle::family, then the role.
The family is bundled JetBrains Mono (OFL, Regular and, with bundled-weights, Bold) from the
bundled-monospace feature, which is on by default. It is the same on every platform: an
installed system monospace font is never used in its place. Replace it with
RunOptions::with_monospace_family, Context::with_font_families or
SharedResources::with_font_families. Without the feature and without a configured family,
monospace text uses the system's generic monospace font and then differs between platforms.
Code ligatures are turned off so one character is always one cell.
Every character of the primary font is one cell wide, so n of them are n × cell_width. A
glyph the font lacks (CJK, emoji, symbols) comes from a fallback font and keeps that font's
own advance; combining marks add no cell. The width of a string is its real shaped width, from
the same layout that paints, measures and positions carets, and not a character count. Text
after a fallback glyph is therefore shifted off the grid by the difference.
Code views take their metrics from the layout instead of estimating:
let cell = ui.monospace_metrics(); // MonospaceMetrics { cell_width, line_height }
let content_width = longest_line_chars as f32 * cell.cell_width;
ScrollArea::vertical().show_rows(ui, cell.line_height, lines.len(), |ui, range| {
for line in &lines[range] {
ui.add(Text::new(line.as_str()).typography(TypographyRole::Code).wrap(false));
}
});Context::monospace_metrics(size, weight) does the same for an arbitrary size.
Tabs are tab stops, tab_size cells apart (default 8), counted from the start of the
line. Use wrap(false) for code and columns: lines are never broken or cut by the layout; the
enclosing clip and scroll area decide what is visible. With wrapping on, words wrap as in
proportional text. The layout cache and the glyph atlas are keyed by family, so changing one
family or text does not rebuild unrelated text, and an unchanged UI shapes nothing. Ranges
with several families or colors in one string are not supported yet; the color is not part of
the cached layout, so range coloring can be added later.
Tabular figures
Text::tabular_numbers(true) turns on the OpenType tnum feature, so every digit has the
width of the widest one and numbers in a column line up. The bundled Inter has it. A custom
font without tnum renders unchanged: zaxis never imitates alignment by padding or fitting
widths. It has no effect on the monospace family, whose digits are already one cell wide.
TextStyle::tabular_numbers and TextEdit::tabular_numbers set it for a subtree or a field,
and Column::numeric(true) of a Table right-aligns its cells and gives their
text tabular figures. Right alignment (Column::align) uses the real measured width.
Binary size
Each bundled Inter file is about 340 KB. Regular is always embedded; Medium, SemiBold and Bold
add about 1 MB to the executable and are controlled by the bundled-weights feature. The data
is include_bytes! static data. A face is indexed when the context starts (name and OS/2
tables only); its outlines are parsed, shaped and rasterized only when a weight that resolves
to it is first used, and only used faces enter the layout and atlas caches. Applications that
only need Regular can disable default features and enable the ones they want:
zaxis = { version = "0.0.3", default-features = false, features = ["bundled-emoji"] }JetBrains Mono adds about 274 KB (Regular) and 278 KB (Bold, with bundled-weights) of static
data; it is parsed only when monospace text is first shaped. Disable bundled-monospace to drop it.
Rasterization and atlas
Glyphs rasterize at logical font size multiplied by DPI. Cache keys include the font face, its weight, the glyph, size, and subpixel position; layout keys add the text, wrap width, family, weight and style slot (italic is not implemented, but the key has room for it). Each weight in use has its own glyphs, so four weights cost about four times the glyph entries of one but share pages: a UI of short labels in four weights still fits one 1024 × 1024 page. Glyphs rasterize through Swash; color emoji retain their own RGB colors and use the requested text alpha.
Atlas pages normally use 1024 × 1024 RGBA8 pixels, with transparent white background and coverage in alpha. Oversized glyphs allocate a larger power-of-two page. Pages are retained for the context lifetime; there is no glyph eviction. New glyphs advance the page's texture revision and trigger a full page upload on the next render.
For editing and native IME composition, use TextEdit. See Limitations for the remaining boundaries.
Accessibility
ui.label, ui.muted and Text are Label nodes whose value is the text; ui.title is a
Heading of level 1 and ui.heading of level 2. The text is published as one run per
visual line with grapheme-cluster characters from the shaped layout.
See Accessibility.