Help me understand behaviour when redefining \textbullet

What’s going on here is that the definition of \textbullet looks several things up at the point of use, so trying to save the symbol with \let fails.

The Fix

If you’re compiling in a modern engine such as LuaLaTeX or XeLaTeX, the libertinus package loads fontspec, unicode-math, and the Unicode versions of the text and math fonts.

These fonts support the Unicode symbols ⚫, •, ∙, etc. It even has unicode-math define the macros \vysmblkcircle, \smblkcircle, \mdblkcircle, etc. for them. The Libertinus fonts have some but not all of these, so if you want to redefine \textbullet as the larger one, you want to first select a font that does contain it. I chose DejaVu Sans.

If you’re compiling in a traditional TeX engine, such as PDFLaTeX, you want to define \oldbullet as slot "88 of the TS1 encoding (essentially duplicating the old definition of \textbullet in textcomp, rather than saving the more complicated command in the modern LaTeX kernel). If you’re going to be using a font that doesn’t come with a TS1 extension, you’d want to define \textbullet to select a font family and encoding along with the font size. Since you’re using libertinus and no other font packages, that’s not a problem here.

However, you don’t want to select \large size all the time, even in, for example, a footnote. What you more likely want is for the symbol to be one size larger than the current font. You can accomplish this with relsize.

A sample:

\tracinglostchars=2 % Print a warning to the console if a character is missing.
\documentclass[11pt]{report}
\usepackage{iftex}
\usepackage{libertinus}

\ifTUTeX
% The libertinus package loaded fontspec and unicode-math, with Libertinus
% Math as the math font.
  \newfontfamily\symbolfont{DejaVu Sans}[Scale=MatchLowercase]
  \DeclareTextSymbol{\oldbullet}{\UnicodeEncodingName}{"2022}
  \DeclareTextSymbolDefault{\oldbullet}{\UnicodeEncodingName}
  \renewcommand\textbullet{{\symbolfont\symbol{"26AB}}}
\else
% The libertinus package loaded the legacy Type 1 font.
  \usepackage[T1]{fontenc}
  \usepackage[libertine]{newtxmath}
  \usepackage{relsize}
  \DeclareTextSymbol{\oldbullet}{TS1}{"88}
  \DeclareTextSymbolDefault{\oldbullet}{TS1}
  \renewcommand\textbullet{{\larger\oldbullet}}
\fi

\begin{document}
\oldbullet\textbullet$\mathord\bullet$
\end{document}

If you also want U+2022 to be equivalent to \textbullet in text mode, and \bullet in math mode, you would add something like:

\usepackage{newunicodechar}
\newunicodechar{^^^^2022}{\ifmmode\bullet\else\textbullet\fi}

What’s Going on?

The LaTeX kernel declares \textbullet in one of three different encodings: the legacy symbol from the TS1 8-bit encoding, the even-older legacy symbol from the OMS 7-bit encoding, and the Unicode symbol. This means the command for \textbullet doesn’t know ahead of time which font or encoding it will be using, and trying to save the definition with \let and then redefine \textbullet won’t work as expected.

The historical reason for this was that the bullet operator was first defined as a math symbol in the ’80s. In 1996, Jörg Knappen defined a text-symbol encoding, TS1, as a companion for the new T1 Cork encoding. However, the majority of fonts available at the time supported less than half of TS1. As a bug workaround, for the next twenty years, LaTeX authors would load the textcomp package, which would attempt to detect the symbols the current font supported and fall back to poor-man’s versions if necessary.

There is no longer any need to load a separate package: this code is now in the LaTeX kernel.

The definition is documented in §20.4.1 of the LaTeX2e sources. The relevant part is that \DeclareTextCommand expands to code that checks the current encoding and whether we are in math mode. This refers to the \?\textbullet defined by \DeclareTextCommandDefault, which eventually expands in a recursive way.


This happens because you just define the top-level \oldbullet, and it expands to \?-cmd \textbullet \?\textbullet, and that eventually expands to your redefined \textbullet which is \large\oldbullet.

As mentioned in the comment, this is basically the same issue as with robust commands: they are not one single macro, but a set of macros under similar name as the top-level macro, and when you redefine one, you have to redefine them all, otherwise you get an unwanted behaviour (this, however, isn't covered by letltxmacro).

To do what you want you can, instead of copying the definition of \textbullet to a new name, you can declare \oldbullet the same way as \textbullet is declared by LaTeX:

\DeclareTextSymbol{\oldbullet}{TS1}{136}
\DeclareTextSymbolDefault{\oldbullet}{TS1}

The first line means that the command \oldbullet will take the character number 136 from the current font in encoding TS1, and the second line says that the default encoding for \oldbullet is TS1, so LaTeX changes the encoding if necessary to use that symbol (otherwise the symbol would always use the current encoding, no matter which it is).

Then you can redefine \textbullet normally:

\documentclass[11pt]{report}
\usepackage{libertinus}

% Resize \textbullet to make it a bit larger (closer to Computer Modern)
\DeclareTextSymbol{\oldbullet}{TS1}{136}
\DeclareTextSymbolDefault{\oldbullet}{TS1}
\renewcommand*\textbullet{{\LARGE\oldbullet}}

\begin{document}
\oldbullet\textbullet$\mathord\bullet$

\indent\kern1pt\raise5pt\hbox{$\smile$}
\end{document}

enter image description here

Tags:

Macros