* Add failing tests for whitespace absorbed into a fallback run
A fallback run is extended for as long as the fallback font has glyphs
instead of ending where the primary font regains coverage. Practically
every font maps U+0020, so the space that follows fallback text is
pulled into the fallback run and shaped with its space glyph.
- itemization: a Hebrew letter followed by " b" must produce a 1-char
fallback run, not a 2-char one that swallows the space
- measurement: the space after an emoji must have the same advance as
the same space elsewhere in the line
* Merge abutting text bounds within the usual float tolerance
The two edges being compared are reached by summing glyph advances along
different paths, so abutting bounds can land an ULP apart and a single
directional span gets reported as two rectangles. Compare them the way
the rest of layout compares coordinates.
* End a fallback run where the default typeface regains coverage
Whitespace was exempt from the return-to-primary check so that a space
wouldn't split a fallback run. Practically every font maps U+0020, so
the exemption let a fallback run reach past the text the default
typeface couldn't render and shape the following space with the
fallback's own advance - a full em in most emoji fonts, which is the
long space reported after an emoji.
- the check now applies to spacing whitespace (Zs), so a fallback run
ends at the first cluster the default typeface can render; control
and format codepoints stay exempt - many fonts map the
default-ignorable bidi controls, and a default typeface whose cmap
merely has such a mark must not pull it out of the fallback run: the
mark renders nothing either way, and the split cuts the run for no
reason
- a default typeface that cannot shape the run's script is still not a
return target for text, but it does reclaim the spacing whitespace
between the words, which carries no shaping
- a run of pure whitespace no longer becomes the anti-thrashing bias for
the next run, so the words on either side of a space keep resolving to
the same fallback font
Test expectations that encoded the old run structure move with it: a
line of emoji separated by spaces is no longer a single run, glyph
clusters are relative to each run's own text (the affected helpers now
add the run's start), and the spaces of a right-to-left line measure
with the primary font, which widens those lines.
* perf(text): O(1) width queries on ShapedBuffer, cached LB-class lookup
- ShapedBuffer: lazy, pooled cluster-prefix cache shared across Split;
adds TotalGlyphAdvance / MeasureCharactersThatFit.
- GlyphRun/TextFormatterImpl/Skia GlyphRunImpl: read the cache, drop
duplicate scans; SplitTextRuns now returns firstLength via out.
- LineBreakEnumerator: cache Next/PreviousClass per advance; remove
unused LineBreakPairTable.
- Tests + benchmarks for the new paths and a dotnet-trace harness.
~1.32x faster / -5% alloc on the emoji-wrap micro-benchmark; no
observable behaviour or public API change.
* perf(TextFormatting): inline LineBreakEnumerator rule dispatch
Replace the BreakUnitDelegate[] s_rules array dispatch in
LineBreakEnumerator.ExecuteRules with a sequence of direct static
calls and a `goto Done` early-exit. This removes 42 indirect calls
per codepoint and lets the JIT reason across rule boundaries, which
in turn makes [MethodImpl(AggressiveInlining)] meaningful — JIT
cannot inline through delegate.Invoke, so the attribute was a no-op
in the previous shape.
Selectively apply AggressiveInlining to the small single-condition
rules (LB03, LB04, LB06, LB07, LB08a, LB11–LB15d, LB18, LB20,
LB21b, LB22, LB29, LB31) and let the JIT decide on the larger ones
to avoid bloating the merged ExecuteRules.
The static BreakUnitDelegate[] s_rules array is removed.
Benchmarks (BDN default job, --inProcess, N=13–22, rel. StdDev <1.5%):
UnicodeBreakEnumeratorBenchmark.LineBreakEnumerator_Sequence
Ascii 154.6 µs -> 24.84 µs (6.22x)
Bmp 190.2 µs -> 32.81 µs (5.80x)
Supplementary 227.6 µs -> 37.56 µs (6.06x)
TextLayoutProfile.BuildEmojisWrapped
Before (branch, pre-inline) 804.5 µs / 570.15 KB
After (branch, post-inline) 547.0 µs / 570.15 KB (-32.0%)
vs upstream/master (1061 µs) -> ~1.94x total speedup
No allocation change; CPU-only dispatch reshape. All
LineBreakEnumerator unit tests pass (5/5).
* Refactor ShapedBuffer to share pool storage via IRef + generation counter
Wrap the ArrayPool-rented glyph and cluster-cache arrays in a small
PooledArray<T> disposable and expose them through IRef<T>. Split children
and WithBidiLevel aliases now Clone() the refs instead of borrowing raw
pool arrays, so the backing storage survives until every sibling has
been disposed - eliminating the UAF risk that existed when a parent was
disposed before its children.
Add a per-glyph-holder generation counter (Volatile.Read / Interlocked
.Increment). The indexer setter bumps the counter on every write, and
EnsureClusterCache compares its recorded generation against the holder's
current value, rebuilding on mismatch. This lets us drop the previous
"no mutation after Split/WithBidiLevel" contract: mutations performed
through any sibling now propagate to the others' caches transparently.
Dispose is made idempotent via a _disposed guard so overlapping cache
eviction and TextLine teardown only release the IRefs once.
Adds ShapedBufferSharedStorageTests covering sibling lifetime,
Dispose idempotency, and generation-driven cache invalidation across
Split children and WithBidiLevel aliases.
* test(ShapedBuffer): cover cluster-cache sharing across Split/WithBidiLevel aliases
Add a regression test hook (`ClusterPrefix`) exposing the
backing cluster-prefix array reference, plus two tests that mutate a
parent buffer before aliasing it and assert the alias reuses the
parent's pooled prefix array instead of rebuilding. Guards against
forgetting to propagate `_cacheGeneration` to alias buffers, which
would silently defeat the cached-split fast path.
* Correctly use MathUtilities.LessThanOrClose
---------
Co-authored-by: Julien Lebosquain <julien@lebosquain.net>
* Introduce a universal IGlyphTypeface implementation that does not rely on any platform implementation
* Revert changes
* Fix Android
* Make the test happy
* Fix build
* Update baseline
* Fix naming
* Fix headless
* Move interfaces to dedicated files
Make GlyphTypeface.GlyphCount an integer
* Fix GlyphCount
* Make IGlyphTypeface NotClientImplementable
* Make sure we cache platform typefaces by their desired name, style, weight and stretch
* Update baseline
* Only use IGlyphTypeface
* Fix Android
* Try to clear the buffer before we encode somethimg
* Add needed test font
* Add more unit tests
* Reduce allocations
* Remove Direct2D1 test files
* More tests
* More complete table implementations
* More adjustments
* Use batch APIs
* Handle invalid timestamps
* Update baseline
* Introduce a CharacterToGlyphMap struct for faster access
* Remove AggressiveInlining
* Remove AggressiveInlining
* Make the head table optional for legacy fonts
* Remove Load method. Fix TextBlockTests
* Fix nullables
* Remove redundant folder
* Update Api baseline
* revert diff helper changes
* revert changes
* Use bare minimum font for Headless platform and introduce a test font manager that uses the Inter font for testing.
* Add missing font file for Headless platform
---------
Co-authored-by: Gillibald <stebner@avaloniaui.net>
Co-authored-by: Julien Lebosquain <julien@lebosquain.net>
* Fixed the issue where a newline character required two backspaces to be deleted in Windows.
* add unit test
* Handling the differences between HeadlessTextShaperStub and TextShaperImpl
* Make unit tests available
* Treat "\r\n" as a single unit.
* The backspace test against CRLF
* Backspace should treat CRLF as a unit.
* - Adjusts the overhang calculation so it matches what WPF produces
- Fixes baseline alignment for inline controls
* Adjust OverhangTrailing
Add unit test
* Only run overhang unit test on Windows
* Reverse text runs for RTL flow direction
* Optimize text run traversal.
* Remove unused LINQ directive from TextLineImpl.cs
* Add RTL newline handling test in TextLineTests
* Add RTL newline handling tests for text formatting.
* Make sure we only apply the cluster offset if we are inside the current cluster
* Better naming
* Guard coveredLength against invalid values
---------
Co-authored-by: Max Katz <maxkatz6@outlook.com>
* Adjust TextLineImpl.GetTextRunBounds so it properly handles substitutions
Adjust TextLineImpl.GetTextRunBounds so it properly reports out of text range bounds
Adjust TextLineImpl.GetTextRunBounds so it properly reports text source run indices
* Remove redundant comments
* Add requested changes
* [Text] fix infinite loop hang on selection of bidi text
* Deal with zero width whitespaces during run bounds calculation
* Do the same thing for RTL
---------
Co-authored-by: Benedikt Stebner <Gillibald@users.noreply.github.com>
* Repro for Line_Formatting_For_Oversized_Embedded_Runs_Does_Not_Freeze
* Repro for Line_With_IncrementalTab_Should_Return_Correct_Backspace_Position
* Fix GetBackspaceCaretCharacterHit
Fix GetPreviousCaretCharacterHit
Fix WrapWithOverflow for not text runs
* Fix custom font manager
* Move DejaVuSans to a different location to prevent using it as a fallback
---------
Co-authored-by: Nikita Tsukanov <keks9n@gmail.com>