# kb-text-layout
Multiline text measurement & layout on top of [kb-text-shape], inspired by [pretext].
Prepare a text once, then lay it out at any width.
[kb-text-shape]: https://github.com/dpwiz/kb-text-shape
[pretext]: https://github.com/chenglou/pretext
```haskell
import Data.Text.IO qualified as Text
import KB.Text.Layout.Break qualified as Break
import KB.Text.Layout.Measure qualified as Measure
import KB.Text.Shape qualified as KBTS
main :: IO ()
main =
KBTS.withContext \shape -> do
font <- KBTS.pushFontFromFile shape "demos/assets/Ubuntu-R.ttf" 0
ctx <- Measure.createLayoutContext shape
style <- Measure.newStyle ctx font 1.0
prepared <- Measure.prepare ctx style "Soft hy\173phen\173ation and non\160breaking\160spaces."
let maxWidth = 24
let ranges = Break.layoutGreedy prepared maxWidth
putStrLn $ "Stats: " <> show (Break.layoutStats ranges)
putStrLn ""
-- Stats: LayoutStats {lineCount = 2, maxLineWidth = 13.909091}
putStrLn "Ranges:"
mapM_ print ranges
-- LineRange {from = Cursor {segment = 0, grapheme = 0}, to = Cursor {segment = 9, grapheme = 0}, width = 13.909091, ended = Wrapped}
-- LineRange {from = Cursor {segment = 10, grapheme = 0}, to = Cursor {segment = 15, grapheme = 0}, width = 13.470421, ended = Finished}
putStrLn ""
putStrLn "Lines:"
let laidout = map (Break.materializeLineRange prepared) ranges
-- Soft hyphenation and
-- non breaking spaces.
mapM_ Text.putStrLn laidout
```
Everything after `prepare` is pure: relayout at another width is a fold over cached widths, with no shaper calls.
## Units
To make sizes comparable across different fonts layout space is cap-height-normalized.
`newStyle ctx font 1.0` scales the font so a capital `H` is exactly 1.0 layout units tall.
`Style.em` carries the em size in the same units for renderers that need CSS or pixel sizes.
Line height is a caller-chosen number of cap units.
Ascenders and descenders overhang the fixed line box, so vertical font metrics never enter layout.
> ⚠️ Fonts without a cap-height metric fail at load time.
## Demos
The demo executables live in `demos/` behind the `demos` package flag (off by default).
- `demo`: basic layout showing a ragged vs justified comparison.
- `masonry`: packs a card corpus by shortest column.
- `justify`: five columns of the same text at 300px.
* Browser's own `text-align: justify`.
* Greedy (fastest).
* Greedy with soft hyphens from the `hyphenation` package.
* `layoutOptimal` (slowest, for extra nice).
* `layoutOptimal` plus hyphenation.
- `obstacles`: Routes justified text around exclusion shapes.
Use `make demos` to run everything and rebuild the HTML pages.
Use `stack bench` to see the relative cost of each layout.
## Limitations
- Grapheme clusters come from kb-text-shape's `KB.Text.Shape.Segmentation`, so combining marks, Hangul jamo, flag pairs, and ZWJ emoji stay whole through emergency breaks and slicing. kbts skips UAX #29 GB11 and LB8a, so the wrapper refuses to cut, or break, adjacent to a ZWJ.
- No dictionary-based segmenter for Thai/Lao/Khmer/Myanmar; no algorithmic breaker provides one.
- Spans are shaped whole, so widths reflect joined and kerned forms. At a chosen break, a line that ends mid-join renders letterforms whose widths differ slightly from the joined measurement; a re-shape refinement pass does not exist yet.
- Pretext's preprocessing rules are largely subsumed by kbts line breaking. `Analysis` adds the two missing tailorings: URL query-separator splits scoped to slash-containing tokens, and en/em-dash digit-range suppression.
- `prepare` needs the open `TextShape.Context`; the `PreparedText` does not. Prepared texts can be laid out, sliced, and emergency-broken after the context closes.
- `pushFontFromFile` and `pushFontFromMemory` reject missing files and fonts without `unitsPerEm` or a cap-height metric at load time, so bad fonts fail fast.