kb-text-layout-0.1.0.0: demos/justify/Main.hs
module Main (main) where
import Data.Foldable (for_)
import Data.Text qualified as Text
import Data.Text.IO qualified as Text.IO
import Data.Vector qualified as Vector
import System.Environment (getArgs)
import Text.Printf (printf)
import Demo qualified
import KB.Text.Layout.Analysis (BreakKind (..))
import KB.Text.Layout.Break (LineEnd (..), LineRange)
import KB.Text.Layout.Break qualified as Break
import KB.Text.Layout.Html qualified as Html
import KB.Text.Layout.Measure (PreparedText)
import KB.Text.Layout.Measure qualified as Measure
import KB.Text.Shape qualified as TextShape
unit, lineHeight, colW :: Float
unit = 11
lineHeight = 2
colW = 300
gapsOf :: PreparedText -> Float -> LineRange -> [(Float, Float)]
gapsOf prep target line
| line.from.grapheme /= 0 || line.to.grapheme /= 0 = []
| otherwise = go 0 line.from.segment []
where
segs = prep.segments
exempt = line.ended == HardBroken || line.ended == Finished
separators =
sum
[ Text.length s.text
| j <- [line.from.segment .. line.to.segment - 1]
, let s = segs Vector.! j
, s.kind == Space
]
extraPerChar
| exempt || separators == 0 = 0
| otherwise = (target - line.width) / fromIntegral separators
go x j acc
| j >= line.to.segment = reverse acc
| otherwise =
let s = segs Vector.! j
in case s.kind of
Space ->
let x' = x + s.width + extraPerChar * fromIntegral (Text.length s.text)
in go x' (j + 1) ((x, x') : acc)
SoftHyphen -> go x (j + 1) acc
HardBreak -> go x (j + 1) acc
ZeroWidthBreak -> go x (j + 1) acc
_ -> go (x + s.width) (j + 1) acc
riverJoins :: [[(Float, Float)]] -> Int
riverJoins rows = sum (zipWith joins rows (drop 1 rows))
where
joins above below = length [() | a <- above, b <- below, fst a < snd b, fst b < snd a]
widestGap :: [[(Float, Float)]] -> Float
widestGap rows = maximum (0 : [hi - lo | row <- rows, (lo, hi) <- row])
main :: IO ()
main = do
args <- getArgs
let fontFile = case args of
a : _ -> a
[] -> "assets/NotoSans-Regular.kbts.zst"
TextShape.withContext \shape -> do
font <- Demo.pushFont shape fontFile
ctx <- Measure.createLayoutContext shape
body <- Measure.newStyle ctx font 1.0
prepPlain <- Measure.prepare ctx body Demo.rivers
prepHyph <- Measure.prepare ctx body (Demo.softHyphenate Demo.rivers)
let
w = colW / unit
variants =
[ ("engine: greedy", prepPlain, Break.layoutGreedy prepPlain w)
, ("engine: greedy + hyphenation", prepHyph, Break.layoutGreedy prepHyph w)
, ("engine: Knuth-Plass", prepPlain, Break.layoutOptimal prepPlain w)
, ("engine: Knuth-Plass + hyphenation", prepHyph, Break.layoutOptimal prepHyph w)
]
cssFont = Demo.fontCss unit body ["Body"]
opts = (Demo.options unit w body cssFont){Html.justify = True}
metrics (_, prep, ranges) =
let rows = map (gapsOf prep w) ranges
in (riverJoins rows, widestGap rows * unit)
column title content = "<div>" <> Demo.captionWidth colW title <> content <> "</div>"
engineColumn v@(name, prep, ranges) =
let (joins, widest) = metrics v
in column
(name <> Text.pack (printf ": %d river joins, widest gap %.1fpx" joins widest))
(Html.render opts prep ranges)
native =
Text.concat
[ "<div style=\"width:" <> Text.pack (printf "%.2fpx" colW)
, ";text-align:justify;white-space:pre-wrap"
, ";line-height:" <> Text.pack (printf "%.2fpx" (lineHeight * unit))
, ";text-box-trim:trim-both;text-box-edge:cap alphabetic"
, ";" <> Html.escape cssFont <> "\">"
, Html.escape Demo.rivers
, "</div>"
]
fontFace = Demo.fontFaces [("Body", fontFile)]
columns =
Text.concat
[ "<div style=\"display:flex;gap:28px;align-items:start\">"
, column "browser: native text-align justify (breaks match engine greedy)" native
, Text.concat (map engineColumn variants)
, "</div>"
]
for_ variants \v@(name, _, ranges) -> do
let (joins, widest) = metrics v
printf "%-34s %2d lines, %2d river joins, widest gap %5.1fpx\n" (Text.unpack name) (length ranges) joins widest
Text.IO.writeFile "justify.html" $
Html.page "kb-text-layout justification" (fontFace <> columns)
printf "wrote justify.html\n"