dioxus-compose guide

Text

Text is measured where it is drawn

Text is the one thing both halves must agree on. The Host decides where each line breaks, and the renderer draws it with the same font in the width it was given. If the two engines measured differently, letters would overlap or leave gaps, so the measurer is something you can replace.

Runs of text

Every piece of text in the display list is a TextRun: the text as it is drawn, its rectangle, the element that owns it and how many lines it took. A paragraph that mixes styles has a run per style, in source order. In samples/article a paragraph wraps at the column width, and the code block does not, because pre keeps its lines as written.

samples/article/src/lib.rs, trimmed
p { id: "first",
    "A Dioxus app written with HTML tags and CSS does not need a browser to be laid "
    "out. The Host runs the same style and layout engines a browser would, and hands "
    "the renderer nothing but rectangles, colours and runs of text."
}
pre { id: "sample", code { "{SAMPLE}" } }

White space and transforms

Text reaches the measurer as it will be drawn. White space is already collapsed or preserved according to white-space (normal, nowrap, pre, pre-wrap, pre-line, break-spaces), and text-transform is already applied. The article's kicker is written "Field notes" under text-transform: uppercase, and the measurer is only ever asked about "FIELD NOTES".

samples/workbench/src/lib.rs
.tree-row {
    display: flex;
    align-items: center;
    height: 22px;
    white-space: nowrap;
}

letter-spacing, line-height, font-family, font-size, font-weight and font-style all reach the measurer in a TextStyle. Generated content (::before and ::after) is measured like any other text.

The measurer

HtmlConfig::measurer takes anything that implements TextMeasurer. Without one, ParleyMeasurer measures with Parley, the text engine blitz-dom ships with. The trait has one method:

dioxus_compose::html
pub trait TextMeasurer {
    fn measure(&mut self, request: &TextMeasureRequest<'_>) -> TextMetrics;
}

// The request: text, style, width and white space.
// WidthConstraint is MaxContent, MinContent or AtMost(width).
// The answer: width, height, first_baseline and line_count, in CSS pixels.

Layout may ask about the same text several times under different widths, because a flex item is asked for its narrowest and widest sizes before it gets its final one. So the answer must depend only on the request.

The examples' tests use a measurer whose answers can be worked out on paper: every character is half the font size wide and every line 1.25 times the font size tall. That is why their expected rectangles are exact numbers.

samples/support/src/lib.rs, trimmed
impl TextMeasurer for Measurer {
    fn measure(&mut self, request: &TextMeasureRequest<'_>) -> TextMetrics {
        let advance = request.style.font_size * 0.5 + request.style.letter_spacing;
        let line = line_height(request.style);
        let max_chars = match request.width {
            WidthConstraint::MaxContent => usize::MAX,
            WidthConstraint::MinContent => 0,
            WidthConstraint::AtMost(width) => (width / advance + 1e-3).floor() as usize,
        };
        let lines = break_lines(request.text, max_chars);
        let width = lines
            .iter()
            .map(|line| line.chars().count() as f32 * advance)
            .fold(0.0, f32::max);
        TextMetrics {
            width,
            height: lines.len() as f32 * line,
            first_baseline: line * 0.75,
            line_count: lines.len() as u32,
        }
    }
}

Which text goes through it

Your measurer sizes a run of text that is all in one font, with no inline box in it and no inline element that pads or borders it. That covers most labels, headings and paragraphs, and text inside a plain inline element such as an a. Everything else is handed back to blitz-dom's own layout and measured by Parley:

This is partial, because the plan is for every run to be measured by the engine that draws it.

Measuring with Compose

Parley's sizes do not match Chromium's closely enough. On the VS Code workbench, 6 of 22 line breaks fell in a different place, a line's width differed by up to 14.3%, and a glyph moved by up to 39.89px, mostly because Parley does not pick the optical size of Apple's system font. Boxes matched to within 1px when the text was sized as VS Code sized it.

So the Host will ask the renderer how big a run is, during layout, in one synchronous call, and Compose will answer with the font it draws with. The renderer side of that call is being written in compose-rust (partial, #43). The TextMeasurer trait is the seam it plugs into, so an app that sets no measurer gets Compose's answers without changing anything.

Not yet