From dbcfcd67306ba9ba4b66b55f70fa6d0f869e90c4 Mon Sep 17 00:00:00 2001 From: Jaro Date: Tue, 21 Mar 2023 16:56:48 +0100 Subject: [PATCH] Allow empty lines when hard line breaking. --- .golden/hardBreaksLTRParagraph/golden | 8 +++-- .golden/hardBreaksRTLParagraph/golden | 8 +++-- .../Text/ParagraphLayout/Internal/Plain.hs | 32 ++++++++++++------- .../ParagraphLayout/Internal/TextContainer.hs | 20 ++++++++++++ .../ParagraphLayout/Internal/BreakSpec.hs | 7 ++++ 5 files changed, 58 insertions(+), 17 deletions(-) diff --git a/.golden/hardBreaksLTRParagraph/golden b/.golden/hardBreaksLTRParagraph/golden index ab877b9..6c4764a 100644 --- a/.golden/hardBreaksLTRParagraph/golden +++ b/.golden/hardBreaksLTRParagraph/golden @@ -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})] }] diff --git a/.golden/hardBreaksRTLParagraph/golden b/.golden/hardBreaksRTLParagraph/golden index 0f4d4b4..98ce54a 100644 --- a/.golden/hardBreaksRTLParagraph/golden +++ b/.golden/hardBreaksRTLParagraph/golden @@ -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})] }] diff --git a/src/Data/Text/ParagraphLayout/Internal/Plain.hs b/src/Data/Text/ParagraphLayout/Internal/Plain.hs index 6005896..3488e1d 100644 --- a/src/Data/Text/ParagraphLayout/Internal/Plain.hs +++ b/src/Data/Text/ParagraphLayout/Internal/Plain.hs @@ -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. diff --git a/src/Data/Text/ParagraphLayout/Internal/TextContainer.hs b/src/Data/Text/ParagraphLayout/Internal/TextContainer.hs index 2cc4737..d0ef485 100644 --- a/src/Data/Text/ParagraphLayout/Internal/TextContainer.hs +++ b/src/Data/Text/ParagraphLayout/Internal/TextContainer.hs @@ -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 diff --git a/test/Data/Text/ParagraphLayout/Internal/BreakSpec.hs b/test/Data/Text/ParagraphLayout/Internal/BreakSpec.hs index 37d9b06..094e8dc 100644 --- a/test/Data/Text/ParagraphLayout/Internal/BreakSpec.hs +++ b/test/Data/Text/ParagraphLayout/Internal/BreakSpec.hs @@ -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) -- 2.30.2