diff --git a/example/src/Chain.hs b/example/src/Chain.hs index a8e64e7..77c9f87 100644 --- a/example/src/Chain.hs +++ b/example/src/Chain.hs @@ -8,6 +8,8 @@ import Haste hiding (fromString) import Haste.JSON import Lens.Family2 hiding (view) import React +import React.Anim +import React.Anim.Class -- model @@ -71,9 +73,8 @@ derive t | t < 1 = ) derive t = (finalWidth, finalHeight) -view :: ChainState -> Chain React' -view status = div_ [ class_ "chain-container" ] $ do - animState <- getAnimationState +view :: ChainState -> Double -> Chain ReactA' +view status animState = div_ [ class_ "chain-container" ] $ do let numStatus = if status == Open then 1 else 0 t = animState + numStatus @@ -92,6 +93,6 @@ view status = div_ [ class_ "chain-container" ] $ do ] "" -chainClass :: IO (Chain ReactClass) +chainClass :: IO (Chain ReactClassA') chainClass = createClass view transition initialClassState initialAnimationState [] diff --git a/example/src/Circles.hs b/example/src/Circles.hs index 0bda86f..79376d2 100644 --- a/example/src/Circles.hs +++ b/example/src/Circles.hs @@ -10,6 +10,8 @@ import Lens.Family2 import Haste hiding (fromString) import Haste.JSON import React +import React.Anim +import React.Anim.Class -- model @@ -93,7 +95,7 @@ fillorange = Color 245 175 51 fill_' = fill_ . fromString . show -circ :: Circ -> Color -> Circles React' +circ :: Circ -> Color -> Circles ReactA' circ c = circ' True (const (Just (SingleFlash c))) (coord c) @@ -101,7 +103,7 @@ circ' :: Bool -> (MouseEvent -> Maybe Transition) -> (Double, Double) -> Color - -> Circles React' + -> Circles ReactA' circ' clickable handler (x, y) color = let lst = [ cx_ x , cy_ y @@ -115,9 +117,8 @@ circ' clickable handler (x, y) color = in circle_ (if clickable then lst' else lst) -mainView :: CircState -> Circles React' -mainView (CircState c _) = div_ $ do - AnimState c1 c2 c3 c4 trans <- getAnimationState +mainView :: CircState -> AnimState -> Circles ReactA' +mainView (CircState c _) (AnimState c1 c2 c3 c4 trans) = div_ $ do svg_ [ width_ 600 , height_ 600 @@ -130,6 +131,6 @@ mainView (CircState c _) = div_ $ do circ' False (const Nothing) (coord c `animSub` trans) fillblue -circlesClass :: IO (Circles ReactClass) +circlesClass :: IO (Circles ReactClassA') circlesClass = createClass mainView transition initialState initialAnimationState [RepeatingFlash] diff --git a/example/src/Easing.hs b/example/src/Easing.hs index 1010bca..4501fb6 100644 --- a/example/src/Easing.hs +++ b/example/src/Easing.hs @@ -11,6 +11,9 @@ import Prelude hiding (lookup) import Haste hiding (fromString) import Haste.JSON hiding ((!)) import React hiding (repeat) +import React.Anim +import React.Anim.Class + import Lens.Family2 hiding (view) -- model @@ -84,16 +87,15 @@ transition Toggle (Easings Open easings) = -- view -buttonBox :: Ease React' +buttonBox :: Ease ReactA' buttonBox = div_ [ class_ "button-box" ] $ button_ [ class_ "btn btn--m btn--gray-border" , onClick (const (Just Toggle)) ] "toggle easing" -view :: EasingState -> Ease React' -view (Easings direction easings) = div_ $ do - EasingMap runningEasings <- getAnimationState +view :: EasingState -> AnimState -> Ease ReactA' +view (Easings direction easings) (EasingMap runningEasings) = div_ $ do let t = if direction == Closed then 0 else 1 buttonBox @@ -126,7 +128,7 @@ safeShow x = in if take 2 shown == "--" then drop 2 shown else shown -- Trying to replicate http://www.objc.io/issue-12/view-layer-synergy.html -subView :: Double -> Easing -> Ease React' +subView :: Double -> Easing -> Ease ReactA' subView t easing = svg_ [ width_ 100 , height_ 100 , viewBox_ "0 0 100 100" @@ -177,6 +179,6 @@ subView t easing = svg_ [ width_ 100 ] -easingClass :: IO (Ease ReactClass) +easingClass :: IO (Ease ReactClassA') easingClass = createClass view transition initialClassState initialAnimationState [] diff --git a/example/src/Simple.hs b/example/src/Simple.hs index 59dafd6..fad83c2 100644 --- a/example/src/Simple.hs +++ b/example/src/Simple.hs @@ -4,6 +4,7 @@ module Simple (simpleClass) where import Haste import Haste.JSON import React +import React.Class -- model @@ -15,16 +16,16 @@ data SimpleState = SimpleState , fighter2 :: JSString , typing :: JSString -- what the user's currently typing } -type Simple a = a SimpleState Transition () +type Simple a = a SimpleState Transition initialState = SimpleState "little mac!" "pit" "" -- update -transition :: Transition -> SimpleState -> (SimpleState, [AnimConfig Transition ()]) -transition (Typing str) state = (state{typing=str}, []) +transition :: Transition -> SimpleState -> SimpleState +transition (Typing str) state = state{typing=str} transition Enter SimpleState{fighter1, typing} = - (SimpleState typing fighter1 "", []) + SimpleState typing fighter1 "" -- view @@ -32,6 +33,7 @@ view :: SimpleState -> Simple React' view (SimpleState fighter1 fighter2 typing) = div_ $ do div_ $ do "send a new competitor into the ring: " + div_ [] $ text_ typing input_ [ value_ typing @@ -52,4 +54,4 @@ view (SimpleState fighter1 fighter2 typing) = div_ $ do text_ fighter2 simpleClass :: IO (Simple ReactClass) -simpleClass = createClass view transition initialState () [] +simpleClass = createClass view transition initialState [] diff --git a/example/src/Slide.hs b/example/src/Slide.hs index ba37e68..c7280a1 100644 --- a/example/src/Slide.hs +++ b/example/src/Slide.hs @@ -8,6 +8,8 @@ import Haste import Haste.JSON import Lens.Family2 hiding (view) import React +import React.Anim +import React.Anim.Class -- model @@ -42,9 +44,9 @@ transition Toggle Closed = (Open, [ slide (-paneWidth) ]) -- view -view :: SlideState -> Slide React' -view slid = div_ [ class_ "slider-container" ] $ do - animWidth <- getAnimationState +view :: SlideState -> Double -> Slide ReactA' +view slid animWidth = div_ [ class_ "slider-container" ] $ do + let inherentWidth = case slid of Open -> paneWidth Closed -> 0 @@ -55,6 +57,6 @@ view slid = div_ [ class_ "slider-container" ] $ do ] "" -slideClass :: IO (Slide ReactClass) +slideClass :: IO (Slide ReactClassA') slideClass = createClass view transition initialClassState initialAnimationState [] diff --git a/lib/stubs.js b/lib/stubs.js index 78b3c7c..07796f8 100644 --- a/lib/stubs.js +++ b/lib/stubs.js @@ -242,6 +242,7 @@ function js_id(a) {return a;} // custom function js_React_DOM_leaf(name, a) { return React.DOM[name](a); } function js_React_DOM_parent(name, a, c) { return React.DOM[name](a, c); } +function js_React_DOM_class(klass) { return React.createElement(klass, null); } function js_parseChangeEvent(raw) { // wrap the string in two constructors - Ptr and JSString @@ -320,9 +321,17 @@ function js_raf(cb) { }); } -function js_createClass(render, setState) { +function js_createClass(render, getInitialState, _) { return React.createClass({ - render: render + render: function() { + // render :: a -> b -> IO ForeignNode + // need either + // - something like runIO + // - render to not run in the IO monad + // - React to use continuation style passing + return B(A(render, [[0, this], [0, this.state.hs], 0]))[1]; + }, + getInitialState: function() { return {hs: B(A(getInitialState, [[0, this], 0]))} } }); } @@ -331,9 +340,25 @@ function js_bezier(x0, y0, x1, y1, x) { } function js_render(e, r){ - React.render(e, r); + React.render(React.createElement(e, null), r); } function js_cancelRaf(id) { window.cancelAnimationFrame(id); } + +function js_getState(inst) { + return inst.state.hs; +} + +function js_setState(inst, state) { + inst.replaceState({hs: state}); +} + +function js_overState(inst, func) { + inst.replaceState({hs: B(A(func, [[0, inst.state.hs], 0]))[1]}); +} + +function js_performance_now() { + return window.performance.now(); +} diff --git a/react-haskell.cabal b/react-haskell.cabal index 2deb988..ef51023 100644 --- a/react-haskell.cabal +++ b/react-haskell.cabal @@ -51,12 +51,11 @@ source-repository head location: https://github.com/joelburget/react-haskell.git library - exposed-modules: React + exposed-modules: React, React.Class, React.Anim, React.Anim.Class other-modules: - React.Anim, React.Attrs, - React.Class, React.Elements, + React.ElemTypes, React.Events, React.Imports, React.Interpret, diff --git a/src/React.hs b/src/React.hs index 0d675cb..a11b005 100644 --- a/src/React.hs +++ b/src/React.hs @@ -9,20 +9,15 @@ module React ( module X -- React.Anim - , Color(..) - , getAnimationState - , Animatable(..) -- XXX - - -- React.Class - , ReactClass() - , createClass + --, Color(..) + --, getAnimationState + --, Animatable(..) -- XXX -- React.Local , locally , GeneralizeSignal(..) -- React.Render - , cancelRender , render -- React.Types @@ -31,8 +26,8 @@ module React , React' , Pure , RenderHandle(..) - , AnimConfig(..) - , Easing(..) + --, AnimConfig(..) + --, Easing(..) , EventProperties(..) , ModifierKeys(..) , MouseEvent(..) @@ -58,13 +53,13 @@ module React -- store elem in monad -- escaping / dangerouslySetInnerHTML -import React.Anim import React.Class -- import React.Imports -- import React.Interpret import React.Local -import React.Render import React.Types +import React.Render +import React.ElemTypes import React.Attrs as X import React.Elements as X diff --git a/src/React/Anim.hs b/src/React/Anim.hs index e8d29b1..e1eca08 100644 --- a/src/React/Anim.hs +++ b/src/React/Anim.hs @@ -1,5 +1,4 @@ -{-# LANGUAGE OverloadedStrings, FlexibleInstances, MultiWayIf, - FlexibleContexts #-} +{-# LANGUAGE OverloadedStrings, FlexibleInstances, MultiWayIf, FlexibleContexts, ExistentialQuantification, Rank2Types #-} module React.Anim where import Control.Applicative @@ -13,6 +12,95 @@ import React.Imports import React.Types +-- | Standard easing functions. These are used to 'interpolate' smoothly. +-- +-- See for visualizations. +data Easing + = Linear + + | EaseInQuad + | EaseOutQuad + | EaseInOutQuad + + | EaseInCubic + | EaseOutCubic + | EaseInOutCubic + + | EaseInQuart + | EaseOutQuart + | EaseInOutQuart + + | EaseInQuint + | EaseOutQuint + | EaseInOutQuint + + | EaseInElastic + | EaseOutElastic + | EaseInOutElastic + + | EaseInBounce + | EaseOutBounce + | EaseInOutBounce + + | EaseBezier Double Double Double Double + | EaseInSine + | EaseOutSine + deriving (Show, Eq, Ord) + +-- | Properties that can animate. +-- +-- Numeric values like 'width' and 'height', as well as colors. +class Animatable a where + -- TODO is `to` always `animZero`? + -- | Use an easing function to interpolate between two values + interpolate :: Easing -- ^ easing function + -> a -- ^ from + -> a -- ^ to + -> Double -- ^ [0..1] ratio of /time/ elapsed + -> a + + -- | Add two animations + animAdd :: a -> a -> a + + -- | Subtract two animations + animSub :: a -> a -> a + animZero :: a + +-- things you might want to control about an animation: +-- * duration +-- * from +-- * to +-- * lens +-- * easing +-- * oncomplete +-- * chaining +-- * delay + +-- possible configurations: +-- * set new state, animate from old to new at same time +-- - need to connect ClassState and AnimationState somehow +-- * animate manually from -> to + +data AnimConfig sig anim = forall a. (Animatable a) => AnimConfig { + -- | How long this animation lasts in milliseconds + duration :: Double + -- | Where does this animation start and end? + , endpoints :: (a, a) + -- | Pointer to this field within 'AnimationState' + , lens :: Lens' anim a + -- | How is the animation eased? + , easing :: Easing + -- | Do something when it's finished? + , onComplete :: Bool -> Maybe sig + } + + +data RunningAnim sig anim = RunningAnim + { config :: AnimConfig sig anim + , beganAt :: Double + } + + -- TODO support delays -- TODO look at velocity @@ -156,8 +244,6 @@ easeDouble (EaseBezier x0 y0 x1 y1) t = js_bezier x0 y0 x1 y1 t easeDouble EaseInSine t = js_bezier 0.47 0 0.745 0.715 t easeDouble EaseOutSine t = js_bezier 0.39 0.575 0.565 1 t -getAnimationState :: Monad m => ReactT state sig anim m anim -getAnimationState = ReactT $ \anim -> return ([], anim) stepRunningAnims :: anim -> [(RunningAnim sig anim, Double)] -> anim stepRunningAnims anim running = diff --git a/src/React/Anim/Class.hs b/src/React/Anim/Class.hs new file mode 100644 index 0000000..b57bbe2 --- /dev/null +++ b/src/React/Anim/Class.hs @@ -0,0 +1,167 @@ +{-# LANGUAGE NamedFieldPuns #-} +module React.Anim.Class + ( ReactClass(..) + , createClass + , ReactA' + , ReactClassA' + ) where + +import Lens.Family2 +import Data.Functor.Identity +import Data.Monoid +import Data.List +import Data.Maybe +import Haste.Prim + +import React.Types +import React.Imports +import React.Interpret +import React.Anim + +import qualified React.Class as V + + + +-- Animation is now a kind of middleware between React JS and ReactClass. + + +data WithAnimState u sig anim = + WithAnimState { userState :: u + , anim :: anim + , runningAnims :: [RunningAnim sig anim] + , renderHandle :: Maybe RenderHandle + } + +type ReactA' state sig anim = React' (WithAnimState state sig anim) sig +type ReactClassA' state sig anim = ReactClass (WithAnimState state sig anim) sig + +-- This class can wrap V.ReactClass, but only if the V.ReactClass transition lives inside the IO monad, or the render lives inside the IO monad. +-- Instead, it will just have to be a different implementation +createClass :: (state -> anim -> React (WithAnimState state sig anim) sig ()) + -> (sig -> state -> (state, [AnimConfig sig anim])) + -> state + -> anim + -> [sig] + -> IO (ReactClass (WithAnimState state sig anim) sig) +createClass render transition initialState anim initialSigs = do + + let initialStateM = (\this -> do + + time <- js_performance_now + rh <- js_raf . toPtr $ animTick this transition + + let state = foldl + (flip $ wrapTrans transition time) + (WithAnimState + initialState + anim + [] + $ Just rh) + initialSigs + + return state) + + + + foreignClass <- js_createClass + (toPtr $ classForeignRender render transition) + (toPtr initialStateM) + + return $ ReactClass foreignClass + + +classForeignRender :: (state -> anim -> React (WithAnimState state sig anim) sig ()) + -> (sig -> state -> (state, [AnimConfig sig anim])) + -> ForeignClassInstance + -> Ptr (WithAnimState state sig anim) + -> IO ForeignNode +classForeignRender classRender + classTransition + this + pstate = do + + let (WithAnimState ustate a ra rh) = fromPtr pstate + + runIdentity $ + interpret (classRender ustate a) (updateCb this classTransition) + +updateCb :: ForeignClassInstance + -> (sig -> state -> (state, [AnimConfig sig anim])) + -> sig + -> IO () +updateCb this trans sig = do + time <- js_performance_now + state <- fromPtr =<< js_getState this + + let newState = wrapTrans trans time sig state + + case renderHandle newState of + Just h -> js_cancelRaf h + Nothing -> return () + + newHandle <- js_raf . toPtr $ animTick this trans + + js_setState + this + $ toPtr + newState{renderHandle=Just newHandle} + + +animTick :: ForeignClassInstance + -> (sig -> state -> (state, [AnimConfig sig anim])) + -> Double + -> IO () +animTick this trans time = do + + state@WithAnimState{runningAnims} <- fromPtr =<< js_getState this + + let (runningAnims', endingAnims) = partition + (\(RunningAnim AnimConfig{duration} beganAt) -> + beganAt + duration > time) + runningAnims + + endAnimSigTimes = mapMaybe + (\(RunningAnim AnimConfig{ duration + , onComplete + } + beganAt) -> do + + sig <- onComplete True + return ( sig + , beganAt + duration + ) + ) + endingAnims + + newState@(WithAnimState _ anim newRunningAnims _) = foldl + (\st (sig, time) -> + wrapTrans trans time sig st) + state{ + runningAnims=runningAnims' + } + endAnimSigTimes + + runningAnims'' = zip newRunningAnims $ map (lerp time) newRunningAnims + newAnim = stepRunningAnims anim (runningAnims'') + + newHandle <- js_raf $ toPtr $ animTick this trans + + js_setState this $ toPtr $ newState{ anim=newAnim + , renderHandle=Just newHandle + } + +wrapTrans :: (sig -> state -> (state, [AnimConfig sig anim])) + -> Double + -> sig + -> WithAnimState state sig anim + -> WithAnimState state sig anim +wrapTrans trans + time + sig + (WithAnimState ustate + anim + runningAnims + rh) + = WithAnimState newUState anim newRunningAnims rh + where (newUState, animConfs) = trans sig ustate + newRunningAnims = runningAnims <> (zipWith RunningAnim animConfs (Data.List.repeat time)) diff --git a/src/React/Attrs.hs b/src/React/Attrs.hs index 8c07fb8..932f298 100644 --- a/src/React/Attrs.hs +++ b/src/React/Attrs.hs @@ -83,6 +83,9 @@ points_ = mkStaticAttr "points" Str transform_ :: JSString -> AttrOrHandler signal transform_ = mkStaticAttr "transform" Str +multiple_ :: Bool -> AttrOrHandler signal +multiple_ = mkStaticAttr "multiple" Bool + -- fillOpacity fontFamily fontSize fx fy gradientTransform -- gradientUnits markerEnd markerMid markerStart offset opacity -- patternContentUnits patternUnits preserveAspectRatio r rx ry diff --git a/src/React/Class.hs b/src/React/Class.hs index f5f43f5..09fa7a1 100644 --- a/src/React/Class.hs +++ b/src/React/Class.hs @@ -1,61 +1,65 @@ -{-# LANGUAGE NamedFieldPuns, OverloadedStrings #-} +{-# LANGUAGE NamedFieldPuns, OverloadedStrings, BangPatterns, TypeFamilies #-} module React.Class ( ReactClass(..) , createClass ) where -import Data.IORef - -import React.Anim +import Data.List +import Data.Monoid +import Data.Maybe +import Data.Functor.Identity +import React.Interpret import React.Imports +import React.ElemTypes import React.Types import Haste import Haste.JSON import Haste.Prim +--import Haste.Foreign -- | A 'ReactClass' is a standalone component of a user interface which --- contains the state necessary to render and animate itself. Classes are +-- contains the state necessary to render itself. Classes are -- a tool for scoping. -- -- Use 'createClass' to construct. -data ReactClass state sig anim = ReactClass - { classRender :: state -> React state sig anim () - , classTransition :: sig - -> state - -> (state, [AnimConfig sig anim]) - - , foreignClass :: ForeignClass - , stateRef :: IORef state - , animRef :: IORef anim - , runningAnimRef :: IORef [RunningAnim sig anim] - , transitionRef :: IORef [sig] - } -- | 'ReactClass' smart constructor. -createClass :: (state -> React state sig anim ()) -- ^ render function - -> (sig -> state -> (state, [AnimConfig sig anim])) +createClass :: (state -> React state sig ()) -- ^ render function + -> (sig -> state -> state) -- ^ transition function -> state -- ^ initial state - -> anim -- ^ initial animation state -> [sig] -- signals to send on startup - -> IO (ReactClass state sig anim) -createClass render transition initialState initialAnim initialTrans = do - foreignClass <- js_createClass $ toPtr render - - stateRef <- newIORef initialState - animRef <- newIORef initialAnim - runningAnimRef <- newIORef [] - transitionRef <- newIORef initialTrans - - return $ ReactClass - render - transition - foreignClass - stateRef - animRef - runningAnimRef - transitionRef + -> IO (ReactClass state sig) +createClass render transition initialState initialTrans = do + + foreignClass <- js_createClass + (toPtr $ classForeignRender render transition) + (toPtr (\_ -> return initialState)) + + return $ ReactClass foreignClass + +classForeignRender :: (state -> React state sig ()) + -> (sig -> state -> state) + -> ForeignClassInstance + -> Ptr state + -> IO ForeignNode +classForeignRender classRender + classTransition + this + pstate = do + + runIdentity $ + interpret (classRender $ fromPtr pstate) (updateCb this classTransition) + +updateCb :: ForeignClassInstance + -> (sig -> state -> state) + -> sig + -> IO () +updateCb this trans sig = js_overState this $ toPtr (toPtr.(trans sig).fromPtr) + + + diff --git a/src/React/ElemTypes.hs b/src/React/ElemTypes.hs new file mode 100644 index 0000000..df86c32 --- /dev/null +++ b/src/React/ElemTypes.hs @@ -0,0 +1,44 @@ +module React.ElemTypes where + +import Haste.Prim +import React.Types +import React.Imports + +-- Useful for defining elements + +foreignParent :: TermParent t + => ForeignRender + -> TermParentArg t + -> t +foreignParent = termParent + + +reactParent :: TermParent t + => JSString + -> TermParentArg t + -> t +reactParent name = termParent (js_React_DOM_parent name) + + +termLeaf :: Monad m + => ForeignRender + -> [AttrOrHandler sig] + -> ReactT state sig m () +termLeaf render attrs = ReactT $ do + let (hs, as) = separateAttrs attrs + return ([Leaf render as hs], ()) + + +foreignLeaf :: Monad m + => ForeignRender + -> [AttrOrHandler sig] + -> ReactT state sig m () +foreignLeaf = termLeaf + + +reactLeaf :: Monad m + => JSString + -> [AttrOrHandler sig] + -> ReactT state sig m () +reactLeaf name = termLeaf (\as' _ -> js_React_DOM_leaf name as') + diff --git a/src/React/Elements.hs b/src/React/Elements.hs index 9102c52..d7d3a51 100644 --- a/src/React/Elements.hs +++ b/src/React/Elements.hs @@ -1,10 +1,12 @@ -{-# LANGUAGE OverloadedStrings, TypeFamilies, FlexibleInstances #-} +{-# LANGUAGE OverloadedStrings, TypeFamilies, FlexibleInstances, NamedFieldPuns #-} module React.Elements where import Haste.Prim import React.Imports import React.Types +import React.Class +import React.ElemTypes -- | Parent nodes always take children, but can also optionally take a list @@ -21,76 +23,15 @@ import React.Types -- @ -- span_ [class_ "example"] $ ... children ... -- @ -class TermParent result where - -- | The argument to a parent term is either: - -- - -- * a list of attributes (@[AttrOrHandler (Signal ty)]@), which leads - -- to a result type of @ReactT ty m a -> ReactT ty m a@. - -- - -- * or children (@ReactT ty m a@), which leads to a result type of - -- @ReactT ty m a@. - type TermParentArg result :: * - termParent :: ForeignRender -> TermParentArg result -> result - - -instance (Monad m, f ~ ReactT state sig anim m a) => - TermParent (f -> ReactT state sig anim m a) where - type TermParentArg (f -> ReactT state sig anim m a) = [AttrOrHandler sig] - - termParent render attrs children = ReactT $ \anim -> do - ~(childNodes, a) <- runReactT children anim - let (hs, as) = separateAttrs attrs - return ([Parent render as hs childNodes], a) - - -instance Monad m => TermParent (ReactT state sig anim m a) where - type TermParentArg (ReactT state sig anim m a) = ReactT state sig anim m a - - termParent render children = ReactT $ \anim -> do - ~(childNodes, a) <- runReactT children anim - return ([Parent render [] [] childNodes], a) - - -foreignParent :: TermParent t - => ForeignRender - -> TermParentArg t - -> t -foreignParent = termParent - - -reactParent :: TermParent t - => JSString - -> TermParentArg t - -> t -reactParent name = termParent (js_React_DOM_parent name) - - -termLeaf :: Monad m - => ForeignRender - -> [AttrOrHandler sig] - -> ReactT state sig anim m () -termLeaf render attrs = ReactT $ \_ -> do - let (hs, as) = separateAttrs attrs - return ([Leaf render as hs], ()) - - -foreignLeaf :: Monad m - => ForeignRender +reactClass_ :: Monad m + => ReactClass cstate csig -> [AttrOrHandler sig] - -> ReactT state sig anim m () -foreignLeaf = termLeaf - - -reactLeaf :: Monad m - => JSString - -> [AttrOrHandler sig] - -> ReactT state sig animj m () -reactLeaf name = termLeaf (\as' _ -> js_React_DOM_leaf name as') - + -> ReactT state sig m () +reactClass_ ReactClass{foreignClass} = termLeaf (\_ _ -> js_React_DOM_class $ foreignClass) -text_ :: JSString -> React state sig anim () -text_ str = ReactT $ \_ -> return ([Text (fromJSStr str)], ()) +text_ :: JSString -> React state sig () +text_ str = ReactT $ return ([Text (fromJSStr str)], ()) a_ :: TermParent t => TermParentArg t -> t a_ = reactParent "a" @@ -366,49 +307,49 @@ video_ :: TermParent t => TermParentArg t -> t video_ = reactParent "video" -area_ :: Monad m => [AttrOrHandler sig] -> ReactT state sig anim m () +area_ :: Monad m => [AttrOrHandler sig] -> ReactT state sig m () area_ = reactLeaf "area" -base_ :: Monad m => [AttrOrHandler sig] -> ReactT state sig anim m () +base_ :: Monad m => [AttrOrHandler sig] -> ReactT state sig m () base_ = reactLeaf "base" -br_ :: Monad m => [AttrOrHandler sig] -> ReactT state sig anim m () +br_ :: Monad m => [AttrOrHandler sig] -> ReactT state sig m () br_ = reactLeaf "br" -col_ :: Monad m => [AttrOrHandler sig] -> ReactT state sig anim m () +col_ :: Monad m => [AttrOrHandler sig] -> ReactT state sig m () col_ = reactLeaf "col" -embed_ :: Monad m => [AttrOrHandler sig] -> ReactT state sig anim m () +embed_ :: Monad m => [AttrOrHandler sig] -> ReactT state sig m () embed_ = reactLeaf "embed" -hr_ :: Monad m => [AttrOrHandler sig] -> ReactT state sig anim m () +hr_ :: Monad m => [AttrOrHandler sig] -> ReactT state sig m () hr_ = reactLeaf "hr" -img_ :: Monad m => [AttrOrHandler sig] -> ReactT state sig anim m () +img_ :: Monad m => [AttrOrHandler sig] -> ReactT state sig m () img_ = reactLeaf "img" -input_ :: Monad m => [AttrOrHandler sig] -> ReactT state sig anim m () +input_ :: Monad m => [AttrOrHandler sig] -> ReactT state sig m () input_ = reactLeaf "input" -keygen_ :: Monad m => [AttrOrHandler sig] -> ReactT state sig anim m () +keygen_ :: Monad m => [AttrOrHandler sig] -> ReactT state sig m () keygen_ = reactLeaf "keygen" -link_ :: Monad m => [AttrOrHandler sig] -> ReactT state sig anim m () +link_ :: Monad m => [AttrOrHandler sig] -> ReactT state sig m () link_ = reactLeaf "link" -meta_ :: Monad m => [AttrOrHandler sig] -> ReactT state sig anim m () +meta_ :: Monad m => [AttrOrHandler sig] -> ReactT state sig m () meta_ = reactLeaf "meta" -param_ :: Monad m => [AttrOrHandler sig] -> ReactT state sig anim m () +param_ :: Monad m => [AttrOrHandler sig] -> ReactT state sig m () param_ = reactLeaf "param" -source_ :: Monad m => [AttrOrHandler sig] -> ReactT state sig anim m () +source_ :: Monad m => [AttrOrHandler sig] -> ReactT state sig m () source_ = reactLeaf "source" -track_ :: Monad m => [AttrOrHandler sig] -> ReactT state sig anim m () +track_ :: Monad m => [AttrOrHandler sig] -> ReactT state sig m () track_ = reactLeaf "track" -wbr_ :: Monad m => [AttrOrHandler sig] -> ReactT state sig anim m () +wbr_ :: Monad m => [AttrOrHandler sig] -> ReactT state sig m () wbr_ = reactLeaf "wbr" -- script :: RawAttrs -> JSString -> IO ForeignNode @@ -448,23 +389,23 @@ stop_ = reactParent "stop" tspan_ :: TermParent t => TermParentArg t -> t tspan_ = reactParent "tspan" -circle_ :: Monad m => [AttrOrHandler sig] -> ReactT state sig anim m () +circle_ :: Monad m => [AttrOrHandler sig] -> ReactT state sig m () circle_ = reactLeaf "circle" -ellipse_ :: Monad m => [AttrOrHandler sig] -> ReactT state sig anim m () +ellipse_ :: Monad m => [AttrOrHandler sig] -> ReactT state sig m () ellipse_ = reactLeaf "ellipse" -line_ :: Monad m => [AttrOrHandler sig] -> ReactT state sig anim m () +line_ :: Monad m => [AttrOrHandler sig] -> ReactT state sig m () line_ = reactLeaf "line" -path_ :: Monad m => [AttrOrHandler sig] -> ReactT state sig anim m () +path_ :: Monad m => [AttrOrHandler sig] -> ReactT state sig m () path_ = reactLeaf "path" -polygon_ :: Monad m => [AttrOrHandler sig] -> ReactT state sig anim m () +polygon_ :: Monad m => [AttrOrHandler sig] -> ReactT state sig m () polygon_ = reactLeaf "polygon" -polyline_ :: Monad m => [AttrOrHandler sig] -> ReactT state sig anim m () +polyline_ :: Monad m => [AttrOrHandler sig] -> ReactT state sig m () polyline_ = reactLeaf "polyline" -rect_ :: Monad m => [AttrOrHandler sig] -> ReactT state sig anim m () +rect_ :: Monad m => [AttrOrHandler sig] -> ReactT state sig m () rect_ = reactLeaf "rect" diff --git a/src/React/Imports.hs b/src/React/Imports.hs index 50b00d2..bf8a665 100644 --- a/src/React/Imports.hs +++ b/src/React/Imports.hs @@ -13,10 +13,39 @@ import Haste.Foreign import Haste.JSON import Haste.Prim + +#ifdef __HASTE__ +foreign import ccall js_performance_now:: IO Double +#else +js_performance_now:: IO Double +js_performance_now = error "cannot evaluate js_performance_now in ghc" +#endif + #ifdef __HASTE__ -foreign import ccall js_render :: ForeignNode -> Elem -> IO () +foreign import ccall js_getState:: ForeignClassInstance -> IO (Ptr state) #else -js_render :: ForeignNode -> Elem -> IO () +js_getState:: ForeignClassInstance -> IO (Ptr state) +js_getState = error "cannot evaluate js_getState in ghc" +#endif + +#ifdef __HASTE__ +foreign import ccall js_setState:: ForeignClassInstance -> Ptr state -> IO () +#else +js_setState:: ForeignClassInstance -> Ptr state -> IO () +js_setState = error "cannot evaluate js_setState in ghc" +#endif + +#ifdef __HASTE__ +foreign import ccall js_overState:: ForeignClassInstance -> Ptr (Ptr state -> Ptr state) -> IO () +#else +js_overState:: ForeignClassInstance -> Ptr (Ptr state -> Ptr state) -> IO () +js_overState = error "cannot evaluate js_overState in ghc" +#endif + +#ifdef __HASTE__ +foreign import ccall js_render :: ForeignClass -> Elem -> IO () +#else +js_render :: ForeignClass -> Elem -> IO () js_render = error "cannot evaluate js_render in ghc" #endif @@ -28,10 +57,12 @@ js_bezier = error "cannot evaluate js_bezier in ghc" #endif #ifdef __HASTE__ -foreign import ccall js_createClass :: Ptr (state -> React state sig anim ()) +foreign import ccall js_createClass :: Ptr (ForeignClassInstance -> Ptr state -> IO ForeignNode) + -> Ptr (ForeignClassInstance -> IO state) -> IO ForeignClass #else -js_createClass :: Ptr (state -> React state sig anim ()) +js_createClass :: Ptr (ForeignClassInstance -> Ptr state -> ForeignNode) + -> Ptr (ForeignClassInstance -> IO state) -> IO ForeignClass js_createClass = error "cannot evaluate js_createClass in ghc" #endif @@ -64,6 +95,13 @@ js_React_DOM_parent :: JSString -> RawAttrs -> ReactArray -> IO ForeignNode js_React_DOM_parent = error "cannot evaluate js_React_DOM_parent in ghc" #endif +#ifdef __HASTE__ +foreign import ccall js_React_DOM_class :: ForeignClass -> IO ForeignNode +#else +js_React_DOM_class :: ForeignClass -> IO ForeignNode +js_React_DOM_class = error "cannot evaluate js_React_DOM_class in ghc" +#endif + #ifdef __HASTE__ foreign import ccall js_empty_arr :: IO RawAttrs #else diff --git a/src/React/Interpret.hs b/src/React/Interpret.hs index dd5ebc8..7b27097 100644 --- a/src/React/Interpret.hs +++ b/src/React/Interpret.hs @@ -70,12 +70,11 @@ setIx arr i Null = return () -- getDomNode r = fmap fromPtr (js_React_getDomNode r) interpret :: Monad m - => ReactT state sig anim m () - -> anim + => ReactT state sig m () -> (sig -> IO ()) -> m (IO ForeignNode) -interpret react anim cb = do - ~(child:_, ()) <- runReactT react anim +interpret react cb = do + ~(child:otherChildren, ()) <- runReactT react return $ interpret' cb child @@ -86,8 +85,11 @@ interpret' cb = \case Parent f as hs children -> do children' <- forM children (interpret' cb) let hs' = map (unHandler cb) hs - element f as hs' children' + node <- element f as hs' children' + return node Leaf f as hs -> do let hs' = map (unHandler cb) hs element f as hs' [] - Text str -> js_React_DOM_text (toJSStr str) + Text str -> do + node <- js_React_DOM_text (toJSStr str) + return node diff --git a/src/React/Local.hs b/src/React/Local.hs index f310589..b547748 100644 --- a/src/React/Local.hs +++ b/src/React/Local.hs @@ -25,12 +25,12 @@ instance GeneralizeSignal Void a where locally :: (Monad m, GeneralizeSignal sigloc siggen) - => ReactT stateloc sigloc anim m x - -> ReactT stategen siggen anim m x + => ReactT stateloc sigloc m x + -> ReactT stategen siggen m x locally nested = result where - result = ReactT $ \anim -> do + result = ReactT $ do let gensig = nodeConvert generalizeSignal - (nodes, x) <- runReactT nested anim + (nodes, x) <- runReactT nested return (map gensig nodes, x) diff --git a/src/React/Render.hs b/src/React/Render.hs index 416331b..f9e9d18 100644 --- a/src/React/Render.hs +++ b/src/React/Render.hs @@ -1,102 +1,11 @@ -{-# LANGUAGE OverloadedStrings, FlexibleContexts, NamedFieldPuns #-} +{-# LANGUAGE NamedFieldPuns #-} +module React.Render where -module React.Render - ( render - , cancelRender - ) where - -import Control.Applicative -import Control.Monad -import Control.Monad.IO.Class -import Data.Functor.Identity -import Data.IORef -import Data.List -import Data.Maybe -import Data.Monoid -import Data.String - -import Haste hiding (fromString) -import Haste.Foreign -import Haste.JSON -import Haste.Prim - -import React.Anim -import React.Attrs -import React.Class -import React.Elements -import React.Events -import React.Imports -import React.Interpret -import React.Local import React.Types +import React.Imports - -doRender :: Elem -> Double -> ReactClass state sig anim -> IO () -doRender elem time ReactClass{ classRender, - classTransition, - transitionRef, - runningAnimRef, - animRef, - stateRef } = do - - transitions <- readIORef transitionRef - runningAnims <- readIORef runningAnimRef - prevState <- readIORef stateRef - prevAnim <- readIORef animRef - - let (newState, newAnims) = - mapAccumL (flip classTransition) prevState transitions - - newAnims' = concat newAnims - newRunningAnims = map (`RunningAnim` time) newAnims' - - (runningAnims', endingAnims) = partition - (\(RunningAnim AnimConfig{duration} beganAt) -> - beganAt + duration > time) - (runningAnims <> newRunningAnims) - - endingAnims' = zip endingAnims [1..] - runningAnims'' = zip runningAnims' (map (lerp time) runningAnims') - newAnim = stepRunningAnims prevAnim (endingAnims' ++ runningAnims'') - - -- TODO should this run before or after rendering? - -- TODO expose a way to cancel / pass False in that case - endAnimTrans = mapMaybe - (\anim -> onComplete (config anim) True) - endingAnims - - foreignNode <- runIdentity $ - interpret (classRender newState) newAnim (updateCb transitionRef) - js_render foreignNode elem - - writeIORef stateRef newState - writeIORef animRef newAnim - writeIORef runningAnimRef runningAnims' - writeIORef transitionRef endAnimTrans - - -updateCb :: IORef [signal] -> signal -> IO () -updateCb ref update = modifyIORef ref (update:) - - -render :: Elem - -> ReactClass state sig anim - -> IO RenderHandle -render elem cls@ReactClass{transitionRef, runningAnimRef} = do - let renderCb time = do - transitions <- readIORef transitionRef - runningAnims <- readIORef runningAnimRef - - -- only rerender when dirty - when (length transitions + length runningAnims > 0) $ - doRender elem time cls - - js_raf $ toPtr renderCb - return () - - doRender elem 0 cls - js_raf $ toPtr renderCb +import Haste.DOM -cancelRender :: RenderHandle -> IO () -cancelRender = js_cancelRaf +render :: Elem -> ReactClass state sig -> IO () +render elem ReactClass{foreignClass} = js_render foreignClass elem diff --git a/src/React/Types.hs b/src/React/Types.hs index 1d12615..7b4a690 100644 --- a/src/React/Types.hs +++ b/src/React/Types.hs @@ -17,7 +17,13 @@ import Haste.JSON import Haste.Prim import Lens.Family2 +import Data.IORef + +data ReactClass state sig = + ReactClass { foreignClass :: ForeignClass + } +newtype ForeignClassInstance = ForeignClassInstance JSAny deriving (Pack, Unpack) newtype ForeignNode = ForeignNode JSAny deriving (Pack, Unpack) newtype RawAttrs = RawAttrs JSAny deriving (Pack, Unpack) newtype ReactArray = ReactArray JSAny deriving (Pack, Unpack) @@ -48,139 +54,48 @@ type Attrs = [(JSString, JSON)] -- it'd be super cool to restrict `Pre` to a string somehow (restrict the -- underlying monad so it can only set attrs and string?) - + -- data ReactNode signal = Parent ForeignRender Attrs [EventHandler signal] [ReactNode signal] | Leaf ForeignRender Attrs [EventHandler signal] -- | Pre Attrs Handlers [ReactNode] | Text String -- TODO(joel) JSString? - --- | Standard easing functions. These are used to 'interpolate' smoothly. --- --- See for visualizations. -data Easing - = Linear - - | EaseInQuad - | EaseOutQuad - | EaseInOutQuad - - | EaseInCubic - | EaseOutCubic - | EaseInOutCubic - - | EaseInQuart - | EaseOutQuart - | EaseInOutQuart - - | EaseInQuint - | EaseOutQuint - | EaseInOutQuint - - | EaseInElastic - | EaseOutElastic - | EaseInOutElastic - - | EaseInBounce - | EaseOutBounce - | EaseInOutBounce - - | EaseBezier Double Double Double Double - | EaseInSine - | EaseOutSine - deriving (Show, Eq, Ord) - --- | Properties that can animate. --- --- Numeric values like 'width' and 'height', as well as colors. -class Animatable a where - -- TODO is `to` always `animZero`? - -- | Use an easing function to interpolate between two values - interpolate :: Easing -- ^ easing function - -> a -- ^ from - -> a -- ^ to - -> Double -- ^ [0..1] ratio of /time/ elapsed - -> a - - -- | Add two animations - animAdd :: a -> a -> a - - -- | Subtract two animations - animSub :: a -> a -> a - animZero :: a - - --- things you might want to control about an animation: --- * duration --- * from --- * to --- * lens --- * easing --- * oncomplete --- * chaining --- * delay - --- possible configurations: --- * set new state, animate from old to new at same time --- - need to connect ClassState and AnimationState somehow --- * animate manually from -> to - -data AnimConfig sig anim = forall a. (Animatable a) => AnimConfig { - -- | How long this animation lasts in milliseconds - duration :: Double - -- | Where does this animation start and end? - , endpoints :: (a, a) - -- | Pointer to this field within 'AnimationState' - , lens :: Lens' anim a - -- | How is the animation eased? - , easing :: Easing - -- | Do something when it's finished? - , onComplete :: Bool -> Maybe sig - } - - -data RunningAnim sig anim = RunningAnim - { config :: AnimConfig sig anim - , beganAt :: Double - } - - -newtype ReactT state sig anim m a = ReactT - { runReactT :: anim -> m ([ReactNode sig], a) } +newtype ReactT state sig m a = ReactT + { runReactT :: m ([ReactNode sig], a) } -type React state sig anim = ReactT state sig anim Identity -type React' state sig anim = ReactT state sig anim Identity () +type React state sig = ReactT state sig Identity +type React' state sig = ReactT state sig Identity () type Pure a = a () Void () -instance (Monad m, Monoid a) => Monoid (ReactT state sig anim m a) where - mempty = ReactT $ \_ -> return ([], mempty) - mappend f1 f2 = ReactT $ \anim -> do - ~(c1, a) <- runReactT f1 anim - ~(c2, b) <- runReactT f2 anim +instance (Monad m, Monoid a) => Monoid (ReactT state sig m a) where + mempty = ReactT $ return ([], mempty) + mappend f1 f2 = ReactT $ do + ~(c1, a) <- runReactT f1 + ~(c2, b) <- runReactT f2 return (c1 <> c2, a <> b) -instance Monad m => Functor (ReactT state sig anim m) where +instance Monad m => Functor (ReactT state sig m) where fmap = liftM -instance Monad m => Applicative (ReactT state sig anim m) where +instance Monad m => Applicative (ReactT state sig m) where pure = return (<*>) = ap -instance (Monad m, a ~ ()) => IsString (ReactT state sig anim m a) where - fromString str = ReactT $ \_ -> return ([Text str], ()) +instance (Monad m, a ~ ()) => IsString (ReactT state sig m a) where + fromString str = ReactT $ return ([Text str], ()) -instance Monad m => Monad (ReactT state sig anim m) where - return a = ReactT $ \_ -> return ([], a) - m >>= f = ReactT $ \anim -> do - ~(c1, a) <- runReactT m anim - ~(c2, b) <- runReactT (f a) anim +instance Monad m => Monad (ReactT state sig m) where + return a = ReactT $ return ([], a) + m >>= f = ReactT $ do + ~(c1, a) <- runReactT m + ~(c2, b) <- runReactT (f a) return (c1 <> c2, b) @@ -292,3 +207,35 @@ data FocusEvent e = instance NFData e => NFData (FocusEvent e) where rnf (FocusEvent a b) = a `seq` b `seq` () + +-- Useful for defining elements +class TermParent result where + -- | The argument to a parent term is either: + -- + -- * a list of attributes (@[AttrOrHandler (Signal ty)]@), which leads + -- to a result type of @ReactT ty m a -> ReactT ty m a@. + -- + -- * or children (@ReactT ty m a@), which leads to a result type of + -- @ReactT ty m a@. + type TermParentArg result :: * + + termParent :: ForeignRender -> TermParentArg result -> result + + +instance (Monad m, f ~ ReactT state sig m a) => + TermParent (f -> ReactT state sig m a) where + type TermParentArg (f -> ReactT state sig m a) = [AttrOrHandler sig] + + termParent render attrs children = ReactT $ do + ~(childNodes, a) <- runReactT children + let (hs, as) = separateAttrs attrs + return ([Parent render as hs childNodes], a) + + +instance Monad m => TermParent (ReactT state sig m a) where + type TermParentArg (ReactT state sig m a) = ReactT state sig m a + + termParent render children = ReactT $ do + ~(childNodes, a) <- runReactT children + return ([Parent render [] [] childNodes], a) +