17

On the left (obtained from the MWE below), I do not use boxes for the title nor the body text and the title and the text appears to be in the correct spot.

However, when I attempt to use \newsaveboxs (MWE with \def\UseBoxes{} uncommented) I obtain the results on the right.

enter image description here enter image description here

Questions:

  • What are this additional skip's being inserted and how do I get both to exhibit identical behavior?
  • In my actual use case I can tweak the addition of the \struts to get correct behavior, but that also seems to need a conditional based on if the body text is more than one line. My understanding is that I should only need a \strut at the end of the body text, but that does not seem to work here.

Notes:

  • A vertical space defined by \DesiredSkipAboveTitle is added before the title and a \DesiredSkipBelowTitle is added after the title.
  • Need to run at least twice to get the tikz drawing in correct position.
  • The adjustbox package can be used to move the boxes into what I think is the correct position. This can be seen with both \def\UseBoxes{} and \def\UseAdjustBox{} uncommented. From this we see that the \TitleBox needs a raise=-1.3pt option and the \BodyBox requires a raise=-13.3pt with the align=t. I would like to know where these amounts come from.

Code:

\def\UseBoxes{}% Uncomment to use boxes
%\def\UseAdjustBox{}% Uncomment to use \adjustbox (with added fudge factors)
%% --------------------
\documentclass{article}
\usepackage{tikz}
\usepackage{tikzpagenodes}
\usepackage[export]{adjustbox}

\usepackage[paperwidth=7.0cm,showframe]{geometry}

\newcommand*{\DesiredSkipAboveTitle}{5pt}
\newcommand*{\DesiredSkipBelowTitle}{10pt}

\newcommand*{\NumberOfTitleLines}{2}
\newcommand*{\Title}{A Title that Takes Up Two Lines}
\newsavebox{\TitleBox}
\newcommand*{\SetupTitleBox}[2]{%
    \setbox#1\vbox{%
        \bfseries\centering%
        #2%
        %\strut%
        \par%
    }%
}

\newcommand*{\BodyTextLarge}{%
    Lorem ipsum dolor sit amet, consectetur adipiscing elit. 
    Integer id nisl porta, porttitor tellus sed, sollicitudin erat. 
    Aliquam tellus urna, feugiat et tortor quis, aliquam mattis orci.% 
}%
\newcommand*{\BodyTextSmall}{%
    Small Text.% 
}%

\newcommand{\SelectedBodyText}{\BodyTextLarge}
%\newcommand{\SelectedBodyText}{\BodyTextSmall}


\newsavebox{\BodyBox}
\newcommand{\SetupBodyBox}[2]{%
    \setbox#1\vbox{%
        \noindent#2%
        %\strut%
    }%
}%

\newlength{\AdditionalSkip}
\newcommand*{\ShowTextGuideLines}[1]{%
\begin{tikzpicture}[remember picture, overlay]
    \coordinate (X) at (current page text area.north west);

    \draw [draw=red, fill=yellow, fill opacity=0.2] 
        (X) circle (1pt);%% DEBUGGING: Ensure (X) is the correct spot.

    \setlength{\AdditionalSkip}{\DesiredSkipAboveTitle}%
    \foreach \X in {1, ..., #1} {%
        \ifnum\X>\NumberOfTitleLines
            %% After title: need to adjust \AdditionalSkip for space after title
            \setlength{\AdditionalSkip}{%
                \dimexpr\DesiredSkipAboveTitle+\DesiredSkipBelowTitle\relax%
             }%
        \fi
        \draw [thin, red] ([yshift=-\X\baselineskip-\AdditionalSkip]X) -- ++ (\hsize,0);
    }%

    \tikzset{Node Style/.style={anchor=south, draw=red, inner sep=1pt}}
    \ifdefined\UseBoxes
        \node [Node Style, fill=yellow] at ([xshift=0.5\hsize]X) {using boxes};
    \else
        \node [Node Style, fill=green] at ([xshift=0.5\hsize]X) {Not using boxes};
    \fi
\end{tikzpicture}%
}%


\begin{document}
    %% ---------------------------------------------------------- Set up the title and body
    \SetupTitleBox{\TitleBox}{\Title}%
    \SetupBodyBox{\BodyBox}{\SelectedBodyText}%
    %% ---------------------------------------------------------- Title
    \hbox{}\kern-\topskip%% See https://tex.stackexchange.com/questions/7676/why-does-vspace0pt-add-vertical-space#comment12433_7681
    \vspace*{\DesiredSkipAboveTitle}%
    %%
    \ifdefined\UseBoxes
        \ifdefined\UseAdjustBox% Following tweaks seem to be close
            \noindent\adjustbox{valign=t,raise=-1.3pt}{\usebox{\TitleBox}}%
            \vspace*{\DesiredSkipBelowTitle}%
            \par\noindent\adjustbox{valign=t,raise=-13.3pt}{\usebox{\BodyBox}}%
        \else
            \noindent\usebox{\TitleBox}%
            \vspace*{\DesiredSkipBelowTitle}%
            \par\noindent\usebox{\BodyBox}%
        \fi
    \else
        \noindent{\bfseries\centering\Title\par}%
        \vspace*{\DesiredSkipBelowTitle}%
        \par\noindent\SelectedBodyText%
    \fi
    \noindent\ShowTextGuideLines{7}%
\end{document}
  • 2
    you used vbox rather than vtop (not that either of them are really latex) – David Carlisle Dec 3 '18 at 20:41
  • @DavidCarlisle: Using \vtop instead of \vbox seems to resolve the issue with the \TitleBox, but not the \BodyBox? – Peter Grill Dec 3 '18 at 20:57
  • 2
    I didn't actually run the code, I'll look in a bit:-) but getting spacing around boxes right is tricky you need to control the first and last baseline but boxes only have one reference point, hence latex3 coffins, boxes with handles. – David Carlisle Dec 3 '18 at 20:58
16
+600

I think it would probably be useful to understand how TeX normally inserts vertical space between lines/boxes. This will make it clear why this spacing is different when you wrap your paragraphs in a \vbox, as in your MWE. A method to achieve identical spacing with and without boxes can be found at the bottom this answer.

About \baselineskip and \lineskip

TeX normally tries to insert an appropriate amount of space between lines so that subsequent baselines are separated by \baselineskip. If it fails to do so without causing the distance between the boxes containing these lines to fall below \lineskiplimit it'll insert \lineskip instead. Since \lineskiplimit=0pt by default, this happens precisely when the boxes would otherwise overlap.

Because \baselineskip=12pt, and \lineskiplimit=0pt by default for a 10pt document, this means that if the distance between consecutive baselines is 12pt if the sum of the depth of the top line and the height of the bottom line is less than 12pt. Otherwise, the (boxes containing these) lines will be separated by \lineskip (which is 1pt by default).

Here's an illustration:

\parskip=0pt \parindent=0pt
An ordinary line of text
\par
Another ordinary line of text

\vspace{\baselineskip}

\rule[-5pt]{10pt}{15pt}This line has a depth of \texttt{5pt}
\par
\rule[-5pt]{10pt}{15pt}This line has a height of \texttt{10pt}

iillutration

Note that if you manually add additional skips, these are just added to whatever amount of space would otherwise be inserted:

\parskip=0pt \parindent=0pt
An ordinary line of text
\par\vspace{2mm}
Another ordinary line of text

\vspace{\baselineskip}

\rule[-5pt]{10pt}{15pt}This line has a depth of \texttt{5pt}
\par\vspace{2mm}
\rule[-5pt]{10pt}{15pt}This line has a height of \texttt{10pt}

with vspace

(Remark: normally, these lines would be separated by an additional \parskip, but I've set this length to zero.)

The same thing, but with vertical boxes

It is no different for your \vboxes. A \vbox containing multiple lines has just a single baseline, which coincides with the baseline of the bottom line of its contents (top line for a \vtop). If TeX can't separate these baselines by exactly \baselineskip without the boxes overlapping, it will completely ignore \baselineskip, put a vertical space of \lineskip between these boxes and call it a day.

This is the result: (green lines indicate the baselines)

\parskip=0pt \parindent=0pt
\vbox{The first vbox containing\\two lines}
\par
\vbox{The first vbox containing\\two lines}

vboxes

So the distance between the baseline of the second line of the first box and the baseline of the first line of the second box is equal to the depth of the former line plus the height of the latter line plus \lineskip. In this case I think that's 0pt + 6.94444pt + 1pt = 7.94444pt (the second number is how tall the letters h, d and b are). Adding a \strut or some character with descenders (gjpqy) to the first \vbox would increase this distance by increasing the depth of the last line of this box.

The same thing happens between consecutive \vtops:

\parskip=0pt \parindent=0pt
\vtop{The first vtop containing\\two lines}
\par
\vtop{The first vtop containing\\two lines}

vtops

The only situation that likely matches your expected/desired outcome is the following:

\parskip=0pt \parindent=0pt
\vbox{A vbox containing\\two lines}
\par
\vtop{A vtop containing\\two lines}

vbox and vtop## Heading ##

This happens because the baselines are close enough together that they can be separated by exactly \baselineskip without the boxes overlapping. The space above this \vbox and below this \vtop is again going to be "wrong", however.

(Remark: \vboxes inserted in vertical mode are not preceded by a \parskip. If you do want this skip you should either insert it manually (using \vspace{\parskip}) or use \noindent to switch to horizontal mode before inserting the box.)

The top of the page

Something similar happens at the top of the page. TeX tries to insert a (positive) skip that wil separate the baseline of the first box on a page from the top of the text area by \topskip. If it can't, it'll just place the box directly at the top. The default value of \topskip is equal to the font size (so 10pt for a 10pt document).

This is why it matters whether you put a \vtop or a \vbox at the top of your page.

\documentclass{article}
\usepackage[showframe]{geometry}
\begin{document}
  \parskip=0pt \parindent=0pt
  \vtop{A vtop containing\\two lines}
  \clearpage
  \vbox{A vbox containing\\two lines}
\end{document}

top vtop top vbox

In your MWE \topskip isn't really relevant because you've inserted \hbox{}\kern-\topskip. The \hbox creates an empty box that will be separated from the top of the text area by the full \topskip, and the subsequent \kern undoes this skip. It is thus as if there is an invisible paragraph at the top of your document that ends where the text area begins, so TeX tries to separate the first box (after this) from the top of the text area by \baselineskip and insert \lineskip if it can't, exactly as described above.



A solution

To get the correct spacing above a vertical box you should use \vtop instead of \vbox for the reasons outlined above. To get the right amount of spacing below a \vtop you can set its depth equal to the depth of the last line and then insert a vertical skip equal to the distance between the first and the last baseline.

The command \insertvbox, which I define below, is intended to replace \usebox in your MWE (except it should not be preceded by \noindent). It wraps the contents of a \vbox (or \vtop or \vcenter, it makes no difference) you supply in both a \vtop and a \vbox, gives the \vtop the depth of the \vbox, prints this \vtop and inserts a vertical space to compensate for the decrease in depth.

Note: Because I'm using \unvcopy, code inside the box is only executed once. The double boxing problem pointed out by jfbu in this comment therefore does not occur.

\makeatletter
\newcommand*\insertvbox[1]{%    %% <- #1 can be a vbox, vtop or vcenter
\begingroup                     %% <- limits scope of assignments
  \setbox0=\vtop{\unvcopy#1}%   %% <- wrap in a vtop
  \setbox2=\vbox{\unvcopy#1}%   %% <- wrap in a vbox
  \@tempdima=\dimexpr\dp0-\dp2\relax %% <- calculate missing depth
  \dp0=\dp2                     %% <- use depth of box2
  \par\box0                     %% <- use the vtop
  \vspace*{\@tempdima}%         %% <- insert missing depth as space
\endgroup
}
\makeatother

This command should not be preceded by \noindent (which switches to horizontal mode) because \insertvbox was intended to insert this box in vertical mode. (The \par in its definition actually switches back to vertical mode.) If it is for some reason important that the box be inserted in horizontal mode (perhaps because you want a non-zero \parskip to be inserted), then you can replace \par\box0 by \par\noindent\box0 in the definition. (I'm afraid I don't know why the lineno package does not like the version with \noindent though.)

Example

The following example produces two identical pages: the first was constructed with vertical boxes, and the second without.

\documentclass{article}

\usepackage{showframe}     %% <- show boundary of text area
\usepackage{tikz}          %% <- To draw the red brackets
\usepackage{tikzpagenodes} %% <- to place them correctly

\makeatletter
\newcommand*\insertvbox[1]{%
\begingroup
  \setbox0=\vtop{\unvcopy#1}%   %% <- box1 is a vtop
  \setbox2=\vbox{\unvcopy#1}%   %% <- box2 is a vbox
  \@tempdima=\dimexpr\dp0-\dp2\relax %% <- calculate missing depth
  \dp0=\dp2                     %% <- use depth of box2
  \par\box0                     %% <- use the vtop
  \vspace*{\@tempdima}%         %% <- insert missing depth as space
\endgroup
}
\makeatother

\parskip=0pt   %% <- default value is "0pt plus 1pt"

\newcommand\redbracket{% %% <- for the little red brackets in the left margin
  \begin{tikzpicture}[remember picture, overlay,]
      \draw[red] (current page text area.north west) -- +(-2pt,0)
                   -- node[left]{\texttt{\detokenize{\topskip}}} +(-2pt,-\topskip) -- +(0,-\topskip);
      \foreach \X in {0, ..., 7} {
        \draw[red] ([yshift=-\topskip-\X\baselineskip]current page text area.north west) -- +(-2pt,0)
                   -- node[left]{\texttt{\detokenize{\baselineskip}}} +(-2pt,-\baselineskip) -- +(0,-\baselineskip);
      }
  \end{tikzpicture}%
}

\begin{document}

\newsavebox{\TitleBox}
\newsavebox{\BodyBox}
\setbox\TitleBox=\vtop{\noindent\textbf{A title that takes\\up more than\\two lines}}
\setbox\BodyBox=\vtop{\noindent Lorem\\ipsum}

\insertvbox\TitleBox

\insertvbox\BodyBox

\insertvbox\BodyBox

\insertvbox\BodyBox

\redbracket


\clearpage

\noindent\textbf{A title that takes\\up more than\\two lines}

\noindent Lorem\\ipsum \par\noindent Lorem\\ipsum \par\noindent Lorem\\ipsum

\redbracket

\end{document}

output

Note that I've set \parskip=0pt because the default value of \parskip is actually 0pt plus 1pt. This means that the amount of space between paragraphs could be stretched to up to 1pt to improve the page layout.

In your MWE

The spacing in your MWE will be correct if you replace \noindent\usebox by \insertvbox (and add the above definition to your preamble, of course):

\def\UseBoxes{}% Uncomment to use boxes
%% --------------------
\documentclass{article}
\usepackage{tikz}
\usepackage{tikzpagenodes}

\usepackage[paperwidth=7.0cm,showframe]{geometry}

\newcommand*{\DesiredSkipAboveTitle}{5pt}
\newcommand*{\DesiredSkipBelowTitle}{10pt}

\newcommand*{\NumberOfTitleLines}{2}
\newcommand*{\Title}{A Title that Takes Up Two Lines}
\newsavebox{\TitleBox}
\newcommand*{\SetupTitleBox}[2]{%
    \setbox#1\vtop{%
        \bfseries\centering%
        #2%
        %\strut%
        \par%
    }%
}

\newcommand*{\BodyTextLarge}{%
    Lorem ipsum dolor sit amet, consectetur adipiscing elit. 
    Integer id nisl porta, porttitor tellus sed, sollicitudin erat. 
    Aliquam tellus urna, feugiat et tortor quis, aliquam mattis orci.% 
}%
\newcommand*{\BodyTextSmall}{%
    Small Text.% 
}%

\newcommand{\SelectedBodyText}{\BodyTextLarge}
%\newcommand{\SelectedBodyText}{\BodyTextSmall}


\newsavebox{\BodyBox}
\newcommand{\SetupBodyBox}[2]{%
    \setbox#1\vtop{%
        \noindent#2%
    }%
}%

\makeatletter
\newcommand*\insertvbox[1]{%
\begingroup
  \setbox0=\vtop{\unvcopy#1}%   %% <- wrap in a vtop
  \setbox2=\vbox{\unvcopy#1}%   %% <- wrap in a vbox
  \@tempdima=\dimexpr\dp0-\dp2\relax %% <- calculate missing depth
  \dp0=\dp2                     %% <- use depth of box2
  \par\box0                     %% <- use the vtop
  \vspace*{\@tempdima}%         %% <- insert missing depth as space
\endgroup
}
\makeatother

\newlength{\AdditionalSkip}
\newcommand*{\ShowTextGuideLines}[1]{%
\begin{tikzpicture}[remember picture, overlay]
    \coordinate (X) at (current page text area.north west);

    \draw [draw=red, fill=yellow, fill opacity=0.2] 
        (X) circle (1pt);%% DEBUGGING: Ensure (X) is the correct spot.

    \setlength{\AdditionalSkip}{\DesiredSkipAboveTitle}%
    \foreach \X in {1, ..., #1} {%
        \ifnum\X>\NumberOfTitleLines
            %% After title: need to adjust \AdditionalSkip for space after title
            \setlength{\AdditionalSkip}{%
                \dimexpr\DesiredSkipAboveTitle+\DesiredSkipBelowTitle\relax%
             }%
        \fi
        \draw [thin, red] ([yshift=-\X\baselineskip-\AdditionalSkip]X) -- ++ (\hsize,0);
    }%

    \tikzset{Node Style/.style={anchor=south, draw=red, inner sep=1pt}}
    \ifdefined\UseBoxes
        \node [Node Style, fill=yellow] at ([xshift=0.5\hsize]X) {using boxes};
    \else
        \node [Node Style, fill=green] at ([xshift=0.5\hsize]X) {Not using boxes};
    \fi
\end{tikzpicture}%
}%


\begin{document}
    %% ---------------------------------------------------------- Set up the title and body
    \SetupTitleBox{\TitleBox}{\Title}%
    \SetupBodyBox{\BodyBox}{\SelectedBodyText}%
    %% ---------------------------------------------------------- Title
    \hbox{}\kern-\topskip%% See https://tex.stackexchange.com/questions/7676/why-does-vspace0pt-add-vertical-space#comment12433_7681
    \vspace*{\DesiredSkipAboveTitle}%
    %%
    \ifdefined\UseBoxes
        \insertvbox{\TitleBox}%
        \vspace*{\DesiredSkipBelowTitle}%
        \insertvbox{\BodyBox}%
    \else
        \noindent{\bfseries\centering\Title\par}%
        \vspace*{\DesiredSkipBelowTitle}%
        \par\noindent\SelectedBodyText%
    \fi
    \noindent\ShowTextGuideLines{7}%
\end{document}

output for MWE

Note that if you want the distance between consecutive lines to be exactly \baselineskip even if this would cause these lines to overlap, you can set \lineskiplimit=-\maxdimen (either inside your \vboxes or globally).


Old solution

The macro \copyboxwithappropriatespace, which I define below (and which could use a better name), does roughly the same thing as \insertvbox. It however makes some assumptions to calculate the right depth to use and the right space to insert. It assumes that (1) it is provided a \vtop, (2) that the distance between the first and the last baseline inside the box is an integer multiple of \baselinekip and (3) that the depth of the last line is less than \baselineskip. This means that the amount of space to be inserted is equal to greatest integer multiple of \baselineskip that is less than the depth of your box, and that that the depth of the box should be equal to the remainder (as suggested by jfbu in the comments).

\makeatletter                %% <-- make @ usable in command sequences
%% Inserts a box of depth 0 and insert an appropriate vspace:
\newcommand*\copyboxwithappropriatespace[1]{%
  \begingroup
    \@tempdima=\dp#1\relax   %% <-- store the depth in \@tempdima
    \count@=\@tempdima\relax %% <-- convert to a number
    \divide\count@ by \baselineskip\relax %% <-- divide by \baselineskip
    \dp#1=\dimexpr\@tempdima-\count@\baselineskip\relax %% <-- set the depth to to the remainder
    \par\copy#1%                 %% <-- insert the box
    \dp#1=\@tempdima\relax   %% <-- reset its depth
    \vspace*{\count@\baselineskip}%
  \endgroup                  %% ^-- insert \count@ \baselineskips
}
\makeatother                 %% <-- revert @
  • You're welcome, and thank you for your generosity. The second bounty wasn't necessary. – Circumscribe Dec 10 '18 at 7:39
  • +1 but I believe there is an issue in \copyboxwithappropriatespace: it should not "insert multiple of \baselineskip closest to \@tempdima" but "insert largest multiple of \baselineskip less than \@tempdima". And not set the \dp of the inserted box to zero, but to the remainder of \@tempdima (i.e. the actual depth of the \vtop box) after removing "largest multiple of \baselineskip". To obtain that largest integer you can do \count@\@tempdima then \divide\count@ by \baselineskip. Then \count@ has this largest integer and it times \baselineskip is to be removed. – user4686 Dec 10 '18 at 8:53
  • @jfbu: You're right. The definition of \copyboxwithappropriatespace isn't perfect and was really only intended for this specific use case. If your boxes contain just normal lines of text, then it doesn't really matter whether you round down or to the nearest integer and this missed depth isn't very relevant, but in general it would be better to do precisely what you say. (It is annoying that \dimexpr doesn't just round down btw…) – Circumscribe Dec 10 '18 at 12:13
  • I only just now thought of a better way to accomplish the same thing: you can create both a \vbox and a \vtop and then insert a \vtop with the depth of the \vbox and a \vspace* equal to the difference between their depths! This would also work if the distance between the baselines inside the box varies, (My answer is getting a little long though…) – Circumscribe Dec 10 '18 at 12:14
  • @Circumscribe it may cause problems to have two boxing operations, typically with LaTeX counters which will get stepped twice. – user4686 Dec 10 '18 at 12:20

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.