packages feed

jsaddle-webkitgtk-0.10.0.0: src/Language/Javascript/JSaddle/WebKitGTK.hs

{-# LANGUAGE CPP #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PatternSynonyms #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE MonoLocalBinds #-}
-----------------------------------------------------------------------------
--
-- Module      :  Language.Javascript.JSaddle.WebKitGTK
-- Copyright   :  (c) Hamish Mackenzie
-- License     :  MIT
--
-- Maintainer  :  Hamish Mackenzie <Hamish.K.Mackenzie@googlemail.com>
--
-- | Run jsaddle apps in a GTK4 window hosting a WebKitGTK 6.0 WebView.
--
-----------------------------------------------------------------------------
module Language.Javascript.JSaddle.WebKitGTK (
  -- * Running JSM in a WebView
    run
#ifndef ghcjs_HOST_OS
  , runInWebView
#endif
  ) where

#ifdef ghcjs_HOST_OS

run :: IO () -> IO ()
run = id
{-# INLINE run #-}

#else

import Control.Monad (void)
import Control.Concurrent (forkIO, yield)

#ifndef mingw32_HOST_OS
import System.Posix.Signals (installHandler, keyboardSignal, Handler(Catch))
#endif
import System.Directory (getCurrentDirectory)

import Data.ByteString.Lazy (toStrict, fromStrict)
import qualified Data.ByteString.Lazy as LB (ByteString)
import Data.Aeson (encode, decode)
import qualified Data.Text as T (pack)
import Data.Text.Encoding (decodeUtf8, encodeUtf8)

import GI.GLib
       (timeoutAdd, idleAdd, mainLoopNew, mainLoopRun, mainLoopQuit,
        pattern PRIORITY_HIGH, pattern PRIORITY_DEFAULT)
import qualified GI.Gtk as Gtk (init)
import GI.Gtk
       (Window, windowNew, windowSetDefaultSize, windowSetChild,
        windowPresent, windowDestroy, scrolledWindowNew,
        scrolledWindowSetChild, onWidgetDestroy)
import GI.Gio (Cancellable)
import GI.JavaScriptCore (valueToString)
import GI.WebKit
       (scriptDialogPromptSetText, scriptDialogPromptGetDefaultText,
        scriptDialogGetMessage, scriptDialogGetDialogType,
        onWebViewScriptDialog, WebView, webViewNew,
        setSettingsEnableWriteConsoleMessagesToStdout,
        setSettingsEnableJavascript,
        onUserContentManagerScriptMessageReceived,
        userContentManagerRegisterScriptMessageHandler,
        webViewGetUserContentManager,
        webViewEvaluateJavascript, LoadEvent(..), webViewLoadHtml,
        onWebViewLoadChanged, setSettingsEnableDeveloperExtras,
        webViewSetSettings, webViewGetSettings, ScriptDialogType(..))

import Language.Javascript.JSaddle (JSM, Results, Batch)
import Language.Javascript.JSaddle.Run (runJavaScript)
import Language.Javascript.JSaddle.Run.Files (initState, runBatch, ghcjsHelpers)

noCancellable :: Maybe Cancellable
noCancellable = Nothing

quitWindow :: Window -> IO ()
quitWindow window = postGUIAsync $ windowDestroy window

installQuitHandler :: Window -> IO ()
#ifdef mingw32_HOST_OS
installQuitHandler _ = return () -- TODO: Maybe figure something out here for Windows users.
#else
installQuitHandler window = void $ installHandler keyboardSignal (Catch (quitWindow window)) Nothing
#endif

postGUIAsync :: IO () -> IO ()
postGUIAsync action =
  void . idleAdd PRIORITY_DEFAULT $ action >> return False

run :: JSM () -> IO ()
run main = do
    Gtk.init
    loop <- mainLoopNew Nothing False
    _ <- timeoutAdd PRIORITY_HIGH 10 (yield >> return True)
    window <- windowNew
    windowSetDefaultSize window 900 600
    scrollWin <- scrolledWindowNew
    webView <- webViewNew
    settings <- webViewGetSettings webView
    setSettingsEnableDeveloperExtras settings True
    setSettingsEnableJavascript settings True
    setSettingsEnableWriteConsoleMessagesToStdout settings True
    webViewSetSettings webView settings
    scrolledWindowSetChild scrollWin (Just webView)
    windowSetChild window (Just scrollWin)
    _ <- onWidgetDestroy window $ mainLoopQuit loop
    windowPresent window
    pwd <- getCurrentDirectory
    let webViewLoadChangedCallback LoadEventFinished = runInWebView main webView
        webViewLoadChangedCallback _                 = return ()
    _ <- onWebViewLoadChanged webView webViewLoadChangedCallback
    webViewLoadHtml webView "" . Just $ "file://" <> T.pack pwd <> "/index.html"
    installQuitHandler window
    mainLoopRun loop

runInWebView :: JSM () -> WebView -> IO ()
runInWebView f webView = do
    (processResults, syncResults, start) <- runJavaScript (\batch -> postGUIAsync $
        webViewEvaluateJavascript webView
            (decodeUtf8 . toStrict $ "runJSaddleBatch(" <> encode batch <> ");")
            (-1) Nothing Nothing noCancellable Nothing)
        f

    addJSaddleHandler webView processResults syncResults
    webViewEvaluateJavascript webView (decodeUtf8 $ toStrict jsaddleJs)
        (-1) Nothing Nothing noCancellable . Just $
            \_obj _asyncResult ->
                void $ forkIO start

addJSaddleHandler :: WebView -> (Results -> IO ()) -> (Results -> IO Batch) -> IO ()
addJSaddleHandler webView processResult syncResults = do
    manager <- webViewGetUserContentManager webView
    _ <- onUserContentManagerScriptMessageReceived manager Nothing $ \value -> do
        bs <- encodeUtf8 <$> valueToString value
        mapM_ processResult (decode (fromStrict bs))
    _ <- onWebViewScriptDialog webView $ \dialog ->
        scriptDialogGetDialogType dialog >>= \case
            ScriptDialogTypePrompt ->
                scriptDialogGetMessage dialog >>= \case
                    "JSaddleSync" -> do
                        resultsText <- scriptDialogPromptGetDefaultText dialog
                        case decode (fromStrict $ encodeUtf8 resultsText) of
                            Just results -> do
                                batch <- syncResults results
                                scriptDialogPromptSetText dialog (decodeUtf8 . toStrict $ encode batch)
                                return True
                            Nothing -> return False
                    _ -> return False
            _ -> return False

    void $ userContentManagerRegisterScriptMessageHandler manager "jsaddle" Nothing

jsaddleJs :: LB.ByteString
jsaddleJs = ghcjsHelpers <> mconcat
    [ "runJSaddleBatch = (function() {\n"
    , initState
    , "\nreturn function(batch) {\n"
    , runBatch (\a -> "window.webkit.messageHandlers.jsaddle.postMessage(JSON.stringify(" <> a <> "));\n")
               (Just (\a -> "JSON.parse(window.prompt(\"JSaddleSync\", JSON.stringify(" <> a <> ")))"))
    , "};\n"
    , "})()"
    ]

#endif