-- | Shaping for a paragraph of plain, unidirectional text using a single font. -- -- The input text must be encoded as UTF-8 in a contiguous byte array. -- -- Positions and distances are represented as 32-bit integers. Their unit must -- be defined by the caller, who must calculate the desired dimensions of the -- EM square of the input font and set them using @hb_font_set_scale()@. For -- example, if @1em = 20px@, if the output pixels are square, and if the output -- coordinates are in 1/64ths of a pixel, you should set both the @x_scale@ and -- the @y_scale@ to @1280@. module Data.Text.ParagraphLayout.Plain (LineHeight(..) ,Paragraph(..) ,ParagraphLayout(..) ,ParagraphOptions(..) ,Rect(..) ,Span(..) ,SpanLayout(..) ,layoutPlain ) where import Data.Int (Int32) import Data.List (mapAccumL) import Data.Text.Array (Array) import Data.Text.Foreign (I8) import Data.Text.Glyphize (Buffer(..) ,ContentType(ContentTypeUnicode) ,Font ,GlyphInfo ,GlyphPos(x_advance, y_advance) ,defaultBuffer ,shape ) import Data.Text.Internal (Text(Text)) import qualified Data.Text.Internal.Lazy as Lazy import Data.Text.ParagraphLayout.Rect import qualified Data.Text.ParagraphLayout.ResolvedSpan as RS import Data.Text.ParagraphLayout.Run import Data.Text.ParagraphLayout.Span -- | Text to be laid out as a paragraph. -- -- May be divided into any number of neighbouring spans, each of which will -- have its own layout rectangle(s) calculated. data Paragraph = Paragraph Array -- ^ A byte array containing the whole text to be laid out, in UTF-8. I8 -- ^ Byte offset of the first span. -- Any characters preceding this offset will not be shaped, but may still -- be used to influence the shape of neighbouring characters. [Span] -- ^ Parts of the text to be laid out, in logical order. -- The offset plus total length of all spans must not exceed array bounds. -- Any characters following the last span will not be shaped, but may still -- be used to influence the shape of neighbouring characters. ParagraphOptions -- ^ Properties applying to the paragraph as a whole. data ParagraphOptions = ParagraphOptions { paragraphFont :: Font , paragraphLineHeight :: LineHeight , paragraphMaxWidth :: Int32 } data LineHeight = Absolute Int32 -- ^ Set line height independently of the font. | Relative Float -- ^ Set line height as a multiplier of the font's built-in value. -- | The resulting layout of the whole paragraph. data ParagraphLayout = ParagraphLayout { paragraphRect :: Rect Int32 , spanLayouts :: [SpanLayout] } deriving (Eq, Read, Show) -- | The resulting layout of each span, which may include multiple bounding -- boxes if broken over multiple lines. data SpanLayout = SpanLayout [Box] deriving (Eq, Read, Show) type Box = ( Rect Int32 -- ^ Rectangle containing all glyph advances in this box. This is the space -- that the glyphs "take up" and is probably what you want to use for -- detecting position-based events such as mouse clicks. -- -- Beware that actual glyphs will not be drawn exactly to the borders of -- this rectangle -- they may be offset inwards and they can also extend -- outwards! -- -- These are not the typographic bounding boxes that you use for determining -- the area to draw on -- you need FreeType or a similar library for that. -- -- The origin coordinates are relative to the paragraph. -- -- The sizes can be positive or negative, depending on the text direction. -- -- X coordinates increase from left to right. -- Y coordinates increase from bottom to top. , [(GlyphInfo, GlyphPos)] ) boxRect :: Box -> Rect Int32 boxRect = fst spanRects :: SpanLayout -> [Rect Int32] spanRects (SpanLayout boxes) = map boxRect boxes base :: (Num a) => Rect a base = Rect 0 0 0 0 containRects :: (Ord a, Num a) => [Rect a] -> Rect a containRects = foldr union base containGlyphs :: [GlyphPos] -> Rect Int32 containGlyphs ps = Rect { x_origin = 0 , y_origin = 0 , x_size = sum $ map x_advance ps , y_size = sum $ map y_advance ps -- TODO add line height } -- | Interface for basic plain text layout. -- -- The entire paragraph will be assumed to have the same text direction and -- will be shaped using a single font, aligned to the left for LTR text or to -- the right for RTL text. layoutPlain :: Paragraph -> ParagraphLayout layoutPlain paragraph = ParagraphLayout pRect arrangedLayouts where pRect = containRects allRects allRects = concat $ map spanRects arrangedLayouts arrangedLayouts = snd $ arrangeSpansH 0 $ layouts layouts = map layoutSpan spans spans = resolveSpans paragraph -- TODO: Break lines. -- TODO: Allow a run across multiple spans (e.g. if they only differ by colour). layoutSpan :: RS.ResolvedSpan -> SpanLayout layoutSpan rs = SpanLayout (map layoutRun $ spanToRuns rs) layoutRun :: Run -> Box layoutRun run = (rect, glyphs) where rs = runOriginalSpan run rect = containGlyphs $ map snd $ glyphs glyphs = shape font buffer features font = RS.spanFont rs -- TODO: Set beginsText / endsText. buffer = defaultBuffer { text = fromStrict $ runText run , contentType = Just ContentTypeUnicode , direction = runDirection run , script = runScript run , language = Just $ RS.spanLanguage rs } features = [] resolveSpans :: Paragraph -> [RS.ResolvedSpan] resolveSpans (Paragraph arr off spans opts) = map resolve $ zip spans texts where resolve (s, t) = RS.ResolvedSpan { RS.spanText = t , RS.spanFont = paragraphFont opts , RS.spanLanguage = spanLanguage s } texts = cuts arr off spans -- | Produce a list of `Text`s, defined by an initial offset and a list of -- consecutive `Span`s, out of the underlying `Array`. -- -- TODO: Consider adding checks for array bounds. cuts :: Array -> I8 -> [Span] -> [Text] cuts arr initialOffset spans = snd $ mapAccumL (cut arr) initialOffset spans -- | Produce a `Text`, defined by an initial offset and a `Span`, out of the -- underlying `Array`. cut :: Array -> I8 -> Span -> (I8, Text) cut arr off s = (end, t) where len = spanLength s end = off + len t = Text arr (fromIntegral off) (fromIntegral len) -- | Arrange all boxes in multiple spans in one horizontal direction -- and return the final x_offset for continuation. arrangeSpansH :: Int32 -> [SpanLayout] -> (Int32, [SpanLayout]) arrangeSpansH currentX sls = mapAccumL arrangeSpanH currentX sls -- | Arrange all boxes in one span in one horizontal direction -- and return the final x_offset for continuation. arrangeSpanH :: Int32 -> SpanLayout -> (Int32, SpanLayout) arrangeSpanH currentX (SpanLayout boxes) = (nextX, SpanLayout newBoxes) where (nextX, newBoxes) = arrangeBoxesH currentX boxes -- | Arrange boxes in one horizontal direction -- and return the final x_offset for continuation. arrangeBoxesH :: Int32 -> [Box] -> (Int32, [Box]) arrangeBoxesH currentX boxes = mapAccumL arrangeBoxH currentX boxes -- | Set the horizontal offset of the given box -- and return the x coordinate of its other side for continuation. arrangeBoxH :: Int32 -> Box -> (Int32, Box) arrangeBoxH currentX (rect, glyphs) = (nextX, (newRect, glyphs)) where nextX = currentX + x_size rect newRect = rect { x_origin = currentX } fromStrict :: Text -> Lazy.Text fromStrict t = Lazy.Chunk t Lazy.Empty