-- | -- Module : Reflex.Dom.Retractable.Class -- Copyright : (c) 2019 ATUM SOLUTIONS AG -- License : MIT -- Maintainer : ncrashed@protonmail.com -- Stability : unstable -- Portability : non-portable -- -- Tagless final interface to the retractable stack of widgets. To use the API -- drop `retractStack` into your code where you want to display widgets with -- history. For instance, turn all your frontend into retractable widget: -- -- @ -- import Control.Monad -- import Reflex.Dom -- import Reflex.Dom.Retractable.Class -- -- frontend :: (MonadWidget t m, MonadRetract t m) => m () -- frontend = void $ retractStack $ pageA 42 -- -- pageA :: (MonadWidget t m, MonadRetract t m) => Int -> m () -- pageA n = do -- e <- button "Go page B" -- void $ nextWidget $ ffor e $ const Retractable { -- retractableNext = pageB $ n + 1 -- , retractablePrev = Just $ pure $ pageA n -- } -- -- pageB :: (MonadWidget t m, MonadRetract t m) => Int -> m () -- pageB n = do -- e <- button "Go page A" -- void $ nextWidget $ ffor e $ const Retractable { -- retractableNext = pageA $ n + 1 -- , retractablePrev = Just $ pure $ pageB n -- } -- @ -- module Reflex.Dom.Retractable.Class( Retractable(..) , morphRetractable , MonadRetract(..) , retractStack ) where import Control.Monad.Fix import Control.Monad.Reader import Data.Sequence (Seq) import GHC.Generics (Generic) import Reflex import Reflex.Network import qualified Data.Sequence as S -- | Information about widget that attaches information how to return to the -- widget. data Retractable t m = Retractable { -- | Which widget we are switching in retractableNext :: m () -- | Possible return to the current widget. 'Nothing' means that the -- current widget is not rembered in retract stack and is forgoten. -- Dynamic allows to save internal state of widget on return. , retractablePrev :: Maybe (Dynamic t (m ())) } deriving (Generic) -- | Helper to transform underlying monad in `Retractable` morphRetractable :: Reflex t => (forall a . n a -> m a) -> Retractable t n -> Retractable t m morphRetractable f (Retractable next prev) = Retractable (f next) (fmap f <$> prev) -- | Defines context of widget that can switch control to next widget -- and can be returned back with preserving of state. class (MonadHold t m, MonadFix m, Reflex t, Adjustable t m) => MonadRetract t m where -- | Switch current widget to the next widget. Returns event that fires -- when the switching is about to happen. nextWidget :: Event t (Retractable t m) -> m (Event t ()) -- | Switch to previous widget in stack. Returns event that fires -- when the switchin is about to happen. retract :: Event t () -> m (Event t ()) -- | Wipe retract history to the given amount of items (or all if `Nothing` is passed). -- Returns event that fires when the history was changed. wipeRetract :: Event t (Maybe Int) -> m (Event t ()) -- | Get event that fires when any of input events in `nextWidget` is triggered. -- It's used for implementation of retractable stack. nextWidgetEvent :: m (Event t (Retractable t m)) -- | Get event that fires when any of input events in `retract` is triggered. -- It's used for implementation of retractable stack. retractEvent :: m (Event t ()) -- | Get event that fires when any of input events in `wipeRetract` is triggered. -- It's used for implementation of retractable stack. wipeRetractEvent :: m (Event t (Maybe Int)) -- | Get current stack of widget history getRetractStack :: m (Dynamic t [Retractable t m]) -- | Execute subcomputation with given widget history. Affects results of `getRetractStack`. withRetractStack :: Dynamic t [Retractable t m] -> m a -> m a -- | Helper ADT to merge actions with retractable stack data StackAction t m = StackPush (Retractable t m) | StackPop | StackWipe (Maybe Int) -- | All body of the widget will be rerendered when some subcomputation emits switching event. -- Plug the function somewhere close to the root of your reflex frontend and use functions from -- `MonadRetract` class to control switching content of the widget. retractStack :: forall t m . MonadRetract t m => m () -> m (Event t ()) retractStack ma = do nextE <- fmap StackPush <$> nextWidgetEvent backE <- fmap (const StackPop) <$> retractEvent wipeE <- fmap StackWipe <$> wipeRetractEvent let actionE = leftmost [nextE, backE, wipeE] let go :: StackAction t m -> [Retractable t m] -> [Retractable t m] go a rs = case a of StackPush r -> maybe rs (const $ r : rs) $ retractablePrev r StackPop -> drop 1 rs StackWipe Nothing -> [] StackWipe (Just i) -> drop i rs stackD :: Dynamic t [Retractable t m] <- foldDyn go [] actionE resD <- withRetractStack stackD $ networkHold ma $ flip push actionE $ \case StackPush r -> pure . Just $ retractableNext r StackPop -> do rs <- sample . current $ stackD case rs of (r : _) | Just maD <- retractablePrev r -> fmap Just . sample . current $ maD _ -> pure . Just $ ma StackWipe _ -> pure Nothing pure $ updated resD instance MonadRetract t m => MonadRetract t (ReaderT r m) where nextWidget e = do r <- ask lift $ nextWidget (morphRetractable (flip runReaderT r) <$> e) {-# INLINE nextWidget #-} retract = lift . retract {-# INLINE retract #-} wipeRetract = lift . wipeRetract {-# INLINE wipeRetract #-} nextWidgetEvent = do e <- lift nextWidgetEvent pure $ morphRetractable lift <$> e {-# INLINE nextWidgetEvent #-} retractEvent = lift retractEvent {-# INLINE retractEvent #-} wipeRetractEvent = lift wipeRetractEvent {-# INLINE wipeRetractEvent #-} getRetractStack = do st <- lift getRetractStack pure $ fmap (morphRetractable lift) <$> st {-# INLINE getRetractStack #-} withRetractStack st ma = do r <- ask let st' = fmap (morphRetractable (flip runReaderT r)) <$> st lift $ withRetractStack st' (runReaderT ma r) {-# INLINE withRetractStack #-}