zaxis 0.1.0
Components

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));
MethodDefault / 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:

SourceApplies to
Text::weightOne Text
TextStyle::weight (Style::text, StyleOverrides::text, Text::style)Text widgets in the subtree
Typography::weights by role: small, body, heading, titleText::typography, ui.heading, ui.title, ui.small; body is the default
ButtonStyle::font_weight, then Typography::weights.controlButton and its variants
Typography::weights.selected (falls back to control)Active tab of ui.tab_bar
WindowStyle::title_font_weight, TitleBarStyle::font_weightWindow and title bar titles
TableStyle::header_font_weightTable column headers
DisclosureStyle::font_weight (tree.row, collapsing.header)Tree node names and collapsing headers
TextEditStyle::font_weightTextEdit 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.

Edit on GitHub

On this page