From c3170a2b51b20f0c932e0ee1733976276847bdf0 Mon Sep 17 00:00:00 2001 From: Jaro Date: Mon, 20 Feb 2023 06:12:53 +0100 Subject: [PATCH] Create "plain" interface. --- .golden/exampleParagraph/golden | 20 +++ balkon.cabal | 2 + src/Data/Text/ParagraphLayout/Plain.hs | 135 ++++++++++++++++++++ test/Data/Text/ParagraphLayout/PlainSpec.hs | 65 ++++++++++ 4 files changed, 222 insertions(+) create mode 100644 .golden/exampleParagraph/golden create mode 100644 src/Data/Text/ParagraphLayout/Plain.hs create mode 100644 test/Data/Text/ParagraphLayout/PlainSpec.hs diff --git a/.golden/exampleParagraph/golden b/.golden/exampleParagraph/golden new file mode 100644 index 0000000..a20caf3 --- /dev/null +++ b/.golden/exampleParagraph/golden @@ -0,0 +1,20 @@ +ParagraphLayout {paragraphRect = Rect {x_origin = 0, y_origin = 0, x_size = 6113, y_size = 0}, spanLayouts = [ + SpanLayout [(Rect {x_origin = 0, y_origin = 0, x_size = 4837, y_size = 0}, + [(GlyphInfo {codepoint = 77, cluster = 4, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False},GlyphPos {x_advance = 253, y_advance = 0, x_offset = 0, y_offset = 0}), + (GlyphInfo {codepoint = 86, cluster = 5, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False},GlyphPos {x_advance = 446, y_advance = 0, x_offset = 0, y_offset = 0}), + (GlyphInfo {codepoint = 72, cluster = 6, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False},GlyphPos {x_advance = 559, y_advance = 0, x_offset = 0, y_offset = 0}), + (GlyphInfo {codepoint = 80, cluster = 7, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False},GlyphPos {x_advance = 861, y_advance = 0, x_offset = 0, y_offset = 0}), + (GlyphInfo {codepoint = 3, cluster = 8, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False},GlyphPos {x_advance = 231, y_advance = 0, x_offset = 0, y_offset = 0}), + (GlyphInfo {codepoint = 87, cluster = 9, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False},GlyphPos {x_advance = 402, y_advance = 0, x_offset = 0, y_offset = 0}), + (GlyphInfo {codepoint = 68, cluster = 10, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False},GlyphPos {x_advance = 522, y_advance = 0, x_offset = 0, y_offset = 0}), + (GlyphInfo {codepoint = 71, cluster = 11, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False},GlyphPos {x_advance = 589, y_advance = 0, x_offset = 0, y_offset = 0}), + (GlyphInfo {codepoint = 92, cluster = 12, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False},GlyphPos {x_advance = 497, y_advance = 0, x_offset = 0, y_offset = 0}), + (GlyphInfo {codepoint = 15, cluster = 13, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False},GlyphPos {x_advance = 246, y_advance = 0, x_offset = 0, y_offset = 0}), + (GlyphInfo {codepoint = 3, cluster = 14, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False},GlyphPos {x_advance = 231, y_advance = 0, x_offset = 0, y_offset = 0})] + )], + SpanLayout [(Rect {x_origin = 4837, y_origin = 0, x_size = 1276, y_size = 0}, + [(GlyphInfo {codepoint = 0, cluster = 15, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False},GlyphPos {x_advance = 500, y_advance = 0, x_offset = 0, y_offset = 0}), + (GlyphInfo {codepoint = 0, cluster = 18, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False},GlyphPos {x_advance = 500, y_advance = 0, x_offset = 0, y_offset = 0}), + (GlyphInfo {codepoint = 4, cluster = 21, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False},GlyphPos {x_advance = 276, y_advance = 0, x_offset = 0, y_offset = 0})] + )] +]} diff --git a/balkon.cabal b/balkon.cabal index d9e211a..23ea52b 100644 --- a/balkon.cabal +++ b/balkon.cabal @@ -97,6 +97,7 @@ library -- Modules exported by the library. exposed-modules: Data.Text.ParagraphLayout, + Data.Text.ParagraphLayout.Plain, Data.Text.ParagraphLayout.Rect, Data.Text.ParagraphLayout.Run, Data.Text.ParagraphLayout.Span @@ -127,6 +128,7 @@ test-suite balkon-test other-modules: Data.Text.ParagraphLayoutSpec, Data.Text.ParagraphLayout.FontLoader, + Data.Text.ParagraphLayout.PlainSpec, Data.Text.ParagraphLayout.RectSpec, Data.Text.ParagraphLayout.RunSpec, Data.Text.ParagraphLayout.SpanData diff --git a/src/Data/Text/ParagraphLayout/Plain.hs b/src/Data/Text/ParagraphLayout/Plain.hs new file mode 100644 index 0000000..077bc8d --- /dev/null +++ b/src/Data/Text/ParagraphLayout/Plain.hs @@ -0,0 +1,135 @@ +-- | 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(..) + ,exampleParagraph + ,layoutPlain + ) +where + +import Data.Int (Int32) +import Data.Text (pack) +import Data.Text.Array (Array) +import Data.Text.Foreign (I8) +import Data.Text.Glyphize (Font, GlyphInfo, GlyphPos) +import Data.Text.Internal (Text(Text)) + +-- | 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. + +data Span = Span + + { spanLength :: I8 + -- ^ Byte offset to the next span or the end of the paragraph text. + + , spanLanguage :: String + -- ^ Used for selecting the appropriate glyphs and line breaking rules. + + } + +-- | 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 [(Rect Int32, [(GlyphInfo, GlyphPos)])] + deriving (Eq, Read, Show) + +-- | Rectangle containing all glyph advances in the paragraph or corresponding +-- span. 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. +data Rect a = Rect + { x_origin :: a + , y_origin :: a + , x_size :: a + , y_size :: a + } + deriving (Eq, Read, Show) + +-- | 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 +-- Stub implementation to make this a valid Haskell source. +-- Of course, this will eventually be replaced by an actual implementation. :) +layoutPlain (Paragraph _ _ spans _) + = ParagraphLayout (Rect 0 0 0 0) (map (\_ -> SpanLayout []) spans) + +exampleArray :: Array +exampleOffset :: Int +(Text exampleArray exampleOffset _) = pack "Tak jsem tady, 世界!" + +exampleParagraph :: Font -> Paragraph +exampleParagraph font = Paragraph + exampleArray + (fromIntegral exampleOffset + 4) + [Span 11 "cs" -- this will contain the text "jsem tady, " + ,Span 7 "ja" -- this will contain the text "世界!" + ] + (ParagraphOptions font (Relative 1.5) 20000) diff --git a/test/Data/Text/ParagraphLayout/PlainSpec.hs b/test/Data/Text/ParagraphLayout/PlainSpec.hs new file mode 100644 index 0000000..3bc0a56 --- /dev/null +++ b/test/Data/Text/ParagraphLayout/PlainSpec.hs @@ -0,0 +1,65 @@ +module Data.Text.ParagraphLayout.PlainSpec (spec) where + +import Data.List (intersperse) + +import Test.Hspec +import Test.Hspec.Golden +import System.FilePath (()) +import Data.Text.ParagraphLayout.Plain +import Data.Text.ParagraphLayout.FontLoader + +prettyShow :: ParagraphLayout -> String +prettyShow (ParagraphLayout pr sls) = showParagraphLayout where + showParagraphLayout = concat + [ "ParagraphLayout {paragraphRect = " + , show pr + , ", spanLayouts = [" + , newline + , showSpanLayouts + , newline + , "]}" + , newline + ] + showSpanLayouts = concat $ intersperse commaNewline $ map showSpanLayout sls + showSpanLayout (SpanLayout boxes) = concat + [ indent1 + , "SpanLayout [" + , concat $ map showBox boxes + , "]" + ] + showBox (r, glyphs) = concat + [ "(" + , show r + , commaNewline + , indent2 + , "[" + , showGlyphs glyphs + , "]" + , newline + , indent1 + , ")" + ] + showGlyphs = concat . intersperse (commaNewline ++ indent2) . map show + indent1 = " " + indent2 = indent1 ++ indent1 + newline = "\n" + commaNewline = "," ++ newline + +shouldBeGolden :: ParagraphLayout -> FilePath -> Golden ParagraphLayout +shouldBeGolden output_ name = Golden + { output = output_ + , encodePretty = show + , writeToFile = \path -> writeFile path . prettyShow + , readFromFile = \path -> readFile path >>= return . read + , goldenFile = ".golden" name "golden" + , actualFile = Just (".golden" name "actual") + , failFirstTime = False + } + +spec :: Spec +spec = do + -- Note: This font does not contain Japanese glyphs. + describe "layoutPlain" $ before loadUbuntuRegular $ do + it "stub works" $ \font -> do + let result = layoutPlain (exampleParagraph font) + result `shouldBeGolden` "exampleParagraph" -- 2.30.2