Add a test for using fragments as a loading indicator

This commit is contained in:
Ryan Trinkle 2017-03-07 01:10:15 -05:00
parent 6fb8761a2c
commit 05fe04354e
4 changed files with 144 additions and 56 deletions

View File

@ -167,12 +167,15 @@ instance MonadTransControl (WithWebView x) where
restoreT = defaultRestoreT WithWebView
instance PerformEvent t m => PerformEvent t (WithWebView x m) where
type Performable (WithWebView x m) = WithWebView x (Performable m)
type Performable (WithWebView x m) = WithWebView x (Performable m) --TODO: Can we eliminate this wrapper?
{-# INLINABLE performEvent_ #-}
performEvent_ e = liftWith $ \run -> performEvent_ $ fmap run e
{-# INLINABLE performEvent #-}
performEvent e = liftWith $ \run -> performEvent $ fmap run e
instance ExhaustiblePerformEvent t m => ExhaustiblePerformEvent t (WithWebView x m) where
withPerformEventExhausted a = liftWith $ \run -> withPerformEventExhausted $ run a
runWithWebView :: WithWebView x m a -> WebViewSingleton x -> m a
runWithWebView = runReaderT . unWithWebView

View File

@ -142,6 +142,11 @@ class (Monad m, Reflex t, DomSpace (DomBuilderSpace m), MonadAdjust t m) => DomB
}
{-# INLINABLE wrapRawElement #-}
class DomBuilder t m => MountableDomBuilder t m where
type DomFragment m :: *
buildDomFragment :: m a -> m (DomFragment m, a)
mountDomFragment :: DomFragment m -> Event t (DomFragment m) -> m ()
type Namespace = Text
data TextNodeConfig t
@ -428,6 +433,11 @@ instance (DomBuilder t m, PerformEvent t m, MonadFix m, MonadHold t m) => DomBui
type DomBuilderSpace (PostBuildT t m) = DomBuilderSpace m
wrapRawElement e cfg = liftWith $ \run -> wrapRawElement e $ fmap1 run cfg
instance (MountableDomBuilder t m, PerformEvent t m, MonadFix m, MonadHold t m) => MountableDomBuilder t (PostBuildT t m) where
type DomFragment (PostBuildT t m) = DomFragment m
buildDomFragment = liftThrough buildDomFragment
mountDomFragment f0 f' = lift $ mountDomFragment f0 f'
instance (DomBuilder t m, Monoid w, MonadHold t m, MonadFix m) => DomBuilder t (DynamicWriterT t w m) where
type DomBuilderSpace (DynamicWriterT t w m) = DomBuilderSpace m
textNode = liftTextNode

View File

@ -415,6 +415,61 @@ instance SupportsImmediateDomBuilder t m => DomBuilder t (ImmediateDomBuilderT t
placeRawElement = append
wrapRawElement = wrap
data FragmentState
= FragmentState_Unmounted
| FragmentState_Mounted (DOM.Text, DOM.Text)
data ImmediateDomFragment = ImmediateDomFragment
{ _immediateDomFragment_document :: DOM.DocumentFragment
, _immediateDomFragment_state :: IORef FragmentState
}
extractFragment :: MonadIO m => ImmediateDomFragment -> m ()
extractFragment fragment = do
state <- liftIO $ readIORef $ _immediateDomFragment_state fragment
case state of
FragmentState_Unmounted -> return ()
FragmentState_Mounted (before, after) -> do
extractBetweenExclusive (_immediateDomFragment_document fragment) before after
liftIO $ writeIORef (_immediateDomFragment_state fragment) FragmentState_Unmounted
-- | s and e must both be children of the same node and s must precede e; all
-- nodes between s and e will be moved into the given DocumentFragment, but s
-- and e will not be moved
extractBetweenExclusive :: (MonadIO m, IsNode start, IsNode end) => DOM.DocumentFragment -> start -> end -> m ()
extractBetweenExclusive df s e = go
where
go = do
Just x <- getNextSibling s -- This can't be Nothing because we should hit 'e' first
when (toNode e /= toNode x) $ do
_ <- appendChild df $ Just x
go
instance SupportsImmediateDomBuilder t m => MountableDomBuilder t (ImmediateDomBuilderT t m) where
type DomFragment (ImmediateDomBuilderT t m) = ImmediateDomFragment
buildDomFragment w = do
initialEnv <- ImmediateDomBuilderT ask
Just df <- createDocumentFragment $ _immediateDomBuilderEnv_document initialEnv
result <- lift $ runImmediateDomBuilderT w $ initialEnv
{ _immediateDomBuilderEnv_parent = toNode df
}
state <- liftIO $ newIORef FragmentState_Unmounted
return (ImmediateDomFragment df state, result)
mountDomFragment fragment setFragment = do
parent <- askParent
extractFragment fragment
before <- textNodeInternal ("" :: Text)
_ <- appendChild parent $ Just $ _immediateDomFragment_document fragment
after <- textNodeInternal ("" :: Text)
xs <- foldDyn (\new (previous, _) -> (new, Just previous)) (fragment, Nothing) setFragment
performEvent_ $ ffor (updated xs) $ \(childFragment, Just previousFragment) -> do
extractFragment previousFragment
extractFragment childFragment
insertBefore (_immediateDomFragment_document childFragment) after
liftIO $ writeIORef (_immediateDomFragment_state childFragment) $ FragmentState_Mounted (before, after)
liftIO $ writeIORef (_immediateDomFragment_state fragment) $ FragmentState_Mounted (before, after)
instance (Reflex t, MonadAdjust t m, MonadIO m, MonadHold t m, PerformEvent t m, MonadIO (Performable m)) => MonadAdjust t (ImmediateDomBuilderT t m) where
runWithReplace a0 a' = do
initialEnv <- ImmediateDomBuilderT ask
@ -538,6 +593,9 @@ instance PerformEvent t m => PerformEvent t (ImmediateDomBuilderT t m) where
{-# INLINABLE performEvent #-}
performEvent e = lift $ performEvent e
instance ExhaustiblePerformEvent t m => ExhaustiblePerformEvent t (ImmediateDomBuilderT t m) where
withPerformEventExhausted a = liftWith $ \run -> withPerformEventExhausted $ run a
instance PostBuild t m => PostBuild t (ImmediateDomBuilderT t m) where
{-# INLINABLE getPostBuild #-}
getPostBuild = lift getPostBuild
@ -1019,58 +1077,3 @@ wrapWindow wv _ = do
}
makeLenses ''GhcjsEventSpec
data FragmentState
= FragmentState_Created
| FragmentState_Mounted (DOM.Text, DOM.Text)
data DomFragment = DomFragment
{ _domFragment_document :: DOM.DocumentFragment
, _domFragment_state :: IORef FragmentState
}
domFragment :: MonadIO m => ImmediateDomBuilderT t m a -> ImmediateDomBuilderT t m (DomFragment, a)
domFragment w = do
initialEnv <- ImmediateDomBuilderT ask
Just df <- createDocumentFragment $ _immediateDomBuilderEnv_document initialEnv
result <- lift $ runImmediateDomBuilderT w $ initialEnv
{ _immediateDomBuilderEnv_parent = toNode df
}
state <- liftIO $ newIORef FragmentState_Created
return (DomFragment df state, result)
mountDomFragment :: (PerformEvent t m, MonadIO m, MonadIO (Performable m), MonadFix m, MonadHold t m) => DomFragment -> Event t DomFragment -> ImmediateDomBuilderT t m ()
mountDomFragment fragment setFragment = do
parent <- askParent
extractFragment fragment
before <- textNodeInternal ("" :: Text)
_ <- appendChild parent $ Just $ _domFragment_document fragment
after <- textNodeInternal ("" :: Text)
xs <- foldDyn (\new (previous, _) -> (new, Just previous)) (fragment, Nothing) setFragment
performEvent_ $ ffor (updated xs) $ \(childFragment, Just previousFragment) -> do
extractFragment previousFragment
extractFragment childFragment
insertBefore (_domFragment_document childFragment) after
liftIO $ writeIORef (_domFragment_state childFragment) $ FragmentState_Mounted (before, after)
liftIO $ writeIORef (_domFragment_state fragment) $ FragmentState_Mounted (before, after)
extractFragment :: MonadIO m => DomFragment -> m ()
extractFragment fragment = do
state <- liftIO $ readIORef $ _domFragment_state fragment
case state of
FragmentState_Created -> return ()
FragmentState_Mounted (before, after) -> do
extractBetweenExclusive (_domFragment_document fragment) before after
liftIO $ writeIORef (_domFragment_state fragment) FragmentState_Created
-- | s and e must both be children of the same node and s must precede e; all
-- nodes between s and e will be moved into the given DocumentFragment, but s
-- and e will not be moved
extractBetweenExclusive :: (MonadIO m, IsNode start, IsNode end) => DOM.DocumentFragment -> start -> end -> m ()
extractBetweenExclusive df s e = go
where
go = do
Just x <- getNextSibling s -- This can't be Nothing because we should hit 'e' first
when (toNode e /= toNode x) $ do
_ <- appendChild df $ Just x
go

72
test/prebuild.hs Normal file
View File

@ -0,0 +1,72 @@
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecursiveDo #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeFamilies #-}
import Control.Monad
import Control.Monad.Trans
import Reflex.Dom
import qualified Data.Text as T
import Control.Concurrent
import Control.Monad.State.Strict
import Data.Monoid
import Data.Word
type Paused t = DynamicWriterT t All
runPaused :: (Reflex t, MonadFix m, MonadHold t m) => Paused t m a -> m (Event t (), a)
runPaused a = do
(result, runnable) <- runDynamicWriterT a
let done = void $ ffilter getAll $ updated $ uniqDyn runnable
return (done, result)
--TODO: Perhaps we could use guaranteed-single-shot events here
pausedUntil :: (Reflex t, MonadFix m, MonadHold t m) => Event t a -> Paused t m ()
pausedUntil e = do
tellDyn =<< holdDyn (All False) (All True <$ e) --TODO: Disconnect after one firing
dyn' :: (DomBuilder t m, PostBuild t m, m ~ Paused t m', MonadFix m', MonadHold t m') => Dynamic t (m a) -> m (Event t a)
dyn' child = do
postBuild <- getPostBuild
let newChild = leftmost [updated child, tagCheap (current child) postBuild]
newChildDone <- snd <$> runWithReplace (return ()) newChild
pausedUntil newChildDone
return newChildDone
main :: IO ()
main = mainWidget $ do
let slow = dynTree
{-
performEventChain = do
postBuild <- delay 0 =<< getPostBuild
rec let maxN = 10000
n = leftmost [0 <$ postBuild, ffilter (<=maxN) $ succ <$> n']
n' <- performEvent $ return <$> n
pausedUntil $ ffilter (==maxN) n'
_ <- widgetHold (text "Starting") $ text . T.pack . show <$> n
return ()
-}
dynTree = elAttr "div" ("style" =: "position:relative;width:256px;height:256px") $ go maxDepth
where maxDepth = 6 :: Int
go 0 = blank
go n = void $ dyn' $ pure $ do
let bgcolor = "rgba(0,0,0," <> T.pack (show (1 - (fromIntegral n / fromIntegral maxDepth) :: Double)) <> ")"
s pos = pos <> ";position:absolute;border:1px solid white;background-color:" <> bgcolor
elAttr "div" ("style" =: s "left:0;right:50%;top:0;bottom:50%") $ go $ pred n
elAttr "div" ("style" =: s "left:50%;right:0;top:0;bottom:50%") $ go $ pred n
elAttr "div" ("style" =: s "left:50%;right:0;top:50%;bottom:0") $ go $ pred n
elAttr "div" ("style" =: s "left:0;right:50%;top:50%;bottom:0") $ go $ pred n
el "h1" $ text "Bad"
el "div" $ do
draw <- button "Draw"
widgetHold blank $ ffor draw $ \_ -> void $ runPaused slow
el "h1" $ text "Good"
el "div" $ do
draw <- button "Draw"
widgetHold blank $ ffor draw $ \_ -> do
(df0, _) <- buildDomFragment $ text "Loading..."
(df', (doneBuilding, _)) <- buildDomFragment $ runPaused slow
mountDomFragment df0 $ df' <$ doneBuilding
postBuild <- getPostBuild
performEvent_ $ liftIO (threadDelay 0) <$ postBuild -- This is necessary so that ghcjs will release the thread back to the DOM so that we see the loading indicator immediately; we could instead adjust the parameters to GHCJS so that the thread quantum is smaller.
return ()