packages feed

FULE-0.3.1: examples/Reactor.hs

-- This module contains a full example of a Presenter in a Model-View-Presenter
-- architecture utilizing `Reactor`s.
--
{-# LANGUAGE GADTs #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE ScopedTypeVariables #-}

module Reactor where

import Control.Monad
import Data.Maybe
import FULE


--------------------------------
-- VIEW
--
-- This section contains types and functions which represent the interaction
-- with the View of the MVP.
--------------------------------

data UIMessage
  = WindowSizeChanged
    { windowWidthOf :: Int
    , windowHeightOf :: Int
    }
  | MouseMoved
    { xPositionOf :: Int
    , yPositionOf :: Int
    , xDeltaOf :: Int
    , yDeltaOf :: Int
    }

data ForView = ForView

instance Semigroup ForView where
  a <> b = ForView

instance Monoid ForView where
  mempty = ForView

getUIMessage :: (Monad m) => m UIMessage
getUIMessage = undefined -- for you to implement

interactWithView :: (Monad m) => ForView -> m UIMessage
interactWithView = undefined -- for you to implement


--------------------------------
-- MODEL
--
-- This section contains types and functions which represent the interaction
-- with the Model of the MVP.
--------------------------------

data FromModel

data ForModel = ForModel

instance Semigroup ForModel where
  a <> b = ForModel

instance Monoid ForModel where
  mempty = ForModel

interactWithModel :: (Monad m) => ForModel -> m FromModel
interactWithModel = undefined -- for you to implement


--------------------------------
-- Reactor
--
-- This section contains types used to interact with a Reactor's contents
-- via the Reaction typeclass.
--
-- These types are generic so that the same set of data constructors could be
-- used with multiple MVP triads.
--------------------------------

data ReactorInput mi
  = LayoutInput Layout Bounds -- bounds for a component
  | ModelInput mi
  | NoInput
  | ViewInput UIMessage

-- GADT for selecting the `getProduct`'s output type (the `o` param)
data ReactorOutput mo po vo o where
  LayoutOutput :: ReactorOutput mo po vo (Layout -> Layout)
  ModelOutput :: ReactorOutput mo po vo mo
  NoOutput :: ReactorOutput mo po vo ()
  PresenterOutput :: ReactorOutput mo po vo po
  ViewOutput :: ReactorOutput mo po vo vo


--------------------------------
-- Window Control
--------------------------------

data WindowWidget
  = ResizeControl
    { currentWidthOf :: Int
    , currentHeightOf :: Int
    , deltaWidthOf :: Int
    , deltaHeightOf :: Int
    , widthGuideOf :: GuideID
    , heightGuideOf :: GuideID
    }

instance Reaction WindowWidget (ReactorInput mi) (ReactorOutput mo po vo) where
  addReactant (ViewInput (WindowSizeChanged w h)) _ ww =
    -- record the delta at a window size change
    ww
    { currentWidthOf = w
    , currentHeightOf = h
    , deltaWidthOf = w - currentWidthOf ww
    , deltaHeightOf = h - currentHeightOf ww
    }
  addReactant _ _ ww = -- catchall for other input cases
    -- reset the deltas
    ww { deltaWidthOf = 0, deltaHeightOf = 0 }
  getProduct _ LayoutOutput ww =
    -- react to a window size change if there has been one
    if dw == 0 && dh == 0 then Nothing else Just . reactToChanges
    . (if dw /= 0 then ((wg, dw):) else id)
    . (if dh /= 0 then ((hg, dh):) else id)
    $ []
    where
      ResizeControl
        { deltaWidthOf = dw
        , deltaHeightOf = dh
        , widthGuideOf = wg
        , heightGuideOf = hg
        } = ww
  getProduct _ _ _ = Nothing -- catchall for other output cases

makeWindowWidget
  :: (Monad m)
  => Int -> Int -> GuideID -> GuideID
  -> Reactor (ReactorInput mi) (ReactorOutput mo po vo) m
makeWindowWidget w h wg hg = reactor (ResizeControl w h 0 0 wg hg)


--------------------------------
-- UI Widgets
--------------------------------

data UIWidget = UIWidget

instance Reaction UIWidget (ReactorInput mi) (ReactorOutput mo po vo) where
  addReactant _ _ uw = uw
  getProduct _ _ _ = Nothing

makeUIWidget
  :: (Monad m)
  => Reactor (ReactorInput mi) (ReactorOutput mo po vo) m
makeUIWidget = reactor UIWidget


--------------------------------
-- Presenter
--------------------------------

data ForPresenter
  = NotQuit
  | Quit
  deriving (Eq)

hasQuit :: [ForPresenter] -> Bool
hasQuit = elem Quit


-- some convenience aliases
type RIN = ReactorInput FromModel
type ROUT = ReactorOutput ForModel ForPresenter ForView
type R = Reactor RIN ROUT
type I m = ItemM m (R m)
type CIRs m = [ComponentInfo (R m)]


getWindowSize :: (Monad m) => m (Int, Int)
getWindowSize = undefined -- for you to implement

-- When using `ItemM` you'll need to specify a `forall` for your monadic
-- type-variable in the type-signature and use the `ScopedTypeVariables`
-- language-extension to get the type-checker to use the same variable for the
-- type-signature and the type-annotation of your `ItemM` list in the body.
genLayout :: forall m . (Monad m) => m (Layout, [ComponentInfo (R m)])
genLayout = do
  (width, height) <- getWindowSize
  layoutM
    (window (width, height) (makeWindowWidget width height)
      (grid (2, 3)
        -- Annoyingly type-annotations are required for the list of items which
        -- must be contained within an extra set of parentheses.
        ([item (makeUIWidget :: R m)
        , item (makeUIWidget :: R m)
        , item (makeUIWidget :: R m)
        , item (makeUIWidget :: R m)
        , item (makeUIWidget :: R m)
        , item (makeUIWidget :: R m)
        ]::[I m])))


-- for whatever reason the compiler has trouble resolving the types of the
-- Reaction methods so we have to do it manually:

addReact :: (Monad m) => RIN -> ROUT o -> R m -> R m
addReact = addReactant

getProd :: (Monad m) => RIN -> ROUT o -> R m -> Maybe o
getProd = getProduct


addInput :: (Monad m) => RIN -> CIRs m -> CIRs m
addInput input = map (addReact input NoOutput <$>)

getOutputs :: (Monad m) => ROUT o -> CIRs m -> [o]
getOutputs outputType = mapMaybe (getProd NoInput outputType . componentOf)

adjustComponentLayouts
  :: (Monad m) => Layout -> CIRs m -> CIRs m
adjustComponentLayouts layout =
  map (\i -> addReact (LayoutInput layout (boundsOf i)) NoOutput <$> i)


presenterUI :: (Monad m) => m ()
presenterUI = do
  (layout, infos) <- genLayout
  viewIn <- getUIMessage
  -- prime each component with layout info
  let infos' = adjustComponentLayouts layout infos
  presenterUILoop layout infos' viewIn

presenterUILoop :: (Monad m) => Layout -> CIRs m -> UIMessage -> m ()
presenterUILoop layout infos0 viewIn = do
  -- VIEW (contd)
  let infos1 = addInput (ViewInput viewIn) infos0
  -- PRESENTER
  let presenterOut = getOutputs PresenterOutput infos1
  unless (hasQuit presenterOut) $ do
    -- MODEL
    let modelOut = mconcat $ getOutputs ModelOutput infos1
    modelIn <- interactWithModel modelOut
    let infos2 = addInput (ModelInput modelIn) infos1
    -- LAYOUT
    let adjustLayout = foldl (.) id $ getOutputs LayoutOutput infos2
    let layout' = adjustLayout layout
    let infos3 = adjustComponentLayouts layout' infos2
    -- VIEW (begin)
    let viewOut = mconcat $ getOutputs ViewOutput infos3
    viewIn' <- interactWithView viewOut
    -- LOOP
    presenterUILoop layout' infos3 viewIn'
    -- PSA: DON'T BIND LIKE THIS FOR THE RECURSION AS IT'S NOT TAIL-RECURSIVE:
    --presenterUILoop layout' infos3 =<< interactWithView viewOut