kb-text-layout-0.1.0.0: src/KB/Text/Layout/Measure.hs
module KB.Text.Layout.Measure
( -- * Layout context
createLayoutContext
, clearCache
, LayoutContext
-- * Preparing text for layout
, prepare
, prepareStyled
, PreparedText (..)
, MeasuredSegment (..)
, Span (..)
-- * Measuring
, measure
-- * Styles
, newStyle
, Style (..)
) where
import Data.IORef (IORef, atomicModifyIORef', modifyIORef', newIORef, readIORef, writeIORef)
import Data.Map.Strict (Map)
import Data.Map.Strict qualified as Map
import Data.Text (Text)
import Data.Text qualified as Text
import Data.Traversable (mapAccumL)
import Data.Vector (Vector)
import Data.Vector qualified as Vector
import KB.Text.Layout.Analysis (BreakKind (..), Segment (..), analyze)
import KB.Text.Layout.Segmentation qualified as Segmentation
import KB.Text.Shape qualified as TextShape
import KB.Text.Shape.Font qualified as Font
prepare :: LayoutContext -> Style -> Text -> IO PreparedText
prepare ctx style t = prepareStyled ctx [TextSpan style t]
prepareStyled :: LayoutContext -> [Span] -> IO PreparedText
prepareStyled ctx spans = do
segments <- Vector.fromList . concat <$> traverse spanSegments spans
pure PreparedText{segments}
where
spanSegments = \case
AtomSpan style label w ->
pure
[ MeasuredSegment
{ text = label
, kind = Atomic
, width = w
, style = style.key
, graphemeWidths = [(Text.length label, w)]
}
]
TextSpan style t -> do
advance <- charAdvances ctx style t
spaceWidth <- measure ctx style " "
hyphenWidth <- measure ctx style "-"
let
prefix = Vector.scanl' (+) 0 advance
slice off n = prefix Vector.! (off + n) - prefix Vector.! off
clusterWidths off = \case
[] -> []
c : rest ->
let k = Text.length c
in (k, slice off k) : clusterWidths (off + k) rest
segmentAt off seg =
let
n = Text.length seg.text
width = case seg.kind of
SoftHyphen -> hyphenWidth
ZeroWidthBreak -> 0
HardBreak -> 0
Tab -> fromIntegral n * spaceWidth
_ -> slice off n
graphemeWidths = case seg.kind of
Word -> clusterWidths off (Segmentation.clusters seg.text)
Glue -> clusterWidths off (Segmentation.clusters seg.text)
_ -> [(n, width)]
in
( off + n
, MeasuredSegment{text = seg.text, kind = seg.kind, width, style = style.key, graphemeWidths}
)
pure (snd (mapAccumL segmentAt 0 (analyze t)))
data PreparedText = PreparedText
{ segments :: Vector MeasuredSegment
}
data MeasuredSegment = MeasuredSegment
{ text :: Text
, kind :: BreakKind
, width :: Float
, style :: Int
, graphemeWidths :: [(Int, Float)]
}
data Span
= TextSpan Style Text
| AtomSpan Style Text Float
charAdvances :: LayoutContext -> Style -> Text -> IO (Vector Float)
charAdvances ctx style t
| Text.null t = pure Vector.empty
| otherwise = do
runs <- TextShape.run ctx.shape (TextShape.withFont_ style.font (TextShape.text_ t))
contributions <- concat <$> traverse runContributions runs
pure (Vector.map (* style.scale) (Vector.accum (+) (Vector.replicate n 0) contributions))
where
n = Text.length t
runContributions (run, glyphs) = do
info <- Font.getFontInfo run.font
let toGrid = style.grid / fromIntegral info.unitsPerEm
pure
[ (g.codepointIndex, fromIntegral g.advanceX * toGrid)
| g <- glyphs
, g.codepointIndex >= 0
, g.codepointIndex < n
]
measure :: LayoutContext -> Style -> Text -> IO Float
measure ctx style t
| Text.null t = pure 0
| otherwise =
Map.lookup (style.key, t) <$> readIORef ctx.metrics >>= \case
Just w -> pure w
Nothing -> do
runs <- TextShape.run ctx.shape (TextShape.withFont_ style.font (TextShape.text_ t))
let runWidth (run, glyphs) = do
info <- Font.getFontInfo run.font
let toGrid = style.grid / fromIntegral info.unitsPerEm
pure (sum [fromIntegral g.advanceX | g <- glyphs] * toGrid)
w <- (* style.scale) . sum <$> traverse runWidth runs
modifyIORef' ctx.metrics (Map.insert (style.key, t) w)
pure w
newStyle :: LayoutContext -> Font.Font -> Float -> IO Style
newStyle ctx font size = do
info <- Font.getFontInfo font
key <- atomicModifyIORef' ctx.styleKeys \n -> (n + 1, n)
pure
Style
{ key
, font
, size
, em = size * Font.emToCaps info
, scale = size / fromIntegral info.capitalHeight
, grid = fromIntegral info.unitsPerEm
}
data Style = Style
{ key :: Int
, font :: Font.Font
, size :: Float
, em :: Float
, scale :: Float
, grid :: Float
}
createLayoutContext :: TextShape.Context -> IO LayoutContext
createLayoutContext shape = do
metrics <- newIORef Map.empty
styleKeys <- newIORef 0
pure LayoutContext{shape, metrics, styleKeys}
clearCache :: LayoutContext -> IO ()
clearCache ctx = writeIORef ctx.metrics Map.empty
data LayoutContext = LayoutContext
{ shape :: TextShape.Context
, metrics :: IORef (Map (Int, Text) Float)
, styleKeys :: IORef Int
}