Emacs: note on mixed font heights

In a recent entry on configuring mixed fonts, I outlined how to specify your typefaces of choice by configuring the default, variable-pitch, and fixed-pitch faces. This would allow you to benefit from variegated typography, such as having paragraph text rendered in a proportionately spaced font, while inline code is displayed as monospaced.

The overall approach of controlling the three basic faces is fine, but the code I shared had the unintended consequence of breaking the built-in text-scale-adjust command (by default bound to C-x C-+, C-x C--, C-x C-0). Here I issue a corrective to the technique that was used before.

The code that breaks ‘text-scale-adjust’

This is the gist of what I was using for several months:

(set-face-attribute 'default nil :font "Hack-16")
(set-face-attribute 'fixed-pitch nil :font "Hack-16")
(set-face-attribute 'variable-pitch nil :font "FiraGO-16")

A variant of the above can be expressed as follows:

(set-face-attribute 'default nil :family "Hack" :height 160)
(set-face-attribute 'fixed-pitch nil :family "Hack" :height 160)
(set-face-attribute 'variable-pitch nil :family "FiraGO" :height 160)

If you set fonts this way and try to use text-scale-adjust in a buffer with mixed fonts, you will notice that only the main text, affected by the default face, gets scaled. The rest retain their height—not good.

This is because of a hard-wired assumption in the text-scale-adjust command to only target the default face: variable-pitch and fixed-pitch remain in tact, thus breaking our expectations.

The problem consists in the fact that we are specifying an absolute size for each font family. Whereas we should be benefiting from relative sizes that all have a single point of reference, which is easy to do.

The recommended way to set font heights with faces

Let us re-purpose the sample code from the previous section, in order to get the behaviour we expect out of text-scale-adjust.

(set-face-attribute 'default nil :font "Hack-16")
(set-face-attribute 'fixed-pitch nil :family "Hack" :height 1.0)
(set-face-attribute 'variable-pitch nil :family "FiraGO" :height 1.0)

A alternative to the above is this:

(set-face-attribute 'default nil :family "Hack" :height 160)
(set-face-attribute 'fixed-pitch nil :family "Hack")
(set-face-attribute 'variable-pitch nil :family "FiraGO")

Notice that we set an absolute point size only for the default face. While we instruct Emacs to interpret the height of fixed-pitch and variable-pitch as relative to that constant. Therein lies the difference between integer and floating point values for the :height attribute (remember to consult C-h f set-face-attribute).

Strictly speaking, the :height 1.0 is not necessary, unless you are overriding a prior state. It is what applies when the specification is omitted. Rendering it explicit here helps us spot the subtleties in notation and be clear about what is at play.

Details are tricky

I was using the old technique for several months, adjusting fonts through a bespoke function of mine that altered their absolute sizes. What inspired me to investigate and eventually address this issue is a particular statement in the doc string of set-face-attribute:

Note that for the ‘default’ face, you must specify an absolute height (since there is nothing for it to be relative to).

Which implied that if the default was a constant, all other faces could simply have a relative height. This is because of the peculiar nature of that face to serve as the foundation upon which all others are established. As such, a :height with a floating point is a multiple of the default font size. Simple and effective!

I am now happily using text-scale-adjust in tandem with the tools I mentioned in my recent video about “Focused editing” for Emacs.

This information is also documented in the official manual of the Modus themes because they are designed to cope well with mixed font scenaria, such as when the user decides to enable the built-in variable-pitch-mode.