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.
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".
.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:
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.
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:
- a paragraph that mixes fonts, such as plain text with
strongoremin it; - text inside table cells;
- form controls and replaced elements such as
img.
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
- System text size and zoom. planned
HTML screens will zoom as a whole, the way VS Code's
window.zoomLeveldoes, so rows, icons and text grow together and nothing is clipped. The starting zoom will follow the system's text size. - Markdown. planned A separate crate,
dioxus-compose-markdown, that turns Markdown into an ordinaryrsx!tree of Compose widgets, streaming included (#24).