~jaro/balkon

dbcfcd67306ba9ba4b66b55f70fa6d0f869e90c4 — Jaro 11 months ago 21f976a
Allow empty lines when hard line breaking.
M .golden/hardBreaksLTRParagraph/golden => .golden/hardBreaksLTRParagraph/golden +5 -3
@@ 1,4 1,4 @@
ParagraphLayout {paragraphRect = Rect {x_origin = 0, y_origin = 0, x_size = 4305, y_size = -8968}, spanLayouts = [
ParagraphLayout {paragraphRect = Rect {x_origin = 0, y_origin = 0, x_size = 4305, y_size = -10089}, spanLayouts = [
    SpanLayout [Fragment {fragmentRect = Rect {x_origin = 0, y_origin = 0, x_size = 1563, y_size = -1121}, fragmentPen = (0,-932), fragmentGlyphs =
        [(GlyphInfo {codepoint = 77, cluster = 1, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False},GlyphPos {x_advance = 262, y_advance = 0, x_offset = 0, y_offset = 0}),
        (GlyphInfo {codepoint = 77, cluster = 2, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False},GlyphPos {x_advance = 262, y_advance = 0, x_offset = 0, y_offset = 0}),


@@ 42,13 42,15 @@ ParagraphLayout {paragraphRect = Rect {x_origin = 0, y_origin = 0, x_size = 4305
        (GlyphInfo {codepoint = 77, cluster = 40, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False},GlyphPos {x_advance = 262, y_advance = 0, x_offset = 0, y_offset = 0}),
        (GlyphInfo {codepoint = 77, cluster = 41, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False},GlyphPos {x_advance = 262, y_advance = 0, x_offset = 0, y_offset = 0}),
        (GlyphInfo {codepoint = 77, cluster = 42, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False},GlyphPos {x_advance = 253, y_advance = 0, x_offset = 0, y_offset = 0})]
    }, Fragment {fragmentRect = Rect {x_origin = 0, y_origin = -6726, x_size = 3675, y_size = -1121}, fragmentPen = (0,-932), fragmentGlyphs =
    }, Fragment {fragmentRect = Rect {x_origin = 0, y_origin = -6726, x_size = 0, y_size = -1121}, fragmentPen = (0,-932), fragmentGlyphs =
        []
    }, Fragment {fragmentRect = Rect {x_origin = 0, y_origin = -7847, x_size = 3675, y_size = -1121}, fragmentPen = (0,-932), fragmentGlyphs =
        [(GlyphInfo {codepoint = 80, cluster = 45, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False},GlyphPos {x_advance = 861, y_advance = 0, x_offset = 0, y_offset = 0}),
        (GlyphInfo {codepoint = 80, cluster = 46, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False},GlyphPos {x_advance = 861, y_advance = 0, x_offset = 0, y_offset = 0}),
        (GlyphInfo {codepoint = 3, cluster = 47, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False},GlyphPos {x_advance = 231, y_advance = 0, x_offset = 0, y_offset = 0}),
        (GlyphInfo {codepoint = 80, cluster = 48, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False},GlyphPos {x_advance = 861, y_advance = 0, x_offset = 0, y_offset = 0}),
        (GlyphInfo {codepoint = 80, cluster = 49, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False},GlyphPos {x_advance = 861, y_advance = 0, x_offset = 0, y_offset = 0})]
    }, Fragment {fragmentRect = Rect {x_origin = 0, y_origin = -7847, x_size = 1722, y_size = -1121}, fragmentPen = (0,-932), fragmentGlyphs =
    }, Fragment {fragmentRect = Rect {x_origin = 0, y_origin = -8968, x_size = 1722, y_size = -1121}, fragmentPen = (0,-932), fragmentGlyphs =
        [(GlyphInfo {codepoint = 80, cluster = 51, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False},GlyphPos {x_advance = 861, y_advance = 0, x_offset = 0, y_offset = 0}),
        (GlyphInfo {codepoint = 80, cluster = 52, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False},GlyphPos {x_advance = 861, y_advance = 0, x_offset = 0, y_offset = 0})]
    }]

M .golden/hardBreaksRTLParagraph/golden => .golden/hardBreaksRTLParagraph/golden +5 -3
@@ 1,4 1,4 @@
ParagraphLayout {paragraphRect = Rect {x_origin = 0, y_origin = 0, x_size = 5852, y_size = -12000}, spanLayouts = [
ParagraphLayout {paragraphRect = Rect {x_origin = 0, y_origin = 0, x_size = 5852, y_size = -13500}, spanLayouts = [
    SpanLayout [Fragment {fragmentRect = Rect {x_origin = 0, y_origin = 0, x_size = 2808, y_size = -1500}, fragmentPen = (0,-1085), fragmentGlyphs =
        [(GlyphInfo {codepoint = 642, cluster = 11, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False},GlyphPos {x_advance = 468, y_advance = 0, x_offset = 0, y_offset = 0}),
        (GlyphInfo {codepoint = 642, cluster = 9, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False},GlyphPos {x_advance = 468, y_advance = 0, x_offset = 0, y_offset = 0}),


@@ 43,13 43,15 @@ ParagraphLayout {paragraphRect = Rect {x_origin = 0, y_origin = 0, x_size = 5852
        (GlyphInfo {codepoint = 642, cluster = 73, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False},GlyphPos {x_advance = 468, y_advance = 0, x_offset = 0, y_offset = 0}),
        (GlyphInfo {codepoint = 642, cluster = 71, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False},GlyphPos {x_advance = 468, y_advance = 0, x_offset = 0, y_offset = 0}),
        (GlyphInfo {codepoint = 642, cluster = 69, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False},GlyphPos {x_advance = 468, y_advance = 0, x_offset = 0, y_offset = 0})]
    }, Fragment {fragmentRect = Rect {x_origin = 0, y_origin = -9000, x_size = 4156, y_size = -1500}, fragmentPen = (0,-1085), fragmentGlyphs =
    }, Fragment {fragmentRect = Rect {x_origin = 0, y_origin = -9000, x_size = 0, y_size = -1500}, fragmentPen = (0,-1085), fragmentGlyphs =
        []
    }, Fragment {fragmentRect = Rect {x_origin = 0, y_origin = -10500, x_size = 4156, y_size = -1500}, fragmentPen = (0,-1085), fragmentGlyphs =
        [(GlyphInfo {codepoint = 687, cluster = 90, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False},GlyphPos {x_advance = 1211, y_advance = 0, x_offset = 0, y_offset = 0}),
        (GlyphInfo {codepoint = 370, cluster = 88, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False},GlyphPos {x_advance = 749, y_advance = 0, x_offset = 0, y_offset = 0}),
        (GlyphInfo {codepoint = 3, cluster = 87, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False},GlyphPos {x_advance = 236, y_advance = 0, x_offset = 0, y_offset = 0}),
        (GlyphInfo {codepoint = 687, cluster = 85, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False},GlyphPos {x_advance = 1211, y_advance = 0, x_offset = 0, y_offset = 0}),
        (GlyphInfo {codepoint = 370, cluster = 83, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False},GlyphPos {x_advance = 749, y_advance = 0, x_offset = 0, y_offset = 0})]
    }, Fragment {fragmentRect = Rect {x_origin = 0, y_origin = -10500, x_size = 1960, y_size = -1500}, fragmentPen = (0,-1085), fragmentGlyphs =
    }, Fragment {fragmentRect = Rect {x_origin = 0, y_origin = -12000, x_size = 1960, y_size = -1500}, fragmentPen = (0,-1085), fragmentGlyphs =
        [(GlyphInfo {codepoint = 687, cluster = 95, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False},GlyphPos {x_advance = 1211, y_advance = 0, x_offset = 0, y_offset = 0}),
        (GlyphInfo {codepoint = 370, cluster = 93, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False},GlyphPos {x_advance = 749, y_advance = 0, x_offset = 0, y_offset = 0})]
    }]

M src/Data/Text/ParagraphLayout/Internal/Plain.hs => src/Data/Text/ParagraphLayout/Internal/Plain.hs +21 -11
@@ 118,9 118,9 @@ positionLineH :: Int32 -> [WithSpan PF.ProtoFragment] ->
    (Int32, [WithSpan Fragment])
positionLineH originY pfs = (nextY, frags)
    where
        -- A line with no glyphs will be considered to have zero height.
        -- A line with no fragments will be considered to have zero height.
        -- This can happen when line breaking produces a line that contains
        -- onls spaces.
        -- only spaces.
        nextY = if null rects then originY else maximum $ map y_min rects
        rects = map (\(WithSpan _ r) -> fragmentRect r) frags
        frags = snd $ mapAccumL (positionFragmentH originY) originX pfs


@@ 158,28 158,32 @@ layoutAndWrapRunsH maxWidth runs = NonEmpty.head $ validLayouts
        validLayouts = dropWhile1 tooLong layouts
        tooLong (pfs, _) = totalAdvances pfs > maxWidth
        layouts = NonEmpty.map layoutFst splits
        layoutFst (runs1, runs2) = (layout runs1, runs2)
        layout runs1 = layoutRunsH $ trimTextsEnd isEndSpace runs1
        layoutFst (runs1, runs2) = (layoutRunsH runs1, runs2)
        -- TODO: Consider optimising.
        --       We do not need to look for soft breaks further than the
        --       shortest hard break.
        -- TODO: Add a "strut" for empty lines.
        splits = hardSplit runs :| softSplits runs

-- | Treat a list of runs as a contiguous sequence, and split them into two
-- lists so that the first list contains as much of the input text as possible
-- without crossing a hard line break (typically after a newline character).
-- lists so that the first list contains as many non-whitespace characters as
-- possible without crossing a hard line break (typically after a newline
-- character).
--
-- If the input is non-empty and starts with a hard line break, then the first
-- output list will contain a run of zero characters. This can be used to
-- correctly size an empty line.
--
-- If there is no hard line break in the input, the first output list will
-- contain the whole input, and the second output list will be empty.
hardSplit :: [WithSpan Run] -> ([WithSpan Run], [WithSpan Run])
hardSplit runs = NonEmpty.last $ splits
hardSplit runs = trimFst $ NonEmpty.last $ splits
    where
        trimFst (runs1, runs2) = (trim runs1, runs2)
        trim = trimTextsEndPreserve isEndSpace . trimTextsEndPreserve isNewline
        -- TODO: Consider optimising.
        --       We do not need to look for any line breaks further than the
        --       shortest hard break.
        splits = noSplit :| map trimFst hSplits
        trimFst (runs1, runs2) = (trimTextsEnd isNewline runs1, runs2)
        splits = noSplit :| hSplits
        noSplit = (runs, [])
        hSplits = -- from longest to shortest
            splitTextsBy (map fst . filter isHard . runLineBreaks) runs


@@ 190,12 194,18 @@ hardSplit runs = NonEmpty.last $ splits
-- using soft line break opportunities (typically after words) and then
-- using character boundaries.
--
-- Runs of zero characters will not be created. If line breaking would result
-- in a line that consists entirely of whitespace, this whitespace will be
-- skipped, so an empty line is not created.
--
-- The results in the form (prefix, suffix) will be ordered so that items
-- closer to the start of the list are preferred for line breaking, but without
-- considering overflows.
softSplits :: [WithSpan Run] -> [([WithSpan Run], [WithSpan Run])]
softSplits runs = splits
softSplits runs = map trimFst splits
    where
        trimFst (runs1, runs2) = (trim runs1, runs2)
        trim = trimTextsEnd isEndSpace
        splits = lSplits ++ cSplits
        lSplits = splitTextsBy (map fst . runLineBreaks) runs
        -- TODO: Consider optimising.

M src/Data/Text/ParagraphLayout/Internal/TextContainer.hs => src/Data/Text/ParagraphLayout/Internal/TextContainer.hs +20 -0
@@ 6,6 6,7 @@ module Data.Text.ParagraphLayout.Internal.TextContainer
    ,splitTextAt8
    ,splitTextsBy
    ,trimTextsEnd
    ,trimTextsEndPreserve
    )
where



@@ 77,9 78,24 @@ collapse (tc :| tcs)

-- | Treat a list of text containers as a contiguous sequence,
-- and remove a suffix of characters that match the given predicate.
--
-- Empty text containers are removed from the output, so the result may
-- potentially be an empty list.
trimTextsEnd :: SeparableTextContainer a => (Char -> Bool) -> [a] -> [a]
trimTextsEnd p tcs = trimTextsEnd' p (reverse tcs)

-- | Treat a list of text containers as a contiguous sequence,
-- and remove a suffix of characters that match the given predicate.
--
-- Empty text containers are removed from the output except the first one,
-- which is instead truncated to zero length.
trimTextsEndPreserve ::
    SeparableTextContainer a => (Char -> Bool) -> [a] -> [a]
trimTextsEndPreserve _ [] = []
trimTextsEndPreserve p ins@(in1:_) = case trimTextsEnd' p (reverse ins) of
    [] -> [truncateText in1]
    out -> out

trimTextsEnd' :: SeparableTextContainer a => (Char -> Bool) -> [a] -> [a]
trimTextsEnd' _ [] = []
trimTextsEnd' p (tc:tcs)


@@ 87,3 103,7 @@ trimTextsEnd' p (tc:tcs)
    | otherwise = reverse $ trimmed:tcs
    where
        trimmed = dropWhileEnd p tc

-- | Discard all text from the container by creating a prefix of length 0.
truncateText :: SeparableTextContainer a => a -> a
truncateText tc = fst $ splitTextAt8 0 tc

M test/Data/Text/ParagraphLayout/Internal/BreakSpec.hs => test/Data/Text/ParagraphLayout/Internal/BreakSpec.hs +7 -0
@@ 37,6 37,13 @@ spec = do
                    ,(0, BreakStatus.Soft)
                    ]

            it "finds hard break after each of newlines" $
                b "en" (pack "hello\n\nworld") `shouldBe`
                    [(7, BreakStatus.Hard)
                    ,(6, BreakStatus.Hard)
                    ,(0, BreakStatus.Soft)
                    ]

            it "finds soft breaks after spaces and tabs" $
                b "en" (pack "a few\twords") `shouldBe`
                    [(6, BreakStatus.Soft)