kb-text-layout: Multiline text measurement & layout.

[ bsd3, library, text ] [ Propose Tags ] [ Report a vulnerability ]

Flags

Manual Flags

NameDescriptionDefault
demos

Build the demo executables and benchmarks.

Disabled

Use -f <flag> to enable a flag, or -f -<flag> to disable that flag. More info

Downloads

Maintainer's Corner

Package maintainers

For package maintainers and hackage trustees

Candidates

  • No Candidates
Versions [RSS] 0.1.0.0, 0.1.0.1
Change log CHANGELOG.md
Dependencies base (>=4.18 && <5), bytestring, containers, hyphenation, kb-text-layout, kb-text-shape (>=0.2.1.0 && <0.3), text, vector, zstd [details]
License BSD-3-Clause
Copyright 2026 IC Rainbow
Author IC Rainbow
Maintainer aenor.realm@gmail.com
Uploaded by AlexanderBondarenko at 2026-08-16T00:32:10Z
Category Text
Source repo head: git clone https://gitlab.com/dpwiz/text-layout
Distributions
Executables kb-text-layout-obstacles, kb-text-layout-masonry, kb-text-layout-justify, kb-text-layout-demo, kb-text-layout-bubbles
Downloads 5 total (5 in the last 30 days)
Rating (no votes yet) [estimated by Bayesian average]
Your Rating
  • λ
  • λ
  • λ
Status Docs available [build log]
Last success reported on 2026-08-16 [all 1 reports]

Readme for kb-text-layout-0.1.0.1

[back to package description]

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.

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.