# Print Layout Templates

**Status**: Implemented

**Author**: Toluwaleke Ogundipe

**Reviewers**: Federico Mena Quintero, Jonathan Blandford

## Goals

- Specify a format for defining reusable print layout templates.

## Rationale

Different puzzle kinds require slightly different layouts when printed on paper.
All of them require the puzzle grid and the clues, but for example,
in arrowword puzzles, the clues are embedded in the grid and they do not
require a list of clues.

## Overall Approach

Print layout templates are reusable, declarative specifications that define
how different elements of a puzzle are arranged and rendered on a printed page.

At its core, a template describes a layout applicable to a set of puzzle
kind(s), breaking down the printable area into a hierarchy of elements, each
with a distinct role, such as displaying the grid, clues, title, metadata, or
other visual components. Thanks to the flexibility of the layout engine, the
same template may be used for multiple page sizes.

These templates provide a consistent and flexible way to support various puzzle
kinds and page sizes.

### The Top-Level

At the top-level, a TEMPLATE contains some metadata, the main page, and an
optional overflow page with space for additional clues. Each page is typically
a nested set of ELEMENTS.

Templates are defined in the JSON format as follows.

:::{important}
- The code blocks herein are not valid JSON but descriptions of the expected
  format.
- Field/Member names are case-sensitive.
- Unknown/Unexpected fields are ignored.
:::

```js
{
  // Applicable puzzle kinds (case-insensitive).
  "puzzle_kinds": [ String, ... ],

  // The font description with which to render clues
  "clue_font": Font,

  // The font description with which to render page footers
  "footer_font": Font,

  // The main page.
  "main_page": Box,

  // Optional: An overflow page for additional clues.
  "overflow": Box
}
```

Where:

- `clue_font` is optional when the template contains no CLUES elements.
- `Font` is a string in the format accepted by
  [`pango_font_description_from_string()`](
  https://docs.gtk.org/Pango/type_func.FontDescription.from_string.html),
  but the font size is required and must be in points.

- `Box` is a container of elements and has the following format:

  ```js
    {
      // The orientation of the box (case-insensitive); one of:
      //
      // - horizontal
      // - vertical
      "orientation": String
  
      // Sub-elements of the box.
      "elements": [ Element, ... ]
    }
  ```
  
  - `Element` defines a template element.
  - must contain at least one non-DIVIDER element.
  
  :::{note}
  When dealing with a BOX:
  
  - the axis of orientation is called the MAJOR axis.
  - the other axis is called the MINOR axis.
  :::

### Template Elements

There are two basic categories of elements:

1. DISPLAY elements: These might contain text, a grid, or something else,
   or even nothing at all (to insert white space between other elements).
2. CONTAINER elements: These are used to arrange other elements.

Primarily, each element has a KIND and may include kind-specific data to
customize its look/behavior, e.g font size/styles for text, or clue flow
information for multi-region clue layouts. Additionally, it may also include
data for sizing within a container.

#### Element Dimension Kinds

Each dimension (width or height) of an element can be one of the following
kinds (or be computed in one of the following ways):

- **INTRINSIC**: This dimension can be computed from just the element data.
- **DERIVED**: Given the other dimension (width), this dimension (height)
  can be computed from the element data.

  :::{important}
  No element may have a DERIVED width. Otherwise, it could result in a
  situation where a CONTAINER element has both DERIVED width and height,
  in which case it would be imposible to compute either.

  Thankfully, no element of puzzle printing naturally has a DERIVED width.
  :::

- **WEIGHTED**: This dimension cannot be computed from the element data;
  it is computed by the element's parent box using the element's ratio.

Every DISPLAY element kind has a pre-defined dimension kind along each axis,
but the dimension kind of a container element on either axis depends on
the dimension kinds of the elements it contains. The dimension kinds for each
element kind are defined later on.

#### Element Format

An element has the following format:

```js
{
  // The element kind (case-insensitive).
  "kind": String,

  // Optional: Relative ratio of the element within its parent box;
  // Positive.
  "ratio": Float,

  // Optional: The minimum dimension of the element along its parent box'
  // major axis, in millimeters;
  // Positive.
  "minimum": Float,

  // Optional: The maximum dimension of the element along its parent box'
  // major axis, in millimeters;
  // Positive.
  "maximum": Float,

  // Optional: Whether to omit the element if its minimum dimension is not
  // satisfied.
  "cut_off": Boolean,

  // Optional: Whether the element's dimension along its parent box' major
  // axis may be added to, if there's excess space left in the box.
  "takes_excess": Boolean,

  // Optional: Used for element kinds that require extra info.
  "data": ...
}
```

- for `ratio`:

  - the default is 1.0 i.e when not specified.
  - it applies only if the element has a WEIGHTED dimension along its
    parent box' major axis e.g a NOTES element in a horizontal box,
    and ignored otherwise e.g for a NOTES element in a vertical box.
  - within each box, sub-element ratios are relative
    i.e the normalized/absolute ratio of each sub-element (WEIGHTED) is
    `(sub-element ratio) / (sum of all sub-element ratios)`; this is more
    flexible than absolute ratios, without significant additional cost.

- for `minimum`:

  - the default is 0.0 i.e when not specified.

  :::{note}
  - The minimum dimension is not guranteed; the parent box can only allocate
    as much as is available.
  - The minimum dimension applies if and only if the computed dimension is
    greater than zero.
  :::

- for `maximum`:

  - if not specified, the element's dimension is only limited by:

    - its `ratio`, if it has a WEIGHTED dimension along its parent box'
      major axis, or
    - the space available in its parent box.

  - if specified, it must be greater than or equal to `minimum`.

- for `cut_off`:

  - the default is `false` i.e when not specified.
  - if `true` and the dimension allocated to the element is less than `minimum`,
    the element is omitted and other elements may take up its space.

- for `takes_excess`:

  - the default is `false` i.e when not specified.
  - it only applies to elements with non-WEIGHTED dimensions along the parent
    box' major axis (reason explained below); ignored otherwise.
  - if true and the element's parent box has space left (after computing the
    dimensions of all sub-elements), the excess space is distributed amongst
    all sub-elements of the box which have this field set to `true` and
    haven't been cut off, such that they don't exceed their `maximum`s.

  :::{note}
  Excess space in a box occurs when it contains sub-elements with WEIGHTED
  dimensions along its **major axis** and all those sub-elements either get
  cut off or limited by their `maximum`s.

  Since all sub-elements with WEIGHTED dimensions are already either cut off
  or allocated their `maximum`s, only sub-elements with non-WEIGHTED
  dimensions (if any) may take up the excess space.

  Even if there are sub-elements within a box that have this field set
  to `true`, all the excess space may not be taken up if those sub-elements
  have been cut off or are limited by their `maximum`s.
  :::

#### Element Kinds

The element kinds are as follows. Each doesn't use the `data` field except
stated otherwise.

- **TITLE**: The puzzle's title.

  - The width is WEIGHTED.
  - The height is DERIVED.
  - The `data` field has the following format:

    ```js
    {
      // The font description with which to render the title
      "font": Font
    }
    ```

    where `Font` is as earlier defined for the top-level font fields.

- **METADATA**: The publisher, author, date, etc (as many as are defined).

  - The dimension kinds are as for the **TITLE** element kind.
  - The `data` field is as for the **TITLE** element kind.

- **INTRO**: The puzzle's intro.

  - The dimension kinds are as for the **TITLE** element kind.
  - The `data` field is as for the **TITLE** element kind.

- **NOTES**: The puzzle's notes.

  - The dimension kinds are as for the **TITLE** element kind.
  - The `data` field is as for the **TITLE** element kind.

- **GRID**: The puzzle's grid.

  - The width is INTRINSIC.
  - The height is DERIVED.

    :::{note}
    This is so that when the allocated width is less than the intrinsic width,
    just the right amount of height needed can be computed. Unfortunately,
    the converse can't be easily/neatly achieved.
    :::

- **CLUES**: The puzzle's clues.

  - The width is WEIGHTED.
  - The height is WEIGHTED.
  - The `data` field has the following format:

    ```js
    {
      // A unique (amongst CLUES elements) identifier;
      // Positive.
      "id": Integer,

      // Clue direction(s) to contain (case-insensitive). One of:
      //
      // - all: Contain all clue directions, one after another
      // - any: Any available clue direction
      // - <any of the standard IPUZ directions>
      "direction": String,

      // The ID of another CLUES element into which the content of this
      // element may flow.
      "flows_into": Integer
    }
    ```

  - `id` is required.
  - a _source_ is a CLUES element into which no other flows.
  - an _extension_ is a CLUES element into which another flows.
  - all data fields other than `id` are optional, with the following exception(s):

    - `direction` is required for a _source_.

  - for `direction`:

    - if specified, `direction` is ignored for an _extension_.
    - if direction `all` is used, there must be only one _source_.
    - if direction `any` is used, the `direction` of all _sources_ must be `any`.
    - no two _sources_ may have the same IPUZ direction.

  - for `flows_into`:

    - if specified, there must exist another CLUES element with that value
      as its `id`.
    - a CLUES element must not flow into itself.
    - multiple CLUES elements must not flow into the same other CLUES element.
    - clues may flow across pages, but only forward i.e a CLUES element must
      not flow into another on a page before it.
    - a cyclic flow (i.e a loop of CLUES elements that have no apparent
      source nor end) is invalid.

- **SOLUTION**: The puzzle's solution in some form.

  - The width is INTRINSIC.
  - The height is DERIVED (for the same reason as the **GRID** element kind).

- **DIVIDER**: A vertical/horizontal divider.

  - The dimension kinds depend on the orientation of its parent box:

    |        | horizontal box | vertical box |
    |--------|----------------|--------------|
    | width  | INTRINSIC      | WEIGHTED     |
    | height | WEIGHTED       | INTRINSIC    |

  - Has a pre-defined thickness.
  - **NOTE:** This is a DISPLAY element, NOT a CONTAINER.

- **SPACER**: A blank space.

  - The width is WEIGHTED.
  - The height is WEIGHTED.

- **BOX**: A container to organize elements.

  - Along each axis, the dimension kind is determined by a set of rules which
    take priority in the order in which they're stated below; the first one
    whose condition is true determines the dimension kind.
  - Along the major axis:

    - If the box contains at least one element with a WEIGHTED dimension
      along that axis, the dimension is WEIGHTED.

    - If the box contains at least one element with a DERIVED dimension
      along that axis, the dimension is DERIVED.

    - Otherwise, the dimension is INTRINSIC.

  - Along the minor axis:

    - If the box contains at least one element with a DERIVED dimension
      along that axis, the dimension is DERIVED.

    - If the box contains at least one element with an INTRINSIC dimension
      along that axis, the dimension is INTRINSIC.

    - Otherwise, the dimension is WEIGHTED.

  - The `data` field has the format of a `Box` as earlier defined for the
    top-level page fields.

### Template Config

The template loader accepts a configuration parameter consisting of the
following fields:

- `clues_min_width` (`double`): If greater than zero, then for every CLUES
  element within a horizontal BOX:

  - if `minimum` is specified, it is overriden with this value.
  - if `maximum` is also specified (i.e only if `minimum` is overriden),
    but less than this value, it is also overriden (to preserve the
    constraint `maximum >= minimum`).

  This is mostly a hack to help reduce the number of templates needed to
  cover various combinations of cases, but is actually effective enough.

### Template Selection/Matching

A set of templates are pre-defined and used for printing puzzles.
A template is selected based on the following criteria:

- Puzzle kind: If a template's `puzzle_kinds` field contains the target
  puzzle kind, it's a match.

The pre-defined templates are loaded and tested in succession until one
matches. The first to match (for all criteria) is selected. If none matches,
selection fails. Ideally, there should be one and only one potential match
per puzzle.

## Areas For Improvement

- [ ] Allow user-defined templates.
      This will require the following (amongst other things):

  - [ ] a version field in the format to prevent breakage when there are
        breaking changes to the format.
  - [ ] report errors during template loading, instead of treating them as
        programming errors.

- [ ] A GUI to allow users to interactively create, modify and save layouts.
      This will require the following (amongst other things):

  - [ ] recursively serializing element structures to JSON (could use
        [`JsonBuilder`](https://gnome.pages.gitlab.gnome.org/json-glib/class.Builder.html) +
        [`JsonGenerator`](https://gnome.pages.gitlab.gnome.org/json-glib/class.Generator.html)).
