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.