~jaro/balkon

c3170a2b51b20f0c932e0ee1733976276847bdf0 — Jaro 1 year, 10 months ago 8438e16
Create "plain" interface.
A .golden/exampleParagraph/golden => .golden/exampleParagraph/golden +20 -0
@@ 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})]
    )]
]}

M balkon.cabal => balkon.cabal +2 -0
@@ 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

A src/Data/Text/ParagraphLayout/Plain.hs => src/Data/Text/ParagraphLayout/Plain.hs +135 -0
@@ 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)

A test/Data/Text/ParagraphLayout/PlainSpec.hs => test/Data/Text/ParagraphLayout/PlainSpec.hs +65 -0
@@ 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"