Font substitution and missing text
I recently found a bug in my work-in-progress-next-major-version of Counterparts Lite where text in certain languages was not showing up in the text editor. The problem ended up being related to font substitution. If characters from certain languages aren’t showing up in your Cocoa text views, here’s something to check…
The first thing to understand is that no font contains all the characters defined in Unicode. When the text system encounters characters that aren’t representable in the current font, it chooses another font that has the correct glyphs to display those characters.
Every time you change some part of a NSTextStorage
object in Cocoa, its processEditing
method is called to optimize attribute ranges and fix things that could be incoherent (such as a paragraph style spanning only part of a paragraph). This is also where font substitution happens.
You can observe what processEditing
is doing easily by setting a delegate on the text storage implementing the willProcessEditing
and didProcessEditing
methods. After changing the text, printing the text storage’s content in willProcessEditing
will result in this single text section with uniform attributes:
Hello 漢字{
NSColor = "Catalog color: System textColor";
NSFont = "\"Helvetica 12.00 pt. P [] (0x7f85d9f60070)
fobj=0x7f85d9f4c320, spc=3.33\"";
}
And then in didProcessEditing
you will see it was split in two text sections with different font attributes:
Hello {
NSColor = "Catalog color: System textColor";
NSFont = "\"Helvetica 12.00 pt. P [] (0x7fbdc15a25c0)
fobj=0x7fbdbec6a0d0, spc=3.33\"";
}漢字{
NSColor = "Catalog color: System textColor";
NSFont = "\"HiraginoSans-W3 12.00 pt. P [] (0x7fbdc15b2330)
fobj=0x7fbdc15adbe0, spc=4.00\"";
}
The font was changed for the characters not representable in Helvetica to ensure they can still be shown. Font substitution at work!
Perhaps however our app wants to control the font in the text view in order to replace whatever style could come from the user pasting some text. Those delegate methods sure seem like a good place to do that, and they are.
It is important to know however that there’s a crucial difference between changing the font in willProcessEdting
and in didProcessEditing
:
willProcessEditing
is called before font substitution. Any font you set while inwillProcessEditing
will get substituted with an appropriate font for characters not available in the font you choose.didProcessEditing
is called after font substitution. If you change the font while indidProcessEditing
there will be no further font substitution, and thus you should make sure the font you set can display all the characters.
In summary, if you have to change the font after the text storage was edited, do it in willProcessEditing
and it’ll do the right thing. Don’t do it in didProcessEditing
.
It’s also important to note that you can change the font in the text storage without manipulating the text storage directly (such as in textView.font = myFont
). The same rule apply: do it in willProcessEditing
because if you do it from didProcessEditing
no font substitution will occur and you can end up with characters not showing up.