Whereas speech, like for Rhapsode, positions text only in the single dimension of time, Haphaestus positions text in 2-dimensional space. This requires a choice of non-trivial formulas across several tree traversals. In a back & forth negotiation regarding how to size elements for your device.
Each of these formulas were implemented in seperate files, with a common file abstracting over all of them with tree traversals. Each traversal updates the tree to inform later traversals.
I call my layout-engine implementing this “CatTrap”, since this being the internet some of those boxes will inevitably contain cats. Hence the catphotos I’m decorating this blogpost with.
Beyond providing updates on Haphaestus’s progress, I hope this blogpost helps webdevs better understand what various CSS properties actually do.
In CSS every box is surrounded by extra space representing it’s “paddings”, “borders”, & “margins”. That border has special meaning to later components of the Argonaut Stack, but as far as CatTrap is concerned it’s just extra space to sum into the layout like paddings & margins are.
Currently I’ve only really implemented this for block layout, other layouts get wrapped in an extra block element so they can be assigned a size.
CatTrap defines a datastructure representing a size-bounded box with these surrounding whitespace. This PaddedBox
type uses generic type-arguments so it can store sizes in various stages of unit resolution.
Block layout (for horizontal text) involves computing the size from the maximum of all children’s widths, followed by summing all their heights to compute the block-layout’s own “content height”. Though that sum does involve excluding the smaller of each contiguous pair of margins, as if we’re letting them overlap.
Adding support for minimum & maximum sizes involves incorporating a couple extra tree-traversals before computing the final size, with the “natural” size computed & cached seperately. These values are compared to select the final size for each dimension.
I surprisingly found percentages tricky to get working correctly with everything else, since regardless of whether I’d normally use preorder or postorder tree-traversals percentages require preorder traversals. I solved this by creating utilities aiding me in resolving percentage units last, in the preorder passes finalizing width then height sizes.
The logic to layout inline text has been implemented seperately as “Balkón”, to which CatTrap hands off this work. To compute size bounds, CatTrap asks Balkón for how wide the text would be given 0 width or (no-wrapping) given infinite width. The finalized width is likely somewhere in-between, inherited from parent width.
The main trick for CatTrap to deal with is that upon desugaring a tree of parsed CSS properties it has to capture & flatten runs of inline text ready for Balkón to process. This required special logic to be added processing a node’s children. Special care had to be taken to extract any block elements embedded within inline elements, especially since HTML Conduit’s error recovery does not comply with WHATWG’s convoluted error correction algorithm.
The computation in both dimensions for grids is identical. Minimum & natural sizes are computed & cached before summing up known widths so it can redistribute excess space. Upon computing the width an estimated width is required for the sake of resolving percentages.
The same routines & datastructures are used for both dimensions. With the aid of utilities to select children exclusively in a given column or row.
Additional utilities sum the width or height to inform other layout formulas, whether for the full grid or for contiguous spans of gridcells. Running sums are consulted to compute positions, & excess space for alignment.
In place of scrolling, Haphaestus will use pagination to minimize the number of button presses needed to comfortably read a webpage on a TV screen.
Pagination is implemented for inline & block elements. Grids aren’t paginated yet.
For block elements I recursively call a boxSplit
function on the root element gathering results up into a sequence of uniformly-sized pages containing elements to be laid-out. Each boxSplit
call gathers all children on this page, returning a possibly-modified element fitting in the remaining height & maybe a modified overflow element to be examined by another call to boxSplit
.
For inline elements I feed Balkón both the remaining height & page height so it can decide whether & where to split the laid-out paragraph. Since Balkón’s pagination operates upon it’s output not it’s input I altered the layout-tree datastructure so it can hold this fixed-width output.
CatTrap has an entire subsystem for parsing relevant CSS properties (from Haskell Stylist via it’s PropertyParser
typeclass) & desugaring them into CatTrap’s normal layout tree. This gives an opportunity to apply useful preprocessing steps prior to layout.
These preprocessing steps include:
There’s plenty more to do on CatTrap, and I did take a few wrong turns before arriving at an API & implementation I am happy to maintain. Though I still need to figure out how this API would change upon adding support for vertical text.
Also I did face some burnout after failing to get Haphaestus ready for a live demo at LibrePlanet this year… Thankfully that didn’t stop my audience from being excited to hear my efforts!
But I am happy with CatTrap now! The layout formulas it implements are reasonably straightforward if under-documented, & I can easily add more in the future!
Short-term, I plan to move on to other Haphaestus components including improvements to Haskell Stylist’s ordered-list support & a new rendering engine.
Longer term I have enhancements I’d love to add to CatTrap!
calc()
function, which I’ve started to implement.I’ve thought about how to implement most of these, & have notes on it in the codebase.
Also, Balkón is being improved to support richtext options. This will require integration effort from CatTrap!
Correction 2nd May: “Generics” link replaced to refer to what I was actually talking about. Previously linked to the DerivingGeneric
, which is something I do use elsewhere.