How can I underline a math symbol independent of its descent and without changing its spacing or script placement?

I am trying to roughly reproduce and maybe even improve upon the behavior of the following LaTeX macro in Typst:

\makeatletter
% knob: multiply font rule thickness by MUL/DIV (integers only)
\newcount\matrixsymbolthickmul \matrixsymbolthickmul=14   % numerator
\newcount\matrixsymbolthickdiv \matrixsymbolthickdiv=10   % denominator
\newcount\matrixsymboloffsetdiv \matrixsymboloffsetdiv=14 % vertical offset = ht(box)/this

% \matrixsymbol@ takes 5 args:
%   #1 = math style token (\displaystyle, \textstyle, \scriptstyle, \scriptscriptstyle)
%   #2 = a math font (e.g. \textfont3, \scriptfont3, \scriptscriptfont3) used to read \fontdimen8
%   #3 = the actual symbol/formula to typeset (usually #1 from \matrixsymbol)
%   #4 = trim (each side) to subtract from underline width (left & right)
%   #5 = extend (total) to add to underline width (centered extension)
\protected\def\matrixsymbol@#1#2#3#4#5{% 
    % Typeset the symbol in the chosen math style and store it in box0
    \setbox0=\hbox{$\m@th#1#3$}
    %
    % Rule thickness: take the font's default rule thickness (fontdimen8) for the chosen font #2, then scale it by thickmul/thickdiv
    \dimen6=\fontdimen8#2\relax
    \multiply\dimen6 by \matrixsymbolthickmul
    \divide\dimen6 by \matrixsymbolthickdiv
    %
    % Vertical placement: fixed below the baseline as a fraction of the symbol box height
    % (distance = ht(box0)/matrixsymboloffsetdiv)
    \dimen8=\ht0 \divide\dimen8 by \matrixsymboloffsetdiv
    %
    % Underline width computation:
    % start with symbol width
    \dimen0=\wd0 
    % add total extension (#5)
    \advance\dimen0 by #5\relax
    % subtract 2 * trim (#4) because trim is per-side
    \dimen2=#4\relax 
    \advance\dimen0 by -2\dimen2
    %
    % Horizontal start shift for the underline relative to the symbol:
    % We want the extension (#5) split equally left/right (so shift left by #5/2), then apply trim (#4) (shift right by trim).
    % net shift = trim - extend/2
    \dimen4=#5\relax \divide\dimen4 by 2 \dimen4=-\dimen4 \advance\dimen4 by \dimen2
    %
    % Output: a box whose visible content is copy0, plus an overlaid underline:
    % - \rlap makes the underline overlay without consuming horizontal space
    % - \smash makes it add no height/depth (so it won't affect vertical spacing)
    % - \raise moves it below the baseline by dimen8
    \hbox{\rlap{\smash{\raise-\dimen8\hbox{\kern\dimen4\vrule width\dimen0 height0pt depth\dimen6}}}\copy0}%
}
\DeclareRobustCommand{\matrixsymbol}[1]{%
    \begingroup
    \let\@nomath\@gobble \mathversion{bold}% make #1 bold by switching to the bold math version
    %
    % \math@atom{#1}{<stuff>} preserves the *atom type* of #1 (ord/op/bin/rel/...)
    % so spacing and script placement behave like the original symbol.
    \math@atom{#1}{%
    \mathchoice
    {\matrixsymbol@{\displaystyle}{\textfont3}{#1}{0.05em}{0.00em}}% 
    {\matrixsymbol@{\textstyle}{\textfont3}{#1}{0.05em}{0.00em}}%
    {\matrixsymbol@{\scriptstyle}{\scriptfont3}{#1}{0.05em}{0.00em}}%
    {\matrixsymbol@{\scriptscriptstyle}{\scriptscriptfont3}{#1}{0.05em}{0.00em}}%
    }%
    \endgroup
}
\makeatother

What this does in LaTeX

  • Typesets the symbol in bold math.
  • Draws a custom underline at a fixed location below the baseline.
  • preserves the original math atom type with \math@atom, so
    • spacing between neighbors is unchanged,
    • subscripts/superscripts attach exactly as if it were just \mathbf{A},
    • stacked scripts behave identically,
    • the underline does not influence vertical spacing at all.

Goal in Typst

I want a makro matr(A) such that:

  • the underline has the same depth below the baseline of the symbol A for all characters, regardless of of their descent, i.e. the underline sits at the same location for a Q as well as an A.
  • if equivalent to the LaTeX version, then the spacing should not be affected at all and the symbol would be typeset as if it were just a normal bold(A)
  • optionally: the resulting symbol is typeset as if it were a letter with the ascent of A and the descent being the distance between the baseline and the underline
  • there is a knob to define the trimming of the underline on both ends
  • another knob, a boolean, to determine if the trimming should adjust to the current math env (normal, script, sscript)
  • thickness of the underline is determined by the thickness of the current math mode (normal, script, sscript)

What I have tried

I attempted something like:

#let matrixsymbol(
  x,
  trim: 0.01em,
  thickness: 0.05em,
  offset_div: 14,
  base_rule: 0.04em,
  class: "normal",
) = context {
  let sym = math.bold(x)
  let m = measure(sym)

  let dy = m.height / offset_div
  let ul_w = m.width - 2 * trim
  let dx = m.width * 0.58
  
  let underline = box(width: 0pt, height: 0pt, baseline: 0pt)[
    #move(
      dx: dx,
      dy: dy,
      rect(
        width: ul_w,
        height: thickness,
        fill: black,
        inset: 0pt,
        outset: 0pt,
        radius: 0pt,
      ),
    )
  ]
  
  math.class(class, underline + sym)
}

#let matr(x, ..args) = matrixsymbol(x, ..args)


$ 
matr(Q)_(matr(Q)_matr(Q))^matr(Q)^matr(Q)
bold(Q)_(bold(Q)_bold(Q))^bold(Q)^bold(Q) 
$
$
matr(A)_(matr(A)_matr(A))^matr(A)^matr(A)
bold(A)_(bold(A)_bold(A))^bold(A)^bold(A)
$
$
matr(q)_(matr(q)_matr(q))^matr(q)^matr(q) 
bold(q)_(bold(q)_bold(q))^bold(q)^bold(q) 
$

Question

Is it possible in Typst to:

  1. Decorate a math symbol (e.g. with a custom underline)
  2. While keeping its original math atom type and script attachment behavior
  3. And without changing its layout metrics?

I would appreciate guidance on whether this is achievable pure typst, or fundamentally requires engine adjustments.

Thanks!

Could you provide a few examples of source code and image of rendered output?

I thought the maybe the code could be run in the forum entry, but I don’t see the option to configure that.
Unfortunately, uploading images is not possible for me at the moment, it says “Sorry, new users can not upload attachments.”

But there are some examples in the Typst code I posted…

And here’s a MWE in LaTeX for the same examples:

\documentclass{article}
\usepackage{amsmath} % \boldsymbol


% this one prints the symbol as if it were a bold symbol
\makeatletter
% knob: multiply font rule thickness by MUL/DIV (integers only)
\newcount\matrixsymbolthickmul \matrixsymbolthickmul=14   % numerator
\newcount\matrixsymbolthickdiv \matrixsymbolthickdiv=10   % denominator
\newcount\matrixsymboloffsetdiv \matrixsymboloffsetdiv=14 % vertical offset = ht(box)/this

% \matrixsymbol@ takes 5 args:
%   #1 = math style token (\displaystyle, \textstyle, \scriptstyle, \scriptscriptstyle)
%   #2 = a math font (e.g. \textfont3, \scriptfont3, \scriptscriptfont3) used to read \fontdimen8
%   #3 = the actual symbol/formula to typeset (usually #1 from \matrixsymbol)
%   #4 = trim (each side) to subtract from underline width (left & right)
%   #5 = extend (total) to add to underline width (centered extension)
\protected\def\matrixsymbol@#1#2#3#4#5{% 
    % Typeset the symbol in the chosen math style and store it in box0
    \setbox0=\hbox{$\m@th#1#3$}
    %
    % Rule thickness: take the font's default rule thickness (fontdimen8) for the chosen font #2, then scale it by thickmul/thickdiv
    \dimen6=\fontdimen8#2\relax
    \multiply\dimen6 by \matrixsymbolthickmul
    \divide\dimen6 by \matrixsymbolthickdiv
    %
    % Vertical placement: fixed below the baseline as a fraction of the symbol box height
    % (distance = ht(box0)/matrixsymboloffsetdiv)
    \dimen8=\ht0 \divide\dimen8 by \matrixsymboloffsetdiv
    %
    % Underline width computation:
    % start with symbol width
    \dimen0=\wd0 
    % add total extension (#5)
    \advance\dimen0 by #5\relax
    % subtract 2 * trim (#4) because trim is per-side
    \dimen2=#4\relax 
    \advance\dimen0 by -2\dimen2
    %
    % Horizontal start shift for the underline relative to the symbol:
    % We want the extension (#5) split equally left/right (so shift left by #5/2), then apply trim (#4) (shift right by trim).
    % net shift = trim - extend/2
    \dimen4=#5\relax \divide\dimen4 by 2 \dimen4=-\dimen4 \advance\dimen4 by \dimen2
    %
    % Output: a box whose visible content is copy0, plus an overlaid underline:
    % - \rlap makes the underline overlay without consuming horizontal space
    % - \smash makes it add no height/depth (so it won't affect vertical spacing)
    % - \raise moves it below the baseline by dimen8
    \hbox{\rlap{\smash{\raise-\dimen8\hbox{\kern\dimen4\vrule width\dimen0 height0pt depth\dimen6}}}\copy0}%
}
\DeclareRobustCommand{\matrixsymbol}[1]{%
    \begingroup
    \let\@nomath\@gobble \mathversion{bold}% make #1 bold by switching to the bold math version
    %
    % \math@atom{#1}{<stuff>} preserves the *atom type* of #1 (ord/op/bin/rel/...)
    % so spacing and script placement behave like the original symbol.
    \math@atom{#1}{%
    \mathchoice
    {\matrixsymbol@{\displaystyle}{\textfont3}{#1}{0.05em}{0.00em}}% 
    {\matrixsymbol@{\textstyle}{\textfont3}{#1}{0.05em}{0.00em}}%
    {\matrixsymbol@{\scriptstyle}{\scriptfont3}{#1}{0.05em}{0.00em}}%
    {\matrixsymbol@{\scriptscriptstyle}{\scriptscriptfont3}{#1}{0.05em}{0.00em}}%
    }%
    \endgroup
}
\makeatother

\newcommand{\matr}[1]{\matrixsymbol{#1}}
\begin{document}



\[
\matr{Q}_{\matr{Q}_{\matr{Q}}}^{\matr{Q}^{\matr{Q}}}
\boldsymbol{Q}_{\boldsymbol{Q}_{\boldsymbol{Q}}}^{\boldsymbol{Q}^{\boldsymbol{Q}}}
\]
\[
\matr{A}_{\matr{A}_{\matr{A}}}^{\matr{A}^{\matr{A}}}
\boldsymbol{A}_{\boldsymbol{A}_{\boldsymbol{A}}}^{\boldsymbol{A}^{\boldsymbol{A}}}
\]
\[
\matr{q}_{\matr{q}_{\matr{q}}}^{\matr{q}^{\matr{q}}}
\boldsymbol{q}_{\boldsymbol{q}_{\boldsymbol{q}}}^{\boldsymbol{q}^{\boldsymbol{q}}}
\]



\end{document}

This is the output of your code:

2 Likes