Fixes#1664
I previously asserted that we got exactly one run from CoreText because
I assumed that our run iterator was perfectly splitting runs for
CoreText. This assumption appears to be false and that seems okay.
The test case in this commit produces two runs that are directly next to
each other and there's no downside to simply iterating over them. So
this commit changes to iterate over the runs.
Up to this point, every font I've experienced with ligatures has
replaced the codepoints that were replaced for combining with a space.
For example, if a font has a ligature for "!=" to turn it into a glyph,
it'd shape to `[not equal glyph, space]`, so it'd still take up two
cells, allowing us to style both.
Monaspace, however, does not do this. It turns "!=" into `[not equal
glyph]` so styles like backgrounds, underlines, etc. were not extending.
This commit detects multi-cell glyphs and inserts synthetic blank cells
so that styling returns. I decided to do this via synthetic blank cells
instead of introducing a `cell_width` to the shaper result because this
simplifies the renderers to assume each shaper cell is one cell. We can
change this later if we need to.
Annoyingly, this does make the shaper slightly slower for EVERYONE to
accomodate one known font that behaves this way. I haven't benchmarked
it but my belief is that the performance impact will be negligible
because to figure out cell width we're only accessing subsequent cells
so they're likely to be in the CPU cache and also 99% of cells are going
to be width 1.
Fixes#845
Quick background: Emoji codepoints are either default text or default
graphical ("Emoji") presentation. An example of a default text emoji
is ❤. You have to add VS16 to this emoji to get: ❤️. Some font are
default graphical and require VS15 to force text.
A font face can only advertise text vs emoji presentation for the entire
font face. Some font faces (i.e. Cozette) include both text glyphs and
emoji glyphs, but since they can only advertise as one, advertise as
"text".
As a result, if a user types an emoji such as 👽, it will fallback to
another font to try to find a font that satisfies the "graphical"
presentation requirement. But Cozette supports 👽, its just advertised
as "text"!
Normally, this behavior is what you want. However, if a user explicitly
requests their font-family to be a font that contains a mix of test and
emoji, they _probably_ want those emoji to be used regardless of default
presentation. This is similar to a rich text editor (like TextEdit on
Mac): if you explicitly select "Cozette" as your font, the alien emoji
shows up using the text-based Cozette glyph.
This commit changes our presentation handling behavior to do the
following:
* If no explicit variation selector (VS15/VS16) is specified,
any matching codepoint in an explicitly loaded font (i.e. via
`font-family`) will be used.
* If an explicit variation selector is specified or our explicitly
loaded fonts don't contain the codepoint, fallback fonts will be
searched but require an exact match on presentation.
* If no fallback is found with an exact match, any font with any
presentation can match the codepoint.
This commit should generally not change the behavior of Emoji or VS15/16
handling for almost all users. The only users impacted by this commit
are specifically users who are using fonts with a mix of emoji and text.