2

I use the algpseudocode package to write code and I would like to draw boxes around some lines of the code. This answer gets pretty close to what I want. The idea is to mark the first and last line of code with \tikzmark and then draw a box around the obtained coordinates. The problem with this approach is that it only works for lines starting with \State. For example when I use \tikzmark after an if-statement, it does not mark the beginning of the line but some position farther right.

Here is an example:

\documentclass{article}
\usepackage{algpseudocode} 
\usepackage{tikz}
\usetikzlibrary{fit,tikzmark}

\newcommand\drawCodeBox[2]{%
    \begin{tikzpicture}[remember picture,overlay]
        \coordinate (start) at ([yshift=1.7ex]pic cs:#1);
        \coordinate (end) at ([yshift=-0.3ex]pic cs:#2);
        \node[inner sep=2pt,draw=red,fit=(start) (end)] {};
    \end{tikzpicture}%
}

\begin{document}
\begin{algorithmic}
    \State \tikzmark{s1}%
    $x \gets 0$\tikzmark{e1}
    \drawCodeBox{s1}{e1}
    \State $y \gets 1$
    \If{$x = y$}\tikzmark{s2}
        \State PANIC
    \EndIf\tikzmark{e2}
    \drawCodeBox{s2}{e2}
\end{algorithmic}
\end{document}

This is what I get:

What I get

The desired result is:

What I want

2 Answers2

3

To stick to your \drawCodeBox macro, you'll have to position the \tikzmarks at the appropriate location. We can move \tikzmark{e2} to the right by placing it inside a \makebox[0pt][r] and explicitly stacking it (horizontally) with exactly what \EndIf sets (\algorithmicend\ \algorithmicif).

enter image description here

\documentclass{article}

\usepackage{algpseudocode,tikz}

\usetikzlibrary{fit,tikzmark}

\newcommand\drawCodeBox[2]{%
  \begin{tikzpicture}[remember picture,overlay]
    \coordinate (start) at ([yshift=1.7ex]pic cs:#1);
    \coordinate (end) at ([yshift=-0.3ex]pic cs:#2);
    \node[inner sep=2pt,draw=red,fit=(start) (end)] {};
  \end{tikzpicture}%
}

\begin{document}

\begin{algorithmic}
  \State \tikzmark{s1}%
  $x \gets 0$\tikzmark{e1}
  \drawCodeBox{s1}{e1}
  \State $y \gets 1$
  \If{$x = y$}\tikzmark{s2}
    \State PANIC
  \EndIf\makebox[0pt][r]{\tikzmark{e2}\phantom{\algorithmicend\ \algorithmicif}}
  \drawCodeBox{s2}{e2}
\end{algorithmic}

\end{document}
Werner
  • 603,163
  • That's great, thanks! Is there a more generic way to get the length of the current line than looking up \algorithmicend\ \algorithmicif etc.? Ideally, one could then create a macro \tikzmarkLeft that automatically positions the mark at the left of the current line. – Christian Matt Mar 15 '17 at 21:30
  • @ChristianMatt: You could probably leave a \tikzmark{lmargin} against the left margin, and then calculate the difference between a placed \tikzmarkLeft{here} and lmargin using some details in Extract x, y coordinate of an arbitrary point in TikZ. Alternative, use the page coordinates. I'm too unfamiliar with tikz to know how to do that... – Werner Mar 15 '17 at 21:42
  • ...perhaps a better option would be to define a \tikzmarkEndIf{<mark>} macro that sets a mark just before setting \algorithmicend. That way you don't have to know anything about the current indentation. – Werner Mar 15 '17 at 21:44
1

Based on Werner's comment on his answer, I have defined \BoxedState, \BoxedIf, \BoxedWhile, etc., which set a mark at the beginning of the line and draw a box. The optional argument sets the style of the box. The command \EndBox sets the lower end of the box, \SetBoxEast can be used at the end of the longest line in the box to set the right end. If the last line is the longest one, \SetBoxEast can be ommited.

The counter tmkcount is used to keep the marks unique. The box is drawn before its contents, so it can be filled. Limitations include:

  • The box does not adapt to very high first lines.
  • It is assumed that no line within the box start further left than the first one.

Code and example:

\documentclass{article}
\usepackage{algpseudocode,tikz}
\usetikzlibrary{fit,tikzmark}

\newcounter{tmkcount}
\tikzset{%
    tikzmark suffix={-\thetmkcount},%
    defaultCodeBox/.style={draw=red}%
}

\newcommand{\drawCodeBox}[4]{%
    \begin{tikzpicture}[remember picture,overlay]
        \coordinate (start) at ([yshift=1.4ex]pic cs:#2);
        \coordinate (middle) at (pic cs:#3);
        \coordinate (end) at ([yshift=-0.2ex]pic cs:#4);
        \node[inner sep=2pt,#1,fit=(start) (middle) (end)] {};
    \end{tikzpicture}%
}

\newcommand{\BeginBox}[1]{%
    \drawCodeBox{#1}{beginCB}{middleCB}{endCB}%
    \tikzmark{beginCB}\tikzmark{middleCB}%
}

\newcommand{\SetBoxEast}{%
    \unskip%
    \tikzmark{middleCB}%
}

\newcommand{\EndBox}{%
    \unskip%
    \tikzmark{endCB}%
    \stepcounter{tmkcount}%
}

\newcommand{\BoxedState}[1][defaultCodeBox]{\State\BeginBox{#1}\ignorespaces}

\algdef{S}[WHILE]{BoxedWhile}[2][defaultCodeBox]{\BeginBox{#1}\algorithmicwhile\ #2\ \algorithmicdo}%
\algdef{S}[FOR]{BoxedFor}[2][defaultCodeBox]{\BeginBox{#1}\algorithmicfor\ #2\ \algorithmicdo}%
\algdef{S}[FOR]{BoxedForAll}[2][defaultCodeBox]{\BeginBox{#1}\algorithmicforall\ #2\ \algorithmicdo}%
\algdef{S}[LOOP]{BoxedLoop}[1][defaultCodeBox]{\BeginBox{#1}\algorithmicloop}%
\algdef{S}[REPEAT]{BoxedRepeat}[1][defaultCodeBox]{\BeginBox{#1}\algorithmicrepeat}%
\algdef{S}[IF]{BoxedIf}[2][defaultCodeBox]{\BeginBox{#1}\algorithmicif\ #2\ \algorithmicthen}%
\algdef{C}[IF]{IF}{BoxedElsIf}[2][defaultCodeBox]{\BeginBox{#1}\algorithmicelse\ \algorithmicif\ #2\ \algorithmicthen}%
\algdef{Ce}[ELSE]{IF}{BoxedElse}{EndIf}[1][defaultCodeBox]{\BeginBox{#1}\algorithmicelse}%


\begin{document}
\begin{algorithmic}
    \BoxedState $x \gets 0$
    \State $y \gets x + 1$\EndBox
    \If{$x < y$}
        \State $x \gets x + 1$
    \BoxedElse[fill=yellow]
        \State do some complicated stuff\SetBoxEast
        \State $y \gets y + 1$\EndBox
    \EndIf
\end{algorithmic}
\end{document}

Result