packages feed

kb-text-layout-0.1.0.0: demos/bubbles/Main.hs

module Main (main) where

import Data.Text qualified as Text
import Data.Text.IO qualified as Text.IO
import System.Environment (getArgs)
import Text.Printf (printf)

import Demo qualified

import KB.Text.Layout.Break qualified as Break
import KB.Text.Layout.Html qualified as Html
import KB.Text.Layout.Measure (Span (..), Style)
import KB.Text.Layout.Measure qualified as Measure
import KB.Text.Shape qualified as TextShape

unit, maxBubble, containerW, pad, gap :: Float
unit = 11
maxBubble = 18
containerW = 26
pad = 9
gap = 10

data Side = Them | Us

main :: IO ()
main = do
  args <- getArgs
  let (bodyFile, emojiFile) = case args of
        a : b : _ -> (a, b)
        [a] -> (a, a)
        [] ->
          ( "assets/NotoSans-Regular.kbts.zst"
          , "assets/NotoColorEmoji.kbts.zst"
          )
  TextShape.withContext \shape -> do
    bodyFont <- Demo.pushFont shape bodyFile
    _emojiFont <- Demo.pushFont shape emojiFile
    ctx <- Measure.createLayoutContext shape
    body <- Measure.newStyle ctx bodyFont 1.0
    mark <- Measure.newStyle ctx bodyFont 1.0

    pill <- AtomSpan mark "v2.4" . (+ 0.6) <$> Measure.measure ctx mark "v2.4"
    prepped <- traverse (traverse (Measure.prepareStyled ctx)) (conversation body pill)

    let
      cssFont style = Demo.fontCss unit style ["Body", "Emoji"]
      styleCss k
        | k == mark.key = cssFont mark <> ";background:#fff3a8;border-radius:4px;text-align:center"
        | otherwise = cssFont body
      bubble (side, prep) =
        let
          w = Break.shrinkwrap prep maxBubble
          ranges = Break.layoutGreedy prep w
          opts = (Demo.options unit w body (cssFont body)){Html.styleCss = styleCss}
          h = Html.height opts (length ranges) + 2 * pad
          boxW = w * unit + 2 * pad
          (x, bg, fg) = case side of
            Them -> (0 :: Float, "#f0f0f3", "#1f2430")
            Us -> (containerW * unit - boxW, "#dceaff", "#103a75")
        in
          ( h
          , \y ->
              Demo.at
                x
                y
                ( ";width:"
                    <> Demo.px (boxW - 2 * pad)
                    <> ";height:"
                    <> Demo.px (h - 2 * pad)
                    <> ";padding:"
                    <> Demo.px pad
                    <> ";background:"
                    <> bg
                    <> ";color:"
                    <> fg
                    <> ";border-radius:12px"
                )
                (Html.render opts prep ranges)
          )
      stack y = \case
        [] -> ([], y - gap)
        (h, render) : rest ->
          let (divs, total) = stack (y + h + gap) rest
          in (render y : divs, total)
      (bubbleDivs, totalH) = stack 0 (map bubble prepped)
      fontFace = Demo.fontFaces [("Body", bodyFile), ("Emoji", emojiFile)]
      container = Demo.canvas (containerW * unit) totalH (Text.concat bubbleDivs)
    Text.IO.writeFile "bubbles.html" $
      Html.page "kb-text-layout bubbles" (fontFace <> container)
    printf "wrote bubbles.html (%d bubbles)\n" (length prepped)

conversation :: Style -> Span -> [(Side, [Span])]
conversation body pill =
  [ (Them, [t "Hey! Does the engine handle emoji families like \128104\8205\128105\8205\128103 yet?"])
  , (Us, [t "Yep. They measure as one cluster and never split across lines \128077"])
  , (Them, [t "And long words? Antidis\173establish\173ment\173arian\173ism used to blow up the bubbles."])
  , (Us, [t "Soft hy\173phens and grapheme emergency breaks both work, and non\160breaking\160spaces hold on."])
  , (Them, [t "Where does the font live? /usr/share/fonts/truetype/noto/NotoSans-Regular.ttf"])
  , (Us, [t "Shipping it in ", pill, t " \8212 bubbles shrinkwrap to their widest line, so short replies stay snug."])
  , (Them, [t "ok"])
  , (Us, [t "\127881"])
  ]
  where
    t = TextSpan body