Quote boxes in Dialog

Dialog can’t do the classic Z-machine trick that Trinity used to make quote boxes. So why not do it a different way?

Used like this:

(quote 2)
	Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
	(draw quote box [
		{All that is gold does not glitter,}
		{Not all those who wander are lost,}
		{The old that is strong does not wither,}
		{Deep roots are not touched by the frost.}
		{\(J. R. R. Tolkien\)}
	] with final indent 7)
	Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

You pass it a list of closures for the individual lines, and (optionally) an amount to indent the last line. Note: you can’t use any of the spacing predicates inside the closures, which is why that indent is necessary. Dialog handles the rest of it automatically: measuring the lines, measuring the window, choosing a size for the box, and so on.

quotebox.dg (3.0 KB)

Give it a shot, see if it works for you. I’ll put it on Bitbucket or Github eventually if people find it useful. (We’ve been meaning to make a sort of Public Library of extensions…)

4 Likes

So how does this work?

First, we need a way to measure the length of a line of text. This means we need to know how Dialog’s built-in spacing tools work. There are certain characters that inhibit space on their left or right:

(suppresses space left @./@,/@:/@;/@!/@?/@\)/@\]/@\}/@>/@\%/@-)
(suppresses space right @\(/@\[/@\{/@</@-)

All the @ signs are a bit unreadable, but this is saying . , : ; ! ? ) ] } > % - inhibit space on their left, and ( [ { < - inhibit space on their right.

Otherwise, there’s an automatic space between each word. So this predicate succeeds once for each pair of words that doesn’t have space inhibited between them.

(space between words in [$First $Second | $])
	~(suppresses space right $First)
	~(suppresses space left $Second)
(space between words in [$ | $Rest])
	*(space between words in $Rest)

The first rule looks at a pair of words, and succeeds if the space between them should not be suppressed. The second rule recurses through the list.

The words themselves are easier:

(word $Word has $Length characters)
	(split word $Word into $List)
	(length of $List into $Length)
What's that length-measuring predicate?

(length of $ into $) is from the standard library. I included my own copy here, because I’m not using the full library when testing it. Classic recursion:

(length of [] into 0)
(length of [$|$Rest] into $N)
	(length of $Rest into $Nm1)
	($Nm1 plus 1 into $N)

Now we can put these together.

(number of characters in $Text is $Total)
	(collect words)
		(query $Text)
	(into $Words)
	(accumulate $Length)
		*($Word is one of $Words)
		(word $Word has $Length characters)
	(into $WordLength)
	(accumulate 1)
		*(space between words in $Words)
	(into $SpaceLength)
	($WordLength plus $SpaceLength into $Total)

The (collect words) turns the closure into a list of words, and the two (accumulate $) blocks count the characters needed for words and for inter-word spaces. Then we just add them up.

2 Likes

How do we actually print the box, though?

(draw quote box $Lines with final indent $Final)
	(collect $Length)
		*($Line is one of $Lines)
		(number of characters in $Line is $Length)
	(into $Lengths)
	(maximum of $Lengths is $MaxLength)
	(if) (current div width $TotalWidth) (then)
		($TotalWidth minus $MaxLength into $Excess)
		($Excess divided by 2 into $MarginTmp)
		($MarginTmp minus 2 into $Margin) %% Two characters padding
	(else) %% Fallback if not provided
		($Margin = 2)
	(endif)
	(div @quotebox) {
		(exhaust) *(draw lines $Lines with widths $Lengths into box $MaxLength with margin $Margin final $Final)
	}
(draw quote box $Lines)
	(draw quote box $Lines with final indent 0)

First, we calculate the length of each line of text, and store them in the list $Lengths. (We’re going to be keeping this around so we don’t have to do the calculation again, because it’s rather expensive, but we could also just recompute it later when we need it.)

We take the maximum of those lengths, $MaxLength.

We take the screen width, subtract the max length, divide by 2, and subtract 2. This is how much whitespace we’re going to leave on either side of the quote box. (That “subtract 2” is for padding on the inside of the box.)

Then, we make a @quotebox div, and draw all the lines inside it.

What’s a @quotebox, you ask?

(style class @quotebox)
	font-family: monospace;
	margin-top: 2em;
	margin-bottom: 2em;

It’s a block of monospace text with two lines of empty space above and below it.

Now, how do we draw each line?

(draw single line $Line with width $Width into box $Box with margin $Margin plus $Extra)
	(space $Margin) %% Left margin
	($Box minus $Width into $PadTotal)
	($PadTotal divided by 2 into $LeftPaddingRaw)
	($PadTotal modulo 2 into $LeftPaddingExtra) %% In case there's an extra character
	($LeftPaddingRaw plus $Extra into $LeftPadding) %% The final row needs to be offset
	($LeftPaddingRaw minus $Extra into $RightPadding)
	(span @quoteline) {
		(space 2) %% Automatic two chars padding
		(space $LeftPadding) %% The amount on both sides
		(space $LeftPaddingExtra) %% And the remainder
		(query $Line)
		(space $RightPadding) %% And now the right side
		(space 2)
	}

This function takes a lot of parameters: the line $Line, the line width $Width, the box size $Box, the outer margin $Margin, and the offset $Extra to shift by. Passing values around is easier than computing them every time we need them! And Dialog isn’t very fond of global variables compared to Inform.

So first, we print $Margin blank spaces. Then we calculate how much padding to put on the inside of the box: $Box minus $Width, divided by two, with any remainder from the division put on the left. If $Extra is provided, add it to the left and remove it from the right.

The line itself goes in a @quoteline, along with the padding on each side. What is a @quoteline?

(style class @quoteline)
	font-decoration: reverse;

It’s just a reverse-video span.

Finally, we need to do this for each line. Since we’re iterating over two lists at once (texts and lengths), recursion works better than iteration.

(draw lines [$Line|$LRest] with widths [$Width|$WRest] into box $Box with margin $Margin final $Final)
	(draw single line $Line with width $Width into box $Box with margin $Margin plus 0)
	(if) ($LRest = [$LLast]) ($WRest = [$WLast]) (then) %% Final one
		(line)
		(draw single line $LLast with width $WLast into box $Box with margin $Margin plus $Final)
	(else)
		(line)
		*(draw lines $LRest with widths $WRest into box $Box with margin $Margin final $Final)
	(endif)

And one little tidbit that’s not in the library but should be:

(maximum of [$Single] is $Single)
(maximum of [$Head|$Rest] is $Max)
	(maximum of $Rest is $MaxRest)
	(if) ($Head > $MaxRest) (then)
		($Max = $Head)
	(else)
		($Max = $MaxRest)
	(endif)
2 Likes

And we’re done!

Well, not quite done. This is a quick alpha-test thrown together in an hour. It has no way of handling various edge cases, currently:

  • What if a quote is only one line long?
  • What if a line is longer than the screen is wide?
  • What if the final line plus the offset is too much for the box?
  • What if this is done on the web?

But I think it works well enough for people to try out. Let me know what you think, and if you run into any issues!

1 Like

I didn’t know Dialog had that kind of technical detail with line width. Thanks for the post, great resource!

1 Like

It mostly doesn’t! (current div width $) was introduced by me last year so I could center the automaps for Wise-Woman’s Dog. Now I’m using a bunch of hacks to measure the width of the lines, because there’s no built-in way to get the number of characters in a text. I would love to add one, and it would be easy on Z-machine and web…but any new feature also needs to work (or fail gracefully) on 6502, and my assembly skills aren’t up to snuff.

So for now, we measure the word lengths in user-facing Dialog code, and estimate the spaces.

2 Likes

But I think it works well enough for people to try out. Let me know what you think, and if you run into any issues!

This is the kind of thing that makes solving the “repository of Dialog libraries” problem front-of-mind. It’s a great thing to have, and a whole bunch of people would use it, but we don’t want to bloat stdlib.dg. Having an easy place for folks to find stuff like this would help.

For that matter, should we split the choice mode out of stdlib.dg? A lot of games don’t use it, and for the uses that I’d think of making for it, I’d want to make some changes to it.

Yeah, it’s a good time to think about that! Personally, I think the best approach is a new repository under the Dialog-IF organization that anyone can contribute to, and then a small number of libraries that are packaged with the official distribution in a lib/ directory. Things like stddebug.dg and unit.dg (and potentially choices.dg) could go in lib/, while things like this could go in the more general repository.

The problem with breaking things out, though, is there’s currently no way for a Dialog file to indicate what libraries it depends on. If we break choice mode out into its own library, for example, how will The Impossible Stairs indicate that it requires it?

So I think we need to figure out some architectural issues first before we start compiling things.

1 Like

I’m not convinced that this is as problematic as you’re suggesting. Dialog is already built around an assumption that the user may want to supply a modified version of the standard library and there’s no way to indicate that either. But Dialog isn’t supposed to be an npm-like system where you can just type the name of a game and have it build automatically with all of the correct dependencies. You can just write “building this game requires version 1.3 of choicelib.dg”.