~jaro/balkon

65d952e2f615751db54267097c706545be5e6be6 — Jaro 1 year, 9 months ago 32e1c83
Slightly decompose the Plain module.
M balkon.cabal => balkon.cabal +1 -0
@@ 98,6 98,7 @@ library balkon-internal
        Data.Text.ParagraphLayout.Internal.Break,
        Data.Text.ParagraphLayout.Internal.Fragment,
        Data.Text.ParagraphLayout.Internal.LineHeight,
        Data.Text.ParagraphLayout.Internal.Paragraph,
        Data.Text.ParagraphLayout.Internal.ParagraphConstruction,
        Data.Text.ParagraphLayout.Internal.Plain,
        Data.Text.ParagraphLayout.Internal.Rect,

M lib/Data/Text/ParagraphLayout.hs => lib/Data/Text/ParagraphLayout.hs +1 -0
@@ 31,5 31,6 @@ where

import Data.Text.ParagraphLayout.Internal.Fragment
import Data.Text.ParagraphLayout.Internal.LineHeight
import Data.Text.ParagraphLayout.Internal.Paragraph
import Data.Text.ParagraphLayout.Internal.Plain
import Data.Text.ParagraphLayout.Internal.Span

A src/Data/Text/ParagraphLayout/Internal/Paragraph.hs => src/Data/Text/ParagraphLayout/Internal/Paragraph.hs +110 -0
@@ 0,0 1,110 @@
module Data.Text.ParagraphLayout.Internal.Paragraph
    (Paragraph(..)
    ,ParagraphLayout(..)
    ,ParagraphOptions(..)
    ,paragraphLayout
    ,paragraphOriginX
    ,paragraphOriginY
    ,paragraphSpanBounds
    )
where

import Data.Int (Int32)
import Data.List.NonEmpty (NonEmpty)
import qualified Data.List.NonEmpty as NonEmpty
import Data.Text.Array (Array)
import Data.Text.Glyphize (Font)

import Data.Text.ParagraphLayout.Internal.LineHeight
import Data.Text.ParagraphLayout.Internal.Rect
import Data.Text.ParagraphLayout.Internal.Span

-- | Text to be laid out as a single paragraph.
--
-- May be divided into any number of neighbouring spans, each of which will
-- be represented as a separate `SpanLayout` in the resulting layout.
--
-- The input text must be encoded as UTF-8 in a contiguous byte array.
--
-- You may need to use "Data.Text.Internal" in order to determine the byte
-- array and the necessary offsets to construct the paragraph without copying
-- data.
--
-- For simple use cases, it may be sufficient to construct paragraphs using
-- [ParagraphConstruction]("Data.Text.ParagraphLayout.ParagraphConstruction").
data Paragraph = Paragraph

    Array
    -- ^ A byte array containing the whole text to be laid out, in UTF-8.

    Int
    -- ^ Byte offset of the first span from the start of the byte array.
    -- Any characters preceding this offset will not be shaped, but may still
    -- be used to influence the shape of neighbouring characters.

    [Span]
    -- ^ Parts of the text to be laid out, in logical order.
    -- The initial offset plus total length of all spans must not exceed
    -- the bounds of the byte array.
    -- Any characters following the last span will not be shaped, but may still
    -- be used to influence the shape of neighbouring characters.

    ParagraphOptions
    -- ^ Options applying to the paragraph as a whole.

data ParagraphOptions = ParagraphOptions

    { paragraphFont :: Font
    -- ^ Font to be used for shaping and measurement.
    -- Make sure to set its scale (see `Data.Text.Glyphize.optionScale`) using
    -- the same units that you want in the output.

    , paragraphLineHeight :: LineHeight
    -- ^ Preferred line height of the resulting box fragments.

    , paragraphMaxWidth :: Int32
    -- ^ Line width at which line breaking should occur.
    -- Lines will be broken at language-appropriate boundaries.
    -- If a line still exceeds this limit then, it will be broken at character
    -- boundaries, and if it already consists of a single cluster that cannot
    -- be further broken down, it will overflow.

    }
    deriving (Eq, Show)

-- | The resulting layout of the whole paragraph.
data ParagraphLayout = ParagraphLayout
    { paragraphRect :: Rect Int32
    -- ^ The containing block (CSS3).
    , spanLayouts :: [SpanLayout]
    }
    deriving (Eq, Read, Show)

-- | Calculate the offsets into the `Paragraph`'s underlying `Array` where each
-- span starts and ends, in ascending order. The resulting list will be one
-- larger than the list of input spans.
paragraphSpanBounds :: Paragraph -> NonEmpty Int
paragraphSpanBounds (Paragraph _ initialOffset spans _) =
    NonEmpty.scanl (+) initialOffset (map spanLength spans)

paragraphOriginX :: (Num a) => a
paragraphOriginX = 0

paragraphOriginY :: (Num a) => a
paragraphOriginY = 0

empty :: (Num a) => Rect a
empty = Rect
    { x_origin = paragraphOriginX
    , y_origin = paragraphOriginY
    , x_size = 0
    , y_size = 0
    }

containRects :: (Ord a, Num a) => [Rect a] -> Rect a
containRects = foldr union empty

-- | Wrap the given `SpanLayout`s and compute their containing rectangle.
paragraphLayout :: [SpanLayout] -> ParagraphLayout
paragraphLayout sls = ParagraphLayout pRect sls
    where pRect = containRects $ concat $ map spanRects sls

M src/Data/Text/ParagraphLayout/Internal/Plain.hs => src/Data/Text/ParagraphLayout/Internal/Plain.hs +24 -145
@@ 7,17 7,16 @@ module Data.Text.ParagraphLayout.Internal.Plain
    )
where

import Control.Applicative (ZipList(ZipList), getZipList)
import Data.Int (Int32)
import Data.List (mapAccumL)
import Data.List.NonEmpty (NonEmpty((:|)))
import qualified Data.List.NonEmpty as NonEmpty
import Data.Maybe (catMaybes, fromMaybe, listToMaybe)
import qualified Data.Text as Text
import Data.Text.Array (Array)
import Data.Text.Foreign (lengthWord8)
import Data.Text.Glyphize
    (Buffer(..)
    ,ContentType(ContentTypeUnicode)
    ,Font
    ,FontExtents(..)
    ,GlyphInfo
    ,GlyphPos(x_advance)


@@ 33,118 32,21 @@ import qualified Data.Text.Lazy as Lazy
import Data.Text.ParagraphLayout.Internal.Break
import Data.Text.ParagraphLayout.Internal.Fragment
import Data.Text.ParagraphLayout.Internal.LineHeight
import Data.Text.ParagraphLayout.Internal.Paragraph
import qualified Data.Text.ParagraphLayout.Internal.ProtoFragment as PF
import Data.Text.ParagraphLayout.Internal.Rect
import Data.Text.ParagraphLayout.Internal.ResolvedSpan (WithSpan(WithSpan))
import qualified Data.Text.ParagraphLayout.Internal.ResolvedSpan as RS
import Data.Text.ParagraphLayout.Internal.Run
import Data.Text.ParagraphLayout.Internal.Span
import Data.Text.ParagraphLayout.Internal.TextContainer

-- | Text to be laid out as a single paragraph.
--
-- May be divided into any number of neighbouring spans, each of which will
-- be represented as a separate `SpanLayout` in the resulting layout.
--
-- The input text must be encoded as UTF-8 in a contiguous byte array.
--
-- You may need to use "Data.Text.Internal" in order to determine the byte
-- array and the necessary offsets to construct the paragraph without copying
-- data.
--
-- For simple use cases, it may be sufficient to construct paragraphs using
-- [ParagraphConstruction]("Data.Text.ParagraphLayout.ParagraphConstruction").
data Paragraph = Paragraph

    Array
    -- ^ A byte array containing the whole text to be laid out, in UTF-8.

    Int
    -- ^ Byte offset of the first span from the start of the byte array.
    -- Any characters preceding this offset will not be shaped, but may still
    -- be used to influence the shape of neighbouring characters.

    [Span]
    -- ^ Parts of the text to be laid out, in logical order.
    -- The initial offset plus total length of all spans must not exceed
    -- the bounds of the byte array.
    -- Any characters following the last span will not be shaped, but may still
    -- be used to influence the shape of neighbouring characters.

    ParagraphOptions
    -- ^ Options applying to the paragraph as a whole.

data ParagraphOptions = ParagraphOptions

    { paragraphFont :: Font
    -- ^ Font to be used for shaping and measurement.
    -- Make sure to set its scale (see `Data.Text.Glyphize.optionScale`) using
    -- the same units that you want in the output.

    , paragraphLineHeight :: LineHeight
    -- ^ Preferred line height of the resulting box fragments.

    , paragraphMaxWidth :: Int32
    -- ^ Line width at which line breaking should occur.
    -- Lines will be broken at language-appropriate boundaries.
    -- If a line still exceeds this limit then, it will be broken at character
    -- boundaries, and if it already consists of a single cluster that cannot
    -- be further broken down, it will overflow.

    }
    deriving (Eq, Show)

-- | The resulting layout of the whole paragraph.
data ParagraphLayout = ParagraphLayout
    { paragraphRect :: Rect Int32
    -- ^ The containing block (CSS3).
    , spanLayouts :: [SpanLayout]
    }
    deriving (Eq, Read, Show)

-- | The resulting layout of each span, which may include multiple fragments if
-- broken over multiple lines.
data SpanLayout = SpanLayout [Fragment]
    -- TODO: Consider merging. fragments created by script changes.
    deriving (Eq, Read, Show)

-- | Wrapper for temporarily mapping the relationship to a `Span`.
data WithSpan a = WithSpan RS.ResolvedSpan a

instance Functor WithSpan where
    fmap f (WithSpan s a) = WithSpan s (f a)

instance TextContainer a => TextContainer (WithSpan a) where
    getText (WithSpan _ c) = getText c

instance SeparableTextContainer a => SeparableTextContainer (WithSpan a) where
    splitTextAt8 n (WithSpan rs c) = (WithSpan rs c1, WithSpan rs c2)
        where (c1, c2) = splitTextAt8 n c

splitBySpanIndex :: [WithSpan a] -> [[a]]
splitBySpanIndex xs = [getBySpanIndex i xs | i <- [0..]]

getBySpanIndex :: Int -> [WithSpan a] -> [a]
getBySpanIndex idx xs = map contents $ filter matchingIndex $ xs
    where
        matchingIndex (WithSpan rs _) = (RS.spanIndex rs) == idx
        contents (WithSpan _ x) = x

spanRects :: SpanLayout -> [Rect Int32]
spanRects (SpanLayout frags) = map fragmentRect frags

base :: (Num a) => Rect a
base = Rect 0 0 0 0

containRects :: (Ord a, Num a) => [Rect a] -> Rect a
containRects = foldr union base

-- | Lay out a paragraph of plain, unidirectional text using a single font.
layoutPlain :: Paragraph -> ParagraphLayout
layoutPlain p@(Paragraph _ _ _ opts) = ParagraphLayout pRect sls
layoutPlain p@(Paragraph _ _ _ opts) = paragraphLayout sls
    where
        pRect = containRects $ concat $ map spanRects sls
        sls = map SpanLayout fragsBySpan
        fragsBySpan = take (length spans) $ splitBySpanIndex frags
        fragsBySpan = take (length spans) $ RS.splitBySpanIndex frags
        frags = layoutAndAlignLines maxWidth $ spansToRunsWrapped spans
        maxWidth = paragraphMaxWidth opts
        spans = resolveSpans p


@@ 169,7 71,7 @@ layoutAndAlignLines maxWidth runs = frags
        frags = concat fragsInLines
        (_, fragsInLines) = mapAccumL alignLineH originY protoFragsInLines
        protoFragsInLines = layoutLines maxWidth runs
        originY = 0
        originY = paragraphOriginY

-- | Create a multi-line layout from the given runs, splitting them as
-- necessary to fit within the requested line width.


@@ 190,7 92,7 @@ layoutLines maxWidth runs
    where
        (fitting, rest) = tryAddRunsH maxWidth originX runs
        overflowing = addRunsH originX runs
        originX = 0
        originX = paragraphOriginX

-- TODO: Allow a run across multiple spans (e.g. if they only differ by colour).



@@ 272,12 174,6 @@ breakSplits closed (x:xs) = splits ++ breakSplits (x:closed) xs
        mapFunc ((x1, x2), _) =
            (reverse $ collapse $ x1 :| xs, collapse $ x2 :| closed)

-- | If the first run is empty, remove it.
collapse :: NonEmpty (WithSpan Run) -> [WithSpan Run]
collapse (x :| xs)
    | Text.null (getText x) = xs
    | otherwise = x:xs

-- | Calculate layout for multiple runs on the same line and
-- arrange them in one horizontal direction starting from the given x_offset.
addRunsH :: Int32 -> [WithSpan Run] -> [WithSpan PF.ProtoFragment]


@@ 318,21 214,28 @@ shapeRun (WithSpan rs run) = shape font buffer features
        features = []

resolveSpans :: Paragraph -> [RS.ResolvedSpan]
resolveSpans p@(Paragraph arr off spans opts) = do
    let (end, textsAndMarks) = cutsAndMarks arr off spans
    let indexes = [0..]

    (s, (o, t), i) <- zip3 spans textsAndMarks indexes
resolveSpans p@(Paragraph arr pStart spans opts) = do
    let sBounds = paragraphSpanBounds p
    let pEnd = NonEmpty.last sBounds
    let sStarts = NonEmpty.init sBounds
    let sLengths = map spanLength spans

    (i, s, sStart, sLen) <- getZipList $ (,,,)
        <$> ZipList [0..]
        <*> ZipList spans
        <*> ZipList sStarts
        <*> ZipList sLengths
    let lang = spanLanguage s
    let breaks = paragraphLineBreaks p end lang
    let breaks = paragraphLineBreaks p pEnd lang
    return RS.ResolvedSpan
        { RS.spanIndex = i
        , RS.spanOffsetInParagraph = o - off
        , RS.spanText = t
        , RS.spanOffsetInParagraph = sStart - pStart
        -- TODO: Consider adding checks for array bounds.
        , RS.spanText = Text arr sStart sLen
        , RS.spanFont = paragraphFont opts
        , RS.spanLineHeight = paragraphLineHeight opts
        , RS.spanLanguage = lang
        , RS.spanLineBreaks = subOffsetsDesc (o - off) breaks
        , RS.spanLineBreaks = subOffsetsDesc (sStart - pStart) breaks
        }

paragraphLineBreaks :: Paragraph -> Int -> String -> [(Int, BreakStatus.Line)]


@@ 358,27 261,3 @@ runLineBreaks (WithSpan rs run) = dropWhile (not . valid) $
-- TODO: Identify and correct for differences between the two.
localeFromLanguage :: String -> LocaleName
localeFromLanguage x = Locale x

-- | Given an underlying `Array`, an initial offset, and a list of consecutive
-- `Span`s, produce a list of `Text`s corresponding to the given spans, as well
-- as the offset of the start of each `Text` and the end of the last `Text`.
--
-- TODO: Consider adding checks for array bounds.
cutsAndMarks :: Array -> Int -> [Span] -> (Int, [(Int, Text)])
cutsAndMarks arr initialOffset spans =
    mapAccumL (cutAndMark arr) initialOffset spans

-- | Like `cut`, but also include the starting offset in the output.
cutAndMark :: Array -> Int -> Span -> (Int, (Int, Text))
cutAndMark arr off s = (end, (off, t))
    where
        (end, t) = cut arr off s

-- | Produce a `Text`, defined by an initial offset and a `Span`, out of the
-- underlying `Array`.
cut :: Array -> Int -> Span -> (Int, Text)
cut arr off s = (end, t)
    where
        len = spanLength s
        end = off + len
        t = Text arr off len

M src/Data/Text/ParagraphLayout/Internal/ResolvedSpan.hs => src/Data/Text/ParagraphLayout/Internal/ResolvedSpan.hs +27 -1
@@ 1,4 1,8 @@
module Data.Text.ParagraphLayout.Internal.ResolvedSpan (ResolvedSpan(..))
module Data.Text.ParagraphLayout.Internal.ResolvedSpan
    (ResolvedSpan(..)
    ,WithSpan(WithSpan)
    ,splitBySpanIndex
    )
where

import Data.Text (Text)


@@ 26,3 30,25 @@ instance Eq ResolvedSpan where

instance TextContainer ResolvedSpan where
    getText = spanText

-- | Wrapper for temporarily mapping the relationship to a `Span`.
data WithSpan a = WithSpan ResolvedSpan a

instance Functor WithSpan where
    fmap f (WithSpan s a) = WithSpan s (f a)

instance TextContainer a => TextContainer (WithSpan a) where
    getText (WithSpan _ c) = getText c

instance SeparableTextContainer a => SeparableTextContainer (WithSpan a) where
    splitTextAt8 n (WithSpan rs c) = (WithSpan rs c1, WithSpan rs c2)
        where (c1, c2) = splitTextAt8 n c

splitBySpanIndex :: [WithSpan a] -> [[a]]
splitBySpanIndex xs = [getBySpanIndex i xs | i <- [0..]]

getBySpanIndex :: Int -> [WithSpan a] -> [a]
getBySpanIndex idx xs = map contents $ filter matchingIndex $ xs
    where
        matchingIndex (WithSpan rs _) = (spanIndex rs) == idx
        contents (WithSpan _ x) = x

M src/Data/Text/ParagraphLayout/Internal/Span.hs => src/Data/Text/ParagraphLayout/Internal/Span.hs +19 -1
@@ 1,6 1,15 @@
module Data.Text.ParagraphLayout.Internal.Span (Span(..))
module Data.Text.ParagraphLayout.Internal.Span
    (Span(..)
    ,SpanLayout(..)
    ,spanRects
    )
where

import Data.Int (Int32)

import Data.Text.ParagraphLayout.Internal.Fragment
import Data.Text.ParagraphLayout.Internal.Rect

-- | A paragraph is broken into spans by the caller.
--
-- Each span could have a different font family, size, style, text decoration,


@@ 16,3 25,12 @@ data Span = Span

    }
    deriving (Eq, Read, Show)

-- | The resulting layout of each span, which may include multiple fragments if
-- broken over multiple lines.
data SpanLayout = SpanLayout [Fragment]
    -- TODO: Consider merging. fragments created by script changes.
    deriving (Eq, Read, Show)

spanRects :: SpanLayout -> [Rect Int32]
spanRects (SpanLayout frags) = map fragmentRect frags

M src/Data/Text/ParagraphLayout/Internal/TextContainer.hs => src/Data/Text/ParagraphLayout/Internal/TextContainer.hs +9 -0
@@ 1,13 1,16 @@
module Data.Text.ParagraphLayout.Internal.TextContainer
    (SeparableTextContainer
    ,TextContainer
    ,collapse
    ,getText
    ,splitTextAt8
    ,splitTextsAt8
    )
where

import Data.List.NonEmpty (NonEmpty((:|)))
import Data.Text (Text)
import qualified Data.Text as Text
import Data.Text.Foreign (lengthWord8)

-- | Class of data types containing `Text` that can be accessed.


@@ 43,3 46,9 @@ splitTextsAt8' n rpre (r:rs)
    | otherwise = let (r1, r2) = splitTextAt8 n r in (r1:rpre, r2:rs)
    where
        l = lengthWord8 $ getText r

-- | If the first container in the list is empty, remove it.
collapse :: SeparableTextContainer a => NonEmpty a -> [a]
collapse (x :| xs)
    | Text.null (getText x) = xs
    | otherwise = x:xs