~jaro/balkon

ref: 7119d635d91170071a52846f983c43fc9ff435d2 balkon/src/Data/Text/ParagraphLayout.hs -rw-r--r-- 4.7 KiB
7119d635Jaro WIP version with WIP interface. 1 year, 7 months ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
module Data.Text.ParagraphLayout (Span(..), layout)
where

import Data.Text.Glyphize
    (Buffer(..)
    ,ContentType(ContentTypeUnicode)
    ,Direction(..)
    ,Font
    ,GlyphInfo
    ,GlyphPos
    ,defaultBuffer
    ,shape
    )
import qualified Data.Text.ICU.Char as ICUChar
import Data.Text.Lazy (Text)
import qualified Data.Text.Lazy as Text
import Data.Text.Script (charScript)

type ScriptCode = String
type Language = String

-- Paragraph is broken into spans by the caller.
--
-- Each span could have a different font family, size, style, text decoration,
-- colour, language, etc.
--
-- TODO: Add all relevant attributes.
--
data Span = Span
    { spanText :: Text
    , spanFont :: Font
    , spanLanguage :: Maybe Language
    }
    deriving (Eq, Show)

-- Each span can be broken into one or more runs by Balkón.
--
-- Each run could have a different script, language, or direction.
--
data Run = Run
    { runText :: Text
    , runDirection :: Maybe Direction
    , runScript :: Maybe ScriptCode
    , runOriginalSpan :: Span
    }
    deriving (Eq, Show)

type ProtoRun = (String, Maybe Direction, ScriptCode)

data Merged a = Incompatible | Merged a

data Position = Beginning | Middle | End | Only
    deriving (Eq)

-- TODO: Add maximum line length as input.
-- TODO: Compute and return bounding box for each provided span.
--       More if implementing the CSS Box Model.
-- TODO: Also compute and return overall bounding box, in addition to individual
--       ones.
-- TODO: Allow a run across multiple spans (e.g. if they only differ by colour).
layout :: [Span] -> [[(GlyphInfo, GlyphPos)]]
layout = layoutRuns . concat . map spanToRuns

-- TODO: Optimise and preserve the Data.Text.Lazy structure.
spanToRuns :: Span -> [Run]
spanToRuns s = map run $ protoRuns chars
    where
        chars = reverse $ Text.unpack $ spanText s
        run (t, d, sc) = Run
            { runText = Text.pack t
            , runDirection = d
            , runScript = Just sc
            , runOriginalSpan = s
            }

-- TODO: Try to avoid reversing.
protoRuns :: [Char] -> [ProtoRun]
protoRuns = reverse . map (\(t, d, s) -> (reverse t, d, s)) . foldr foldRun []

foldRun :: Char -> [ProtoRun] -> [ProtoRun]
foldRun c [] =
    -- If there are no runs, create a new run with a single character.
    [([c], charDirection c, charScript c)]
foldRun c (r@(oldString, d1, s1):rs) =
    case (mergeDirections d1 d2, mergeScripts s1 s2) of
        -- If direction & script are compatible, add to existing run.
        (Merged d, Merged s) -> ((c:oldString, d, s):rs)
        -- Otherwise create a new run.
        _ -> (([c], d2, s2):r:rs)
    where
        d2 = charDirection c
        s2 = charScript c

-- Simplified detection of text direction for unidirectional text.
mergeDirections :: Maybe Direction -> Maybe Direction -> Merged (Maybe Direction)
mergeDirections Nothing Nothing = Merged Nothing
mergeDirections (Just d1) Nothing = Merged (Just d1)
mergeDirections Nothing (Just d2) = Merged (Just d2)
mergeDirections (Just d1) (Just d2)
    | d1 == d2 = Merged (Just d1)
    | otherwise = Incompatible

-- TODO: Implement proper inheritance rules.
mergeScripts :: ScriptCode -> ScriptCode -> Merged ScriptCode
mergeScripts "Zyyy" s2 = Merged s2
mergeScripts s1 "Zyyy" = Merged s1
mergeScripts s1 "Zinh" = Merged s1
mergeScripts s1 s2
    | s1 == s2 = Merged s1
    | otherwise = Incompatible

-- TODO: Use the BiDi algorithm to support bidirectional text.
charDirection :: Char -> Maybe Direction
charDirection c = case ICUChar.direction c of
    ICUChar.LeftToRight -> Just DirLTR
    ICUChar.RightToLeft -> Just DirRTL
    ICUChar.RightToLeftArabic -> Just DirRTL
    _ -> Nothing

layoutRuns :: [Run] -> [[(GlyphInfo, GlyphPos)]]
layoutRuns [] = []
layoutRuns [s] = [layoutOneRun Only s]
-- TODO: What if there are no visible characters in the edge runs?
layoutRuns (s1:s2:ss) = (layoutOneRun Beginning s1):(layoutRemainingRuns s2 ss)

layoutRemainingRuns :: Run -> [Run] -> [[(GlyphInfo, GlyphPos)]]
layoutRemainingRuns s [] = [layoutOneRun End s]
layoutRemainingRuns s1 (s2:ss) = (layoutOneRun Middle s1):(layoutRemainingRuns s2 ss)

layoutOneRun :: Position -> Run -> [(GlyphInfo, GlyphPos)]
layoutOneRun pos run = shape font buffer features
    where
        originalSpan = runOriginalSpan run
        font = spanFont originalSpan
        lang = spanLanguage originalSpan
        buffer = defaultBuffer { text = runText run
                        , contentType = Just ContentTypeUnicode
                        , direction = runDirection run
                        , script = runScript run
                        , language = lang
                        , beginsText = pos == Beginning || pos == Only
                        , endsText = pos == End || pos == Only
                        }
        features = []