limn
    ↑↓ ChooseEnter OpenEsc Close
    GitHub

    Components/System

    TextLayout

    Text that limn shapes, breaks into lines and places itself. Where the caret goes, what a selection covers and the glyphs a backend draws all come from one layout, so they always agree: no backend lays the text out again.

    Native componentGuide source

    Preview

    CodeSource
    let layout = TextLayout::new("Interfaces, drawn in Rust.", TextStyle::new(22.0, FontFamily::Sans));
    let size = layout.size();
    Element::drawing(move |_| layout.draw(Point::new(0.0, 0.0), ink))
        .size(Some(size.width), Some(size.height))

    Examples

    Each one is the library drawing it. Press, type and hover.

    Families2 / 5
    CodeSource

    Read the source of this example.

    Families3 / 5
    CodeSource

    Read the source of this example.

    Families4 / 5
    CodeSource

    Read the source of this example.

    Families5 / 5
    CodeSource

    Read the source of this example.

    Leading
    CodeSource
    super::row(vec![leading(Leading::Even), leading(Leading::Below)])
    Wrapping
    CodeSource
    wrapped
    Caret and selection
    CodeSource
    selected

    TextLayout

    Text that limn shapes, breaks into lines and places itself. Where the caret goes, what a selection covers and the glyphs a backend draws all come from one layout, so they always agree: no backend lays the text out again.

    In a tree told to with UiTree::set_text_layout(true), which limn_egui::attach_fonts does, every Element::text and Element::formatted_text is laid out in a TextLayout, and selected, underlined and pressed from it: so are Markdown and every component made of them. So are text fields and text areas: their caret, selection, wrapping and scrolling, where a click puts the caret and where Up and Down take it, all come from one layout of their value. The code editor still measures its text through the backend. An application also draws text with it in an Element::drawing.

    Import

    use limn::{Color, Element, FontFamily, Point, TextLayout, TextStyle};

    Usage

    let layout = TextLayout::new("Interfaces, drawn in Rust.", TextStyle::new(22.0, FontFamily::Sans));
    let size = layout.size();
    Element::drawing(move |_| layout.draw(Point::new(0.0, 0.0), ink))
        .size(Some(size.width), Some(size.height))

    TextLayout::new(text, style) shapes the text: kerning, ligatures, marks, right-to-left runs, and a fallback face for each character the family lacks. It keeps one line for each line the text has. size() is as wide as its widest line and as tall as its lines; empty text is one line tall.

    draw(origin, color) returns one DrawCommand::Glyphs for each run of one font, placed from origin. A monochrome glyph is drawn in color. A layout and its clones give the same glyphs every time, so a backend can keep what it drew for them while they stay in place.

    Style

    TextStyle::new(size, family) sets the size in logical pixels and the FontFamily, which carries the weight and the slant. .line_height(px) sets every line that far apart; without it the font’s own ascent, descent and line gap decide. A size that is not finite is 14 and one below zero is zero; a line height that is not finite or below zero is ignored.

    .leading(Leading) says where the glyphs stand in a line taller than they are: Leading::Even, the default, sets half the spare room above them, as CSS does; Leading::Below sets them at the top of the line, the room under them, as limn’s own text is set. A tree lays Element::text out with Below and the line height of its metrics, so text sits where it did when the backend laid it out.

    Wrapping

    let paragraph = TextLayout::new(text, style).wrapped(360.0);

    wrapped(width) breaks the same text into lines no wider than width, where the language allows a break, without shaping it again: a layout made once can follow its container’s width. A word longer than width is broken between graphemes, so no line runs past it. A width that is not finite does not wrap.

    centered(width) breaks it as wrapped does and centers each line across width.

    lines() gives each line’s box, its baseline and the bytes it holds. A line’s box leaves out the spaces that hang past its end where it wraps.

    Formatted text

    let spans = [
        TextSpan::new("Read ", ink, 14.0, FontFamily::Sans),
        TextSpan::new("the guide", link, 14.0, FontFamily::Sans).underline(),
        TextSpan::new(" first.", ink, 14.0, FontFamily::Sans),
    ];
    let paragraph = TextStyle::new(14.0, FontFamily::Sans).line_height(18.0);
    let layout = TextLayout::formatted(&spans, paragraph).wrapped(320.0);

    TextLayout::formatted(spans, paragraph) sets each TextSpan in its own size, family and ink, all on one baseline, with the lines, background, icon or picture it asks for, as Element::formatted_text draws them. The paragraph’s line height is the least height of every line, and a line whose glyphs need more takes more; its family draws nothing.

    A span with a background stays on one line unless it is wider than any. An icon goes to the next line with the first word after it. A picture stands in for its span’s text, which a selection copies whole or not at all.

    max_lines(n) keeps at most n lines. When more would follow, the last ends in «…» in the style of the text before it; no offset reaches the «…».

    span_bounds(index) gives one box for each line a span is on, as tall as the line. A span’s action and tooltip belong to the tree, which puts their targets and hints there.

    Caret and selection

    let caret = layout.caret(index);            // zero wide, as tall as its line
    let index = layout.index_at(pointer);       // where a click puts the caret
    let boxes = layout.selection(start..end);   // one box for each line it touches

    Offsets are bytes into the text. One inside a character is taken as the start of that character, and the caret never lands inside a grapheme. At a line that wraps, the end of one line and the start of the next are the same offset: the caret stands at the start of the next. An offset past the end is the end, and a point that is not finite is the start.

    Fonts

    The faces text is laid out in are one set for the whole process, as the tokens are:

    let ui = limn::Font::new(std::fs::read("Inter-Regular.ttf")?)?;
    limn::set_fonts(limn::Fonts::bundled().face(FontFamily::Sans, ui));

    Fonts::bundled(), with the default bundled-fonts feature, is limn’s own: Ubuntu for Sans in regular, medium and bold, Hack for Monospace, Noto Sans Math and Noto Sans Symbols 2 behind both, and Noto Emoji for emoji. .face(family, font) replaces a family’s face; .fallback(font) adds a face tried, in order, for characters the families lack; .emoji(font) sets the face tried last, so it never takes a symbol a text face has. A family without a face of its own takes Sans: SansItalic slanted and SansBold thickened. Behind Sans come the fallbacks, then Monospace, then the emoji face; behind Monospace come Sans, the fallbacks, then the emoji face. Only the faces given are used, never the system’s, so text is laid out alike on every machine and in a browser.

    Font::new(bytes) refuses bytes that hold no face; Font::from_static keeps a face compiled into the program, which a backend can then use without copying.

    Drawing the glyphs

    A backend draws each GlyphRun from run.font.bytes() at run.size, glyph by glyph where the run places it, slanting the outlines by run.skew degrees and thickening them when run.embolden asks. run.text is the text the run shows, for tests and tools. limn-egui draws them from an atlas of its own, rasterized as egui rasterizes its own text, so both look alike.

    Limits

    • Only text, formatted text, text fields and text areas are laid out with it yet, and only in a tree told to.
    • No pointer position reaches a drawing, so selecting with the pointer inside one is the host’s.
    • The bundled faces have no Arabic, Hebrew or CJK glyphs: such text is laid out in its own direction and broken by its own rules, but drawn with each face’s missing-glyph mark until a face that has it is given.
    • Tabs take the width of a space.

    API reference

    The guide covers composition, options and actions. Generate the complete Rust type and builder reference from the checkout:

    cargo doc -p limn --open

    Component API overview · State and actions

    The example is drawn by crates/limn-gallery/src/pages/text_layout.rs.