From 85a32d42819fa08632855b2106af255f2e52e3fa Mon Sep 17 00:00:00 2001 From: Jaro Date: Tue, 13 Jun 2023 19:49:01 +0200 Subject: [PATCH] Add centre/right paragraph alignment. --- balkon.cabal | 1 + lib/Data/Text/ParagraphLayout/Rich.hs | 3 ++ .../Text/ParagraphLayout/Internal/Layout.hs | 17 +++++++--- .../Internal/ParagraphAlignment.hs | 32 +++++++++++++++++++ .../Internal/ParagraphOptions.hs | 5 +++ .../Text/ParagraphLayout/Internal/Rich.hs | 3 +- 6 files changed, 56 insertions(+), 5 deletions(-) create mode 100644 src/Data/Text/ParagraphLayout/Internal/ParagraphAlignment.hs diff --git a/balkon.cabal b/balkon.cabal index f6c6574..375ea59 100644 --- a/balkon.cabal +++ b/balkon.cabal @@ -114,6 +114,7 @@ library balkon-internal Data.Text.ParagraphLayout.Internal.LineHeight, Data.Text.ParagraphLayout.Internal.LinePagination, Data.Text.ParagraphLayout.Internal.Paginable, + Data.Text.ParagraphLayout.Internal.ParagraphAlignment, Data.Text.ParagraphLayout.Internal.ParagraphConstruction, Data.Text.ParagraphLayout.Internal.ParagraphOptions, Data.Text.ParagraphLayout.Internal.Plain, diff --git a/lib/Data/Text/ParagraphLayout/Rich.hs b/lib/Data/Text/ParagraphLayout/Rich.hs index a84c6d0..f320b38 100644 --- a/lib/Data/Text/ParagraphLayout/Rich.hs +++ b/lib/Data/Text/ParagraphLayout/Rich.hs @@ -5,6 +5,7 @@ module Data.Text.ParagraphLayout.Rich -- * Input paragraph ( Paragraph (Paragraph) , constructParagraph + , ParagraphAlignment (AlignLeft, AlignRight, AlignCentreH) , BoxSpacing (BoxSpacingLeftRight) , LineHeight (Absolute, Normal) , ParagraphOptions @@ -12,6 +13,7 @@ module Data.Text.ParagraphLayout.Rich -- ** Paragraph options -- | These are record selectors that can be used for reading -- as well as updating specific option fields. + , paragraphAlignment , paragraphMaxWidth -- NOTE: `paragraphFont` is only used by the legacy plain text interface, -- and is therefore not exported here. @@ -67,6 +69,7 @@ import Data.Text.ParagraphLayout.Internal.AncestorBox import Data.Text.ParagraphLayout.Internal.BoxOptions import Data.Text.ParagraphLayout.Internal.Fragment import Data.Text.ParagraphLayout.Internal.LineHeight +import Data.Text.ParagraphLayout.Internal.ParagraphAlignment import Data.Text.ParagraphLayout.Internal.ParagraphOptions import Data.Text.ParagraphLayout.Internal.Rich import Data.Text.ParagraphLayout.Internal.Rich.Paragraph diff --git a/src/Data/Text/ParagraphLayout/Internal/Layout.hs b/src/Data/Text/ParagraphLayout/Internal/Layout.hs index bf7ccf6..2b8d983 100644 --- a/src/Data/Text/ParagraphLayout/Internal/Layout.hs +++ b/src/Data/Text/ParagraphLayout/Internal/Layout.hs @@ -32,6 +32,7 @@ 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.ParagraphAlignment import Data.Text.ParagraphLayout.Internal.ParagraphExtents import qualified Data.Text.ParagraphLayout.Internal.ProtoFragment as PF import qualified Data.Text.ParagraphLayout.Internal.ProtoLine as PL @@ -57,14 +58,15 @@ type ProtoFragmentWithBoxes d = WithBoxes d (ProtoFragmentWithSpan d) -- The output is a flat list of fragments positioned in both dimensions. layoutAndAlignLines :: Direction + -> ParagraphAlignment -> Int32 -> NonEmpty (WithSpan d Run) -> [FragmentWithSpan d] -layoutAndAlignLines dir maxWidth runs = frags +layoutAndAlignLines dir align maxWidth runs = frags where frags = concatMap NonEmpty.toList fragsInLines (_, fragsInLines) = mapAccumL positionLine originY numberedLines - positionLine = positionLineH dir maxWidth + positionLine = positionLineH dir align maxWidth numberedLines = zip [1 ..] canonicalLines canonicalLines = fmap reorderProtoFragments logicalLines logicalLines = nonEmptyItems $ layoutLines maxWidth [] runs @@ -106,11 +108,12 @@ layoutLines maxWidth openBoxes runs = case nonEmpty rest of -- TODO: For rich text, allow other types of vertical alignment. positionLineH :: Direction + -> ParagraphAlignment -> Int32 -> Int32 -> (Int, PL.ProtoLine NonEmpty d) -> (Int32, NonEmpty (FragmentWithSpan d)) -positionLineH dir maxWidth originY (num, pl) = (nextY, frags) +positionLineH dir align maxWidth originY (num, pl) = (nextY, frags) where nextY = minimum $ fmap y_min rects rects = fmap (\ (WithSpan _ r) -> fragmentRect r) frags @@ -118,7 +121,7 @@ positionLineH dir maxWidth originY (num, pl) = (nextY, frags) wpfs = PL.applyBoxes pl originX = paragraphOriginX + if lineWidth > maxWidth then overflowingLineOffset dir (lineWidth - maxWidth) - else 0 + else fittingLineOffset align (maxWidth - lineWidth) lineWidth = PL.width pl -- | Inline offset of the first fragment on a line that overflows. @@ -129,6 +132,12 @@ overflowingLineOffset DirRTL excess = -excess -- TODO: Check if the sign needs to be flipped for vertical text. overflowingLineOffset DirBTT excess = -excess +-- | Inline offset of the first fragment on a line with extra blank space. +fittingLineOffset :: ParagraphAlignment -> Int32 -> Int32 +fittingLineOffset AlignLeft _ = 0 +fittingLineOffset AlignRight slack = slack +fittingLineOffset AlignCentreH slack = slack `div` 2 + -- | 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. diff --git a/src/Data/Text/ParagraphLayout/Internal/ParagraphAlignment.hs b/src/Data/Text/ParagraphLayout/Internal/ParagraphAlignment.hs new file mode 100644 index 0000000..b1ba7ab --- /dev/null +++ b/src/Data/Text/ParagraphLayout/Internal/ParagraphAlignment.hs @@ -0,0 +1,32 @@ +module Data.Text.ParagraphLayout.Internal.ParagraphAlignment + ( ParagraphAlignment (..) + ) +where + +-- | Determines how the contents of the paragraph should be aligned +-- horizontally, that is, how should fragments be stacked inside each line +-- when there is space available. +-- +-- This has no effect on lines with overflowing content. +data ParagraphAlignment + + = AlignLeft + -- ^ Stack fragments to the left edge of each line, + -- leaving free space on the right side. + -- + -- The leftmost fragment on each non-overflowing line will have its + -- `Data.Text.ParagraphLayout.Rect.x_min` equal to @0@. + + | AlignRight + -- ^ Stack fragments to the right edge of each line, + -- leaving free space on the left side. + -- + -- The rightmost fragment on each non-overflowing line will have its + -- `Data.Text.ParagraphLayout.Rect.x_max` equal to + -- `Data.Text.ParagraphLayout.Rich.paragraphMaxWidth`. + + | AlignCentreH + -- ^ Stack fragments to the horizontal centre of each line, + -- leaving free space split evenly between each side. + + deriving (Eq, Read, Show) diff --git a/src/Data/Text/ParagraphLayout/Internal/ParagraphOptions.hs b/src/Data/Text/ParagraphLayout/Internal/ParagraphOptions.hs index 3adf859..df88a25 100644 --- a/src/Data/Text/ParagraphLayout/Internal/ParagraphOptions.hs +++ b/src/Data/Text/ParagraphLayout/Internal/ParagraphOptions.hs @@ -8,6 +8,7 @@ import Data.Int (Int32) import Data.Text.Glyphize (Font, emptyFont) import Data.Text.ParagraphLayout.Internal.LineHeight +import Data.Text.ParagraphLayout.Internal.ParagraphAlignment -- | Defines options relevant to the entire paragraph. -- @@ -24,6 +25,9 @@ data ParagraphOptions = ParagraphOptions , paragraphLineHeight :: LineHeight -- ^ Preferred line height of the resulting fragments. + , paragraphAlignment :: ParagraphAlignment + -- ^ Alignment of non-overflowing lines within the paragraph. + , paragraphMaxWidth :: Int32 -- ^ Line width at which line breaking should occur. -- Lines will be broken at language-appropriate boundaries. @@ -39,5 +43,6 @@ defaultParagraphOptions :: ParagraphOptions defaultParagraphOptions = ParagraphOptions { paragraphFont = emptyFont , paragraphLineHeight = Normal + , paragraphAlignment = AlignLeft , paragraphMaxWidth = maxBound } diff --git a/src/Data/Text/ParagraphLayout/Internal/Rich.hs b/src/Data/Text/ParagraphLayout/Internal/Rich.hs index 96f0212..f9b5777 100644 --- a/src/Data/Text/ParagraphLayout/Internal/Rich.hs +++ b/src/Data/Text/ParagraphLayout/Internal/Rich.hs @@ -30,10 +30,11 @@ layoutRich p = paragraphLayout $ map unwrap frags unwrap (WithSpan rs frag) = frag { fragmentUserData = RS.spanUserData rs } frags = case nonEmpty wrappedRuns of - Just xs -> layoutAndAlignLines dir maxWidth xs + Just xs -> layoutAndAlignLines dir align maxWidth xs Nothing -> [] wrappedRuns = spansToRunsWrapped spans dir = textDirection rootTextOpts + align = paragraphAlignment opts maxWidth = paragraphMaxWidth opts spans = resolveSpans p -- 2.30.2