Sunday, December 26, 2010

\vspace* is broken

As LaTeX tutorials are quick to point out, \vspace{x} can disappear at the top or bottom of pages. This is because each \vspace expands to two \vskips which are discardable. Thus, if you write \vspace{3in} near the bottom of the page, the next paragraph will start at the top of the next page.

Of course, if the goal is to start writing the text below the top of the page, then \vspace will not work. The solution, or so say the tutorials, is to use \vspace* instead. This, we're led to believe will always insert that much space. Want to start writing 1 inch from the top margin? No problem, use \vspace*{1in}. The trouble is, that's not quite true. It turns out that TeX has a parameter called \topskip which is related to the amount of glue TeX puts at the top of the page. It's goal is to make the baseline of the first box on the main vertical list of each page start at the same place.

Consider the first line of a page consisted of a single x and the first line of the next page consisted of a single X. We'd like the first line of text on each page to line up, even though they have different heights. So that's exactly what TeX does. If h is the height of the first line of text on the page and t is the value of \topskip, then TeX inserts max {t - h, 0} glue before the first box on the page.

So what does that have to do with \vspace*? The answer lies in how LaTeX implements \vspace*. It is implemented as a 0 height \hrule (because \hrule is not discardable), a penalty to prevent page breaks, and then two \vskips (the second is for 0 pt, presumably so that \unskip doesn't remove it). There is also some code to deal with \vspace* in horizontal mode as well as some bookkeeping involving \prevdepth that isn't important here. Because it starts off with an \hrule, TeX inserts \topskip glue. In practice, this means that \vspace*{0pt} will add vertical space at the top of a page. This is undesirable. TeX by Topic suggests that one use \hbox{}\kern-\topskip when one desires material start at the very top of the page. Obviously, this solution is not workable for \vspace* since it will insert -\topskip space every time. A better solution is to save \topskip, set it equal to 0 pt, perform the space and then restore it. Something like the following.
\begingroup
\topskip0pt
\vspace*{3in}%
\endgroup
Here, we're using TeX's grouping facility to keep the assignment local to the group. A better way is to define a new macro, \Vspace, that performs all of the relevant code. Most of this is copied from the definition of \@vspacer—a helper macro for \vspace*.
\newskip\Vspace@topskip
\providecommand\Vspace[1]{%
        \Vspace@topskip\topskip
        \topskip\z@skip
        \ifvmode
                \dimen@\prevdepth
                \hrule\@height\z@
                \nobreak
                \vskip\glueexpr#1\relax
                \vskip\z@skip
                \prevdepth\dimen@
        \else
                \@bsphack
                \vadjust{%
                        \@restorepar
                        \hrule\@height\z@
                        \nobreak
                        \vskip#1%
                        \vskip\z@skip
                }%
                \@esphack
        \fi
        \topskip\Vspace@topskip
}
This macro can be used wherever \vspace* was used and it will always give exactly the space in the argument. Of course, this might not always be desired. In particular, if you want to start 4 lines down from the top of the page, then \vspace*{4\baselineskip} will do exactly that and keep all lines on various pages aligned. Using \Vspace to get the same space would require \Vspace{4\baselineskip + \topskip}.

No comments:

Post a Comment