Turns rich text documents into GPU-ready glyph quads.
text-typeset is the text pillar of Teksilo, FernTech's pure-Rust GUI framework: every label, editor and document view in Teksilo is shaped, laid out and rasterized here. It does not depend on Teksilo and works with any GPU framework.
Typesetting crate for the text-document ecosystem. Takes a rich text document model (styled paragraphs, tables, lists, frames) and produces positioned glyph quads, decoration rectangles, and a glyph atlas texture that any GPU framework can render in a few draw calls.
text-document (model) --> text-typeset (shaping + layout) --> framework adapter (rendering)
text-typeset is split along the axis of "what is shareable":
| Type | Lifetime | Owns | Shared across |
|---|---|---|---|
[TextFontService] |
one per process / window | font registry, glyph atlas, glyph cache, swash scale context, HiDPI scale factor |
every widget that emits text |
[DocumentFlow] |
one per widget | viewport, zoom, scroll, wrap mode, flow layout, cursor, colors, rendered frame | nothing — strictly per-widget |
A DocumentFlow does not own font data. Every layout_* and render method takes a TextFontService reference and reads the font registry and glyph atlas through it. This makes it cheap to run many widgets against a single shared atlas — one GPU upload per frame, one shaped glyph rasterized at most once, and no cross-widget contamination of viewport / zoom / scroll state.
Put the service behind whatever smart pointer your host framework uses (an Rc<RefCell<TextFontService>> for single-threaded UIs, a plain &mut inside a render loop that already has exclusive access). Every widget owns its own DocumentFlow and calls flow.render(&mut *service.borrow_mut()) when it paints. Because the service carries no per-widget state, changes that widget A makes to set_viewport, set_zoom, set_scroll_offset, or set_cursor live on A's flow and never touch B's.
- Text shaping via harfrust (Rust port of HarfBuzz) with OpenType feature support
- Font management via fontdb with CSS-spec font matching, generic family mapping, and glyph fallback
- Glyph rasterization via swash with color emoji support (COLR/CBDT)
- Glyph atlas backed by etagere shelf packing with auto-grow and LRU eviction — shared across every widget through the
TextFontService - Paragraph layout with line breaking (icu_segmenter, UAX #14), four alignment modes (left, right, center, justify), and first-line indent
- Hyphenation via hypher, Western European languages by default (
hyphenation-all-languagesfor every language hypher ships), plus soft-hyphen breaking - BiDi text analysis via unicode-bidi with per-run directional shaping
- Tables with column width distribution, cell layout, borders, and cell backgrounds
- Lists with marker rendering (bullet, decimal, alpha, roman) at configurable indent levels
- Frames with inline, float-left, float-right, and absolute positioning
- Text decorations: underline, strikeout, overline (from font metrics)
- Letter spacing and word spacing
- Hit testing: screen coordinates to document position with region detection (text, margin, link, image)
- Cursor display with caret rendering, blink support, multi-cursor, and selection painting with full-width line highlighting
- Zoom: display zoom (0.1x to 10x) without text reflow, scales all output coordinates — owned per-widget
- Cell-level selection: table cell highlighting via
selected_cellsin cursor display - Scrolling:
ensure_caret_visible,scroll_to_position(automatic 1/3-viewport placement), viewport culling - Incremental relayout: update a single block without re-laying-out the entire document
- Content width modes: auto (follows viewport) or fixed (for page-like WYSIWYG layout)
- HiDPI: the service carries a single scale factor and bumps a
scale_generationcounter on every change; each flow stamps the counter during layout and exposeslayout_dirty_for_scaleso the framework can detect and act on HiDPI transitions without tracking them itself
text-typeset produces a RenderFrame containing:
- Glyph quads (
[screen_rect, atlas_rect, color]) drawn as textured rectangles from the atlas - Image quads (position + resource name) for inline images loaded by the adapter
- Decoration rects (underline, strikeout, selection, cursor, table borders, backgrounds)
- Atlas texture (RGBA, updated incrementally)
The rendering contract is thin: "draw N sub-rects from a texture + M colored rects." Any framework that supports textured quads can serve as a backend: Godot (draw_texture_rect_region), Qt (QPainter::drawImage), wgpu, egui, iced.
use text_typeset::{DocumentFlow, TextFontService};
// 1. Build the shared service once and register fonts on it.
let mut service = TextFontService::new();
let font = service.register_font(include_bytes!("test-fonts/NotoSans-Variable.ttf"));
service.set_default_font(font, 16.0);
// 2. Build a per-widget flow and give it a viewport.
let mut flow = DocumentFlow::new();
flow.set_viewport(800.0, 600.0);
// 3. Lay out + render. Every layout / render method takes the
// service by reference so the glyph atlas stays shared.
let doc = text_document::TextDocument::new();
doc.set_plain_text("Hello, world!").unwrap();
flow.layout_full(&service, &doc.snapshot_flow());
let frame = flow.render(&mut service);
// frame.glyphs -> glyph quads (textured rects from the shared atlas)
// frame.atlas_pixels -> RGBA texture to upload (or read via service.atlas_pixels())
// frame.decorations -> cursor, selection, underlines, bordersTwo widgets — say, a live editor and a read-only preview — share a TextDocument, a TextFontService, and produce glyphs into the same atlas. Each owns its own DocumentFlow with its own viewport / zoom / scroll:
use std::cell::RefCell;
use std::rc::Rc;
use text_typeset::{DocumentFlow, TextFontService};
let service: Rc<RefCell<TextFontService>> = {
let mut svc = TextFontService::new();
let face = svc.register_font(include_bytes!("test-fonts/NotoSans-Variable.ttf"));
svc.set_default_font(face, 16.0);
Rc::new(RefCell::new(svc))
};
let mut editor_flow = DocumentFlow::new();
editor_flow.set_viewport(600.0, 400.0);
let mut preview_flow = DocumentFlow::new();
preview_flow.set_viewport(500.0, 400.0);
preview_flow.set_zoom(0.8); // zoom lives on the flow — only affects this widget
let doc = text_document::TextDocument::new();
doc.set_plain_text("Hello, world!").unwrap();
editor_flow.layout_full(&service.borrow(), &doc.snapshot_flow());
preview_flow.layout_full(&service.borrow(), &doc.snapshot_flow());
let _editor_frame = editor_flow.render(&mut service.borrow_mut());
let _preview_frame = preview_flow.render(&mut service.borrow_mut());Neither render call mutates state that belongs to the other flow. The atlas, shaper cache, and font registry are shared; the viewport, scroll, zoom, cursor, and flow layout are not.
By default, text wraps at the viewport width (web/editor style). For page-like layout:
// Fixed width: text wraps at 600px regardless of viewport size
flow.set_content_width(600.0);
// Back to auto: text reflows when viewport resizes
flow.set_content_width_auto();set_scale_factor lives on the service because it drives the physical ppem of every rasterized glyph — one atlas, one scale:
service.set_scale_factor(2.0); // clears the atlas + glyph cache, bumps scale_generation
// Every flow stamps the service's scale_generation at layout time.
// After a change, ask whether a flow is stale and relayout if so:
if editor_flow.layout_dirty_for_scale(&service.borrow()) {
editor_flow.layout_full(&service.borrow(), &doc.snapshot_flow());
}The service cannot reach into per-widget flows, so it cannot clear their layouts for you. The generation counter makes that invalidation observable; the caller decides what to do with it.
| Crate | Purpose |
|---|---|
| harfrust | OpenType shaping |
| swash | Font metrics and glyph rasterization |
| fontdb | Font discovery and CSS-spec matching |
| ttf-parser | OS/2 and cmap reading for writing-system classification |
| etagere | Glyph atlas allocation (shelf packing) |
| icu_segmenter | Line break opportunities (UAX #14) |
| unicode-bidi | Bidirectional text (UAX #9) |
| hypher | Hyphenation patterns |
| thiserror | Error type derivation |
| text-document | Document model (optional, default feature) |
Licensed under the Mozilla Public License, Version 2.0 (LICENSE).