{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE CPP #-}
{-# LANGUAGE ExplicitNamespaces #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE FunctionalDependencies #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE InstanceSigs #-}
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE UndecidableInstances #-}
module Shpadoinkle.Core (
Html(..), Prop(..)
, textProp, listenerProp, flagProp
, listenRaw, listen, listenM, listenM_, listenC, listener
, h, baked, text
, props, children, name, textContent
, hoistHtml, hoistProp
, cataH, cataProp
, mapProps, mapChildren, injectProps, eitherH
, RawNode(..), RawEvent(..)
, Backend (..)
, type (~>)
, shpadoinkle
, runJSorWarp
, fullPage
, fullPageJSM
, simple
, JSM, MonadJSM, TVar, newTVarIO, readTVarIO
) where
import Control.Arrow (second)
import qualified Control.Categorical.Functor as F
import Control.Category ((.))
import Control.PseudoInverseCategory
import Data.Functor.Identity
import Data.Kind
import Data.String
import Data.Text
import GHCJS.DOM.Types (JSM, MonadJSM)
import Language.Javascript.JSaddle (FromJSVal (..), JSVal,
ToJSVal (..))
import Prelude hiding ((.))
import UnliftIO.STM (TVar, newTVarIO, readTVarIO)
#ifndef ghcjs_HOST_OS
import Language.Javascript.JSaddle.Warp (run)
#endif
import Shpadoinkle.Continuation
data Html :: (Type -> Type) -> Type -> Type where
Node :: Text -> [(Text, Prop m a)] -> [Html m a] -> Html m a
Potato :: JSM RawNode -> Html m a
TextNode :: Text -> Html m a
data Prop :: (Type -> Type) -> Type -> Type where
PText :: Text -> Prop m a
PListener :: (RawNode -> RawEvent -> JSM (Continuation m a)) -> Prop m a
PFlag :: Bool -> Prop m a
listenM :: Monad m => Text -> m (a -> a) -> (Text, Prop m a)
listenM k = listenC k . impur
listenM_ :: Monad m => Text -> m () -> (Text, Prop m a)
listenM_ k = listenC k . causes
type Props' m a = [(Text, Prop m a)]
hoistHtml :: Functor m => (m ~> n) -> Html m a -> Html n a
hoistHtml f = \case
Node t ps cs -> Node t (fmap (hoistProp f) <$> ps) (hoistHtml f <$> cs)
Potato p -> Potato p
TextNode t -> TextNode t
{-# INLINE hoistHtml #-}
hoistProp :: Functor m => (m ~> n) -> Prop m a -> Prop n a
hoistProp f = \case
PListener g -> PListener (\x y -> hoist f <$> g x y)
PText t -> PText t
PFlag b -> PFlag b
{-# INLINE hoistProp #-}
instance IsString (Html m a) where
fromString = TextNode . pack
{-# INLINE fromString #-}
instance IsString (Prop m a) where
fromString = PText . pack
{-# INLINE fromString #-}
instance Monad m => F.Functor EndoIso EndoIso (Html m) where
map (EndoIso f g i) = EndoIso (mapC . piapply $ map' (piendo f))
(mapC . piapply $ map' (piiso g i))
(mapC . piapply $ map' (piiso i g))
where map' :: EndoIso a b -> EndoIso (Continuation m a) (Continuation m b)
map' = F.map
{-# INLINE map #-}
instance Monad m => F.Functor EndoIso EndoIso (Prop m) where
map :: forall a b. EndoIso a b -> EndoIso (Prop m a) (Prop m b)
map f = EndoIso id mapFwd mapBack
where f' :: EndoIso (Continuation m a) (Continuation m b)
f' = F.map f
mapFwd (PText t) = PText t
mapFwd (PListener g) = PListener (\r e -> piapply f' <$> g r e)
mapFwd (PFlag b) = PFlag b
mapBack (PText t) = PText t
mapBack (PListener g) = PListener (\r e -> piapply (piinverse f') <$> g r e)
mapBack (PFlag b) = PFlag b
{-# INLINE map #-}
instance Continuous Html where
mapC f (Node t ps es) = Node t (unMapProps . mapC f $ MapProps ps) (mapC f <$> es)
mapC _ (Potato p) = Potato p
mapC _ (TextNode t) = TextNode t
{-# INLINE mapC #-}
newtype MapProps m a = MapProps { unMapProps :: Props' m a }
instance Monad m => F.Functor EndoIso EndoIso (MapProps m) where
map f = piiso MapProps unMapProps . fmapA (pisecond (F.map f)) . piiso unMapProps MapProps
{-# INLINE map #-}
instance Continuous MapProps where
mapC f = MapProps . fmap (second (mapC f)) . unMapProps
{-# INLINE mapC #-}
instance Continuous Prop where
mapC _ (PText t) = PText t
mapC f (PListener g) = PListener (\r e -> f <$> g r e)
mapC _ (PFlag b) = PFlag b
{-# INLINE mapC #-}
textProp :: Text -> Prop m a
textProp = PText
{-# INLINE textProp #-}
listenerProp :: (RawNode -> RawEvent -> JSM (Continuation m a)) -> Prop m a
listenerProp f = PListener (\r e -> f r e)
{-# INLINE listenerProp #-}
flagProp :: Bool -> Prop m a
flagProp = PFlag
{-# INLINE flagProp #-}
cataProp :: (Text -> b)
-> ((RawNode -> RawEvent -> JSM (Continuation m a)) -> b)
-> (Bool -> b)
-> Prop m a -> b
cataProp f g h' = \case
PText t -> f t
PListener l -> g l
PFlag b -> h' b
h :: Text -> [(Text, Prop m a)] -> [Html m a] -> Html m a
h = Node
{-# INLINE h #-}
baked :: JSM RawNode -> Html m a
baked = Potato
{-# INLINE baked #-}
text :: Text -> Html m a
text = TextNode
{-# INLINE text #-}
props :: Applicative f => ([(Text, Prop m a)] -> f [(Text, Prop m a)]) -> Html m a -> f (Html m a)
props inj = \case
Node t ps cs -> (\ps' -> Node t ps' cs) <$> inj ps
t -> pure t
{-# INLINE props #-}
children :: Applicative f => ([Html m a] -> f [Html m a]) -> Html m a -> f (Html m a)
children inj = \case
Node t ps cs -> Node t ps <$> inj cs
t -> pure t
{-# INLINE children #-}
name :: Applicative f => (Text -> f Text) -> Html m a -> f (Html m a)
name inj = \case
Node t ps cs -> (\t' -> Node t' ps cs) <$> inj t
t -> pure t
{-# INLINE name #-}
textContent :: Applicative f => (Text -> f Text) -> Html m a -> f (Html m a)
textContent inj = \case
TextNode t -> TextNode <$> inj t
n -> pure n
{-# INLINE textContent #-}
eitherH :: Monad m => (a -> Html m a) -> (b -> Html m b) -> Either a b -> Html m (Either a b)
eitherH = eitherC
{-# INLINE eitherH #-}
cataH :: (Text -> [(Text, Prop m a)] -> [b] -> b)
-> (JSM RawNode -> b)
-> (Text -> b)
-> Html m a -> b
cataH f g h' = \case
Node t ps cs -> f t ps (cataH f g h' <$> cs)
Potato p -> g p
TextNode t -> h' t
type m ~> n = forall a. m a -> n a
newtype RawNode = RawNode { unRawNode :: JSVal }
instance ToJSVal RawNode where toJSVal = return . unRawNode
instance FromJSVal RawNode where fromJSVal = return . Just . RawNode
newtype RawEvent = RawEvent { unRawEvent :: JSVal }
instance ToJSVal RawEvent where toJSVal = return . unRawEvent
instance FromJSVal RawEvent where fromJSVal = return . Just . RawEvent
instance {-# OVERLAPPING #-} IsString [(Text, Prop m a)] where
fromString = pure . ("className", ) . textProp . pack
{-# INLINE fromString #-}
listener :: Continuation m a -> Prop m a
listener = listenerProp . const . const . return
{-# INLINE listener #-}
listenRaw :: Text -> (RawNode -> RawEvent -> JSM (Continuation m a)) -> (Text, Prop m a)
listenRaw k = (,) k . listenerProp
{-# INLINE listenRaw #-}
listenC :: Text -> Continuation m a -> (Text, Prop m a)
listenC k = listenRaw k . const . const . return
{-# INLINE listenC #-}
listen :: Text -> a -> (Text, Prop m a)
listen k = listenC k . constUpdate
{-# INLINE listen #-}
mapProps :: ([(Text, Prop m a)] -> [(Text, Prop m a)]) -> Html m a -> Html m a
mapProps f = runIdentity . props (Identity . f)
{-# INLINE mapProps #-}
mapChildren :: ([Html m a] -> [Html m a]) -> Html m a -> Html m a
mapChildren f = runIdentity . children (Identity . f)
{-# INLINE mapChildren #-}
injectProps :: [(Text, Prop m a)] -> Html m a -> Html m a
injectProps ps = mapProps (++ ps)
{-# INLINE injectProps #-}
class Backend b m a | b m -> a where
type VNode b m
interpret
:: (m ~> JSM)
-> Html (b m) a
-> b m (VNode b m)
patch
:: RawNode
-> Maybe (VNode b m)
-> VNode b m
-> b m (VNode b m)
setup :: JSM () -> JSM ()
shpadoinkle
:: forall b m a
. Backend b m a => Monad (b m) => Eq a
=> (m ~> JSM)
-> (TVar a -> b m ~> m)
-> a
-> TVar a
-> (a -> Html (b m) a)
-> b m RawNode
-> JSM ()
shpadoinkle toJSM toM initial model view stage = do
let
j :: b m ~> JSM
j = toJSM . toM model
go :: RawNode -> VNode b m -> a -> JSM (VNode b m)
go c n a = j $ do
!m <- interpret toJSM (view a)
patch c (Just n) m
setup @b @m @a $ do
(c,n) <- j $ do
c <- stage
n <- interpret toJSM (view initial)
_ <- patch c Nothing n
return (c,n)
_ <- shouldUpdate (go c) n model
return ()
fullPage
:: Backend b m a => Monad (b m) => Eq a
=> (m ~> JSM)
-> (TVar a -> b m ~> m)
-> a
-> (a -> Html (b m) a)
-> b m RawNode
-> JSM ()
fullPage g f i view getStage = do
model <- newTVarIO i
shpadoinkle g f i model view getStage
{-# INLINE fullPage #-}
fullPageJSM
:: Backend b JSM a => Monad (b JSM) => Eq a
=> (TVar a -> b JSM ~> JSM)
-> a
-> (a -> Html (b JSM) a)
-> b JSM RawNode
-> JSM ()
fullPageJSM = fullPage id
{-# INLINE fullPageJSM #-}
runJSorWarp :: Int -> JSM () -> IO ()
#ifdef ghcjs_HOST_OS
runJSorWarp _ = id
{-# INLINE runJSorWarp #-}
#else
runJSorWarp = run
{-# INLINE runJSorWarp #-}
#endif
simple
:: Backend b JSM a => Monad (b JSM) => Eq a
=> (TVar a -> b JSM ~> JSM)
-> a
-> (a -> Html (b JSM) a)
-> b JSM RawNode
-> JSM ()
simple = fullPageJSM
{-# INLINE simple #-}