File: Dom.hs

package info (click to toggle)
haskell-lambdahack 0.11.0.1-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 4,056 kB
  • sloc: haskell: 45,636; makefile: 219
file content (283 lines) | stat: -rw-r--r-- 11,323 bytes parent folder | download | duplicates (3)
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
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
-- | Text frontend running in a browser.
module Game.LambdaHack.Client.UI.Frontend.Dom
  (
#ifdef USE_BROWSER
-- to molify doctest, but don't break stylish-haskell parsing
   startup, frontendName
#endif
  ) where

#ifdef USE_BROWSER
import Prelude ()

import Game.LambdaHack.Core.Prelude

import           Control.Concurrent
import qualified Control.Monad.IO.Class as IO
import           Control.Monad.Trans.Reader (ask)
import           Data.IORef
import qualified Data.Vector as V
import qualified Data.Vector.Unboxed as U
import           Data.Word (Word32)

import GHCJS.DOM (currentDocument, currentWindow)
import GHCJS.DOM.CSSStyleDeclaration (setProperty)
import GHCJS.DOM.Document (createElement, getBodyUnchecked)
import GHCJS.DOM.Element (Element (Element), setInnerHTML)
import GHCJS.DOM.ElementCSSInlineStyle (getStyle)
import GHCJS.DOM.EventM
  ( EventM
  , mouseAltKey
  , mouseButton
  , mouseCtrlKey
  , mouseMetaKey
  , mouseShiftKey
  , on
  , preventDefault
  , stopPropagation
  )
import GHCJS.DOM.GlobalEventHandlers (contextMenu, keyDown, mouseUp, wheel)
import GHCJS.DOM.HTMLCollection (itemUnsafe)
import GHCJS.DOM.HTMLElement (focus)
import GHCJS.DOM.HTMLTableElement
  (HTMLTableElement (HTMLTableElement), getRows, setCellPadding, setCellSpacing)
import GHCJS.DOM.HTMLTableRowElement
  (HTMLTableRowElement (HTMLTableRowElement), getCells)
import GHCJS.DOM.KeyboardEvent
  (getAltGraphKey, getAltKey, getCtrlKey, getKey, getMetaKey, getShiftKey)
import GHCJS.DOM.Node (appendChild_, replaceChild_, setTextContent)
import GHCJS.DOM.NonElementParentNode (getElementByIdUnsafe)
import GHCJS.DOM.RequestAnimationFrameCallback
import GHCJS.DOM.Types
  ( CSSStyleDeclaration
  , DOM
  , HTMLDivElement (HTMLDivElement)
  , HTMLTableCellElement (HTMLTableCellElement)
  , IsMouseEvent
  , JSString
  , Window
  , runDOM
  , unsafeCastTo
  )
import GHCJS.DOM.WheelEvent (getDeltaY)
import GHCJS.DOM.Window (requestAnimationFrame_)

import           Game.LambdaHack.Client.UI.Content.Screen
import           Game.LambdaHack.Client.UI.Frame
import           Game.LambdaHack.Client.UI.Frontend.Common
import qualified Game.LambdaHack.Client.UI.Key as K
import           Game.LambdaHack.Client.UI.PointUI
import           Game.LambdaHack.Common.Area
import           Game.LambdaHack.Common.ClientOptions
import           Game.LambdaHack.Common.Point
import qualified Game.LambdaHack.Common.PointArray as PointArray
import           Game.LambdaHack.Content.TileKind (floorSymbol)
import qualified Game.LambdaHack.Definition.Color as Color

-- | Session data maintained by the frontend.
data FrontendSession = FrontendSession
  { scurrentWindow :: Window
  , scharCells     :: V.Vector (HTMLTableCellElement, CSSStyleDeclaration)
  , spreviousFrame :: IORef SingleFrame
  }

-- | The name of the frontend.
frontendName :: String
frontendName = "browser"

-- | Starts the main program loop using the frontend input and output.
startup :: ScreenContent -> ClientOptions -> IO RawFrontend
startup coscreen soptions = do
  rfMVar <- newEmptyMVar
  flip runDOM undefined $ runWeb coscreen soptions rfMVar
  takeMVar rfMVar

runWeb :: ScreenContent -> ClientOptions -> MVar RawFrontend -> DOM ()
runWeb coscreen ClientOptions{..} rfMVar = do
  -- Init the document.
  Just doc <- currentDocument
  Just scurrentWindow <- currentWindow
  body <- getBodyUnchecked doc
  pageStyle <- getStyle body
  setProp pageStyle "background-color" (Color.colorToRGB Color.Black)
  setProp pageStyle "color" (Color.colorToRGB Color.AltWhite)
  -- Create the session record.
  divBlockRaw <- createElement doc ("div" :: Text)
  divBlock <- unsafeCastTo HTMLDivElement divBlockRaw
  let cell = "<td>\x00a0"
      row = "<tr>" ++ concat (replicate (rwidth coscreen) cell)
      rows = concat (replicate (rheight coscreen) row)
  tableElemRaw <- createElement doc ("table" :: Text)
  tableElem <- unsafeCastTo HTMLTableElement tableElemRaw
  -- Get rid of table spacing. Spurious hacks just in case.
  setCellPadding tableElem ("0" :: Text)
  setCellSpacing tableElem ("0" :: Text)
  appendChild_ divBlock tableElem
  setInnerHTML tableElem rows
  scharCells <- flattenTable coscreen tableElem
  spreviousFrame <- newIORef $ blankSingleFrame coscreen
  let sess = FrontendSession{..}
  rf <- IO.liftIO $ createRawFrontend coscreen (display sess) shutdown
  let readMod = do
        modCtrl <- ask >>= getCtrlKey
        modShift <- ask >>= getShiftKey
        modAlt <- ask >>= getAltKey
        modMeta <- ask >>= getMetaKey
        modAltG <- ask >>= getAltGraphKey
        return $! modifierTranslate modCtrl modShift (modAlt || modAltG) modMeta
  gameMap <- getElementByIdUnsafe doc ("gameMap" :: Text)
  divMap <- unsafeCastTo HTMLDivElement gameMap
  focus divMap
  void $ divMap `on` keyDown $ do
    keyId <- ask >>= getKey
    modifier <- readMod
--  This is currently broken at least for Shift-F1, etc., so won't be used:
--    keyLoc <- ask >>= getKeyLocation
--    let onKeyPad = case keyLoc of
--          3 {-KEY_LOCATION_NUMPAD-} -> True
--          _ -> False
    let key = K.keyTranslateWeb keyId (modifier == K.Shift)
        modifierNoShift = case modifier of  -- to prevent S-!, etc.
          K.Shift -> K.NoModifier
          K.ControlShift -> K.Control
          K.AltShift -> K.Alt
          _ -> modifier
    -- IO.liftIO $ do
    --   putStrLn $ "keyId: " ++ keyId
    --   putStrLn $ "key: " ++ K.showKey key
    --   putStrLn $ "modifier: " ++ show modifier
    when (key == K.Esc) $ IO.liftIO $ resetChanKey (fchanKey rf)
    IO.liftIO $ saveKMP rf modifierNoShift key (PointUI 0 0)
    -- Pass through C-+ and others, but disable special behaviour on Tab, etc.
    let browserKeys = "+-0tTnNdxcv"
    unless (modifier == K.Alt
            || modifier == K.Control && key `elem` map K.Char browserKeys
            || key == K.DeadKey) $ do  -- NumLock in particular
      preventDefault
      stopPropagation
  -- Handle mouseclicks, per-cell.
  let setupMouse i a =
        let Point{..} = punindex (rwidth coscreen) i
              -- abuse of convention in that @Point@ used for screen, not map
            pUI = squareToUI $ PointSquare px py
        in handleMouse rf a pUI
  V.imapM_ setupMouse scharCells
  -- Display at the end to avoid redraw. Replace "Please wait".
  pleaseWait <- getElementByIdUnsafe doc ("pleaseWait" :: Text)
  replaceChild_ gameMap divBlock pleaseWait
  IO.liftIO $ putMVar rfMVar rf
    -- send to client only after the whole webpage is set up
    -- because there is no @mainGUI@ to start accepting

shutdown :: IO ()
shutdown = return () -- nothing to clean up

setProp :: CSSStyleDeclaration -> JSString -> Text -> DOM ()
setProp style propRef propValue =
  setProperty style propRef propValue (Nothing :: Maybe JSString)

-- | Let each table cell handle mouse events inside.
handleMouse :: RawFrontend
            -> (HTMLTableCellElement, CSSStyleDeclaration) -> PointUI
            -> DOM ()
handleMouse rf (cell, _) pUI = do
  let readMod :: IsMouseEvent e => EventM HTMLTableCellElement e K.Modifier
      readMod = do
        modCtrl <- mouseCtrlKey
        modShift <- mouseShiftKey
        modAlt <- mouseAltKey
        modMeta <- mouseMetaKey
        return $! modifierTranslate modCtrl modShift modAlt modMeta
      saveWheel = do
        wheelY <- ask >>= getDeltaY
        modifier <- readMod
        let mkey = if | wheelY < -0.01 -> Just K.WheelNorth
                      | wheelY > 0.01 -> Just K.WheelSouth
                      | otherwise -> Nothing  -- probably a glitch
        maybe (return ())
              (\key -> IO.liftIO $ saveKMP rf modifier key pUI) mkey
      saveMouse = do
        -- <https://hackage.haskell.org/package/ghcjs-dom-0.2.1.0/docs/GHCJS-DOM-EventM.html>
        but <- mouseButton
        modifier <- readMod
        let key = case but of
              0 -> K.LeftButtonRelease
              1 -> K.MiddleButtonRelease
              2 -> K.RightButtonRelease  -- not handled in contextMenu
              _ -> K.LeftButtonRelease  -- any other is alternate left
        -- IO.liftIO $ putStrLn $
        --   "m: " ++ show but ++ show modifier ++ show pUI
        IO.liftIO $ saveKMP rf modifier key pUI
  void $ cell `on` wheel $ do
    saveWheel
    preventDefault
    stopPropagation
  void $ cell `on` contextMenu $ do
    preventDefault
    stopPropagation
  void $ cell `on` mouseUp $ do
    saveMouse
    preventDefault
    stopPropagation

-- | Get the list of all cells of an HTML table.
flattenTable :: ScreenContent
             -> HTMLTableElement
             -> DOM (V.Vector (HTMLTableCellElement, CSSStyleDeclaration))
flattenTable coscreen table = do
  rows <- getRows table
  let f y = do
        rowsItem <- itemUnsafe rows y
        unsafeCastTo HTMLTableRowElement rowsItem
  lrow <- mapM f [0 .. toEnum (rheight coscreen - 1)]
  let getC :: HTMLTableRowElement
           -> DOM [(HTMLTableCellElement, CSSStyleDeclaration)]
      getC row = do
        cells <- getCells row
        let g x = do
              cellsItem <- itemUnsafe cells x
              cell <- unsafeCastTo HTMLTableCellElement cellsItem
              style <- getStyle cell
              return (cell, style)
        mapM g [0 .. toEnum (rwidth coscreen - 1)]
  lrc <- mapM getC lrow
  return $! V.fromListN (rwidth coscreen * rheight coscreen) $ concat lrc

-- | Output to the screen via the frontend.
display :: FrontendSession  -- ^ frontend session data
        -> SingleFrame  -- ^ the screen frame to draw
        -> IO ()
display FrontendSession{..} !curFrame = flip runDOM undefined $ do
  let setChar :: Int -> (Word32, Word32) -> DOM Int
      setChar !i (!w, !wPrev) | w == wPrev = return $! i + 1
      setChar i (w, wPrev) = do
        let Point{..} = toEnum i
            Color.AttrChar{acAttr=Color.Attr{fg=fgRaw,bg}, acChar} =
              Color.attrCharFromW32 $ Color.AttrCharW32 w
            fg | even py && fgRaw == Color.White = Color.AltWhite
               | otherwise = fgRaw
            (!cell, !style) = scharCells V.! i
        if | acChar == ' ' -> setTextContent cell $ Just ("\x00a0" :: JSString)
           | acChar == floorSymbol && not (Color.isBright fg) ->
             setTextContent cell $ Just ("\x22C5" :: JSString)
           | otherwise -> setTextContent cell $ Just [acChar]
        setProp style "color" $ Color.colorToRGB fg
        let bgPrev = Color.bgFromW32 $ Color.AttrCharW32 wPrev
        when (bg /= bgPrev) $ do
          let background = if bg == Color.HighlightBackground
                           then "#251F1F"
                           else Color.colorToRGB Color.Black
          setProp style "background-color" background
          setProp style "border-color"
                        (Color.colorToRGB $ Color.highlightToColor bg)
        return $! i + 1
  !prevFrame <- readIORef spreviousFrame
  writeIORef spreviousFrame curFrame
  -- This continues asynchronously, if can't otherwise.
  callback <- newRequestAnimationFrameCallbackSync $ \_ ->
    U.foldM'_ setChar 0 $ U.zip (PointArray.avector $ singleArray curFrame)
                                (PointArray.avector $ singleArray prevFrame)
  -- This attempts to ensure no redraws while callback executes
  -- and a single redraw when it completes.
  requestAnimationFrame_ scurrentWindow callback
#endif