From a0aa0972616b041c06f206451c4fd2845f6e4142 Mon Sep 17 00:00:00 2001 From: Jaro Date: Wed, 26 Apr 2023 18:12:09 +0200 Subject: [PATCH] Separate interface-independent layout algorithm. --- balkon.cabal | 1 + .../Text/ParagraphLayout/Internal/Layout.hs | 276 ++++++++++++++++++ .../Text/ParagraphLayout/Internal/Plain.hs | 265 +---------------- 3 files changed, 279 insertions(+), 263 deletions(-) create mode 100644 src/Data/Text/ParagraphLayout/Internal/Layout.hs diff --git a/balkon.cabal b/balkon.cabal index c8e172a..41f9d4d 100644 --- a/balkon.cabal +++ b/balkon.cabal @@ -121,6 +121,7 @@ library balkon-internal -- Modules used purely internally and not in any tests. other-modules: + Data.Text.ParagraphLayout.Internal.Layout, Data.Text.ParagraphLayout.Internal.ParagraphExtents, Data.Text.ParagraphLayout.Internal.ParagraphLine, Data.Text.ParagraphLayout.Internal.ProtoFragment, diff --git a/src/Data/Text/ParagraphLayout/Internal/Layout.hs b/src/Data/Text/ParagraphLayout/Internal/Layout.hs new file mode 100644 index 0000000..983f1a2 --- /dev/null +++ b/src/Data/Text/ParagraphLayout/Internal/Layout.hs @@ -0,0 +1,276 @@ +-- | Implementation of paragraph layout, decoupled from external interfaces. +module Data.Text.ParagraphLayout.Internal.Layout + ( FragmentWithSpan + , layoutAndAlignLines + ) +where + +import Data.Foldable (toList) +import Data.Int (Int32) +import Data.List (mapAccumL) +import Data.List.NonEmpty (NonEmpty ((:|)), nonEmpty, (<|)) +import qualified Data.List.NonEmpty as NonEmpty +import Data.Maybe (catMaybes) +import Data.Text.Foreign (lengthWord8) +import Data.Text.Glyphize + ( Buffer (..) + , ContentType (ContentTypeUnicode) + , FontExtents (..) + , GlyphInfo + , GlyphPos + , defaultBuffer + , fontExtentsForDir + , shape + ) +import qualified Data.Text.ICU as BreakStatus (Line (Hard)) +import qualified Data.Text.Lazy as Lazy + +import Data.Text.ParagraphLayout.Internal.BiDiReorder +import Data.Text.ParagraphLayout.Internal.Break +import Data.Text.ParagraphLayout.Internal.Fragment +import Data.Text.ParagraphLayout.Internal.LineHeight +import Data.Text.ParagraphLayout.Internal.ParagraphExtents +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.TextContainer + +-- This is redundant. +-- TODO: Consider using `ResolvedSpan` as `fragmentUserData`, then swapping it +-- for the actual `spanUserData` before returning it to the user. +type FragmentWithSpan d = WithSpan d (Fragment d) + +-- | Create a multi-line layout from the given runs, splitting them as +-- necessary to fit within the requested line width. +-- +-- The output is a flat list of fragments positioned in both dimensions. +layoutAndAlignLines :: Int32 -> NonEmpty (WithSpan d Run) -> + [FragmentWithSpan d] +layoutAndAlignLines maxWidth runs = frags + where + frags = concatMap NonEmpty.toList fragsInLines + (_, fragsInLines) = mapAccumL positionLineH originY numberedLines + numberedLines = zip [1 ..] canonicalLines + canonicalLines = fmap reorder logicalLines + logicalLines = nonEmptyItems $ layoutLines maxWidth runs + originY = paragraphOriginY + +nonEmptyItems :: Foldable t => t [a] -> [NonEmpty a] +nonEmptyItems = catMaybes . map nonEmpty . toList + +-- | Create a multi-line layout from the given runs, splitting them as +-- necessary to fit within the requested line width. +-- +-- The output is a two-dimensional list of fragments positioned along the +-- horizontal axis. +layoutLines :: + Int32 -> NonEmpty (WithSpan d Run) -> NonEmpty [WithSpan d PF.ProtoFragment] +layoutLines maxWidth runs = case nonEmpty rest of + -- Everything fits. We are done. + Nothing -> fitting :| [] + -- Something fits, the rest goes on the next line. + Just rest' -> fitting <| layoutLines maxWidth rest' + where + (fitting, rest) = layoutAndWrapRunsH maxWidth runs + +-- TODO: Allow a run across multiple spans (e.g. if they only differ by colour). + +-- | Position all the given horizontal fragments on the same line, +-- using @originY@ as its top edge, and return the bottom edge for continuation. +-- +-- Glyphs will be aligned by their ascent line, similar to the behaviour of +-- @vertical-align: top@ in CSS. +-- +-- TODO: For rich text, allow other types of vertical alignment. +positionLineH :: Int32 -> (Int, NonEmpty (WithSpan d PF.ProtoFragment)) -> + (Int32, NonEmpty (FragmentWithSpan d)) +positionLineH originY (line, pfs) = (nextY, frags) + where + nextY = maximum $ fmap y_min rects + rects = fmap (\ (WithSpan _ r) -> fragmentRect r) frags + (_, frags) = mapAccumL (positionFragmentH line originY) originX pfs + originX = paragraphOriginX + +-- | Position the given horizontal fragment on a line, +-- using @originY@ as its top edge and @originX@ as its left edge, +-- returning the X coordinate of its right edge for continuation. +positionFragmentH :: Int -> Int32 -> Int32 -> WithSpan d PF.ProtoFragment -> + (Int32, FragmentWithSpan d) +positionFragmentH line originY originX (WithSpan rs pf) = + (nextX, WithSpan rs frag) + where + nextX = originX + PF.advance pf + frag = Fragment userData line rect (penX, penY) (PF.glyphs pf) + userData = RS.spanUserData rs + rect = Rect originX originY (PF.advance pf) (-lineHeight) + penX = 0 + penY = descent + leading `div` 2 - lineHeight + lineHeight = case RS.spanLineHeight rs of + Normal -> normalLineHeight + Absolute h -> h + leading = lineHeight - normalLineHeight + normalLineHeight = ascent + descent + ascent = ascender extents + descent = - descender extents + extents = fontExtentsForDir font (PF.direction pf) + font = RS.spanFont rs + +-- | Calculate layout for multiple horizontal runs, breaking them as necessary +-- to fit as much content as possible without exceeding the maximum line width, +-- and return the remaining runs to be placed on other lines. +layoutAndWrapRunsH :: Int32 -> NonEmpty (WithSpan d Run) -> + ([WithSpan d PF.ProtoFragment], [WithSpan d Run]) +layoutAndWrapRunsH maxWidth runs = NonEmpty.head $ validLayouts + where + validLayouts = dropWhile1 tooLong layouts + tooLong (pfs, _) = totalAdvances pfs > maxWidth + layouts = fmap layoutFst splits + layoutFst (runs1, runs2) = (layoutRunsH runs1, runs2) + -- TODO: Consider optimising. + -- We do not need to look for soft breaks further than the + -- shortest hard break. + 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 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 :: NonEmpty (WithSpan d Run) -> ([WithSpan d Run], [WithSpan d Run]) +hardSplit runs = allowFstEmpty $ trimFst $ NonEmpty.last $ splits + where + trimFst (runs1, runs2) = (trim runs1, runs2) + trim + = trimTextsStartPreserve isStartSpace + . 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 allowSndEmpty hSplits + noSplit = (runs, []) + hSplits = -- from longest to shortest + splitTextsBy (map fst . filter isHard . runLineBreaks) runs + isHard (_, status) = status == BreakStatus.Hard + +-- | Treat a list of runs as a contiguous sequence, +-- and find all possible ways to split them into two non-empty lists, +-- 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 :: NonEmpty (WithSpan d Run) -> + [([WithSpan d Run], [WithSpan d Run])] +softSplits runs = map (allowSndEmpty . trimFst) splits + where + trimFst (runs1, runs2) = (trim runs1, runs2) + trim = trimTextsStart isStartSpace . trimTextsEnd isEndSpace + splits = lSplits ++ cSplits + lSplits = splitTextsBy (map fst . runLineBreaks) runs + -- TODO: Consider optimising. + -- We do not need to look for character breaks further than the + -- shortest line break. + cSplits = splitTextsBy (map fst . runCharacterBreaks) runs + +allowFstEmpty :: (NonEmpty a, b) -> ([a], b) +allowFstEmpty (a, b) = (NonEmpty.toList a, b) + +allowSndEmpty :: (a, NonEmpty b) -> (a, [b]) +allowSndEmpty (a, b) = (a, NonEmpty.toList b) + +-- | The suffix remaining after removing the longest prefix of the list for +-- which the predicate holds, except always including at least the last element +-- of the original list. +dropWhile1 :: (a -> Bool) -> NonEmpty a -> NonEmpty a +dropWhile1 p list = case NonEmpty.uncons list of + (_, Nothing) -> list + (x, Just xs) -> if p x + then dropWhile1 p xs + else list + +-- | Calculate layout for multiple horizontal runs on the same line, without +-- any breaking. +layoutRunsH :: [WithSpan d Run] -> [WithSpan d PF.ProtoFragment] +layoutRunsH runs = map layoutRunH runs + +-- | Sum of all advances within the given fragments. +totalAdvances :: [WithSpan d PF.ProtoFragment] -> Int32 +totalAdvances pfs = sum $ map (\ (WithSpan _ pf) -> PF.advance pf) pfs + +-- | Calculate layout for the given horizontal run and attach extra information. +layoutRunH :: WithSpan d Run -> WithSpan d PF.ProtoFragment +layoutRunH (WithSpan rs run) = WithSpan rs pf + where + pf = PF.protoFragmentH dir glyphs + glyphs = shapeRun (WithSpan rs run) + dir = runDirection run + +-- | Calculate layout for the given run independently of its position. +shapeRun :: WithSpan d Run -> [(GlyphInfo, GlyphPos)] +shapeRun (WithSpan rs run) = shape font buffer features + where + font = RS.spanFont rs + buffer = defaultBuffer + { text = Lazy.fromStrict $ runText run + , contentType = Just ContentTypeUnicode + , direction = runDirection run + , script = runScript run + , language = Just $ RS.spanLanguage rs + -- Perhaps counter-intuitively, the `beginsText` and `endsText` + -- flags refer to everything that "Data.Text.Glyphize" can see, + -- not just the current run. + -- + -- Since all runs are cut from a single continuous `Text` that + -- represents the entire paragraph, and "Data.Text.Glyphize" peeks + -- at the whole underlying byte array, HarfBuzz will be able to see + -- both the beginning and the end of the paragraph at all times, + -- so these flags can always be set. + , beginsText = True + , endsText = True + } + features = [] + +runLineBreaks :: WithSpan d Run -> [(Int, BreakStatus.Line)] +runLineBreaks (WithSpan rs run) = + runBreaksFromSpan run $ RS.spanLineBreaks rs + +runCharacterBreaks :: WithSpan d Run -> [(Int, ())] +runCharacterBreaks (WithSpan rs run) = + runBreaksFromSpan run $ RS.spanCharacterBreaks rs + +-- | Constrain span breaks to a selected run and adjust offsets. +runBreaksFromSpan :: Run -> [(Int, a)] -> [(Int, a)] +runBreaksFromSpan run spanBreaks = + dropWhile (not . valid) $ subOffsetsDesc (runOffsetInSpan run) spanBreaks + where + valid (off, _) = off < runLength + runLength = lengthWord8 $ getText run + +-- | Predicate for characters that can be potentially removed from the +-- beginning of a line according to the CSS Text Module. +isStartSpace :: Char -> Bool +isStartSpace c = c `elem` [' ', '\t'] + +-- | Predicate for characters that can be potentially removed from the end of +-- a line according to the CSS Text Module. +isEndSpace :: Char -> Bool +isEndSpace c = c `elem` [' ', '\t', '\x1680'] + +-- | Predicate for characters that should be removed from the end of a line in +-- the case of a hard line break. +isNewline :: Char -> Bool +isNewline c = c == '\n' diff --git a/src/Data/Text/ParagraphLayout/Internal/Plain.hs b/src/Data/Text/ParagraphLayout/Internal/Plain.hs index ce4ff1f..21c1d7f 100644 --- a/src/Data/Text/ParagraphLayout/Internal/Plain.hs +++ b/src/Data/Text/ParagraphLayout/Internal/Plain.hs @@ -2,48 +2,20 @@ module Data.Text.ParagraphLayout.Internal.Plain (layoutPlain) where import Control.Applicative (ZipList (ZipList), getZipList) -import Data.Foldable (toList) -import Data.Int (Int32) -import Data.List (mapAccumL) -import Data.List.NonEmpty (NonEmpty ((:|)), nonEmpty, (<|)) +import Data.List.NonEmpty (nonEmpty) import qualified Data.List.NonEmpty as NonEmpty -import Data.Maybe (catMaybes) import Data.Text (Text) -import Data.Text.Foreign (lengthWord8) -import Data.Text.Glyphize - ( Buffer (..) - , ContentType (ContentTypeUnicode) - , FontExtents (..) - , GlyphInfo - , GlyphPos - , defaultBuffer - , fontExtentsForDir - , shape - ) import Data.Text.ICU (Breaker, LocaleName, breakCharacter, breakLine) -import qualified Data.Text.ICU as BreakStatus (Line (Hard)) -import qualified Data.Text.Lazy as Lazy -import Data.Text.ParagraphLayout.Internal.BiDiReorder import Data.Text.ParagraphLayout.Internal.Break -import Data.Text.ParagraphLayout.Internal.Fragment -import Data.Text.ParagraphLayout.Internal.LineHeight -import Data.Text.ParagraphLayout.Internal.ParagraphExtents +import Data.Text.ParagraphLayout.Internal.Layout import Data.Text.ParagraphLayout.Internal.ParagraphOptions import Data.Text.ParagraphLayout.Internal.Plain.Paragraph import Data.Text.ParagraphLayout.Internal.Plain.ParagraphLayout -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 - --- This is redundant. --- TODO: Consider using `ResolvedSpan` as `fragmentUserData`, then swapping it --- for the actual `spanUserData` before returning it to the user. -type FragmentWithSpan d = WithSpan d (Fragment d) -- | Lay out a paragraph of plain, unidirectional text using a single font. layoutPlain :: Paragraph d -> ParagraphLayout d @@ -68,208 +40,6 @@ spansToRunsWrapped ss = concat $ map spanToRunsWrapped ss spanToRunsWrapped :: RS.ResolvedSpan d -> [WithSpan d Run] spanToRunsWrapped s = map (WithSpan s) (spanToRuns s) --- | Create a multi-line layout from the given runs, splitting them as --- necessary to fit within the requested line width. --- --- The output is a flat list of fragments positioned in both dimensions. -layoutAndAlignLines :: Int32 -> NonEmpty (WithSpan d Run) -> - [FragmentWithSpan d] -layoutAndAlignLines maxWidth runs = frags - where - frags = concatMap NonEmpty.toList fragsInLines - (_, fragsInLines) = mapAccumL positionLineH originY numberedLines - numberedLines = zip [1 ..] canonicalLines - canonicalLines = fmap reorder logicalLines - logicalLines = nonEmptyItems $ layoutLines maxWidth runs - originY = paragraphOriginY - -nonEmptyItems :: Foldable t => t [a] -> [NonEmpty a] -nonEmptyItems = catMaybes . map nonEmpty . toList - --- | Create a multi-line layout from the given runs, splitting them as --- necessary to fit within the requested line width. --- --- The output is a two-dimensional list of fragments positioned along the --- horizontal axis. -layoutLines :: - Int32 -> NonEmpty (WithSpan d Run) -> NonEmpty [WithSpan d PF.ProtoFragment] -layoutLines maxWidth runs = case nonEmpty rest of - -- Everything fits. We are done. - Nothing -> fitting :| [] - -- Something fits, the rest goes on the next line. - Just rest' -> fitting <| layoutLines maxWidth rest' - where - (fitting, rest) = layoutAndWrapRunsH maxWidth runs - --- TODO: Allow a run across multiple spans (e.g. if they only differ by colour). - --- | Position all the given horizontal fragments on the same line, --- using @originY@ as its top edge, and return the bottom edge for continuation. --- --- Glyphs will be aligned by their ascent line, similar to the behaviour of --- @vertical-align: top@ in CSS. --- --- TODO: For rich text, allow other types of vertical alignment. -positionLineH :: Int32 -> (Int, NonEmpty (WithSpan d PF.ProtoFragment)) -> - (Int32, NonEmpty (FragmentWithSpan d)) -positionLineH originY (line, pfs) = (nextY, frags) - where - nextY = maximum $ fmap y_min rects - rects = fmap (\ (WithSpan _ r) -> fragmentRect r) frags - (_, frags) = mapAccumL (positionFragmentH line originY) originX pfs - originX = paragraphOriginX - --- | Position the given horizontal fragment on a line, --- using @originY@ as its top edge and @originX@ as its left edge, --- returning the X coordinate of its right edge for continuation. -positionFragmentH :: Int -> Int32 -> Int32 -> WithSpan d PF.ProtoFragment -> - (Int32, FragmentWithSpan d) -positionFragmentH line originY originX (WithSpan rs pf) = - (nextX, WithSpan rs frag) - where - nextX = originX + PF.advance pf - frag = Fragment userData line rect (penX, penY) (PF.glyphs pf) - userData = RS.spanUserData rs - rect = Rect originX originY (PF.advance pf) (-lineHeight) - penX = 0 - penY = descent + leading `div` 2 - lineHeight - lineHeight = case RS.spanLineHeight rs of - Normal -> normalLineHeight - Absolute h -> h - leading = lineHeight - normalLineHeight - normalLineHeight = ascent + descent - ascent = ascender extents - descent = - descender extents - extents = fontExtentsForDir font (PF.direction pf) - font = RS.spanFont rs - --- | Calculate layout for multiple horizontal runs, breaking them as necessary --- to fit as much content as possible without exceeding the maximum line width, --- and return the remaining runs to be placed on other lines. -layoutAndWrapRunsH :: Int32 -> NonEmpty (WithSpan d Run) -> - ([WithSpan d PF.ProtoFragment], [WithSpan d Run]) -layoutAndWrapRunsH maxWidth runs = NonEmpty.head $ validLayouts - where - validLayouts = dropWhile1 tooLong layouts - tooLong (pfs, _) = totalAdvances pfs > maxWidth - layouts = fmap layoutFst splits - layoutFst (runs1, runs2) = (layoutRunsH runs1, runs2) - -- TODO: Consider optimising. - -- We do not need to look for soft breaks further than the - -- shortest hard break. - 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 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 :: NonEmpty (WithSpan d Run) -> ([WithSpan d Run], [WithSpan d Run]) -hardSplit runs = allowFstEmpty $ trimFst $ NonEmpty.last $ splits - where - trimFst (runs1, runs2) = (trim runs1, runs2) - trim - = trimTextsStartPreserve isStartSpace - . 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 allowSndEmpty hSplits - noSplit = (runs, []) - hSplits = -- from longest to shortest - splitTextsBy (map fst . filter isHard . runLineBreaks) runs - isHard (_, status) = status == BreakStatus.Hard - --- | Treat a list of runs as a contiguous sequence, --- and find all possible ways to split them into two non-empty lists, --- 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 :: NonEmpty (WithSpan d Run) -> - [([WithSpan d Run], [WithSpan d Run])] -softSplits runs = map (allowSndEmpty . trimFst) splits - where - trimFst (runs1, runs2) = (trim runs1, runs2) - trim = trimTextsStart isStartSpace . trimTextsEnd isEndSpace - splits = lSplits ++ cSplits - lSplits = splitTextsBy (map fst . runLineBreaks) runs - -- TODO: Consider optimising. - -- We do not need to look for character breaks further than the - -- shortest line break. - cSplits = splitTextsBy (map fst . runCharacterBreaks) runs - -allowFstEmpty :: (NonEmpty a, b) -> ([a], b) -allowFstEmpty (a, b) = (NonEmpty.toList a, b) - -allowSndEmpty :: (a, NonEmpty b) -> (a, [b]) -allowSndEmpty (a, b) = (a, NonEmpty.toList b) - --- | The suffix remaining after removing the longest prefix of the list for --- which the predicate holds, except always including at least the last element --- of the original list. -dropWhile1 :: (a -> Bool) -> NonEmpty a -> NonEmpty a -dropWhile1 p list = case NonEmpty.uncons list of - (_, Nothing) -> list - (x, Just xs) -> if p x - then dropWhile1 p xs - else list - --- | Calculate layout for multiple horizontal runs on the same line, without --- any breaking. -layoutRunsH :: [WithSpan d Run] -> [WithSpan d PF.ProtoFragment] -layoutRunsH runs = map layoutRunH runs - --- | Sum of all advances within the given fragments. -totalAdvances :: [WithSpan d PF.ProtoFragment] -> Int32 -totalAdvances pfs = sum $ map (\ (WithSpan _ pf) -> PF.advance pf) pfs - --- | Calculate layout for the given horizontal run and attach extra information. -layoutRunH :: WithSpan d Run -> WithSpan d PF.ProtoFragment -layoutRunH (WithSpan rs run) = WithSpan rs pf - where - pf = PF.protoFragmentH dir glyphs - glyphs = shapeRun (WithSpan rs run) - dir = runDirection run - --- | Calculate layout for the given run independently of its position. -shapeRun :: WithSpan d Run -> [(GlyphInfo, GlyphPos)] -shapeRun (WithSpan rs run) = shape font buffer features - where - font = RS.spanFont rs - buffer = defaultBuffer - { text = Lazy.fromStrict $ runText run - , contentType = Just ContentTypeUnicode - , direction = runDirection run - , script = runScript run - , language = Just $ RS.spanLanguage rs - -- Perhaps counter-intuitively, the `beginsText` and `endsText` - -- flags refer to everything that "Data.Text.Glyphize" can see, - -- not just the current run. - -- - -- Since all runs are cut from a single continuous `Text` that - -- represents the entire paragraph, and "Data.Text.Glyphize" peeks - -- at the whole underlying byte array, HarfBuzz will be able to see - -- both the beginning and the end of the paragraph at all times, - -- so these flags can always be set. - , beginsText = True - , endsText = True - } - features = [] - resolveSpans :: Paragraph d -> [RS.ResolvedSpan d] resolveSpans p@(Paragraph _ pStart spans pOpts) = do let sBounds = paragraphSpanBounds p @@ -300,34 +70,3 @@ resolveSpans p@(Paragraph _ pStart spans pOpts) = do paragraphBreaks :: (LocaleName -> Breaker a) -> Text -> String -> [(Int, a)] paragraphBreaks breakFunc txt lang = breaksDesc (breakFunc (locale lang LBAuto)) txt - -runLineBreaks :: WithSpan d Run -> [(Int, BreakStatus.Line)] -runLineBreaks (WithSpan rs run) = - runBreaksFromSpan run $ RS.spanLineBreaks rs - -runCharacterBreaks :: WithSpan d Run -> [(Int, ())] -runCharacterBreaks (WithSpan rs run) = - runBreaksFromSpan run $ RS.spanCharacterBreaks rs - --- | Constrain span breaks to a selected run and adjust offsets. -runBreaksFromSpan :: Run -> [(Int, a)] -> [(Int, a)] -runBreaksFromSpan run spanBreaks = - dropWhile (not . valid) $ subOffsetsDesc (runOffsetInSpan run) spanBreaks - where - valid (off, _) = off < runLength - runLength = lengthWord8 $ getText run - --- | Predicate for characters that can be potentially removed from the --- beginning of a line according to the CSS Text Module. -isStartSpace :: Char -> Bool -isStartSpace c = c `elem` [' ', '\t'] - --- | Predicate for characters that can be potentially removed from the end of --- a line according to the CSS Text Module. -isEndSpace :: Char -> Bool -isEndSpace c = c `elem` [' ', '\t', '\x1680'] - --- | Predicate for characters that should be removed from the end of a line in --- the case of a hard line break. -isNewline :: Char -> Bool -isNewline c = c == '\n' -- 2.30.2