[Tex/LaTex] Centering contents after \unvbox

boxeshorizontal alignmenttex-core

Trying to build a complex macro, I managed to compute what I need in a vbox.
Now, I want to \unvbox this box to allow page breaks, and would like each element it contains to be centered.

I can't find a way to add some glue on the left and right of each element. Is it at all possible? Any alternative?

Edit: typical boxes I'd like to handle are of this kind:

\vbox{
  \halign{\hfil#&\hfil#\hfil&#\hfil\cr
  aa & b & cc\cr
  d & eee & f\cr}
}

(I don't really want to modify them because I need to access their \wd at some point).

Best Answer

First method

This code is Plain TeX, but can be used also in LaTeX; the macro \centervbox takes as argument a box register number and outputs the same box, but centered with respect to the margins.

\catcode`\@=11
\def\centervbox#1{\begingroup\global\setbox\@ne=\box\voidb@x
  \setbox\z@=\copy#1\relax\docentervbox}
\def\docentervbox{%
  \setbox\z@=\vbox{\unvbox\z@ \setbox\tw@=\lastbox
    \skip@=\lastskip\unskip
    \global\setbox\@ne=\vbox{
      \vskip\skip@
      \hbox to\hsize{\hfil\box\tw@\hfil}
      \unvbox\@ne}}%
  \ifdim\ht\z@=\z@
    \def\next{\unvbox\@ne\endgroup}%
  \else
    \let\next\docentervbox
  \fi
  \next}
\catcode`\@=12

\vsize=5\baselineskip

\setbox4=\vbox{
  \halign{\hfil#&\hfil#\hfil&#\hfil\cr
  aa & b & cc\cr
  aa & b & cc\cr
  aa & b & cc\cr
  aa & b & cc\cr
  aa & b & cc\cr
  aa & b & cc\cr
  d & eee & f\cr}
}

\centervbox{4}

\bye

In the example I've used box register 4, but if you say \newbox\mybox and then

\setbox\mybox=\vbox{...}

then you can say \centervbox\mybox. The box register remains untouched (if it's not box register 1, but using this would be a very bad thing).

Comments to the first method
We assume that there is already a box built as a \vbox in a box register, that is something like \setbox\mybox=\vbox{...} and that this box contains only hboxes and glue between them. The procedure is to copy this box into a scratch box (number 0); we reserve also box register 1, on which we'll act always globally, first of all ensuring it's void. Then we start the recursion: at each step we retrieve the bottom hbox from box 0 and transfer it into box 1; we also transfer the interline glue. However, the retrieved box is put inside a box as wide as \hsize and centered in it.

The recursion ends when the height of box 0 is zero. At that point we \unvbox box register 1.

Second method

An alternative way to do the same is to typeset twice the alignment, first gathering its natural width and then with the desired preamble:

\newdimen\alignlen \newtoks\preamble
\long\def\centeralign#1#2{\par\preamble{#1}%
  \setbox0=\vbox{\tabskip=0pt\halign{&##\cr#2\crcr}}%
  \alignlen=\wd0
  \begingroup\tabskip=0pt plus 1fil
  \halign to\hsize{\tabskip=0pt \the\preamble\tabskip=0pt plus 1fil\cr
    #2\crcr}\endgroup}

\bigskip

\centeralign{\hfil#&\hfil#\hfil&#\hfil}
 {aa & b & cc\cr
  aa & b & cc\cr
  aa & b & cc\cr
  aa & b & cc\cr
  aa & b & cc\cr
  aa & b & cc\cr
  d & eee & f\cr}

This requires a slightly different syntax, but not so complicated. First the alignment is typeset in a box with a standard preamble, nothing more than "print each entry". The length register \alignlen is loaded with the width. Then the alignment is typeset with infinite glue on each side, with the desired preamble. Since the \halign is outside any box, it can be split across pages. This method has the advantage that \noalign commands can be put after \cr, stating penalties and vertical skips.

Comments to the second method
If the box to center is an \halign we can use a different method. First of all we typeset the \halign inside a box with a trivial preamble &#\cr means "any number of columns with left alignment"; this will give the possibility to measure the natural width of the alignment, as requested.

Then we typeset the alignment with the requested preamble, but with infinitely stretchable \tabskip glue at both sides. The preamble had been saved in the token register \preamble, we simply augment it by adding \tabskip=0pt which will hold for all intercolumn spacing, at the start and the infinite glue specification for the spacing at the right.

Third method

It's possible to carry in the newly formed box also kerns and penalties, in addition to skips and boxes. No other items can come along.

\catcode`\@=11
\newif\ifboxended
\def\centervbox#1{\begingroup\global\setbox\@ne=\box\voidb@x
  \global\boxendedfalse
  \setbox\z@=\copy#1\relax\docentervbox}
\def\docentervbox{%
  \setbox\z@=\vbox{\unvbox\z@ 
    \ifcase\lastnodetype
% char node (can't remove)
    \or
% hlist node
    \setbox\tw@=\lastbox
    \def\next{%
      \global\setbox\@ne=\vbox{
        \hbox to\hsize{\hfil\box\tw@\hfil}\unvbox\@ne}}%
    \or
% vlist node
    \setbox\tw@=\lastbox
    \def\next{%
      \global\setbox\@ne=\vbox{
        \hbox to\hsize{\hfil\box\tw@\hfil}\unvbox\@ne}}%
    \or
% rule node (can't remove)
    \or
% ins node (can't remove)
    \or
% mark node (can't remove)
    \or
% adjust node (can't remove)
    \or
% ligature node (can't happen)
    \or
% disc node (can't happen)
    \or
% whatsit node (can't remove)
    \or
% math node (can't remove)
    \or
% glue node
    \skip@=\lastskip\unskip
    \def\next{\global\setbox\@ne=\vbox{\vskip\skip@\unvbox\@ne}}%
    \or
% kern node
    \dimen@=\lastkern\unkern
    \def\next{\global\setbox\@ne=\vbox{\kern\dimen@\unvbox\@ne}}%
    \or
% penalty node
    \count@=\lastpenalty\unpenalty
    \def\next{\global\setbox\@ne=\vbox{\penalty\count@\unvbox\@ne}}%
    \or
% unset node (can't happen)
    \or
% math mode node (can't remove)
    \else
% empty list
    \def\next{\global\boxendedtrue}
    \fi
    \next}
  \ifboxended
    \def\next{\unvbox\@ne\endgroup}
  \else
    \let\next\docentervbox
  \fi
  \next}
\catcode`\@=12

The list of node types can be found in the documentation of e-TeX (texdoc etex). The special case -1 is when the list is empty, which we use to end the recursion.