Thursday, January 07, 2021

Fallback Fonts in XAML

I have to admit that I feel foolish for not knowing about this before. Let me tell you about the power of fallback fonts in XAML so you can avoid the ignorance I lived in for so long. I'm doing this in UWP code but the same applies to WPF.

Let's say you want the following displayed (with XAML) as simply as possible.

I [filled heart] You [smiley face] [red lips] [music notes]

The "I" and the "You" are rendered with the "Segoe Script" font.

The "filled heart", "smiley face", and "music notes" are from the "Segoe MDL2 Assets" font.

The "red lips" are the "kiss mark" emoji.

Off the top of your head, how do you render this?

You might put multiple TextBlocks next to each other (probably in a Horizontally-oriented StackPanel) and assign different FontFamily values to each one.

Or, you might use multiple Runs within a single TextBlock and assign different FontFamily values to each one.

But, let me tell you there's a way to do this with a single TextBlock and no runs!

The XAML looks like this:

<TextBlock>I &#xEB52; You &#xED54; 💋 &#xE189;</TextBlock>

To get the script font I set the FontFamily to "Segoe Script" and I get this:

I [unrendered character] You [unrendered character] [red lips] [unrendered character]
Note that the red lips / kiss mark are rendered as a glyph even though they are not in the "Segoe Script" font. This is due to a built-in feature of XAML text rendering. The underlying text rendering system is doing a lot of work to try and avoid try and display all the characters. When the specified character isn't in the defined font it tries to use a value from the `ContentControlThemeFontFamily` resource. This is a list of fonts that it looks in for something to display the appropriate glyph. By default, this includes "Segoe UI" and "Segoe UI Emoji". It's the second of these that means that emojis are rendered in most places without any extra effort needed.

So, we're part of the way there by specifying a single FontFamily.

FontFamily="Segoe Script"

But what about the glyphs from the "Segoe MDL2 Assets" font?
Well, just as in CSS it's possible to specify multiple fonts/typefaces to use as a fallback, the same is possible with XAML.
Why I never thought of this before I don't know.

FontFamily="Segoe Script,Segoe MDL2 Assets"

In my head, I always thought of the HTML/CSS specification of multiple fonts as applying all or nothing to the text. If the first specified font is available use that for all the text, if it's not available use the next font, etc.

I don't know if HTML/CSS rendering is cleverer than that (please comment below, if you know) but the XAML text renderer is smarter than just using the same font for everything. It uses the specified list (family) of fonts on a character by character basis.

If it can't render a character in the first defined font, it will look to the next in the family and if none provide the character it will look to the ContentControlThemeFontFamily resource. Only if none of these fonts have a glyph for the character do you get the placeholder for a missing glyph.

This is a simplified explanation as the real solution considered different languages too. Don't ever think displaying text is simple. ;)

I discovered all this after going down a rabbit hole investigating how to avoid characters not displaying. I started by looking at intercepting what is displayed on the screen and then checking all the characters there are in the font family specified. (I didn't even consider multiple font-family values.) That lead me to investigate how to detect binding changes which led me down a side path looking at how the `x:bind` markup extension creates and reflects binding changes.

I wanted to know when any text in an app isn't rendered correctly so that the app could be updated to support the fonts/glyphs it needed so users can always see the characters they expect.

Now knowing about fallback fonts, I think a simpler solution is to make sure I include multiple fonts in the default FontFamily used within apps.

1 comment:

I get a lot of comment spam :( - moderation may take a while.