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)