Repository navigation
Conversation
<!-- Please ensure that you have reviewed our LLM ("AI") policy at
https://linebender.org/wiki/llm-policy/.
If you did not use any LLM tools, please replace `Unspecified` with
`None`. -->
LLM Contributions: Review.
In linebender#667 we introduced a look-ahead in the line breaker, such that a CR
character followed by an LF would no longer cause two forced breaks.
Later, we fixed our grapheme handling and the line breaker now sees CRLF
as a unit. The look-ahead is no longer necessary for the common case.
This proposes removing the check. I think this improves consistency, and
also makes it somewhat simpler to perform an optimization that I'll open
a PR for soon.
Separately, note that while we currently treat CR as a forced break, CSS
Text 4 says we should treat it as a space... this PR does not move us to
that model -- and Gecko/Blink also don't do that, they mostly just
ignore the CR -- but it's something to keep in mind.
The reason I say this improves consistency, is because linebender#667
intentionally forms two breaks if an inline box is placed inside the
sequence. That means the check now only prevents the CR from breaking if
it's in a different run from the LF; and that happens precisely when the
user creates a shaping-relevant style boundary, or, in fact, places an
inline box inside the sequence. Across such item boundaries, we
intentionally reset grapheme segmentation; for example, like browsers,
we also don't form emoji across these boundaries.
(Orthogonally, do note we may work on grapheme segmentation a bit more:
we probably should actually run the segmentation *after* itemization,
instead of intersecting the segmentation with items.)
<!--
If our users need to know about this change, please describe that in the
quote block below.
What you write here will be edited by us later - it doesn't need to be
perfect.
If this change doesn't need a changelog entry, please replace the next
line with `**Changelog: None**`.
-->
**Changelog: None**
…nder#826) <!-- Please ensure that you have reviewed our LLM ("AI") policy at https://linebender.org/wiki/llm-policy/. If you did not use any LLM tools, please replace `Unspecified` with `None`. --> LLM Contributions: Review. This removes the `source_char: char` field from `ClusterInfo`, shrinking `Character` from 16 to 12 bytes. We get one `Character` for each character in the source `&str`, so it's useful to keep it small. If users ever need the `source_char` for something, they can refer to the character byte ranges and the source `&str`. # Performance linebender#824, linebender#825 and this PR together measure as follows. ``` Line Breaking - arabic 8000 characters, narrow [ 144.4 us ... 143.4 us ] -0.69% Line Breaking - arabic 8000 characters, wider [ 96.9 us ... 93.5 us ] -3.54%* Line Breaking - arabic 8000 characters, unwrapped [ 77.9 us ... 77.9 us ] +0.01% Line Breaking - latin 8000 characters, narrow [ 113.7 us ... 109.5 us ] -3.73%* Line Breaking - latin 8000 characters, wider [ 63.4 us ... 60.3 us ] -4.85%* Line Breaking - latin 8000 characters, unwrapped [ 47.4 us ... 43.9 us ] -7.54%* Line Breaking - japanese 8000 characters, narrow [ 137.5 us ... 135.7 us ] -1.35%* Line Breaking - japanese 8000 characters, wider [ 96.2 us ... 96.6 us ] +0.44% Line Breaking - japanese 8000 characters, unwrapped [ 83.1 us ... 83.9 us ] +0.92% Line Breaking - arabic 8000 characters, wrapped + spacing [ 114.9 us ... 110.2 us ] -4.08%* Line Breaking - latin 8000 characters, wrapped + spacing [ 81.5 us ... 75.2 us ] -7.72%* Line Breaking - japanese 8000 characters, wrapped + spacing [ 117.4 us ... 117.5 us ] +0.05% Page Break - arabic 8000 characters [ 81.7 us ... 80.0 us ] -2.08%* Page Break - latin 8000 characters [ 51.1 us ... 47.1 us ] -7.87%* Page Break - japanese 8000 characters [ 86.9 us ... 87.3 us ] +0.47% ``` <!-- If our users need to know about this change, please describe that in the quote block below. What you write here will be edited by us later - it doesn't need to be perfect. If this change doesn't need a changelog entry, please replace the next line with `**Changelog: None**`. --> **Changelog: None**
…tunities (linebender#819) LLM Contributions: None. ### Summary Decouple UAX linebender#29 word boundaries from UAX linebender#14 line break opportunities in `Cluster::is_word_boundary()`. ### Problem In `analyze_text`, line break boundaries (`Boundary::Line` / `Boundary::Mandatory`) and word boundaries (`Boundary::Word`) were merged into a single `Boundary` enum where line breaks took precedence. Because CJK scripts (including Korean Hangul) allow soft line breaks between almost every character under UAX linebender#14 rules, nearly every Korean character was marked as `Boundary::Line`. Consequently, `Cluster::is_word_boundary()` (which checked `boundary != Boundary::None`) treated every Korean syllable as a word boundary. In text editors and downstream consumers (like Blitz/Dioxus), word navigation (`Option + Left/Right`) and word deletion (`Option + Backspace`) stopped at every single syllable instead of jumping word by word across whitespace. ### Fix - Retain UAX linebender#29 `is_word` boundary information from `WordSegmenter` in a dedicated bit (`WORD_BOUNDARY_SHIFT`) within `CharInfo`. - Pass this flag to `ClusterInfo::is_word_boundary`. - Update `Cluster::is_word_boundary()` to check this dedicated flag rather than line break boundaries. - Line break behavior (`Boundary::Line`) remains unchanged for layout wrapping. **Changelog** > ### Fixed > > - Fixed CJK and Korean word boundary navigation in `Cluster::is_word_boundary()`, properly distinguishing UAX linebender#29 word boundaries from UAX linebender#14 line break opportunities. --------- Co-authored-by: Tom Churchman <thomas@churchman.nl>
…inebender#716) As discussed at RustWeek 2026, the current API of the AccessKit integration is not a great fit for many consumers. The conclusion we reached was that removing the integration entirely would allow users to design their own according to their needs. The existing integration can be moved to the vello_editor example to serve as a reference implementation or a starting point. This require making some items public though. The changes here are mostly mechanical. I am guessing that most of the changes made to parley's API will be controversial, so this pull request should be considered as a way to open the discussion, not necessarily as something to merge as is. See slint-ui/slint#2895 for my current motivation. --------- Co-authored-by: Nico Burns <nico@nicoburns.com>
…nebender#829) <!-- Please ensure that you have reviewed our LLM ("AI") policy at https://linebender.org/wiki/llm-policy/. If you did not use any LLM tools, please replace `Unspecified` with `None`. --> LLM Contributions: Investigation, review, added tests. For complex scripts that require a dictionary, ICU4X reports runs of characters as a single word. However, according to [UAX linebender#29 Rule WB999](https://www.unicode.org/reports/tr29/#WB999) (which applies if there is no dictionary), there should be word boundaries around every ideograph. Potentially this is something that could be improved in ICU4X; in the meantime, I propose to patch this up ourselves. Prior to linebender#819, we were accidentally quite close for this fallback case. As linebender#819 made it such that we correctly classify word boundaries separately from line boundaries, we are now wrong for complex scripts if we don't have the dictionary (which is always, if we don't have `complex-scripts`; and otherwise we're still wrong for complex scripts that don't get a dictionary, like (apparently) Ahom). This spends one bit in `parley_data` to track the relevant Unicode property. I did consider implementing this check with some heuristics, but it didn't feel great to spend serious runtime on processing some table and still be incorrect for at least a handful of code points. I've added a test that starts failing if the ICU4X segmenter implements WB999 in the future, which says to clean all of this up again (including freeing the bit from `parley_data`). Do note that, unfortunately, `has_word_dictionary` is also just approximate, but the alternative is tracking an ICU4X implementation detail. I *hope* the samples in the added tests are correct. I don't speak those languages, so I'm not fully sure. While most other LLM-generated tests have been correct, some were effectively just checking against output that the code happened to generate at the time of it generating the test. At the very least this will detect accidental changes. <!-- If our users need to know about this change, please describe that in the quote block below. What you write here will be edited by us later - it doesn't need to be perfect. If this change doesn't need a changelog entry, please replace the next line with `**Changelog: None**`. --> **Changelog** > ## Parley > ### Fixed > > - **Note to the editor:** this entry should be merged with linebender#819. Word boundaries of complex scripts, in the absence of dictionary data, now follow Unicode rules.
…n analysis) (linebender#833) LLM Contributions: Generated with GPT Astra 6 High Builds on: - linebender#812 - linebender#814 - linebender#790 Supercedes: - linebender#820 ## Summary Adds support for `WhiteSpaceCollapse::BreakSpaces`. This maps to the existing `Preserve` mode for actual whitespace collapsing, but has it's own unique behaviour during line-breaking and hanging. This is the remaining (widely implemented) value of `WhiteSpaceCollapse` needed to complete our whitespace handling. ## Context When looking into @tomcur's review comments, I looked more closely at what `soft_line_break` from linebender#820 was doing, and I noticed that it was computing additional soft line break opportunities during layout / content width computation. That seemed like a bit of a code smell to me, as in Parley we normally compute line break opportunities during analysis and just consume them during line-breaking / content width computation. This PR implements `break-spaces` in that way (line breaking opportunities computed during analysis), and is both simpler (subjective, but true IMO) and faster (measured: no observable overhead vs. main!) than linebender#820 (and this benefit composes with the optimizations from linebender#822 / linebender#823). ## Changes made - Add `WhiteSpaceCollapse::BreakSpaces` enum variant - Add `break_spaces: Vec<Range<usize>>` to `LayoutContext`. This populated with text ranges that have `WhiteSpaceCollapse` set to `BreakSpaces` similar to the existing `word_break` Vec (so empty if `break-spaces` is not used). - An additional check for `break-spaces` when computing line breaking opportunities in `analyze_text` (essentially free if `break-spaces` is not used (overhead not detectable by benchmarks at all), very cheap even when it is (something on the order of 0.5-1%)) - Minimal changes in line-breaking / content width computation to map the new enum variant to existing whitespace modes. ## Changelog > ### Added > > - Support for `WhiteSpaceCollapse::BreakSpaces`
<!-- Please ensure that you have reviewed our LLM ("AI") policy at
https://linebender.org/wiki/llm-policy/.
If you did not use any LLM tools, please replace `Unspecified` with
`None`. -->
LLM Contributions: Rewritten tests, review.
On top of linebender#829.
Since linebender#819, we now correctly track word and line boundaries separately.
`Boundary::Word` is no longer read (in favor of
`ClusterInfo::is_word_boundary`). This now removes that variant.
I'm thinking of a slightly bigger overhaul, and this PR itself is a
tactical change as these things go, but I think it's worth doing:
`is_word_boundary()` no longer implies `boundary >= Boundary::Word`.
I'm thinking to just track the `(grapheme, word, line break
opportunity)` as separate bits. Note that would drop
`Boundary::Mandatory`, which I believe is effectively unread already,
because we encode that case `Whitespace::Newline`. That probably
requires a bit more scrutiny than this change on its own, though.
<!--
If our users need to know about this change, please describe that in the
quote block below.
What you write here will be edited by us later - it doesn't need to be
perfect.
If this change doesn't need a changelog entry, please replace the next
line with `**Changelog: None**`.
-->
**Changelog: None**
<!-- Please ensure that you have reviewed our LLM ("AI") policy at
https://linebender.org/wiki/llm-policy/.
If you did not use any LLM tools, please replace `Unspecified` with
`None`. -->
LLM Contributions: Review.
This explains the reading of the spec, and explains our model based on
that. Wrote this as an explanation in
linebender#790 (comment),
and figured it may be useful in the docs for posterity.
<details>
<summary>HTML sample</summary>
```html
<style>
@import url("https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,100..900;1,100..900&display=swap");
body {
font-size: 16;
font-family: "Roboto";
}
</style>
<h1><code>collapse</code> hangs unconditionally</h1>
<div style="width: 200px; border: 1px solid; text-align: right">
X<span style="white-space-collapse: collapse"> </span><br />Y
</div>
<h1><code>preserve</code> hangs conditionally</h1>
<div style="width: 200px; border: 1px solid; text-align: right">
X<span style="white-space-collapse: preserve"> </span><br />Y
</div>
<h1>
<code>preserve</code> preceding <code>collapse</code> hangs unconditionally
</h1>
<div style="width: 200px; border: 1px solid; text-align: right">
X<span style="white-space-collapse: preserve"> </span
><span style="white-space-collapse: collapse"> </span><br />Y
</div>
<h1>
<code>collapse</code> preceding <code>preserve</code> hangs conditionally
</h1>
<p>
The <code>collapse</code> part hangs in full if the <code>preserve</code> part
hangs in full.
</p>
<p>Here, the <code>preserve</code> part fits in full, so nothing hangs:</p>
<div style="width: 200px; border: 1px solid; text-align: right">
X<span style="white-space-collapse: collapse">   </span
><span style="white-space-collapse: preserve"> </span><br />Y
</div>
<p>Here, the <code>preserve</code> part doesn't fit, so everything hangs:</p>
<div style="width: 200px; border: 1px solid; text-align: right">
X<span style="white-space-collapse: collapse"
>                          </span
><span style="white-space-collapse: preserve"> </span><br />Y
</div>
```
</details>
https://developer.mozilla.org/en-US/play?id=Xvs2WibuByVkeRPwc7hAy%2BfYc%2B5W6ZVidyHcfdDy1%2F%2F41DBLRABlyaBHukdkDbLhL4%2BnN5%2FL5YHGdy5B
<!--
If our users need to know about this change, please describe that in the
quote block below.
What you write here will be edited by us later - it doesn't need to be
perfect.
If this change doesn't need a changelog entry, please replace the next
line with `**Changelog: None**`.
-->
**Changelog: None**
…der#835) <!-- Please ensure that you have reviewed our LLM ("AI") policy at https://linebender.org/wiki/llm-policy/. If you did not use any LLM tools, please replace `Unspecified` with `None`. --> LLM Contributions: Review, updated tests, initial implementation. `Boundary::Mandatory` is redundant given we encode these mandatory breaks using `Whitespace::Newline`. `parley` doesn't read `Boundary::Mandatory` (and it has not been in a published API). This proposes reporting soft wrap opportunities and `Whitespace::Newline` separately. That means soft wrap opportunities become a single bit, like grapheme starts and word boundaries. Note, technically `Boundary::Mandatory` reports `Whitespace::Newline | Whitespace::ControlWhitespace`: e.g., vertical tabs are `Boundary::Mandatory`, but CSS Text 4 says to render them as glyphs. To make this a bit more than a tactical removal, I've replaced `ClusterInfo` by `CharacterFlags`. I've moved the `Whitespace` field of `ClusterInfo` onto `Character`, as bitpacking it benched slightly worse (even though it allows `Character` to shrink to 8 bytes). # Performance Neutral. ``` Default Style - arabic 20 characters [ 9.1 us ... 9.1 us ] +0.05% Default Style - latin 20 characters [ 4.4 us ... 4.4 us ] +1.25%* Default Style - japanese 20 characters [ 7.9 us ... 7.8 us ] -0.46% Default Style - arabic 400 characters [ 197.5 us ... 194.1 us ] -1.72%* Default Style - latin 400 characters [ 63.0 us ... 63.8 us ] +1.26%* Default Style - japanese 400 characters [ 136.4 us ... 137.5 us ] +0.82% Default Style - arabic 8000 characters [ 4.6 ms ... 4.6 ms ] -0.39% Default Style - latin 8000 characters [ 1.4 ms ... 1.4 ms ] -0.99% Default Style - japanese 8000 characters [ 2.9 ms ... 2.9 ms ] +0.18% Styled - arabic 20 characters [ 10.3 us ... 10.4 us ] +1.11%* Styled - latin 20 characters [ 5.6 us ... 5.7 us ] +1.75%* Styled - japanese 20 characters [ 8.6 us ... 8.5 us ] -1.02%* Styled - arabic 400 characters [ 216.9 us ... 214.7 us ] -1.00%* Styled - latin 400 characters [ 81.2 us ... 82.2 us ] +1.18%* Styled - japanese 400 characters [ 149.7 us ... 152.0 us ] +1.57%* Styled - arabic 8000 characters [ 5.1 ms ... 5.1 ms ] +0.49% Styled - latin 8000 characters [ 1.7 ms ... 1.8 ms ] +0.32% Styled - japanese 8000 characters [ 3.2 ms ... 3.3 ms ] +0.99% Line Breaking - arabic 8000 characters, narrow [ 144.2 us ... 143.2 us ] -0.65% Line Breaking - arabic 8000 characters, wider [ 93.7 us ... 92.7 us ] -1.05%* Line Breaking - arabic 8000 characters, unwrapped [ 76.4 us ... 76.0 us ] -0.49% Line Breaking - latin 8000 characters, narrow [ 108.2 us ... 107.9 us ] -0.29% Line Breaking - latin 8000 characters, wider [ 58.0 us ... 57.5 us ] -0.86% Line Breaking - latin 8000 characters, unwrapped [ 43.6 us ... 44.4 us ] +1.77%* Line Breaking - japanese 8000 characters, narrow [ 134.4 us ... 137.4 us ] +2.19%* Line Breaking - japanese 8000 characters, wider [ 97.0 us ... 97.9 us ] +0.92% Line Breaking - japanese 8000 characters, unwrapped [ 83.3 us ... 87.4 us ] +4.98%* ``` There seemingly is some code-placement-related branch misprediction going on in the Japanese case (almost every character there is an opportunity). Adding NOPs moves the benches between ~+0% and ~+5%. The performance of Content Widths also once again shifts on my machine, seemingly similarly due to code placement (the change as-is is +~10%, but I get +~0% by simply inserting some NOPs). <!-- If our users need to know about this change, please describe that in the quote block below. What you write here will be edited by us later - it doesn't need to be perfect. If this change doesn't need a changelog entry, please replace the next line with `**Changelog: None**`. --> **Changelog: None**
<!-- Please ensure that you have reviewed our LLM ("AI") policy at
https://linebender.org/wiki/llm-policy/.
If you did not use any LLM tools, please replace `Unspecified` with
`None`. -->
LLM Contributions: None.
When uncommented, this doesn't compile anymore.
(There's some more code like this in the file, though those still appear
to work. We could consider removing that as well.)
<!--
If our users need to know about this change, please describe that in the
quote block below.
What you write here will be edited by us later - it doesn't need to be
perfect.
If this change doesn't need a changelog entry, please replace the next
line with `**Changelog: None**`.
-->
**Changelog: None**
LLM Contributions: Generate with Fable 5.1 Low CSS specifies that `max_content` width should be floored by `min_content` width. This PR implements that directly. The case where `max_content` end up smaller is an `InlineBox` with negative width, which is caused by negative margins. **Changelog** > ### Fix > > - The `max_content` width is always floored by `min_content` width.
<!-- Please ensure that you have reviewed our LLM ("AI") policy at
https://linebender.org/wiki/llm-policy/.
If you did not use any LLM tools, please replace `Unspecified` with
`None`. -->
LLM Contributions: Review.
The terminology of items/segments/runs has seen some churn, which has
led to debris in this function and some surrounding areas.
For posterity, as it stands, these are the names for this hierarchy of
three levels:
1. Item: a shaping context (e.g., a font size changes creates a new
item); we don't shape across item boundaries, and graphemes also don't
form across them;
2. Segment: a span of constant script and bidi level within an item
(graphemes *could* form across segments, which browsers do, but we don't
currently do that);
3. Font run: the maximal span within a segment for which a single font
was selected.
Possibly we may do another pass over the names at some point (see also
linebender#727 (comment)),
but in the meantime, without this PR, this function is clearly quite
confused.
<!--
If our users need to know about this change, please describe that in the
quote block below.
What you write here will be edited by us later - it doesn't need to be
perfect.
If this change doesn't need a changelog entry, please replace the next
line with `**Changelog: None**`.
-->
**Changelog: None**
…der#840) <!-- Please ensure that you have reviewed our LLM ("AI") policy at https://linebender.org/wiki/llm-policy/. If you did not use any LLM tools, please replace `Unspecified` with `None`. --> LLM Contributions: Initial implementation, review. Follow-up to linebender#837 (comment). This makes our font run loop a bit more idiomatic (extend the font run as far as possible until font selection returns a different font, then shape). This now also builds a single iterator that's consumed, where we previously resliced the text multiple times, making this *slightly* faster. # Performance Neutral to slightly positive (here again, with these tiny numbers, there's some noise build-to-build from code placement alone -- I asked an LLM to confirm these numbers with various NOP padding and alignment). ``` Default Style - arabic 20 characters [ 9.0 us ... 9.0 us ] +0.05% Default Style - latin 20 characters [ 4.3 us ... 4.4 us ] +1.28%* Default Style - japanese 20 characters [ 7.8 us ... 7.9 us ] +0.29% Default Style - arabic 400 characters [ 193.2 us ... 189.0 us ] -2.19%* Default Style - latin 400 characters [ 62.6 us ... 62.5 us ] -0.17% Default Style - japanese 400 characters [ 136.3 us ... 136.8 us ] +0.39% Default Style - arabic 8000 characters [ 4.7 ms ... 4.6 ms ] -1.57%* Default Style - latin 8000 characters [ 1.4 ms ... 1.4 ms ] -1.86%* Default Style - japanese 8000 characters [ 2.9 ms ... 2.9 ms ] +0.08% Styled - arabic 20 characters [ 10.3 us ... 10.3 us ] -0.10% Styled - latin 20 characters [ 5.6 us ... 5.6 us ] +0.33% Styled - japanese 20 characters [ 8.4 us ... 8.4 us ] +0.38% Styled - arabic 400 characters [ 213.0 us ... 207.1 us ] -2.75%* Styled - latin 400 characters [ 81.0 us ... 80.7 us ] -0.42% Styled - japanese 400 characters [ 149.5 us ... 150.0 us ] +0.31% Styled - arabic 8000 characters [ 5.1 ms ... 5.0 ms ] -0.69% Styled - latin 8000 characters [ 1.7 ms ... 1.7 ms ] -1.23%* Styled - japanese 8000 characters [ 3.2 ms ... 3.2 ms ] -0.12% ``` <!-- If our users need to know about this change, please describe that in the quote block below. What you write here will be edited by us later - it doesn't need to be perfect. If this change doesn't need a changelog entry, please replace the next line with `**Changelog: None**`. --> **Changelog: None**
<!-- Please ensure that you have reviewed our LLM ("AI") policy at
https://linebender.org/wiki/llm-policy/.
If you did not use any LLM tools, please replace `Unspecified` with
`None`. -->
LLM Contributions: Refactor, review.
It turns out that the atomic refcount bumps of the font `Blob` cost
quite a bit. This change is a pretty nice performance win on my x86
machine; it measures -20% on a full build on Latin in some cases!
This introduces a `FontInstanceRef` so we don't have to clone the `Blob`
as much. To enable that, this ties the font returned by
`Query::matches_with` to the longer lifetime of the query borrow.
Relatedly, see Nico's linebender#796,
which works on the query performance (these two should compose nicely).
Note that due limitations in the borrow checker, we have to move the
last-resort check to be before the query, but [that would be reversible
with
Polonius](https://blog.rust-lang.org/2026/08/04/enabling-polonius-alpha-on-nightly/).
I *think* this shouldn't really cost too much anyway, as we still only
do the last-resort scan if `Query` would've ended up failing to return
any font.
# Performance
```
Default Style - arabic 20 characters [ 9.3 us ... 8.6 us ] -8.08%*
Default Style - latin 20 characters [ 4.5 us ... 3.9 us ] -13.01%*
Default Style - japanese 20 characters [ 8.0 us ... 7.7 us ] -4.67%*
Default Style - arabic 400 characters [ 196.8 us ... 184.6 us ] -6.17%*
Default Style - latin 400 characters [ 66.1 us ... 52.8 us ] -20.09%*
Default Style - japanese 400 characters [ 140.4 us ... 133.4 us ] -4.97%*
Default Style - arabic 8000 characters [ 4.7 ms ... 4.4 ms ] -5.15%*
Default Style - latin 8000 characters [ 1.4 ms ... 1.2 ms ] -20.19%*
Default Style - japanese 8000 characters [ 3.0 ms ... 2.8 ms ] -4.99%*
Styled - arabic 20 characters [ 10.4 us ... 9.9 us ] -5.02%*
Styled - latin 20 characters [ 5.6 us ... 5.3 us ] -6.00%*
Styled - japanese 20 characters [ 8.4 us ... 8.2 us ] -2.14%*
Styled - arabic 400 characters [ 214.2 us ... 204.3 us ] -4.62%*
Styled - latin 400 characters [ 81.9 us ... 73.6 us ] -10.18%*
Styled - japanese 400 characters [ 150.8 us ... 146.7 us ] -2.75%*
Styled - arabic 8000 characters [ 5.1 ms ... 4.9 ms ] -2.61%*
Styled - latin 8000 characters [ 1.7 ms ... 1.6 ms ] -9.64%*
Styled - japanese 8000 characters [ 3.2 ms ... 3.2 ms ] -0.72%
Page Full - arabic 8000 characters [ 4.5 ms ... 4.4 ms ] -3.19%*
Page Full - latin 8000 characters [ 1.4 ms ... 1.2 ms ] -14.95%*
Page Full - japanese 8000 characters [ 3.2 ms ... 3.1 ms ] -1.64%*
Page Build - arabic 8000 characters [ 4.3 ms ... 4.1 ms ] -2.98%*
Page Build - latin 8000 characters [ 1.3 ms ... 1.1 ms ] -16.97%*
Page Build - japanese 8000 characters [ 2.9 ms ... 2.8 ms ] -2.40%*
```
<!--
If our users need to know about this change, please describe that in the
quote block below.
What you write here will be edited by us later - it doesn't need to be
perfect.
If this change doesn't need a changelog entry, please replace the next
line with `**Changelog: None**`.
-->
**Changelog**
> ## Fontique
> ### Changed
> The `&QueryFont` passed by `Query::matches_with` now lives as long as
the query borrow itself.
LLM Contributions: Generated with Fable 5.1 Low Builds on: - linebender#790 Gets almost all of the 0.11 performance back for `calculate_content_widths` (only 10% slower than 0.11 with this PR vs ~60% slower on main). A little slower is expected as this function is doing a lot more work now (for correct whitespace hanging). This PR contains two changes (in separate commits): - **Add the first character's whitespace and soft wrap opportunity bits to `ShapedClusterFlags`**. Avoids an indirection to read the first character in common cases. - **Fast path in `calculate_content_widths` when a run has no letter/word spacing**. Iterates shaped clusters rather than atoms.
…ng (linebender#842) <!-- Please ensure that you have reviewed our LLM ("AI") policy at https://linebender.org/wiki/llm-policy/. If you did not use any LLM tools, please replace `Unspecified` with `None`. --> LLM Contributions: Generated. Follow-up to linebender#822, which already did this for the non-spacing path. I believe this matches Blink and Gecko. <!-- If our users need to know about this change, please describe that in the quote block below. What you write here will be edited by us later - it doesn't need to be perfect. If this change doesn't need a changelog entry, please replace the next line with `**Changelog: None**`. --> **Changelog: None**
…er#847) <!-- Please ensure that you have reviewed our LLM ("AI") policy at https://linebender.org/wiki/llm-policy/. If you did not use any LLM tools, please replace `Unspecified` with `None`. --> LLM Contributions: Generated. The normal zero-spacing case has some fast paths. <!-- If our users need to know about this change, please describe that in the quote block below. What you write here will be edited by us later - it doesn't need to be perfect. If this change doesn't need a changelog entry, please replace the next line with `**Changelog: None**`. --> **Changelog: None**
… inline boxes (linebender#844) LLM Contributions: Generated with Fable 5.1 Low Previously line-breaking opportunities following inline boxes were unconditional (ignoring wrapping being disabled). Here we apply text-wrap-mode to these opportunities the same as any other. **Changelog** > ### Fixed > > - Apply `TextWrapMode::NoWrap` to line-breaking opportunities following inline boxes
<!-- Please ensure that you have reviewed our LLM ("AI") policy at
https://linebender.org/wiki/llm-policy/.
If you did not use any LLM tools, please replace `Unspecified` with
`None`. -->
LLM Contributions: Review.
This field is unread.
Skips quite a bit of work, especially in the spacing case. We were doing
a second walk over the atoms/shaped clusters (which actually also
wouldn't be strictly necessary, as we could use the accumulation of
`LineState::x` instead).
# Performance
```
Line Breaking - arabic 8000 characters, narrow [ 143.3 us ... 144.7 us ] +0.99%
Line Breaking - arabic 8000 characters, wider [ 93.4 us ... 89.2 us ] -4.48%*
Line Breaking - arabic 8000 characters, unwrapped [ 75.6 us ... 75.2 us ] -0.47%
Line Breaking - latin 8000 characters, narrow [ 108.9 us ... 108.6 us ] -0.30%
Line Breaking - latin 8000 characters, wider [ 58.5 us ... 57.6 us ] -1.55%*
Line Breaking - latin 8000 characters, unwrapped [ 43.8 us ... 41.7 us ] -4.76%*
Line Breaking - japanese 8000 characters, narrow [ 136.4 us ... 136.6 us ] +0.14%
Line Breaking - japanese 8000 characters, wider [ 97.2 us ... 94.0 us ] -3.29%*
Line Breaking - japanese 8000 characters, unwrapped [ 83.6 us ... 80.9 us ] -3.24%*
Line Breaking - arabic 8000 characters, wrapped + spacing [ 113.8 us ... 92.8 us ] -18.48%*
Line Breaking - latin 8000 characters, wrapped + spacing [ 74.4 us ... 62.2 us ] -16.47%*
Line Breaking - japanese 8000 characters, wrapped + spacing [ 117.6 us ... 95.5 us ] -18.82%*
Page Break - arabic 8000 characters [ 79.5 us ... 74.3 us ] -6.63%*
Page Break - latin 8000 characters [ 46.6 us ... 45.1 us ] -3.18%*
Page Break - japanese 8000 characters [ 87.4 us ... 84.4 us ] -3.45%*
```
<!--
If our users need to know about this change, please describe that in the
quote block below.
What you write here will be edited by us later - it doesn't need to be
perfect.
If this change doesn't need a changelog entry, please replace the next
line with `**Changelog: None**`.
-->
**Changelog: None**
LLM Contributions: Generated with Opus 5.5. This update required work due to unicode-org/icu4x#6067, so I thought it would be worth opening separately. The test change is unrelated, but could provide value in the future when the properties change (unicode 18?); we now check that `Properties::new` never has overflow. **Changelog: None** --------- Co-authored-by: Tom Churchman <thomas@churchman.nl>
**Implements vertical alignment.** LLM Contributions: Generated with Fable 5.1 Low. Reviewed by GPT 5.6 Sol XHigh. This has also been manually reviewed, and gone through several rounds of iteration. I probably still need to do another round on the details of the the actual alignment, but I think this is architecturally in a good place. - Builds on top of linebender#762 - Blitz integration PR (runs WPT tests) DioxusLabs/blitz#832 This PR is designed to be reviewed commit-by-commit (4 commits): 1. **Introduce a minimal tree structure**: Adds `parent: u16` to every style, which allows for upwards tree-traversal. The `TreeBuilder` encodes it's tree with this new field. The `RangedBuilder` builds a trivial tree with one root, and where all style spans are direct children of that root. This builds on the existing convention that the 0th style is the root/paragraph level style. 2. **Adds `VerticalAlign` style**. `VerticalAlign` is struct which consists of `BaselineShift`, and `AlignmentBaseline` fields, and can be set both as a span style and on inline boxes. 3. **Introduces the concept of first available font and resolves a font + metrics for every style**. We thus end up with a tree of spans each with associated line metrics before we even begin layout. We do not use the actual resolved fonts for any run/atom/cluster for vertical alignment at all (this matches browsers, but there are other reasonable choices here, and we could bring back using run/cluster/atom metrics for this as an option if desired). Some level of alignment relative to the parent is already pre-computed at this stage. (Note: these style lookups are kinda expensive - I have a follow-up PR that eliminates that overhead with a global cache on the `FontContext` - DioxusLabs#19 for those who want a preview) 4. **Implements the actual vertical alignment logic** by building a list of "aligned subtrees" for each line, and computing the offsets of each span in each aligned subtree relative to each other. Most lines actually only have one "aligned subtree", so it is alignment within a tree doing most of the work. `vertical-align: top` and `bottom` introduce new "aligned subtrees" which are initially aligned independently of the other "aligned subtrees" on the line. There is then a final alignment step when completing each line that aligns the "aligned subtrees" to each other. Some notes: - The mental model for how spans interact with lines is that each span creates a box fragment in **every** line it intersects, and each a span's box fragment on a given line is always contained within (and aligned with) a box fragment for **every** ancestor span up to the root level, as well as with other spans (and their ancestors) that exist on the same line. - This is a generalisation of the "strut metric" concept: spans align with not just the root, but every span in between. `RangedBuilder` layouts fallback gracefully to "strut metric"-style layout (spans in ranged layouts only have one ancestor: the root). **Changelog** > ### Added > > - `vertical-align` style. > - TODO --------- Signed-off-by: Nico Burns <nico@nicoburns.com>
…ebender#848) <!-- Please ensure that you have reviewed our LLM ("AI") policy at https://linebender.org/wiki/llm-policy/. If you did not use any LLM tools, please replace `Unspecified` with `None`. --> LLM Contributions: Review, initial implementation. This lets both the non-zero-spacing and zero-spacing cases of `calculate_content_widths` loop over shaped clusters. This moves us back to having just a single loop for both cases: I removed the loop that walks atoms, and and added a const-generic `SPACED` to the loop introduced in linebender#822, which, if set, adds the spacing gaps. This keeps the logic in one place, and speeds up `calculate_content_widths` in the presence of non-zero spacing. ``` Content Widths - arabic 8000 characters, spacing [ 29.2 us ... 26.2 us ] -7.6%* Content Widths - latin 8000 characters, spacing [ 28.9 us ... 22.5 us ] -18.1%* Content Widths - japanese 8000 characters, spacing [ 26.8 us ... 29.9 us ] -8.4%* ``` Do note there are once again some code placement artifacts, making the function slightly tricky to measure. The above is from having an LLM bench at a bunch of code offsets. The non-spacing case is unaffected. <!-- If our users need to know about this change, please describe that in the quote block below. What you write here will be edited by us later - it doesn't need to be perfect. If this change doesn't need a changelog entry, please replace the next line with `**Changelog: None**`. --> **Changelog: None**
…ender#850) <!-- Please ensure that you have reviewed our LLM ("AI") policy at https://linebender.org/wiki/llm-policy/. If you did not use any LLM tools, please replace `Unspecified` with `None`. --> LLM Contributions: Investigation, implementation and tests with Claude Code, reviewed by me. The description is my own, edited for grammar. `set_text` crashed because the old cursor could be outside the new text. The cursor is now clamped to the new text's bounds. Fixes linebender#849 <!-- If our users need to know about this change, please describe that in the quote block below. What you write here will be edited by us later - it doesn't need to be perfect. If this change doesn't need a changelog entry, please replace the next line with `**Changelog: None**`. --> **Changelog** > ### Fixed > > - `PlainEditor::set_text` no longer crashes on the next edit when the old cursor is outside the new text; the cursor is clamped to the new text.
<!-- Please ensure that you have reviewed our LLM ("AI") policy at
https://linebender.org/wiki/llm-policy/.
If you did not use any LLM tools, please replace `Unspecified` with
`None`. -->
LLM Contributions: None.
Started to be used in linebender#811.
<!--
If our users need to know about this change, please describe that in the
quote block below.
What you write here will be edited by us later - it doesn't need to be
perfect.
If this change doesn't need a changelog entry, please replace the next
line with `**Changelog: None**`.
-->
**Changelog: None**
LLM Contributions: Generated with Fable 5.1 Low (code changes reviewed by me). Implements the CSS `line-break` property (https://drafts.csswg.org/css-text/#line-break-property). This turned out to be quite straightforward because all/most of the complexity is handled for us by `icu4x`s `LineSegmenter`, and this PR is 90% just plumbing that through: - Add `LineBreak` enum with `Loose`, `Normal`, `Strict` and `Anywhere` variants. - Replace the `LayoutContext::word_break` Vec which recorded ranges in the text where the `word-break` property changed with `LayoutContext::line_break` Vec which tracks a struct of `(word_break, line_break, locale)` instead (new entry in the Vec when any of those change). - Set `strictness` and `content_locale` options on `icu4x` `LineSegmenter` (in addition to `word_option` that we were already setting) - Handle `line-break: anywhere` taking precedence over line breaks suppressed by other styles. Benches neutral. Blitz PR: DioxusLabs/blitz#933 (+540 -0 WPT subtests) **Changelog** > ### Added > > - Support for the CSS `line-break` style. This controls line-breaking behaviour of CJK text.
…nder#857) <!-- Please ensure that you have reviewed our LLM ("AI") policy at https://linebender.org/wiki/llm-policy/. If you did not use any LLM tools, please replace `Unspecified` with `None`. --> LLM Contributions: Convinced me to add a test, review. This information is only needed for the analysis, the result of which we store as `Analysis::bidi_levels`. We don't need to keep the classification information around. This shrinks `CharInfo` from 16 to 4 bytes. That also allows us to only get the bidi information when it's needed. This is mostly a clean up, but benches on my machine as a slight performance improvement. # Performance ``` Default Style - arabic 20 characters [ 8.8 us ... 8.8 us ] -0.39% Default Style - latin 20 characters [ 4.2 us ... 4.2 us ] -0.88% Default Style - japanese 20 characters [ 8.1 us ... 8.0 us ] -1.55%* Default Style - arabic 400 characters [ 183.9 us ... 185.3 us ] +0.75% Default Style - latin 400 characters [ 54.5 us ... 53.2 us ] -2.39%* Default Style - japanese 400 characters [ 136.1 us ... 133.0 us ] -2.28%* Default Style - arabic 8000 characters [ 4.5 ms ... 4.5 ms ] -0.52% Default Style - latin 8000 characters [ 1.2 ms ... 1.2 ms ] +0.41% Default Style - japanese 8000 characters [ 2.9 ms ... 2.8 ms ] -1.58%* Styled - arabic 20 characters [ 10.7 us ... 10.8 us ] +0.63% Styled - latin 20 characters [ 6.1 us ... 6.0 us ] -1.44%* Styled - japanese 20 characters [ 9.1 us ... 9.0 us ] -1.36%* Styled - arabic 400 characters [ 206.5 us ... 209.3 us ] +1.32%* Styled - latin 400 characters [ 78.6 us ... 77.0 us ] -1.99%* Styled - japanese 400 characters [ 154.1 us ... 151.0 us ] -2.00%* Styled - arabic 8000 characters [ 5.0 ms ... 5.1 ms ] +0.40% Styled - latin 8000 characters [ 1.7 ms ... 1.7 ms ] -0.56% Styled - japanese 8000 characters [ 3.3 ms ... 3.2 ms ] -1.95%* Page Full - arabic 8000 characters [ 4.5 ms ... 4.4 ms ] -2.32%* Page Full - latin 8000 characters [ 1.2 ms ... 1.2 ms ] -1.79%* Page Full - japanese 8000 characters [ 3.2 ms ... 3.2 ms ] -1.24%* Page Build - arabic 8000 characters [ 4.1 ms ... 4.1 ms ] -0.66% Page Build - latin 8000 characters [ 1.1 ms ... 1.1 ms ] -1.01%* Page Build - japanese 8000 characters [ 2.9 ms ... 2.8 ms ] -1.95%* ``` <!-- If our users need to know about this change, please describe that in the quote block below. What you write here will be edited by us later - it doesn't need to be perfect. If this change doesn't need a changelog entry, please replace the next line with `**Changelog: None**`. --> **Changelog: None**
…nder#851) <!-- Please ensure that you have reviewed our LLM ("AI") policy at https://linebender.org/wiki/llm-policy/. If you did not use any LLM tools, please replace `Unspecified` with `None`. --> LLM Contributions: Investigation and test with Claude Code, reviewed by me. The description is my own, edited for grammar. Adds a test so the bug doesn't come back. It was already fixed by the ICU update in linebender#845. Closes linebender#638 **Changelog: None** --------- Co-authored-by: Daniel McNab <36049421+DJMcNab@users.noreply.github.com>
…inebender#859) LLM Contributions: Generated with Opus 5.5 High. Reviewed by GPT Sol 6. Reviewed by me. A small refactor to `calculate_content_widths` to deduplicate code that runs for every hard and soft line break. It's almost-but-not-quite a purely mechanical refactor. There are small logic changes, but they are not intended to affect the output. (LLM finds that a -0 in the old output changes to +0 with this new code, which doesn't overly concern me) **Changelog**: None
…ing a new line (linebender#853) LLM Contributions: Generated with Opus 5.5 High. Reviewed by me. Adds some missing state synchronisation for the `max_height_exceeded` variable. Blitz can get itself stuck in an infinite loop without this. **Changelog** > ### Fixed > > - `max_height_exceeded` is correctly updated/reset when updating the max height, and when starting a new line
LLM Contributions: Generated with Fable 5.1 Low Enable the base direction override to be set on `PlainEditor` (passes it through to `Layout`). This enables Blitz to set `<textarea>`s to RTL layout when needed. **Changelog** > ### Added > > - `PlainEditor::set_base_direction`. Same as `Layout::set_base_direction` but for the editor.
<!-- Please ensure that you have reviewed our LLM ("AI") policy at
https://linebender.org/wiki/llm-policy/.
If you did not use any LLM tools, please replace `Unspecified` with
`None`. -->
LLM Contributions: It flagged them.
I've made two of them doclinks.
<!--
If our users need to know about this change, please describe that in the
quote block below.
What you write here will be edited by us later - it doesn't need to be
perfect.
If this change doesn't need a changelog entry, please replace the next
line with `**Changelog: None**`.
-->
**Changelog: None**
…der#862) <!-- Please ensure that you have reviewed our LLM ("AI") policy at https://linebender.org/wiki/llm-policy/. If you did not use any LLM tools, please replace `Unspecified` with `None`. --> LLM Contributions: None. Another one of these got out-of-date with linebender#846; best to just remove entirely, I think. <!-- If our users need to know about this change, please describe that in the quote block below. What you write here will be edited by us later - it doesn't need to be perfect. If this change doesn't need a changelog entry, please replace the next line with `**Changelog: None**`. --> **Changelog: None**
<!-- Please ensure that you have reviewed our LLM ("AI") policy at
https://linebender.org/wiki/llm-policy/.
If you did not use any LLM tools, please replace `Unspecified` with
`None`. -->
LLM Contributions: Had it look these up after
linebender#852
<!--
If our users need to know about this change, please describe that in the
quote block below.
What you write here will be edited by us later - it doesn't need to be
perfect.
If this change doesn't need a changelog entry, please replace the next
line with `**Changelog: None**`.
-->
**Changelog: None**
<!-- Please ensure that you have reviewed our LLM ("AI") policy at
https://linebender.org/wiki/llm-policy/.
If you did not use any LLM tools, please replace `Unspecified` with
`None`. -->
LLM Contributions: Review.
We have three sources for soft wrapping, and none should lead to
opportunities right around a mandatory line break. Currently, (without
this PR) line break overrides can add an opportunity before a mandatory
line break, but note we are already preventing the other sources (the
ICU4X line segmenter and `break-spaces`) from adding those. Also note we
are already removing all soft wrap opportunities right *after* a
mandatory line break, regardless of the opportunity's source.
If we let such soft wrap opportunities be, they can mess with invariants
in consumers (they must simply take the mandatory line break), and in
particular soft wrap opportunities before a mandatory line break are not
allowed by [UAX linebender#14 LB6](https://www.unicode.org/reports/tr14/#LB6).
This proposes placing the guarding we were doing in a single place, and
like we were doing with the opportunities after the mandatory break, we
also now just remove all opportunities before one.
<!--
If our users need to know about this change, please describe that in the
quote block below.
What you write here will be edited by us later - it doesn't need to be
perfect.
If this change doesn't need a changelog entry, please replace the next
line with `**Changelog: None**`.
-->
**Changelog: None**
---------
Co-authored-by: Daniel McNab <36049421+DJMcNab@users.noreply.github.com>
LLM Contributions: Generated with Opus 5.5 High. Reviewed by me. This is a small refactor motivated by code quality. Previously an `&mut` reference to `contributed` was being passed into `LineBoxMetrics`'s `reset` method. This is a violation of separation of concerns. Resetting contributed now happens in the `reset_line` method of `BreakerState` which actually owns `contributed`. **Changelog**: None (pure refactor)
<!-- Please ensure that you have reviewed our LLM ("AI") policy at
https://linebender.org/wiki/llm-policy/.
If you did not use any LLM tools, please replace `Unspecified` with
`None`. -->
LLM Contributions: None.
We added the public `parley::Layout::Run::line_height` out of
convenience during the `parley_engine` refactor, and it hasn't been
released yet (linebender#679). A `Run`
doesn't really have a singular line height as styles (line height and
vertical alignment) can change inside the run. It does have "layout
bounds", which we may or may not we want to expose.
In the meantime, I think we should drop this accessor.
Split from linebender#839.
<!--
If our users need to know about this change, please describe that in the
quote block below.
What you write here will be edited by us later - it doesn't need to be
perfect.
If this change doesn't need a changelog entry, please replace the next
line with `**Changelog: None**`.
-->
**Changelog: None**
Fixes linebender#683. `load_fonts_from_paths` kept a single `families` map for the whole scan and never cleared it, then re-merged the entire map into `self.families` on every callback, so each face got re-added on later callbacks. On top of that it re-scanned the whole file with `scan_memory` even though `scan_paths` had already parsed the font, which multiplied the duplicates further for collection (`.ttc`) files. This registers the font that `scan_paths` already handed us directly and gives each callback its own accumulator, so every face lands exactly once. `register_fonts` still scans the memory blob it is given, but now goes through the same per-font registration path. Added a regression test that loads the system font directory and checks that no family ends up with a duplicate face (same source and index). It is gated the same way as the existing `make_shared_matches_local` test and returns early when no font directory is present. --------- Co-authored-by: Chris (ChrisJr404) <11917633+ChrisJr404@users.noreply.github.com> Co-authored-by: Nico Burns <nico@nicoburns.com>
…inebender#863) <!-- Please ensure that you have reviewed our LLM ("AI") policy at https://linebender.org/wiki/llm-policy/. If you did not use any LLM tools, please replace `Unspecified` with `None`. --> LLM Contributions: Initial implementation, review. This fixes an issue where the "run box" was always taken from the run's first character and was then applied to all the run's characters. In the case where the line height changes mid-run, that's not correct. Conceptually this is split from linebender#839. This takes the idea of precalculating the box sizes from linebender#839, but in a slightly simpler way, and stores it only if it's uniform within the run. This helps performance a bit, because especially quantization on x86 is somewhat expensive. (And long runs can wrap into many lines.) This is roughly a 5-15% performance win on line breaking, and seems to be neutral for building. Before this change, the test renders as follows. <img width="63" height="173" alt="line_height_changes_per_line-0" src="https://github.com/user-attachments/assets/dbb0f30e-e000-4c29-84ed-c011f6d8a0f3" /> # Performance ``` Line Breaking - arabic 8000 characters, narrow [ 221.8 us ... 200.2 us ] -9.70%* Line Breaking - arabic 8000 characters, wider [ 121.9 us ... 106.4 us ] -12.75%* Line Breaking - arabic 8000 characters, unwrapped [ 92.2 us ... 78.3 us ] -15.10%* Line Breaking - latin 8000 characters, narrow [ 177.4 us ... 167.5 us ] -5.57%* Line Breaking - latin 8000 characters, wider [ 86.2 us ... 80.0 us ] -7.16%* Line Breaking - latin 8000 characters, unwrapped [ 55.0 us ... 51.9 us ] -5.71%* Line Breaking - japanese 8000 characters, narrow [ 224.5 us ... 212.2 us ] -5.47%* Line Breaking - japanese 8000 characters, wider [ 149.5 us ... 141.6 us ] -5.28%* Line Breaking - japanese 8000 characters, unwrapped [ 127.8 us ... 118.4 us ] -7.38%* Line Breaking - arabic 8000 characters, wrapped + spacing [ 129.8 us ... 112.8 us ] -13.11%* Line Breaking - latin 8000 characters, wrapped + spacing [ 93.8 us ... 87.2 us ] -7.00%* Line Breaking - japanese 8000 characters, wrapped + spacing [ 155.1 us ... 146.4 us ] -5.63%* Page Break - arabic 8000 characters [ 97.6 us ... 83.0 us ] -14.98%* Page Break - latin 8000 characters [ 63.2 us ... 59.1 us ] -6.40%* Page Break - japanese 8000 characters [ 140.1 us ... 130.0 us ] -7.23%* Default Style - arabic 20 characters [ 8.9 us ... 8.9 us ] +0.16% Default Style - latin 20 characters [ 4.2 us ... 4.2 us ] +1.01%* Default Style - japanese 20 characters [ 8.0 us ... 8.1 us ] +1.97%* Default Style - arabic 400 characters [ 188.8 us ... 185.6 us ] -1.66%* Default Style - latin 400 characters [ 54.1 us ... 53.7 us ] -0.78% Default Style - japanese 400 characters [ 135.0 us ... 136.6 us ] +1.13%* Default Style - arabic 8000 characters [ 4.5 ms ... 4.5 ms ] -0.26% Default Style - latin 8000 characters [ 1.2 ms ... 1.2 ms ] -2.85%* Default Style - japanese 8000 characters [ 2.9 ms ... 2.9 ms ] +0.83% Styled - arabic 20 characters [ 10.7 us ... 10.7 us ] -0.11% Styled - latin 20 characters [ 6.0 us ... 6.1 us ] +2.30%* Styled - japanese 20 characters [ 9.0 us ... 9.1 us ] +1.89%* Styled - arabic 400 characters [ 216.2 us ... 211.0 us ] -2.40%* Styled - latin 400 characters [ 78.3 us ... 79.0 us ] +0.84% Styled - japanese 400 characters [ 154.7 us ... 155.0 us ] +0.19% Styled - arabic 8000 characters [ 5.1 ms ... 5.1 ms ] -0.62% Styled - latin 8000 characters [ 1.7 ms ... 1.7 ms ] +0.28% Styled - japanese 8000 characters [ 3.3 ms ... 3.3 ms ] +1.42% ``` <!-- If our users need to know about this change, please describe that in the quote block below. What you write here will be edited by us later - it doesn't need to be perfect. If this change doesn't need a changelog entry, please replace the next line with `**Changelog: None**`. --> **Changelog: None**
<!-- Please ensure that you have reviewed our LLM ("AI") policy at
https://linebender.org/wiki/llm-policy/.
If you did not use any LLM tools, please replace `Unspecified` with
`None`. -->
LLM Contributions: Investigation, initial implementation, review.
On top of linebender#863. If the line height changes within an atom, we add the
spans of all the atom's characters to the line. I believe this matches
Blink; but note we do still diverge a bit from Blink regarding the
"strut" and accounting styles per-character instead of per-span.
Supercedes linebender#761, linebender#828, linebender#839.
Without this change, the test renders as follows.
<img width="54" height="80" alt="line_height_change_inside_ligature-0"
src="https://github.com/user-attachments/assets/2f3f0baa-8928-4739-9640-1b23db0b14a1"
/>
# Performance
Together with linebender#863, this benches as follows (but there may be some
placement noise again).
```
Line Breaking - arabic 8000 characters, narrow [ 221.7 us ... 203.0 us ] -8.44%*
Line Breaking - arabic 8000 characters, wider [ 121.7 us ... 109.7 us ] -9.84%*
Line Breaking - arabic 8000 characters, unwrapped [ 92.1 us ... 83.4 us ] -9.44%*
Line Breaking - latin 8000 characters, narrow [ 176.7 us ... 168.0 us ] -4.96%*
Line Breaking - latin 8000 characters, wider [ 86.3 us ... 80.5 us ] -6.78%*
Line Breaking - latin 8000 characters, unwrapped [ 54.8 us ... 52.8 us ] -3.77%*
Line Breaking - japanese 8000 characters, narrow [ 224.7 us ... 213.7 us ] -4.88%*
Line Breaking - japanese 8000 characters, wider [ 149.8 us ... 143.3 us ] -4.35%*
Line Breaking - japanese 8000 characters, unwrapped [ 127.4 us ... 121.2 us ] -4.89%*
Line Breaking - arabic 8000 characters, wrapped + spacing [ 130.7 us ... 116.8 us ] -10.67%*
Line Breaking - latin 8000 characters, wrapped + spacing [ 94.0 us ... 88.2 us ] -6.16%*
Line Breaking - japanese 8000 characters, wrapped + spacing [ 155.5 us ... 147.7 us ] -5.01%*
Page Break - arabic 8000 characters [ 98.2 us ... 88.0 us ] -10.38%*
Page Break - latin 8000 characters [ 63.1 us ... 60.2 us ] -4.58%*
Page Break - japanese 8000 characters [ 141.0 us ... 131.6 us ] -6.68%*
```
<!--
If our users need to know about this change, please describe that in the
quote block below.
What you write here will be edited by us later - it doesn't need to be
perfect.
If this change doesn't need a changelog entry, please replace the next
line with `**Changelog: None**`.
-->
**Changelog**
> ### Fixed
>
> - All line height changes within a run now contribute to the line box.
<!-- Please ensure that you have reviewed our LLM ("AI") policy at
https://linebender.org/wiki/llm-policy/.
If you did not use any LLM tools, please replace `Unspecified` with
`None`. -->
LLM Contributions: It found these.
These `.clone()`s were unnecessary. Doesn't seem to be noticeable in any
measurements, though.
<!--
If our users need to know about this change, please describe that in the
quote block below.
What you write here will be edited by us later - it doesn't need to be
perfect.
If this change doesn't need a changelog entry, please replace the next
line with `**Changelog: None**`.
-->
**Changelog: None**
LLM Contributions: Generated with LLM. Implements support for [`text-align-last`](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/text-align-last) which allows the alignment of the last line to be configured independently. This turns out to be pretty trivial as alignment logic is per-line, so we just switch in a different style for the last line, falling back to the existing logic ("same alignment as other lines, unless justified") if a last line alignment is not specified. Checking if we're on the last line every line is inefficient in theory, but this is not a hot path (runs per-line rather than per glyph/character/cluster) so it's inconsequential in practice. **Changelog** > ### Added > > - `AlignOption::last_line_alignment`, which allows the alignment of the last line to be configured.
<!-- Please ensure that you have reviewed our LLM ("AI") policy at
https://linebender.org/wiki/llm-policy/.
If you did not use any LLM tools, please replace `Unspecified` with
`None`. -->
LLM Contributions: It flagged these were write-only.
These reach `parley_engine` through `ShapeOptions`, so they're getting
used for shaping; they just aren't necessary for for font selection
proper.
https://github.com/linebender/parley/blob/5794c7461bdeea70366cd2489e42c0864c97f7a8/parley_engine/src/shape/shaper.rs#L19-L37
[CSS Fonts 4](https://www.w3.org/TR/css-fonts-4) says:
§ 8.2:
> The use of font variation settings does not affect font selection
where a fallback font is to be used, where using font properties for the
same axis would have an effect on font selection.
§ 7.2:
> When a font lacks support for a given underlying font feature, text is
simply rendered as if that font feature was not enabled
<!--
If our users need to know about this change, please describe that in the
quote block below.
What you write here will be edited by us later - it doesn't need to be
perfect.
If this change doesn't need a changelog entry, please replace the next
line with `**Changelog: None**`.
-->
**Changelog: None**
<!-- Please ensure that you have reviewed our LLM ("AI") policy at
https://linebender.org/wiki/llm-policy/.
If you did not use any LLM tools, please replace `Unspecified` with
`None`. -->
LLM Contributions: Asked it to flag some issues.
<!--
If our users need to know about this change, please describe that in the
quote block below.
What you write here will be edited by us later - it doesn't need to be
perfect.
If this change doesn't need a changelog entry, please replace the next
line with `**Changelog: None**`.
-->
**Changelog: None**
… -> `SpanMetrics` (linebender#867) LLM Contributions: Naming rubber ducking, initial drafting, validation of Blink's semantics. We plan to use this for bullet list marker placement and sizing, to match Chrome. This is as discussed in [#office hours > Text 2026-09-29](https://xi.zulipchat.com/#narrow/channel/359642-office-hours/topic/Text.202026-09-29/with/627538333). The fields I've made public are only those needed for bullet points; there are reasonable arguments for tweaking the subset of fields exposed (e.g. to also include `over/under/line_height`); however, I've already found that exposing other fields can serve as an attractive nuisance - my LLM believed they were needed, but it actually had the wrong semantics. **Changelog** > ### Added > > - `Layout::root_span_metrics`, to help with placing directly drawn bullet points.
LLM Contributions: None. 😅 See linebender#867 **Changelog: None**
<!-- Please ensure that you have reviewed our LLM ("AI") policy at
https://linebender.org/wiki/llm-policy/.
If you did not use any LLM tools, please replace `Unspecified` with
`None`. -->
LLM Contributions: Review.
This is unread.
Conceptually, if `StyleMetrics` are the *measured* metrics of the span,
I think perhaps the "line height" doesn't quite survive at this level:
it's then more an input style than something used for layout (but I'm
not sure that's really the model we want to go for). In any case, this
field doesn't have consumers at the moment.
<!--
If our users need to know about this change, please describe that in the
quote block below.
What you write here will be edited by us later - it doesn't need to be
perfect.
If this change doesn't need a changelog entry, please replace the next
line with `**Changelog: None**`.
-->
**Changelog: None**
LLM Contributions: Generated with Fable 5.1 Low Depends on: - linebender#854 ## Context This is a follow-up to linebender#766 (`vertical-align`) that gets us back to performance parity with before that PR was merged. It was purposefully left as a follow-up to make things easier to review, but we probably need to land this or some alternative performance fix before we can release. ## Summary Eliminate `SmallVec<[SubtreeExtents; 2]>` on `LineBoxMetrics`, allowing it to impl `Copy` which is performance-critical because it is copied at every line breaking opportunity. ## New design - `LineBoxMetrics` loses the general ``SmallVec<[SubtreeExtents; 2]>`` and gains `root: SubtreeExtents`. So it still stores metrics for the root subtree on the line but extra subtrees generated by `vertical-align: top | bottom` move out. - `LineBoxMetrics` also gains `non_root_height: f32`, the current height of the tallest of those extra subtrees. This saves `line_height()` from having to compute it (which would otherwise be much more expensive in this new design). It is copied into saved break opportunities restored when reverting to one. - `BreakerState` gains `subtrees: SubtreeHistory` for the extra subtrees. This is per-line state that is processed in `finish_line` and then reset for the next line. All of the bookkeeping for it is inside `SubtreeHistory`: - It contains a `Vec<SubtreeExtents>` in which the current extents of a subtree are the *last* entry with its root. - `grow()` adds a box to a subtree. If the subtree's most recent entry in the `Vec` is frozen (see below) it pushes the extents as a new entry, otherwise it overwrites the entry in place. So entries only pile up when there are line-breaking opportunities between them. - `save()` is called at each line-breaking opportunity. It returns the current length, and marks every entry pushed so far as frozen (so they can safely be restored when taking a line-breaking opportunity) - `restore(save)` is called when taking a line-breaking opportunity. It truncates to the saved length, which drops everything pushed since. The entries before the save point are guaranteed to be unmodified as they were frozen when the save point was saved. - `current()` collects the latest entry for each root into a `SmallVec` once per-line in `finish_line`. - `PrevBoundaryState` (saved state for each line-break opportunity) gains `subtrees_save: usize`, the value returned by `save()` at that opportunity. ## Performance This is -25% on our existing line-breaking benchmarks. The below are stress tests with hundreds of `vertical-align: top` spans on a single line, each containing inline boxes of increasing height: | Case | This PR | | --- | ---: | | nowrap, 2048 spans, no boxes | 0.99× | | nowrap, 2048 spans × 8 boxes | 0.34× | | wrap, 2048 spans, no boxes | 0.51× | | wrap, 2048 spans × 8 boxes | 0.45× | | nowrap, 512 spans, 8192 boxes in the first | 1.18× | | wrap, 512 spans, 8192 boxes in the first | 5.85× | As you can see, there is a pathological case. That *may* be fixable, but at the cost of quite a big of complexity and slightly slower performance in the common cases. I've judged it not worth it for now. **Changelog**: None (performance improvement for never-published regression)
<!-- Please ensure that you have reviewed our LLM ("AI") policy at
https://linebender.org/wiki/llm-policy/.
If you did not use any LLM tools, please replace `Unspecified` with
`None`. -->
LLM Contributions: Investigation, review.
In the unlikely event a font does not carry these tables, we now fall
back based on font size. This also fixes the underline offset.
<!--
If our users need to know about this change, please describe that in the
quote block below.
What you write here will be edited by us later - it doesn't need to be
perfect.
If this change doesn't need a changelog entry, please replace the next
line with `**Changelog: None**`.
-->
**Changelog**
> ### Fixed
>
> - In case a font does not carry decoration metrics like underline
size, the fallbacks now scale with the font size instead of staying in
font units, and the fallback underline is now placed below the baseline
instead of above.
<!-- Please ensure that you have reviewed our LLM ("AI") policy at
https://linebender.org/wiki/llm-policy/.
If you did not use any LLM tools, please replace `Unspecified` with
`None`. -->
LLM Contributions: None.
<!--
If our users need to know about this change, please describe that in the
quote block below.
What you write here will be edited by us later - it doesn't need to be
perfect.
If this change doesn't need a changelog entry, please replace the next
line with `**Changelog: None**`.
-->
**Changelog: None**
Measure LTR and mixed-direction layout building with 0, 1, and 100 inline boxes. Add separate line-breaking and justification cases for layouts with 100 boxes, building the input outside those timed loops. Use only existing builder and layout APIs. This commit changes no layout behavior and provides the same benchmark inputs as the final fix. Validation: - cargo fmt --all - cargo clippy --workspace --all-targets --all-features --locked -- -D warnings - cargo nextest run --workspace --locked --all-features --no-fail-fast (481 passed, 1 skipped) - All 10 inline-box benchmark cases build and execute.
Introduce BidiObject::new(index), read-only index/level accessors, and Analyzer::analyze_with_objects without changing the existing entry point. Require sorted source offsets on UTF-8 boundaries, preserving slice order at equal anchors. Merge virtual U+FFFC characters into one bidi pass and project the levels back onto source characters and objects separately. Segmentation and source indices are unchanged. Preserve the object-free and LTR fast paths. Add explicit replacement-character reference-path tests, state-reuse coverage, and invalid-anchor tests. The reference path shares the resolver and verifies merging/projection equivalence, not independent UBA correctness. Validation: - cargo fmt --all - cargo clippy --workspace --all-targets --all-features --locked -- -D warnings - cargo nextest run --workspace --locked --all-features --no-fail-fast (487 passed, 1 skipped)
Stably sort boxes before analysis, resolve every box kind as a virtual U+FFFC, and preserve its level through shaping and line reordering. Remove the previous-run heuristic and handle empty text with boxes without introducing a source character. Include box levels when deciding whether a line needs reordering, including box-only RTL lines. Document the input contract separately from the layout correction: RangedBuilder and StyleRunBuilder now reject indices past text.len() or inside UTF-8 characters while building. Previously, boxes past the text were appended to its end; callers must now supply text.len() explicitly. Reject invalid anchors rather than silently changing their source position. TreeBuilder ignores the supplied index and computes its own anchor after whitespace handling. Builders still sort valid anchors stably. Keep builder/anchor coverage and add a final visual regression across a forced line break. Check line.items() order and relative x positions; the new regression fails on the upstream baseline and passes with the fix. Validation: - cargo fmt --all - cargo clippy --workspace --all-targets --all-features --locked -- -D warnings - cargo nextest run --workspace --locked --all-features --no-fail-fast (494 passed, 1 skipped) - rustdoc for parley and parley_engine, all features, warnings denied - All 10 benchmark cases execute against the commit-1 baseline. Short timing comparisons reported a 1.57%-3.95% increase for mixed-RTL line breaking. A same-binary control varied by 2.74%, with large changes in absolute timings between runs. Performance needs a quiet-machine measurement before drawing a conclusion.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.