module Data.Text.ParagraphLayout.Internal.Paginable
( PageOptions (..)
, Paginable
, paginate
, paginateAll
)
where
import Data.Int (Int32)
import Data.Text.ParagraphLayout.Internal.LineNumbers
import Data.Text.ParagraphLayout.Internal.LinePagination
import Data.Text.ParagraphLayout.Internal.ParagraphLine
import qualified Data.Text.ParagraphLayout.Internal.Plain.ParagraphLayout
as Plain
import qualified Data.Text.ParagraphLayout.Internal.Rich.ParagraphLayout
as Rich
-- | Defines options for breaking a layout into pages.
data PageOptions = PageOptions
{ pageCurrentHeight :: Int32
-- ^ Amount of vertical space available for the paragraph
-- on the current page.
, pageNextHeight :: Int32
-- ^ Expected amount of vertical space available for the paragraph
-- on the next page.
--
-- If this is greater than `pageCurrentHeight`, the paragraph may be pushed
-- onto the next page in order to better satisfy orphan/widow constraints.
, pageOrphans :: Word
-- ^ If a page break is required inside the paragraph, this will be the
-- minimum number of lines to keep at the bottom of this page, if possible.
, pageWidows :: Word
-- ^ If a page break is required inside the paragraph, this will be the
-- minimum number of lines to keep at the top of the next page, if possible.
}
-- | Typeclass for layouts that can be broken into pages.
class Paginable pl where
-- | Break a chunk of content from the given layout, to be placed together
-- on a page.
--
-- Explanation of return values:
--
-- - @(`Continue`, p, `Nothing`)@
-- means that @p@ is the entire layout and fits best on the current page.
--
-- - @(`Break`, p, `Nothing`)@
-- means that @p@ is the entire layout and fits best on a new page.
-- In other words, @p@ should be preceded by a page break.
--
-- - @(`Continue`, p, `Just` rest)@
-- means that @p@ is a part of the layout that fits best on the current
-- page, and @rest@ should be passed to this function again.
-- In other words, @p@ should be followed by a page break.
--
-- - @(`Break`, p, `Just` rest)@
-- means that @p@ is a part of the layout that fits best on a new page,
-- and @rest@ should be passed to this function again.
-- In other words, @p@ should be surrounded by page breaks
-- on both sides.
paginate :: PageOptions -> pl -> (PageContinuity, pl, Maybe pl)
-- | Internal implementation of paginating a simple list of generic lines.
instance Line a => Paginable [a] where
paginate opts ls = case paginateLines o w h1 h2 ls of
(c, p, []) -> (c, p, Nothing)
(c, p, rest) -> (c, p, Just rest)
where
o = pageOrphans opts
w = pageWidows opts
h1 = pageCurrentHeight opts
h2 = pageNextHeight opts
-- | Implementation of paginating a plain text paragraph layout.
-- Breaks the layout on page boundaries and automatically adjusts coordinates.
instance Paginable (Plain.ParagraphLayout d) where
paginate = paginateLayout
-- | Implementation of paginating a rich text paragraph layout.
-- Breaks the layout on page boundaries and automatically adjusts coordinates.
instance Paginable (Rich.ParagraphLayout d) where
paginate = paginateLayout
paginateLayout :: (Line a, LineNumbers a, GenericLayout a) =>
PageOptions -> a -> (PageContinuity, a, Maybe a)
paginateLayout opts pl =
case paginate opts (cutLines pl) of
(c, p, Nothing) -> (c, mergeLines p, Nothing)
(c, p, Just rest) -> (c, mergeLines p, Just (mergeLines rest))
-- | Perform page breaking on the entire input, returning a list of pages.
paginateAll :: Paginable a => PageOptions -> a -> [(PageContinuity, a)]
paginateAll opts pl = case paginate opts pl of
(c, pl1, next) -> (c, pl1) : case next of
Just pl2 -> paginateAll opts' pl2
Nothing -> []
where
opts' = opts { pageCurrentHeight = pageNextHeight opts }