matterhorn-50200.13.0: src/Matterhorn/Draw/RichText/Wrap.hs
-- | This module performs line-wrapping of sequences of flattend inline
-- values produced by 'flattenInlineSeq'.
--
-- This process works by maintaining a 'WrapState' in the 'WrapM'
-- monad, where inline values are pushed onto the current line, and
-- line breaks are introduced as inlines exceed the available width.
-- The most important caveat of this module is that wrapping depends
-- on knowing the width of each 'FlattenedValue', which is provided
-- by the 'fvWidth' function. But 'fvWidth' must return values that
-- are consistent with the how the inlines actually get rendered by
-- 'renderFlattenedValue'. This is because there are visual aspects to
-- how some inlines get rendered that are implicit, such as user or
-- channel sigils that get added at drawing time, that have an impact on
-- their visible width.
module Matterhorn.Draw.RichText.Wrap
( WrappedLine
, doLineWrapping
)
where
import Prelude ()
import Matterhorn.Prelude
import qualified Brick as B
import Control.Monad.State
import qualified Data.Sequence as Seq
import Data.Sequence ( ViewL(..)
, (|>)
)
import qualified Data.Text as T
import Matterhorn.Constants ( normalChannelSigil, userSigil )
import Matterhorn.Draw.RichText.Flatten
import Matterhorn.Constants ( editMarking )
type WrappedLine a = Seq (FlattenedValue a)
data WrapState a =
WrapState { wrapCompletedLines :: Seq (WrappedLine a)
-- ^ The completed lines so far
, wrapCurLine :: (WrappedLine a)
-- ^ The current line we are accumulating
, wrapCurCol :: Int
-- ^ The width of wrapCurLine, in columns
, wrapWidth :: Int
-- ^ The maximum allowable width
}
type WrapM a b = State (WrapState b) a
-- | Push a flattened value onto the current line if possible, or add a
-- line break and add the inline value to a new line if it would cause
-- the current line width to exceed the maximum.
pushValue :: FlattenedValue a -> WrapM () a
pushValue i = do
let iw = fvWidth i
pushThisInline =
modify $ \st -> st { wrapCurLine = wrapCurLine st |> i
, wrapCurCol = wrapCurCol st + iw
}
maxWidth <- gets wrapWidth
curWidth <- gets wrapCurCol
let remaining = maxWidth - curWidth
when (iw > remaining) pushLine
pushThisInline
-- | Insert a new line break by moving the current accumulating line
-- onto the completed lines list and resetting it to empty.
pushLine :: WrapM () a
pushLine = do
let trimLeadingWhitespace s =
case Seq.viewl s of
SingleInline i :< t | fiValue i == FSpace -> trimLeadingWhitespace t
_ -> s
modify $ \st ->
st { wrapCurLine = mempty
, wrapCompletedLines = wrapCompletedLines st |> trimLeadingWhitespace (wrapCurLine st)
, wrapCurCol = 0
}
-- | Given a maximum width and an inline sequence, produce a sequence of
-- lines wrapped at the specified column. This only returns lines longer
-- than the maximum width when those lines have a single inline value
-- that cannot be broken down further (such as a long URL).
doLineWrapping :: Int -> Seq (FlattenedValue a) -> Seq (WrappedLine a)
doLineWrapping maxCols i =
result
where
result = wrapCompletedLines $ execState (mapM_ pushValue i >> pushLine) initialState
initialState = WrapState { wrapCurLine = mempty
, wrapCompletedLines = mempty
, wrapCurCol = 0
, wrapWidth = maxCols
}
-- The widths returned by this function must match the content widths
-- rendered by renderFlattenedValue.
fvWidth :: FlattenedValue a -> Int
fvWidth (SingleInline fi) = fiWidth fi
fvWidth (NonBreaking rs) = sum $ (sum . fmap fvWidth) <$> rs
-- The widths returned by this function must match the content widths
-- rendered by renderFlattenedValue.
fiWidth :: FlattenedInline a -> Int
fiWidth fi =
case fiValue fi of
FText t -> B.textWidth t
FSpace -> 1
FUser t -> T.length userSigil + B.textWidth t
FChannel t -> T.length normalChannelSigil + B.textWidth t
FEmoji t -> B.textWidth t + 2
FEditSentinel _ -> B.textWidth editMarking