* Added failng popup child margin test
* Ignore popup child margin when positioning
Avoids drop shadows and other out-of-bounds visual effects from affecting popup positioning
* Fix warnings in tests
* Pass Deflate to WSurface
---------
Co-authored-by: Tom Edwards <tom.edwards@chaos.com>
Follow-up to #22082, which added the short-circuit to the property accessor
nodes. The cast nodes still called ValidateNonNullSource, so a cast reported
"Value is null" before the following ?. could suppress it.
Casting null now produces null, as in C#, leaving any error to the member
access that follows.
The binding path parser built type names with ParseIdentifier, which doesn't
accept '+', so a cast like ((local:Outer+Nested)DataContext) stopped parsing at
the '+' and then failed expecting a ')'. Parse type names with a variant that
accepts '+'. Ordinary identifiers are unchanged.
* Tighten the comments on the synthetic caching fix
Keep the invariant and the reason, drop the retelling of how the leak was
found. Also plain-ASCII the dashes and correct the doc on the test counter:
the alias resolution goes through the same overload, so the counter proves
the cached result short-circuits the platform call too.
* Match font family names case-insensitively in the glyph typeface cache
Every other family-name comparison in FontCollectionBase is
OrdinalIgnoreCase - the sorted family array, the binary search in
TryGetGlyphTypeface, AddFontFamily's de-duplication - but the cache itself
used the default ordinal comparer. Two consequences:
- A request whose casing differs from the cached one misses the bucket and
so skips the nearest-match branch entirely. The family-name search that
picks it up afterwards returns the nearest match raw, without synthesis
and without caching, so "MyAlias" at Black renders faux-bold while
"MYALIAS" at Black renders regular weight.
- SystemFontCollection registers a resolved face under both the platform's
family name and the requested one. When those differ only in casing that
stores two buckets, while AddFontFamily de-duplicates case-insensitively
and publishes only the first, leaving the second unreachable from every
family-name search.
Also compares the requested and platform family names case-insensitively
before the second registration, so the redundant add goes away rather than
becoming a silent no-op.
* Reuse an already cached synthetic glyph typeface
TryCreateSyntheticGlyphTypeface synthesised unconditionally. When a synthetic
for the same source family and key was already cached, the second one lost
TryAddGlyphTypeface to the instance holding the slot, so it was handed to the
caller but never cached - and GlyphTypeface has no finalizer, only an explicit
Dispose reachable from the cache, so its native typeface stayed alive for the
rest of the process. Each one costs a full copy of the font file, which
TryGetStream reads into memory and SKTypeface.FromStream copies again.
Return the cached synthetic instead when its simulations match the ones this
call would apply. The source family's typefaces are already in hand for the
early-out above; they were fetched and then never used.
Caching under the requested family name removed the path that reached this on
every call, but the method is public on IFontCollection and the race remains,
so guard it at the source.
* Match font family mappings case-insensitively
FontManagerOptions.FontFamilyMappings is supplied by the application, so its
comparer is whatever its author gave it - ordinal for a plain
Dictionary<string, FontFamily>. Family names are matched case-insensitively
everywhere else, so a mapping silently did not apply when the requested name
differed in casing from the configured key.
Copy the mappings into an OrdinalIgnoreCase dictionary in the constructor.
Keys that collide only by casing were separate entries before; the last wins
rather than throwing, since a mapping table is configuration and failing here
would take the application down at startup.
Two behaviour changes worth naming: a mapping now applies whatever casing the
name arrives in, and FontManager no longer observes mutations made to the
caller's dictionary after construction.
* feat: Add IsVisible to TableViewColumn
* test: Add TableView IsVisible tests
* feat: Add option to hide a column in the ControlCatalog
* chore: Remove unnecessary remark from property
* docs: An other xml comment cleanup
* fix: Use NaN as a sign that the width is reset and needs recalculation
Also, set the ActualWidth of a hidden column to 0
* chore: Add a comment explaining what NaN means for columns ActualSize
* Pack the segmentation properties into one Unicode trie
Grapheme, indic-conjunct, word, line and sentence break classes now share a
single SegmentationTrie, together with the Emoji, Emoji_Presentation and
Default_Ignorable_Code_Point flags that rode in the grapheme word. The
UnicodeDataTrie word keeps general category, script and script extensions.
- SegmentationTrieGenerator replaces GraphemeBreakClassTrieGenerator and emits
Segmentation.trie.cs; UnicodeData.trie.cs is regenerated without the break
fields it no longer carries.
- SentenceBreakClass data and Codepoint.SentenceBreakClass expose UAX-29
sentence classes to callers.
- WordSegment carries the source text slice alongside the code-unit and
code-point readouts, matching the shape newer segment types use; the existing
constructor and members stay for compatibility.
Data regenerated against UCD 17.0.0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Add UAX-29 sentence segmentation
SentenceBreakEnumerator walks sentence boundaries over a UTF-16 span (rules
SB1-SB11 and SB998), yielding SentenceSegment values of offset plus text slice.
Sentence is the one segmentation granularity accessibility clients and IME
navigation ask for that had no implementation, so callers had to approximate it
with punctuation heuristics.
The conformance suite runs against the Unicode SentenceBreakTest.txt data.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Make sentence segmentation linear in the text length
Every rule that needed left-hand context rescanned the text backwards from
the current position, so a run of spaces or closing brackets was walked
once per character in it. Enumerating 40k spaces took 5.5 s; 40k closing
brackets took 8.1 s, and both grew quadratically.
- Fold the left-hand context into the enumerator as it advances: the last
two significant classes for SB6/SB7, and a stage counter over the
(STerm | ATerm) Close* Sp* (Sep | CR | LF)? grammar for SB8-SB11.
- Memoize the SB8 lookahead. A scan answers every start position it passed
over, so the Close* Sp* run after an ATerm is scanned once, not once per
position.
- Classify with bitmasks, matching LineBreakEnumerator.
Both pathological inputs now finish in 0.1 ms at 40k characters. Output is
unchanged: the UCD SentenceBreakTest.txt suite passes, and 1.4M random
strings over the break classes segment identically to the previous
implementation.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Read the word segment length from the property valid for every segment
WordSegment.Text is empty for segments built through the code-unit
constructor that shipped in 12.1, so Text.Length holds only for segments
that come from WordBreakEnumerator. Length is set by both constructors.
- Measure the ellipsis break position from Length again.
- Say on the Text property itself, where it is read, that it can be empty.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Benchmark word and sentence segmentation across text shapes
The break enumerator benchmark covered three enumerators over random
codepoint distributions, which measures trie lookup cost but not the
per-character cost of the rules themselves. Long runs of one class are
where a segmentation algorithm degrades, and nothing measured them.
- Add the sentence enumerator to UnicodeBreakEnumeratorBenchmark.
- Add UnicodeSegmentationScalingBenchmark: prose, Han, ZWJ emoji, and runs
of spaces, closing punctuation, terminator-plus-spaces and regional
indicators, each at two lengths so the cost per character is comparable
between them.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Document the sentence break classes and trim the break unit
SentenceBreakClass is public API with nothing but the UCD aliases to go on,
so a caller reading Codepoint.SentenceBreakClass has to consult UAX #29 to
learn what a value means or which rule reacts to it.
- Describe the type and every member in terms of what the sentence rules do
with it, keeping the UCD alias as a trailing comment.
- Give SentenceBreakUnit a primary constructor and drop its Codepoint
property, which nothing reads now that the class is stored directly.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Clean up documentation for SentenceBreakClass
Removed remarks from the SentenceBreakClass documentation.
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
WB15 and WB16 pair regional indicators from the start of their run, and the
run was recounted from that start at every indicator, so word segmentation
was quadratic in the length of an unbroken run of flags. One pass over 4096
flags took 140 ms; the same text with a space between each flag took 121 us.
- Track whether an odd number of regional indicators precedes the position
the walk has reached, updating it as each code point is consumed.
- Drop CountRegionalIndicatorsBefore, which was the only backward scan in
the enumerator not bounded by the first significant code point.
- Cover the pairing rules: even and odd runs, a run reached after a letter,
a run split by a space, and a combining mark inside a flag.
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* Apply tile brush transforms once the tile sits in target space
Brush.Transform was concatenated into the shader matrix before the
translation that moves the tile onto the painted area, so it acted in
the tile's own space instead of the target's. A brush transform on a
fill away from the origin - the common case, since a relative viewport
resolves against the fill's bounds - came out displaced, and WPF, whose
behaviour these brushes follow, disagrees.
Both tile paths now place the tile first and let the transform act on
the result. Where the placement is identity, which is what the existing
goldens cover, the matrices are unchanged.
The cross suite gains an image brush so the non-scalable path is
covered too, plus a smooth ramp fixture: the star line drawing that was
the only image asset survives resampling as sparse speckle, which the
comparison metric cannot tell apart from a geometry error.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Apply the conic gradient transform after its angle
The sweep shader carried Angle as its local matrix and pre-concatenated
Brush.Transform onto it, so the transform acted on the raw sweep and
the angle then turned the result. Every other brush bakes its intrinsic
geometry in first and lets the brush transform act on the finished
pattern.
The visible effect was that a translation moved the gradient in a
direction rotated by Angle - 90 instead of the direction asked for. A
sweep gradient is fully determined by its centre and angle, so the
golden here is checked against the brush with its centre moved by the
same offset and no transform: the two renders are pixel-identical,
while the previous order is 0.169 rmse away against a 0.022 tolerance.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Add RelativeTransform to every brush
A brush transform expressed in the unit space of the painted bounds,
applied before the absolute Transform - WPF's Brush.RelativeTransform.
It lets one brush express a bounds-dependent transform, an SVG
gradientTransform in objectBoundingBox units for instance, without
baking any one consumer's bounds into a matrix, which is what makes a
single brush per gradient definition possible.
The property sits on IBrush and Brush, so every brush kind carries it
and a consumer reads it without a type test. Each immutable brush takes
it through a second constructor, leaving the existing signatures alone
and marking them for collapse in v13. The gradient brush animator
interpolates it alongside Transform, and the composition schema gains
it on both brush bases so the value reaches the render thread as a live
resource rather than a snapshot.
IBrush is NotClientImplementable, so the added member is suppressed for
API validation the way earlier additions to those interfaces are.
Nothing consumes it yet.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Consume RelativeTransform in the Skia backend
The relative matrix is conjugated into target space at draw time - the
unit square maps onto the painted rect, whose origin translation and
scale wrap the matrix - and composes before the absolute brush
transform. All three gradient kinds and both tile brush paths read it;
a solid colour brush has nothing for it to act on, matching WPF. With
no relative transform every path reduces to the previous matrices, so
existing goldens stay byte-identical.
The cross suite compares the result against WPF for the linear and
radial gradients, a drawing brush tiled and untiled, an image brush,
the composition order against Transform, one brush shared by two
differently sized fills, and the solid colour no-op. Goldens under
Skia/Media additionally cover the immediate renderer and the GPU
backends, which the cross suite does not exercise; each was measured
against its WPF counterpart before being committed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* Keep TextBlock text runs in sync with its inlines
_textRuns is built from Inlines, but OnMeasureInvalidated discarded it on
any measure invalidation while Inlines still held the content. Between
that point and the next measure pass, CreateTextLayout read a null
_textRuns as "no inlines" and shaped Text instead, which is null whenever
the content lives in Inlines. That empty result went into the
TextRunCache, keyed by text source index, so every later layout reused it
and the control rendered nothing until something invalidated the cache.
- Discard _textRuns in InvalidateTextLayout, next to the run cache, so
the runs and the cache are dropped by the same event and cannot
disagree about the content.
- Build the runs on demand in EnsureTextRuns, and pick the text source by
HasComplexContent rather than by _textRuns being set.
- Split the constraint-dependent work out of run building. Runs answer to
the content alone; only an embedded control answers to the available
width, so Inline.MeasureEmbeddedControls measures it and
EmbeddedControlRun reports the child's DesiredSize live. Runs now
survive a constraint change instead of being rebuilt every measure.
SelectableTextBlock never hands the run cache to its layout, but it
shared the same fallback and kept the wrong layout on _textLayout.
Fixes#21902
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Invalidate the layout when LineSpacing changes
LineSpacing had no case in the property change switch and is not one of
the properties registered with AffectsRender, so changing it left the
measured size and the rendered text untouched even though CreateTextLayout
feeds it into the paragraph properties. It changes line placement rather
than shaping, so it belongs with LineHeight and the other properties that
keep the run cache.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Drop the text layout whenever the content is invalidated
InvalidateMeasure only raises OnMeasureInvalidated while the measure is
still valid, so a second content change before the next measure pass left
_textLayout holding the layout the first change had already replaced.
MeasureOverride keeps that layout when the constraint has not moved, so
the block measured and rendered the superseded content.
Clear the layout in InvalidateTextLayout and InvalidateTextLayoutKeepCache
rather than relying on OnMeasureInvalidated to run, which is what
TextPresenter already does.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Drop the text layout when an embedded control resizes
A line snapshots its metrics when it is formatted, so a layout built
before a child was measured again keeps reporting the width and height
that child used to have. MeasureOverride reuses the layout whenever the
constraint has not moved, so a control that resizes while the block is
already measure invalid never reaches the measured size.
- MeasureEmbeddedControls reports whether any child came back a different
size, and the layout is dropped only then rather than on every pass.
- Route the remaining layout resets through DisposeTextLayout so every
reset goes through one place.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
X11PlatformLifetimeEvents.Dispose() could close the same session-management
connection more than once. Claim the handle with Interlocked.Exchange and
cancel the ICE pump before closing, so it runs exactly once.
Fixes#22188
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* add failing test for ghost items after showing a hidden ListBox
* fix ghost items
* add a failing test
* fix for ghost item
* more tests
* Fix return statement in VirtualizingStackPanel
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
---------
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
* Add failing tests for SAFEARRAY marshalling of COM providers
SafeArrayRef.CreateFromObjects sizes the SAFEARRAY to the pooled buffer it
rents rather than to the input, and only fills a slot when the object already
has a live COM wrapper. Cover both with tests driven through SafeArrayMarshaller,
which is how the UIA interop actually reaches this code.
Two of the three tests fail: a two-element input produces a 16-element array of
nulls. The string test passes and is the control.
Each provider test checks the length before it reads the entries. A wrongly
sized array holds slots that were never written, and dereferencing those takes
down the test host with an access violation.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RYYMWsTkmkKjptFL88xjAg
* Fix ISelectionProvider.GetSelection() never reaching the UIA client
SafeArrayRef.CreateFromObjects had two defects, and between them every
VT_UNKNOWN SAFEARRAY the automation interop produced was empty.
It sized the array to the buffer it rented from ArrayPool rather than to the
input, so a one-element selection became a 16-element array. The rented buffer
isn't cleared, so stale pointers from an earlier rent could leak in and later be
released by SafeArrayDestroy.
It also filled a slot only when ComWrappers.TryGetComInstance succeeded. That
call resolves a managed object that wraps a native COM instance; for a purely
managed object it always fails, so no slot was ever filled and UIA received an
array of nulls.
Wrappers now come from ComInterfaceMarshaller<T>, which is what the rest of the
interop uses. Sharing the instance matters: UI Automation identifies elements by
IUnknown pointer, so a wrapper from a second ComWrappers would read as a
different element. The element type isn't recoverable from the values, so
SafeArrayMarshaller<T> passes it through a new generic TryCreate<T> overload.
The object branch of the non-generic TryCreate is gone with it. Nothing reached
it: the only other caller is ComVariant.Create, and no property value is an
array of providers.
This fixes ISelectionProvider.GetSelection() and, by the same route,
ITableProvider.GetRowHeaders/GetColumnHeaders, ITableItemProvider, and
ITextProvider.GetSelection/GetVisibleRanges.
Fixes#22150
Claude-Session: https://claude.ai/code/session_01RYYMWsTkmkKjptFL88xjAg
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Fix glyph metrics; expose glyph bounding box
GlyphMetrics previously stored the advance in Width/Height, leaving no
field for the actual ink bounding box. Correct the contract:
- Expand GlyphMetrics with AdvanceWidth / AdvanceHeight (and the bitmap /
vertical-layout members XOffset / YOffset / VerticalOriginX/Y, reserved
for later population).
- Add GlyfTable.TryGetGlyphBounds: an allocation-free, header-only read of
the glyph's control-point bounding box (xMin/yMin/xMax/yMax). Empty
glyphs return true with zero bounds; out-of-range / short data return
false. Composite glyphs use their header bbox without recursion.
- TryGetGlyphMetrics (single + batch) now sources advances into
AdvanceWidth / AdvanceHeight and the bounding box into
Width / Height / XBearing / YBearing via TryGetGlyphBounds. Side bearings
fall back to hmtx/vmtx when the glyph has no outline.
The previously latent API had one test relying on the old behaviour
(Width == advance); it's updated to assert AdvanceWidth. New tests cover
TryGetGlyphBounds (range, empty, letter, descriptor parity) and the
corrected metrics (advance placement, ink width distinct from advance,
empty-glyph advance-without-ink, batch/single parity).
* Add glyph-bounds benchmark
GlyphBoundsBenchmark (Avalonia.Benchmarks): a BenchmarkDotNet comparison of
the table-based bounds read against Skia's SKFont.GetGlyphWidths, in kernel
(font pre-created) and per-run variants across 1 / 16 / 256 glyphs, with
MemoryDiagnoser.
* Add allocation-free batch glyph-bounds path
Adds a bounds-only batch reader so ink-bounds computation (the GlyphRunImpl
use case, which already has advances from shaping) doesn't pay for advance
lookup or per-glyph indirection:
- GlyphBounds: internal value type carrying the control-point box.
- GlyfTable.GetGlyphBounds / GlyphTypeface.TryGetGlyphBounds: fetch the glyf
and loca spans once per batch and read offsets + headers directly via
BinaryPrimitives — no per-glyph ReadOnlyMemory.Span conversion, no
intermediate Memory.Slice, no nested call chain. LocaTable exposes RawData
/ IsShortFormat for this.
- Batch TryGetGlyphMetrics refactored onto the same span-cached reader.
Measured against Skia's SKFont.GetGlyphWidths (bounds-only, in-process
BenchmarkDotNet): the table path is faster at every run length (0.06× / 0.32×
/ 0.66× at 1 / 16 / 256 glyphs) and allocation-free, where Skia allocates
104 B/run for the SKFont. The earlier advance+bounds path via
TryGetGlyphMetrics was ~2.2× slower than Skia at 256 glyphs; the bounds-only
span-cached path is ~1.5× faster.
The benchmark is updated to compare bounds-only on both sides.
* Restore System.Buffers.Binary using in GlyfTable
The glyf bounds readers (TryGetGlyphBounds / GetGlyphBounds) read the
header int16s via BinaryPrimitives. The base pr2 commit trimmed this
using as unused there, but it is needed once pr2b adds the bounds code —
without it GlyfTable does not compile on this branch.
* Clamp glyph bounding-box extents to non-negative
GlyphBounds.Width/Height and both TryGetGlyphMetrics overloads computed
the ink extent as (xMax - xMin) / (yMax - yMin). A malformed glyf header
where xMax < xMin (or yMax < yMin) produced a negative value that, when
narrowed to the ushort GlyphMetrics.Width/Height, wrapped to a huge
positive extent instead of a safe zero.
Clamp the extent to non-negative in GlyphBounds (the single source of
truth for both the single and batch metric paths) and route the single
TryGetGlyphMetrics path through GlyphBounds so both behave identically.
For int16 coordinates the maximum extent is 65535 = ushort.MaxValue, so
the clamped value always fits without overflow. Adds GlyphBoundsTests.
* Allocate batch glyph-metric buffers only when their source is present
Batch TryGetGlyphMetrics unconditionally sized the hMetrics, vMetrics and
bounds temporary buffers to glyphIds.Length, even when the font has no
hmtx, no vmtx, or no glyf table. For CFF/CFF2 fonts (no glyf) the bounds
buffer is never read, and most fonts have no vmtx, so a large batch on
such a font heap-allocated a per-glyph array that was pure waste.
Size each buffer to zero — a free, empty stackalloc — when its source
table is absent, so it only materialises (and only spills to the heap
past 256 glyphs) when it will actually be filled and read.
* Note the halved loca offsets in the batch bounds reader
The short loca format stores every offset divided by two, which is why the
batch reader doubles what it reads. LocaTable's single-glyph path already
carries that note; the inline decode in the batch path did not.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Split the batch metrics loop on the glyf branch instead of per glyph
The glyf check does not vary across the run, so it belongs above the loop
rather than inside it. Splitting it also lets the bounds buffer live only
in the branch that reads it, which drops the zero-length allocation that
CFF / CFF2 fonts used to make just to satisfy the shared loop.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Add vertical glyph advance API to GlyphTypeface
Mirror the existing TryGetHorizontalGlyphAdvance / ...Advances pair with
vertical equivalents. Until now a vertical-layout caller (CJK, Mongolian)
had no advance-only path on GlyphTypeface and had to go through
TryGetGlyphMetrics, which after PR2b reads the glyf bounding box per
glyph — pure waste when only the advance is needed.
VerticalMetricsTable already exposes the per-glyph and batch readers
internally (used by TryGetGlyphMetrics(batch)); this is a thin wrapper
over them.
Tests cover the Latin (no vmtx → false) and CJK (vmtx → positive
advances) cases and batch/single parity using Inter and MiSans-Normal.
Audit note: HorizontalMetricsTable and VerticalMetricsTable batch readers
already span-cache (each fetches _data.Span once per call, reusing a
BigEndianBinaryReader across glyphs). No metrics-table refactor is
needed — the speculative Part 2 of the plan collapses to nothing.
* Expose contiguous glyph indices span on ShapedBuffer
Consumers that need a ReadOnlySpan<ushort> of glyph IDs (the upcoming
GlyphTypeface.TryGetGlyphBounds batch path, SKFont.GetGlyphWidths, atlas
lookups...) previously had to allocate a parallel ushort[] and project
GlyphInfo.GlyphIndex into it. Carry that array on the ShapedBuffer instead:
- ShapedBuffer rents a parallel ushort[] from ArrayPool alongside the
existing GlyphInfo[] rental, exposed as
`public ReadOnlySpan<ushort> GlyphIndices`.
- The indexer setter syncs both, so HarfBuzz shaping and post-shape
mutators (InterWordJustification rewrites the same GlyphIndex) keep
the parallel view in lockstep with no caller change.
- Split / WithBidiLevel slice the indices alongside the GlyphInfos at
the same offsets — both share the parent's pooled arrays.
- Dispose returns both rentals.
Consumer wiring:
- GlyphRunImpl: copies ShapedBuffer.GlyphIndices once via CopyTo when
the source is a ShapedBuffer, dropping the per-glyph extract from the
position loop.
- TextFormatterImpl.CreateEmptyTextLine: switched to the pooled public
ctor + indexer (was passing a heap-allocated GlyphInfo[] to the
internal slice ctor, which now requires a parallel ushort slice).
Tests cover indexer sync, overwrite, ascending split alignment,
empty-leading split, and dispose clearing the view.
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>