~alcinnz/haphaestus

ref: fb550246e5a7cc441639a7fb77e8bb7159ef6440 haphaestus/src/Main.hs -rw-r--r-- 10.0 KiB
fb550246 — Adrian Cochrane Draft integration test between HURL, Stylist, CatTrap, & now Typograffiti 6 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
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
{-# LANGUAGE OverloadedStrings, TemplateHaskell, FlexibleContexts #-}
module Main where

import           FreeType.FontConfig (instantiatePattern, bmpAndMetricsForIndex,
                                      FTFC_Subpixel(..))
import           FreeType.Core.Base (ft_With_FreeType, FT_Library)
import           Typograffiti (makeDrawGlyphs, allocAtlas, AllocatedRendering(..),
                                TextTransform(..), Atlas, TypograffitiError)
import SDL hiding (rotate)
import Graphics.GL.Core32

import Control.Monad.IO.Class (liftIO)
import Control.Monad.Except (runExceptT, MonadError, MonadIO)

import System.Environment (getArgs)
import qualified Data.Map as M
import qualified Data.Set as S
import Data.Maybe (fromJust, fromMaybe)
import qualified Data.Text as Txt
import qualified Data.ByteString as BS
import System.Directory (getCurrentDirectory)
import qualified System.Directory as Dir

import Graphics.Layout.CSS (CSSBox(..), finalizeCSS')
import Graphics.Layout.CSS.Font (placeholderFont, pattern2font, hbUnit,
                                Font'(scale, pattern, fontSize),
                                CSSFont(cssFontSize))
import Graphics.Layout (LayoutItem(..), boxLayout,
                        glyphsPerFont, glyphs, fragmentFont,
                        layoutGetBox, layoutGetChilds, layoutGetInner)
import Graphics.Layout.Box (zeroBox)
import qualified Graphics.Layout.Box as B

import Network.URI.Fetch.XML (Page(..), fetchDocument, applyCSScharset)
import Network.URI.Fetch (newSession, fetchURL)
import Network.URI.Charset (charsets)
import Network.URI (URI(..), nullURI, parseURIReference)
import Data.FileEmbed (makeRelativeToProject, embedStringFile)
import Data.HTML2CSS (el2stylist)

import Text.XML as X (Document(..), Element(..), Node(..), Prologue(..))
import Stylist.Tree (StyleTree(..), preorder, treeMap)
import Stylist (PropertyParser(..), cssPriorityAgent, cssPriorityUser)
import qualified Data.CSS.Style as Style
import qualified Data.CSS.Syntax.StyleSheet as CSS
import qualified Data.CSS.Preprocessor.Text as CSSTxt
import Data.CSS.Preprocessor.Conditions as CSSCond
        (ConditionalStyles, conditionalStyles, loadImports, Datum(..), resolve)
import qualified Data.CSS.Preprocessor.PsuedoClasses as CSSPseudo

import Control.Concurrent.MVar (putMVar, newEmptyMVar, tryReadMVar)
import Control.Concurrent (forkIO)
import Control.DeepSeq (NFData(..), ($!!))

import SDL hiding (rotate)
import Foreign.C.Types (CInt)
import Data.Function (fix)
import Control.Monad (unless, forM)
import qualified Graphics.Text.Font.Choose as FC

import Data.CSS.Syntax.Tokens (Token(..))
import qualified Data.IntSet as IS
import Data.Text.Glyphize (GlyphInfo, GlyphPos)

type Style = Style.VarParser (CSSTxt.TextStyle (CSSBox VizStyle))
data VizStyle = VizStyle (V4 Float)
instance Eq VizStyle where
    VizStyle x == VizStyle y = case compare x y of
        EQ -> True
        _ -> False
instance Style.PropertyParser VizStyle where
    temp = VizStyle (V4 0 0 0 1)
    inherit = id
    longhand _ self "color" [Ident "black"] = Just $ VizStyle (V4 0 0 0 1)
    longhand _ self "color" [Ident "white"] = Just $ VizStyle (V4 1 1 1 1)
    longhand _ self "color" [Ident "red"]   = Just $ VizStyle (V4 1 0 0 1)
    longhand _ self "color" [Ident "green"] = Just $ VizStyle (V4 0 1 0 1)
    longhand _ self "color" [Ident "blue"]  = Just $ VizStyle (V4 0 0 1 1)
    longhand _ _ _ _ = Nothing

renderLayout :: (MonadError TypograffitiError m, MonadIO m) =>
        M.Map (FC.Pattern, Double) Atlas ->
        (Atlas -> [(GlyphInfo, GlyphPos)] ->
            m (AllocatedRendering [TextTransform])) ->
        LayoutItem Double Double ((Double, Double), VizStyle) ->
        m ()
renderLayout atlases drawText (LayoutSpan self)
        | Just atlas <- M.lookup (pattern font, fontSize font) atlases = do
    drawText' <- drawText atlas $ glyphs self
    liftIO $ arDraw drawText' [TextTransformMultiply color] $
        V2 (fromEnum x) (fromEnum y)
  where
    (font, _) = fragmentFont self
    ((x, y), VizStyle color) = layoutGetInner $ LayoutSpan self
{-renderLayout atlases drawText (LayoutSpan ((x, y), VizStyle color) font self)
    | Just atlas <- M.lookup (pattern font, fontSize font) atlases = do
        drawText' <- drawText atlas $ fragmentGlyphs self
        -- FIXME Switch from temp to inherit for inline caller-properties.
        liftIO $ arDraw drawText' [TextTransformMultiply color]
                        (V2 (fromEnum x) (fromEnum y))-} -- FIXME: New API!
renderLayout atlases drawText node = do
    layoutGetChilds node `forM` renderLayout atlases drawText
    return ()

initReferer :: IO (Page (CSSCond.ConditionalStyles (CSSBox VizStyle)))
initReferer = do
    cwd <- getCurrentDirectory
    return $ Page {
        -- Default to URIs being relative to CWD.
        pageURL = URI {uriScheme = "file:", uriPath = cwd,
            uriAuthority = Nothing, uriQuery = "", uriFragment = ""},
        -- Blank values:
        css = conditionalStyles nullURI "temp",
        domain = "temp",
        html = Document {
            documentPrologue = Prologue [] Nothing [],
            documentRoot = Element "temp" M.empty [],
            documentEpilogue = []
        },
        pageTitle = "", pageMIME = "", apps = [],
        backStack = [], forwardStack = [], visitedURLs = S.empty,
        initCSS = conditionalStyles,
        appName = "cattrap"
    }

stylize' style = preorder inner
  where
    inner parent _ el = Style.cascade style el [] $
            Style.inherit $ fromMaybe Style.temp parent

resolveCSS manager page = do
    let agentStyle = cssPriorityAgent (css page) `CSS.parse`
            $(makeRelativeToProject "useragent.css" >>= embedStringFile)
    userStyle <- loadUserStyles agentStyle
    CSSCond.loadImports loadURL lowerVars lowerToks userStyle []
  where
    loadURL url = do
        response <- fetchURL manager ["text/css"] url
        let charsets' = map Txt.unpack charsets
        return $ case response of
            ("text/css", Left text) -> text
            ("text/css", Right bytes) -> applyCSScharset charsets' $ BS.toStrict bytes
            (_, _) -> ""

loadUserStyles styles = do
    dir <- Dir.getXdgDirectory Dir.XdgConfig "rhapsode"
    exists <- Dir.doesDirectoryExist dir
    loadDirectory dir exists
  where
    loadDirectory _ False = return styles
    loadDirectory dir True = do
        files <- Dir.listDirectory dir
        loadFiles (cssPriorityUser styles) files
    loadFiles style (file:files) = do
        source <- readFile file
        CSS.parse style (Txt.pack source) `loadFiles` files
    loadFiles style [] = return style
-- FIXME: Support more media queries!
resolve' = CSSCond.resolve lowerVars lowerToks
lowerVars _ = CSSCond.B False
lowerToks _ = CSSCond.B False

main :: IO ()
main = do
    FC.init
    SDL.initializeAll

    let wcfg = defaultWindow {
            windowInitialSize = V2 1280 480,
            windowGraphicsContext = OpenGLContext defaultOpenGL {
                glProfile = Core Debug 3 3
            },
            -- Simplify moving layout/download out-of-thread
            windowResizable = False
          }
    w <- createWindow "Haphaestus" wcfg
    _ <- glCreateContext w

    args <- getArgs
    let url = case args of
            (url:_) -> url
            [] -> "https://git.argonaut-constellation.org/~alcinnz/CatTrap"
    sess <- newSession
    ref <- initReferer
    xml <- fetchDocument sess ref $ fromMaybe nullURI $ parseURIReference url
    let pseudoFilter = CSSPseudo.htmlPsuedoFilter Style.queryableStyleSheet
    css' <- resolveCSS sess xml
    let css = CSSPseudo.inner $ resolve' pseudoFilter css'
    let styles = CSSTxt.resolve $ treeMap Style.innerParser $
            stylize' css $ el2stylist $ X.documentRoot $ html xml
    let layout = finalizeCSS' placeholderFont styles

    ft_With_FreeType $ \ft -> do
        V2 x y <- get $ windowSize w
        pages' <- forkCompute $ addAtlas ft $ boxLayout zeroBox {
                B.size = B.Size (fromIntegral x) (fromIntegral y)
              } layout True
        drawGlyphs' <- runExceptT makeDrawGlyphs
        let drawGlyphs = case drawGlyphs' of
                Left err -> error $ show err
                Right ret -> ret

        fix $ \loop -> do
            events <- fmap eventPayload <$> pollEvents
            liftIO $ glClearColor 1 1 1 1
            liftIO $ glClear GL_COLOR_BUFFER_BIT

            sz@(V2 dw dh) <- liftIO $ glGetDrawableSize w
            liftIO $ glViewport 0 0 (fromIntegral dw) (fromIntegral dh)

            layout' <- tryReadMVar pages'
            res <- case layout' of
                Just (layout:_, atlases') -> runExceptT $
                    renderLayout atlases' drawGlyphs layout
                _ -> return $ Right ()
            case res of
                Left err -> print err
                Right () -> return ()

            liftIO $ glSwapWindow w
            unless (QuitEvent `elem` events) loop
    SDL.quit
    -- FC.fini -- FIXME: Need to free all Haskell data before freeing FontConfig's

c :: (Enum a, Enum b) => a -> b
c = toEnum . fromEnum

forkCompute dat = do
    ret <- liftIO $ newEmptyMVar
    liftIO $ forkIO (putMVar ret =<< dat)
    return ret

type Layouts = [LayoutItem Double Double ((Double, Double), VizStyle)]
addAtlas :: FT_Library -> Layouts -> IO (Layouts, M.Map (FC.Pattern, Double) Atlas)
addAtlas ft layout = do
    let sysfont = (pattern2font (FC.nameParse "serif") Style.temp { cssFontSize = (12,"pt") }
                   placeholderFont placeholderFont) { scale = 1 }

    let required = glyphsPerFont $ LayoutFlow ((0, 0), temp) zeroBox layout
    atlases <- forM (M.toList required) $ \(key@(pat, size), glyphs) -> do
        font <- instantiatePattern ft pat (-1, size)
        atlas <- runExceptT $ allocAtlas
                (liftIO . bmpAndMetricsForIndex font SubpixelDefault)
                (map toEnum $ IS.toList glyphs)
                (realToFrac $ hbUnit, realToFrac $ hbUnit)
        case atlas of
            Left err -> do
                print err
                return (key, Nothing)
            Right atlas' -> return (key, Just atlas')
    return (layout, M.fromList [(k, v) | (k, Just v) <- atlases])