packages feed

threepenny-gui 0.2.0.1 → 0.3.0.0

raw patch · 26 files changed

+1614/−729 lines, 26 filesdep +MonadCatchIO-transformersdep +attoparsec-enumeratordep +hashabledep −mtlnew-component:exe:threepenny-examples-currencyconverterPVP ok

version bump matches the API change (PVP)

Dependencies added: MonadCatchIO-transformers, attoparsec-enumerator, hashable, template-haskell, unordered-containers, vault, websockets, websockets-snap

Dependencies removed: mtl

API changes (from Hackage documentation)

- Control.Event: Event :: (Handler a -> IO (IO ())) -> Event a
- Control.Event: filterIO :: (a -> IO Bool) -> Event a -> Event a
- Control.Event: filterJust :: Event (Maybe a) -> Event a
- Control.Event: instance Functor Event
- Control.Event: mapIO :: (a -> IO b) -> Event a -> Event b
- Control.Event: newEvent :: IO (Event a, a -> IO ())
- Control.Event: newEventsTagged :: Ord tag => IO (tag -> Event a, (tag, a) -> IO ())
- Control.Event: newtype Event a
- Control.Event: register :: Event a -> Handler a -> IO (IO ())
- Control.Event: type Handler a = a -> IO ()
+ Graphics.UI.Threepenny.Canvas: type Vector = (Int, Int)
+ Graphics.UI.Threepenny.Core: audioStop :: Element -> IO ()
+ Graphics.UI.Threepenny.Core: defaultConfig :: Config
+ Graphics.UI.Threepenny.Core: disconnect :: Window -> Event ()
+ Graphics.UI.Threepenny.Core: getElementsByClassName :: Window -> String -> IO [Element]
+ Graphics.UI.Threepenny.Core: instance Typeable Element
+ Graphics.UI.Threepenny.Core: sink :: ReadWriteAttr x i o -> Behavior i -> IO x -> IO x
+ Graphics.UI.Threepenny.Core: tpLog :: Config -> ByteString -> IO ()
+ Graphics.UI.Threepenny.Events: checkedChange :: Element -> Event Bool
+ Graphics.UI.Threepenny.Events: selectionChange :: Element -> Event (Maybe Int)
+ Graphics.UI.Threepenny.Events: type KeyCode = Int
+ Graphics.UI.Threepenny.Events: valueChange :: Element -> Event String
+ Graphics.UI.Threepenny.Timer: instance Typeable Timer
+ Reactive.Threepenny: (<@) :: Behavior a -> Event b -> Event a
+ Reactive.Threepenny: (<@>) :: Behavior (a -> b) -> Event a -> Event b
+ Reactive.Threepenny: accumB :: a -> Event (a -> a) -> IO (Behavior a)
+ Reactive.Threepenny: accumE :: a -> Event (a -> a) -> IO (Event a)
+ Reactive.Threepenny: apply :: Behavior (a -> b) -> Event a -> Event b
+ Reactive.Threepenny: concatenate :: [a -> a] -> (a -> a)
+ Reactive.Threepenny: currentValue :: Behavior a -> IO a
+ Reactive.Threepenny: data Behavior a
+ Reactive.Threepenny: data Event a
+ Reactive.Threepenny: filterApply :: Behavior (a -> Bool) -> Event a -> Event a
+ Reactive.Threepenny: filterE :: (a -> Bool) -> Event a -> Event a
+ Reactive.Threepenny: filterJust :: Event (Maybe a) -> Event a
+ Reactive.Threepenny: instance Applicative Behavior
+ Reactive.Threepenny: instance Functor Behavior
+ Reactive.Threepenny: instance Functor Event
+ Reactive.Threepenny: mapAccum :: acc -> Event (acc -> (x, acc)) -> IO (Event x, Behavior acc)
+ Reactive.Threepenny: never :: Event a
+ Reactive.Threepenny: newEvent :: IO (Event a, Handler a)
+ Reactive.Threepenny: newEventsNamed :: Ord name => Handler (name, Event a, Handler a) -> IO (name -> Event a)
+ Reactive.Threepenny: onChange :: Behavior a -> Handler a -> IO ()
+ Reactive.Threepenny: register :: Event a -> Handler a -> IO (IO ())
+ Reactive.Threepenny: split :: Event (Either a b) -> (Event a, Event b)
+ Reactive.Threepenny: stepper :: a -> Event a -> IO (Behavior a)
+ Reactive.Threepenny: type Handler a = a -> IO ()
+ Reactive.Threepenny: unionWith :: (a -> a -> a) -> Event a -> Event a -> Event a
+ Reactive.Threepenny: unions :: [Event a] -> Event [a]
+ Reactive.Threepenny: unsafeMapIO :: (a1 -> IO a) -> Event a1 -> Event a
+ Reactive.Threepenny: whenE :: Behavior Bool -> Event a -> Event a
- Graphics.UI.Threepenny.Core: Config :: Int -> Maybe FilePath -> FilePath -> Config
+ Graphics.UI.Threepenny.Core: Config :: Int -> Maybe FilePath -> Maybe FilePath -> (ByteString -> IO ()) -> Config
- Graphics.UI.Threepenny.Core: tpStatic :: Config -> FilePath
+ Graphics.UI.Threepenny.Core: tpStatic :: Config -> Maybe FilePath

Files

src/BarTab.hs view
@@ -20,10 +20,9 @@ main :: IO () main = do     static <- getStaticDir-    startGUI Config+    startGUI defaultConfig         { tpPort       = 10000-        , tpCustomHTML = Nothing-        , tpStatic     = static+        , tpStatic     = Just static         } setup  setup :: Window -> IO ()
src/Buttons.hs view
@@ -19,10 +19,9 @@ main :: IO () main = do     static <- getStaticDir-    startGUI Config+    startGUI defaultConfig         { tpPort       = 10000-        , tpCustomHTML = Nothing-        , tpStatic     = static+        , tpStatic     = Just static         } setup  setup :: Window -> IO ()
src/Chat.hs view
@@ -8,6 +8,7 @@ import Data.Functor import Data.List.Extra import Data.Time+import Data.IORef import Prelude hiding (catch)  import Control.Monad.Trans.Reader as Reader@@ -30,24 +31,24 @@ main = do     static   <- getStaticDir     messages <- Chan.newChan-    startGUI Config+    startGUI defaultConfig         { tpPort       = 10000         , tpCustomHTML = Just "chat.html"-        , tpStatic     = static+        , tpStatic     = Just static         } $ setup messages  type Message = (UTCTime, String, String)  setup :: Chan Message -> Window -> IO ()-setup globalMsgs w = do+setup globalMsgs window = do     msgs <- Chan.dupChan globalMsgs -    return w # set title "Chat"+    return window # set title "Chat"     -    (nick, nickname) <- mkNickname-    messageArea      <- mkMessageArea msgs nick+    (nickRef, nickname) <- mkNickname+    messageArea         <- mkMessageArea msgs nickRef -    getBody w #++    getBody window #+         [ UI.div #. "header"   #+ [string "Threepenny Chat"]         , UI.div #. "gradient"         , viewSource@@ -55,12 +56,16 @@         , element messageArea         ]     -    void $ forkIO $ receiveMessages w msgs messageArea+    messageReceiver <- forkIO $ receiveMessages window msgs messageArea ---    io $ catch (runTP session handleEvents)---             (\e -> do killThread messageReceiver---                       throw (e :: SomeException))+    on UI.disconnect window $ const $ do+        putStrLn "Disconnected!"+        killThread messageReceiver+        now   <- getCurrentTime+        nick  <- readIORef nickRef+        Chan.writeChan msgs (now,nick,"( left the conversation )") + receiveMessages w msgs messageArea = do     messages <- Chan.getChanContents msgs     forM_ messages $ \msg -> do@@ -68,14 +73,14 @@           element messageArea #+ [mkMessage msg]           UI.scrollToBottom messageArea -mkMessageArea :: Chan Message -> Element -> IO Element+mkMessageArea :: Chan Message -> IORef String -> IO Element mkMessageArea msgs nickname = do     input <- UI.textarea #. "send-textarea"          on UI.sendValue input $ (. trim) $ \content -> do         when (not (null content)) $ do             now  <- getCurrentTime-            nick <- trim <$> get value nickname+            nick <- readIORef nickname             element input # set value ""             when (not (null nick)) $                 Chan.writeChan msgs (now,nick,content)@@ -83,15 +88,18 @@     UI.div #. "message-area" #+ [UI.div #. "send-area" #+ [element input]]  -mkNickname :: IO (Element, Element)+mkNickname :: IO (IORef String, Element) mkNickname = do-    i  <- UI.input #. "name-input"-    el <- UI.div   #. "name-area"  #+-        [ UI.span  #. "name-label" #+ [string "Your name "]-        , element i-        ]-    UI.setFocus i-    return (i,el)+    input  <- UI.input #. "name-input"+    el     <- UI.div   #. "name-area"  #++                [ UI.span  #. "name-label" #+ [string "Your name "]+                , element input+                ]+    UI.setFocus input+    +    nick <- newIORef ""+    on UI.keyup input $ \_ -> writeIORef nick . trim =<< get value input+    return (nick,el)  mkMessage :: Message -> IO Element mkMessage (timestamp, nick, content) =
− src/Control/Event.hs
@@ -1,111 +0,0 @@-module Control.Event (-    -- * Synopsis-    -- | Event-driven programming in the imperative style.-        -    -- * Documentation-    Handler, Event(..),-    mapIO, filterIO, filterJust,-    newEvent, newEventsTagged-    ) where---import Data.IORef-import qualified Data.Unique -- ordinary uniques here, because they are Ord--import qualified Data.Map as Map--type Map = Map.Map--{------------------------------------------------------------------------------    Types-------------------------------------------------------------------------------}--- | An /event handler/ is a function that takes an--- /event value/ and performs some computation.-type Handler a = a -> IO ()----- | An /event/ is a facility for registering--- event handlers. These will be called whenever the event occurs.--- --- When registering an event handler, you will also be given an action--- that unregisters this handler again.--- --- > do unregisterMyHandler <- register event myHandler----newtype Event a = Event { register :: Handler a -> IO (IO ()) }--{------------------------------------------------------------------------------    Combinators-------------------------------------------------------------------------------}-instance Functor Event where-    fmap f = mapIO (return . f)---- | Map the event value with an 'IO' action.-mapIO :: (a -> IO b) -> Event a -> Event b-mapIO f e = Event $ \h -> register e $ \x -> f x >>= h ---- | Filter event values that don't return 'True'.-filterIO :: (a -> IO Bool) -> Event a -> Event a-filterIO f e = Event $ \h ->-    register e $ \x -> f x >>= \b -> if b then h x else return ()---- | Keep only those event values that are of the form 'Just'.-filterJust :: Event (Maybe a) -> Event a-filterJust e = Event $ \g -> register e (maybe (return ()) g)---{------------------------------------------------------------------------------    Construction-------------------------------------------------------------------------------}--- | Build a facility to register and unregister event handlers.--- Also yields a function that takes an event handler and runs all the registered--- handlers.------ Example:------ > do--- >     (event, fire) <- newEvent--- >     register event (putStrLn)--- >     fire "Hello!"-newEvent :: IO (Event a, a -> IO ())-newEvent = do-    handlers <- newIORef Map.empty-    let register handler = do-            key <- Data.Unique.newUnique-            atomicModifyIORef_ handlers $ Map.insert key handler-            return $ atomicModifyIORef_ handlers $ Map.delete key-        runHandlers a =-            mapM_ ($ a) . map snd . Map.toList =<< readIORef handlers-    return (Event register, runHandlers)---- | Build several 'Event's from case analysis on a tag.--- Generalization of 'newEvent'.-newEventsTagged :: Ord tag => IO (tag -> Event a, (tag, a) -> IO ())-newEventsTagged = do-    handlersRef <- newIORef Map.empty       -- :: Map key (Map Unique (Handler a))--    let register tag handler = do-            -- new identifier for this handler-            uid <- Data.Unique.newUnique-            -- add handler to map at  key-            atomicModifyIORef_ handlersRef $-                Map.alter (Just . Map.insert uid handler . maybe Map.empty id) tag-            -- remove handler from map at  key-            return $ atomicModifyIORef_ handlersRef $-                Map.adjust (Map.delete uid) tag--    let runHandlers (tag,a) = do-            handlers <- readIORef handlersRef-            case Map.lookup tag handlers of-                Just hs -> mapM_ ($ a) (Map.elems hs)-                Nothing -> return ()--    return (\tag -> Event (register tag), runHandlers)-            -{------------------------------------------------------------------------------    Utilities-------------------------------------------------------------------------------}-atomicModifyIORef_ ref f = atomicModifyIORef ref $ \x -> (f x, ())---
− src/Control/Monad/Extra.hs
@@ -1,30 +0,0 @@--- | Extra utilities for monads.--module Control.Monad.Extra where--import Data.Maybe---- | Ignore the given action's return.-ig :: (Monad m) => m a -> m ()-ig m = m >> return ()---- | A non-operator version of (=<<).-bind :: (Monad m) => (a -> m b) -> m a -> m b-bind = flip (>>=)---- | When the value is Just, run the action.-whenJust :: Monad m => Maybe a -> (a -> m ()) -> m ()-whenJust (Just a) m = m a-whenJust Nothing  _ = return ()---- | Wrap up a functor in a Maybe.-just :: Functor m => m a -> m (Maybe a)-just = fmap Just---- | Flip mapMaybe.-forMaybe :: [a] -> (a -> Maybe b) -> [b]-forMaybe = flip mapMaybe---- | Monadic version of maybe.-maybeM :: (Monad m) => a -> (a1 -> m a) -> Maybe a1 -> m a-maybeM nil cons a = maybe (return nil) cons a
− src/Control/Monad/IO.hs
@@ -1,8 +0,0 @@-{-# OPTIONS -Wall #-}--module Control.Monad.IO where--import Control.Monad.Trans--io :: MonadIO m => IO a -> m a-io = liftIO
+ src/CurrencyConverter.hs view
@@ -0,0 +1,52 @@+{-----------------------------------------------------------------------------+    threepenny-gui+    +    Example: Currency Converter+------------------------------------------------------------------------------}+{-# LANGUAGE CPP, PackageImports #-}++import Control.Monad (void)+import Data.Maybe+import Text.Printf+import Text.Read     (readMaybe)++#ifdef CABAL+import qualified "threepenny-gui" Graphics.UI.Threepenny as UI+import "threepenny-gui" Graphics.UI.Threepenny.Core+#else+import qualified Graphics.UI.Threepenny as UI+import Graphics.UI.Threepenny.Core+#endif++{-----------------------------------------------------------------------------+    Main+------------------------------------------------------------------------------}+main :: IO ()+main = startGUI defaultConfig { tpPort = 10000 } setup++setup :: Window -> IO ()+setup window = void $ do+    return window # set title "Currency Converter"++    dollar <- UI.input+    euro   <- UI.input+    +    getBody window #+ [+            column [+                grid [[string "Dollar:", element dollar]+                     ,[string "Euro:"  , element euro  ]]+            , string "Amounts update while typing."+            ]]++    euroIn   <- stepper "0" $ UI.valueChange euro+    dollarIn <- stepper "0" $ UI.valueChange dollar+    let+        rate = 0.7 :: Double+        withString f = maybe "-" (printf "%.2f") . fmap f . readMaybe+    +        dollarOut = withString (/ rate) <$> euroIn+        euroOut   = withString (* rate) <$> dollarIn+    +    element euro   # sink value euroOut+    element dollar # sink value dollarOut+
src/DragNDropExample.hs view
@@ -20,10 +20,9 @@ main :: IO () main = do     static <- getStaticDir-    startGUI Config+    startGUI defaultConfig         { tpPort       = 10000-        , tpCustomHTML = Nothing-        , tpStatic     = static+        , tpStatic     = Just static         } setup  setup :: Window -> IO ()
src/DrumMachine.hs view
@@ -38,10 +38,9 @@ main :: IO () main = do     static <- getStaticDir-    startGUI Config+    startGUI defaultConfig         { tpPort       = 10000-        , tpCustomHTML = Nothing-        , tpStatic     = static+        , tpStatic     = Just static         } setup  setup :: Window -> IO ()@@ -55,21 +54,17 @@                       ,[UI.string "Beat:", element elTick]]     getBody w #+ [UI.div #. "wrap" #+ (status : map element elInstruments)]     -    timer   <- UI.timer # set UI.interval (bpm2ms defaultBpm)-    refBeat <- newIORef 0-    -    -- play sounds on timer events-    on UI.tick timer $ const $ void $ do-        -- get and increase beat count-        beat <- readIORef refBeat-        writeIORef refBeat $ (beat + 1) `mod` (beats * bars)+    timer <- UI.timer # set UI.interval (bpm2ms defaultBpm)+    eBeat <- accumE (0::Int) $+        (\beat -> (beat + 1) `mod` (beats * bars)) <$ UI.tick timer+    _ <- register eBeat $ \beat -> do+        -- display beat count         element elTick # set text (show $ beat + 1)-                 -- play corresponding sounds         sequence_ $ map (!! beat) kit          -- allow user to set BPM-    on (domEvent "livechange") elBpm $ const $ void $ do+    on UI.keydown elBpm $ \keycode -> when (keycode == 13) $ void $ do         bpm <- read <$> get value elBpm         return timer # set UI.interval (bpm2ms bpm)     @@ -97,7 +92,9 @@     let         play box = do             checked <- get UI.checked box-            when checked $ audioPlay elAudio+            when checked $ do+                audioStop elAudio -- just in case the sound is already playing+                audioPlay elAudio         beats    = map play . concat $ elCheckboxes         elGroups = [UI.span #. "bar" #+ map element bar | bar <- elCheckboxes]     
src/Graphics/UI/Threepenny.hs view
@@ -1,4 +1,11 @@ module Graphics.UI.Threepenny (+    -- * Introduction+    -- $intro++    -- * Example+    -- $example+    +    -- * Modules     module Graphics.UI.Threepenny.Attributes,     module Graphics.UI.Threepenny.Core,     module Graphics.UI.Threepenny.Canvas,@@ -17,3 +24,83 @@ import Graphics.UI.Threepenny.Events import Graphics.UI.Threepenny.JQuery import Graphics.UI.Threepenny.Timer++{- $intro++Welcome to the Threepenny library for graphical user interfaces.++A program written with Threepenny is essentially a small web server+that displays the user interface as a web page to any browser that connects to it.++For an introduction, see the example below.+The module "Graphics.UI.Threepenny.Core" contains the main functions.++This project was originally called Ji.++-}+++{- $example++The following example should help to get you started with Threepenny.+(The lines of code below are meant to be concatenated into a single file.)++> module Main where++First, we have to import the library.+It is a good idea to import the core module verbatim+and import all other functions with a mandatory @UI@ prefix.++> import qualified Graphics.UI.Threepenny       as UI+> import           Graphics.UI.Threepenny.Core++We begin by starting a server on port @10000@ using the 'startGUI' function.+Additional static content is served from the @../wwwroot@ directory.++> main :: IO ()+> main = do+>     startGUI defaultConfig+>         { tpPort       = 10000+>         , tpStatic     = Just "../wwwroot"+>         } setup++Whenever a browser connects to the server,+the following function will be executed to start the GUI interaction.+It builds the initial HTML page.++> setup :: Window -> IO ()+> setup window = do++First, set the title of the HTML document++>     return window # set UI.title "Hello World!"++Then create a button element++>     button <- UI.button # set UI.text "Click me!s"++DOM elements can be accessed much in the same way they are+accessed from JavaScript; they can be searched, updated, moved and+inspected. In the line above, we set the 'text' contents.++To actually display the button, we have to attach it to the body of the HTML element.+The '#+' combinator allows you to nest elements quickly+in the style of a HTML combinator library.++>     getBody window #+ [element button]++Finally, we register an event handler for the 'click' event,+which occurs whenever the user clicks on the button.+When that happens, we change the text of the button.++>     on UI.click button $ const $ do+>         element button # set UI.text "I have been clicked!"++That's it for a first example!++The libary comes with a+<https://github.com/HeinrichApfelmus/threepenny-gui/tree/master/src plethora of additional example code>.+++-}+
src/Graphics/UI/Threepenny/Canvas.hs view
@@ -4,10 +4,9 @@          -- * Documentation     Canvas,-    drawImage, clearCanvas,+    Vector, drawImage, clearCanvas,     ) where -import Control.Event import Graphics.UI.Threepenny.Core import qualified Graphics.UI.Threepenny.Internal.Core as Core import qualified Graphics.UI.Threepenny.Internal.Types as Core
src/Graphics/UI/Threepenny/Core.hs view
@@ -1,10 +1,11 @@+{-# LANGUAGE DeriveDataTypeable, RecordWildCards #-} module Graphics.UI.Threepenny.Core (-    -- * Guide-    -- $guide+    -- * Synopsis+    -- | Core functionality of the Threepenny GUI library.          -- * Server     -- $server-    Config(..), startGUI,+    Config(..), defaultConfig, startGUI,     loadFile, loadDirectory,          -- * Browser Window@@ -16,7 +17,9 @@         getHead, getBody,         children, text, html, attr, style, value,     getValuesList,-    getElementsByTagName, getElementByTagName, getElementsById, getElementById,+    getElementsByTagName, getElementByTagName, +    getElementsById, getElementById,+    getElementsByClassName,          -- * Layout     -- | Combinators for quickly creating layouts.@@ -25,14 +28,14 @@          -- * Events     -- | For a list of predefined events, see "Graphics.UI.Threepenny.Events".-    EventData(..), domEvent, on,-    module Control.Event,+    EventData(..), domEvent, on, disconnect,+    module Reactive.Threepenny,          -- * Attributes     -- | For a list of predefined attributes, see "Graphics.UI.Threepenny.Attributes".     (#), (#.), element,     Attr, WriteAttr, ReadAttr, ReadWriteAttr(..),-    set, get, mkReadWriteAttr, mkWriteAttr, mkReadAttr,+    set, sink, get, mkReadWriteAttr, mkWriteAttr, mkReadAttr,          -- * JavaScript FFI     -- | Direct interface to JavaScript in the browser window.@@ -41,21 +44,23 @@     callDeferredFunction, atomic,          -- * Internal and oddball functions-    updateElement, manifestElement, audioPlay, fromProp,+    updateElement, manifestElement, fromProp,+    audioPlay, audioStop,          ) where +import Data.Dynamic import Data.IORef+import qualified Data.Map as Map import Data.Maybe (listToMaybe) import Data.Functor import Data.String (fromString) import Control.Concurrent.MVar-import Control.Event import Control.Monad import Control.Monad.IO.Class-import Control.Monad.Trans.Reader as Reader import Network.URI import Text.JSON+import Reactive.Threepenny  import qualified Graphics.UI.Threepenny.Internal.Core  as Core import Graphics.UI.Threepenny.Internal.Core@@ -63,38 +68,8 @@      ToJS, FFI, ffi, JSFunction,      debug, clear, callFunction, runFunction, callDeferredFunction, atomic, ) import qualified Graphics.UI.Threepenny.Internal.Types as Core-import Graphics.UI.Threepenny.Internal.Types (Window, Config, EventData)--{------------------------------------------------------------------------------    Guide-------------------------------------------------------------------------------}-{- $guide--Threepenny runs a small web server that displays the user interface-as a web page to any browser that connects to it.-To start the web server, use the 'startGUI' function.--Creating of DOM elements is easy,-the '(#+)' combinator allows a style similar to HTML combinator libraries.--Existing DOM elements can be accessed much in the same way they are-accessed from JavaScript; they can be searched, updated, moved and-inspected. Events can be bound to DOM elements and handled.---Applications written in Threepenny are multithreaded. Each client (user)-has a separate thread which runs with no awareness of the asynchronous-protocol below. Each session should only be accessed from one-thread. There is not yet any clever architecture for accessing the-(single threaded) web browser from multi-threaded Haskell. That's-my recommendation. You can choose to ignore it, but don't blame me-when you run an element search and you get a click event as a-result.--This project was originally called Ji.---}-+import Graphics.UI.Threepenny.Internal.Types+    (Window, Config, defaultConfig, EventData, Session(..))  {-----------------------------------------------------------------------------     Server@@ -106,6 +81,13 @@ (assuming that you have set the port number to @tpPort=10000@ in the server configuration). +The server is multithreaded,+a separate thread is used to communicate with a single browser 'Window'.+However, each window should only be accessed from a single thread,+otherwise the behavior will be undefined,+i.e. you could run an element search and get a click event as a result+if you don't access each window in a single-threaded fashion.+ -}  -- | Start server for GUI sessions.@@ -113,8 +95,7 @@     :: Config               -- ^ Server configuration.     -> (Window -> IO ())    -- ^ Action to run whenever a client browser connects.     -> IO ()-startGUI config handler =-    Core.serve config $ \w -> handler w >> Core.handleEvents w+startGUI config handler = Core.serve config handler   -- | Make a local file available as a relative URI.@@ -146,7 +127,10 @@ type Value = String  -- | Reference to an element in the DOM of the client window.-newtype Element = Element (MVar Elem)+data Element = Element Core.ElementEvents (MVar Elem) deriving (Typeable)+-- Element events mvar+--      events = Events associated to this element+--      mvar   = Current state of the MVar data    Elem     = Alive Core.Element                       -- element exists in a window     | Limbo Value (Window -> IO Core.Element)  -- still needs to be created@@ -155,11 +139,13 @@ -- Note that multiple MVars may now point to the same live reference, -- but this is ok since live references never change. fromAlive :: Core.Element -> IO Element-fromAlive e = Element <$> newMVar (Alive e)+fromAlive e@(Core.Element elid Session{..}) = do+    Just events <- Map.lookup elid <$> readMVar sElementEvents+    Element events <$> newMVar (Alive e)  -- Update an element that may be in Limbo. updateElement :: (Core.Element -> IO ()) -> Element -> IO ()-updateElement f (Element me) = do+updateElement f (Element _ me) = do     e <- takeMVar me     case e of         Alive e -> do   -- update immediately@@ -172,14 +158,23 @@ -- TODO: 1. Throw exception if the element exists in another window. --       2. Don't throw exception, but move the element across windows. manifestElement :: Window -> Element -> IO Core.Element-manifestElement w (Element me) = do-    e1 <- takeMVar me-    e2 <- case e1 of-        Alive e        -> return e-        Limbo v create -> do { e2 <- create w; Core.setAttr "value" v e2; return e2 }-    putMVar me $ Alive e2-    return e2+manifestElement w (Element events me) = do+        e1 <- takeMVar me+        e2 <- case e1 of+            Alive e        -> return e+            Limbo v create -> do+                e2 <- create w+                Core.setAttr "value" v e2+                rememberEvents events e2    -- save events in session data+                return e2+        putMVar me $ Alive e2+        return e2+    +    where+    rememberEvents events (Core.Element elid Session{..}) =+        modifyMVar_ sElementEvents $ return . Map.insert elid events + -- Append a child element to a parent element. Non-blocking. appendTo     :: Element   -- ^ Parent.@@ -194,9 +189,18 @@ mkElement     :: String           -- ^ Tag name     -> IO Element-mkElement tag = Element <$> newMVar (Limbo "" $ \w -> Core.newElement w tag)+mkElement tag = do+    -- create element in Limbo+    ref <- newMVar (Limbo "" $ \w -> Core.newElement w tag)+    -- create events and initialize them when element becomes Alive+    let+        initializeEvent (name,_,handler) = +            flip updateElement (Element undefined ref) $ \e -> do+                Core.bind name e handler+    events  <- newEventsNamed initializeEvent+    return $ Element events ref --- | Retreive the browser 'Window' in which the element resides.+-- | Retrieve the browser 'Window' in which the element resides. --  -- Note that elements do not reside in any browser window when they are first created. -- To move the element to a particular browser window,@@ -205,7 +209,7 @@ -- WARNING: The ability to move elements from one browser window to another -- is currently not implemented yet. getWindow :: Element -> IO (Maybe Window)-getWindow (Element ref) = do+getWindow (Element _ ref) = do     e1 <- readMVar ref     return $ case e1 of         Alive e   -> Just $ Core.getWindow e@@ -248,8 +252,8 @@ value :: Attr Element String value = mkReadWriteAttr get set     where-    get   (Element ref) = getValue =<< readMVar ref-    set v (Element ref) = updateMVar (setValue v) ref+    get   (Element _ ref) = getValue =<< readMVar ref+    set v (Element _ ref) = updateMVar (setValue v) ref          getValue (Limbo v _) = return v     getValue (Alive e  ) = Core.getValue e@@ -316,17 +320,31 @@ getElementsById window name =     mapM fromAlive =<< Core.getElementsById window name +-- | Get a list of elements by particular class.  Blocks.+getElementsByClassName+    :: Window        -- ^ Browser window+    -> String        -- ^ The class string.+    -> IO [Element]  -- ^ Elements with given class.+getElementsByClassName window cls =+    mapM fromAlive =<< Core.getElementsByClassName window cls+ {-----------------------------------------------------------------------------     Oddball ------------------------------------------------------------------------------}-audioPlay = updateElement Core.audioPlay+-- | Invoke the JavaScript expression @audioElement.play();@.+audioPlay = updateElement $ \el -> Core.runFunction (Core.getWindow el) $+    ffi "%1.play()" el +-- | Invoke the JavaScript expression @audioElement.stop();@.+audioStop = updateElement $ \el -> Core.runFunction (Core.getWindow el) $+    ffi "prim_audio_stop(%1)" el+ -- Turn a jQuery property @.prop()@ into an attribute. fromProp :: String -> (JSValue -> a) -> (a -> JSValue) -> Attr Element a fromProp name from to = mkReadWriteAttr get set     where     set x = updateElement (Core.setProp name $ to x)-    get (Element ref) = do+    get (Element _ ref) = do         me <- readMVar ref         case me of             Limbo _ _ -> error "'checked' attribute: element must be in a browser window"@@ -387,7 +405,9 @@         --   the name is @click@ and so on.     -> Element          -- ^ Element where the event is to occur.     -> Event EventData-domEvent name element = Control.Event.Event $ \handler -> do+domEvent name (Element events _) = events name++{-        ref <- newIORef $ return ()     let         -- register handler and remember unregister function@@ -401,7 +421,16 @@          register'     return unregister'+-} +-- | Event that occurs whenever the client has disconnected,+-- be it by closing the browser window or by exception.+--+-- Note: DOM Elements in the browser window that has been closed+-- can no longer be manipulated.+disconnect :: Window -> Event ()+disconnect = Core.disconnect+ -- | Convenience function to register 'Event's for 'Element's. -- -- Example usage.@@ -464,6 +493,19 @@ -- Best used in conjunction with '#'. set :: MonadIO m => ReadWriteAttr x i o -> i -> m x -> m x set attr i mx = do { x <- mx; liftIO (set' attr i x); return x; }++-- | Set the value of an attribute to a 'Behavior', that is a time-varying value.+--+-- Note: For reasons of efficiency, the attribute is only+-- updated when the value changes.+sink :: ReadWriteAttr x i o -> Behavior i -> IO x -> IO x+sink attr bi mx = do+    x <- mx+    do+        i <- currentValue bi+        set' attr i x+        onChange bi $ \i -> set' attr i x  +    return x  -- | Get attribute value. get :: ReadWriteAttr x i o -> x -> IO o
src/Graphics/UI/Threepenny/Events.hs view
@@ -1,16 +1,39 @@ module Graphics.UI.Threepenny.Events (     -- * Synopsis-    -- | Common DOM events, for convenience.+    -- | Events on DOM elements.     -    -- * Documentation+    -- * Convenience events+    valueChange, selectionChange, checkedChange,+    +    -- * Standard DOM events     click, mousemove, hover, blur, leave,-    keyup, keydown,+    KeyCode, keyup, keydown,     ) where +import Graphics.UI.Threepenny.Attributes import Graphics.UI.Threepenny.Core  silence = fmap (const ()) +{-----------------------------------------------------------------------------+    Events+------------------------------------------------------------------------------}+-- | Event that occurs when the /user/ changes the value of the input element.+valueChange :: Element -> Event String+valueChange el = unsafeMapIO (const $ get value el) (domEvent "keydown" el)++-- | Event that occurs when the /user/ changes the selection of a @<select>@ element.+selectionChange :: Element -> Event (Maybe Int)+selectionChange el = unsafeMapIO (const $ get selection el) (click el)++-- | Event that occurs when the /user/ changes the checked status of an input+-- element of type checkbox.+checkedChange :: Element -> Event Bool+checkedChange el = unsafeMapIO (const $ get checked el) (click el)++{-----------------------------------------------------------------------------+    DOM Events+------------------------------------------------------------------------------} -- | Mouse click. click :: Element -> Event () click = silence . domEvent "click"
src/Graphics/UI/Threepenny/Internal/Core.hs view
@@ -1,15 +1,12 @@ {-# LANGUAGE RecordWildCards #-}-{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE OverloadedStrings, PackageImports #-} {-# OPTIONS -fno-warn-name-shadowing #-} --- | The main Threepenny module.-------------------------------------------------------------------------------------- Exports- module Graphics.UI.Threepenny.Internal.Core   (+  -- * Synopsis+  -- | The main internal functionality.+         -- * Server running    serve   ,loadFile@@ -18,9 +15,8 @@   -- * Event handling   -- $eventhandling   ,bind-  ,handleEvent-  ,handleEvents-  ,module Control.Event+  ,disconnect+  ,module Reactive.Threepenny      -- * Setting attributes   -- $settingattributes@@ -44,6 +40,7 @@   ,getBody   ,getElementsByTagName   ,getElementsById+  ,getElementsByClassName   ,getWindow   ,getProp   ,getValue@@ -63,9 +60,6 @@   ,ToJS, FFI, ffi, JSFunction   ,runFunction, callFunction   -  -- * Oddball-  ,audioPlay-     -- * Types   ,Window   ,Element@@ -73,10 +67,8 @@   ,EventData(..)   ) where ------------------------------------------------------------------------------------- Imports + import           Graphics.UI.Threepenny.Internal.Types     as Threepenny import           Graphics.UI.Threepenny.Internal.Resources @@ -84,10 +76,11 @@ import           Control.Concurrent import           Control.Concurrent.Chan.Extra import           Control.Concurrent.Delay-import qualified Control.Exception             as E-import           Control.Event-import           Control.Monad.IO-import           Control.Monad.Reader+import qualified Control.Exception+import           Reactive.Threepenny+import           Control.Monad+import           Control.Monad.IO.Class+import qualified "MonadCatchIO-transformers" Control.Monad.CatchIO as E import           Data.ByteString               (ByteString) import           Data.ByteString.UTF8          (toString,fromString) import           Data.Map                      (Map)@@ -98,17 +91,21 @@ import           Data.Text.Encoding import           Data.Time import           Network.URI+import qualified Network.WebSockets            as WS+import qualified Network.WebSockets.Snap       as WS+import qualified Data.Attoparsec.Enumerator    as Atto import           Prelude                       hiding (init) import           Safe import           Snap.Core-import           Snap.Http.Server              hiding (Config)+import qualified Snap.Http.Server              as Snap import           Snap.Util.FileServe import           System.FilePath+import qualified Text.JSON as JSON import           Text.JSON.Generic   {------------------------------------------------------------------------------    Server running+    Server and and session management ------------------------------------------------------------------------------} newServerState :: IO ServerState newServerState = ServerState @@ -118,12 +115,18 @@  -- | Run a TP server with Snap on the specified port and the given --   worker action.-serve :: Config -> (Session -> IO ()) -> IO () -- ^ A TP server.+serve :: Config -> (Session -> IO ()) -> IO () serve Config{..} worker = do-  server <- newServerState-  _ <- forkIO $ custodian 30 (sSessions server)-  httpServe config (router tpCustomHTML tpStatic worker server)- where config = setPort tpPort defaultConfig+    server <- newServerState+    _      <- forkIO $ custodian 30 (sSessions server)+    let config = Snap.setPort tpPort+               $ Snap.setErrorLog (Snap.ConfigIoLog tpLog)+               $ Snap.setAccessLog (Snap.ConfigIoLog tpLog)+               $ Snap.defaultConfig+    Snap.httpServe config . route $+        routeResources tpCustomHTML tpStatic server+        -- ++ routeCommunication worker server+        ++ routeWebsockets worker server  -- | Kill sessions after at least n seconds of disconnectedness. custodian :: Integer -> MVar Sessions -> IO ()@@ -137,44 +140,282 @@         Disconnected time -> do           now <- getCurrentTime           let dcSeconds = diffUTCTime now time+          -- session is disconnected for more than  seconds           if (dcSeconds > fromIntegral seconds)              then do killThread sThreadId                      return (Just key)              else return Nothing-            +    +    -- remove killed sessions from the map     return (M.filterWithKey (\k _ -> not (k `elem` killed)) sessions) --- Route requests.  If the initFile is Nothing, then a default--- file will be served at /.-router-    :: Maybe FilePath-    -> FilePath-    -> (Session -> IO a)-    -> ServerState-    -> Snap ()-router customHTML wwwroot worker server =-        route [("/static"                    , serveDirectory wwwroot)-              ,("/"                          , root)-              ,("/driver/threepenny-gui.js"  , writeText jsDriverCode )-              ,("/driver/threepenny-gui.css" , writeText cssDriverCode)-              ,("/init"                      , init worker server)-              ,("/poll"                      , withSession  server poll  )-              ,("/signal"                    , withSession  server signal)-              ,("/file/:name"                ,-                    withFilepath (sFiles server) (flip serveFileAs))-              ,("/dir/:name"                 ,-                    withFilepath (sDirs  server) (\path _ -> serveDirectory path))-              ]+-- Run a snap action with the given session.+withSession :: ServerState -> (Session -> Snap a) -> Snap a+withSession server cont = do+    token <- readInput "token"+    case token of+        Nothing    -> error $ "Invalid session token format."+        Just token -> withGivenSession token server cont++-- Do something with the session given by its token id.+withGivenSession :: Integer -> ServerState -> (Session -> Snap a) -> Snap a+withGivenSession token ServerState{..} cont = do+    sessions <- liftIO $ withMVar sSessions return+    case M.lookup token sessions of+        Nothing      -> error $ "Nonexistant token: " ++ show token+        Just session -> cont session++{-----------------------------------------------------------------------------+    Implementation of two-way communication+    - POST and GET requests+------------------------------------------------------------------------------}+-- | Route the communication between JavaScript and the server+routeCommunication :: (Session -> IO a) -> ServerState -> Routes+routeCommunication worker server =+    [("/init"   , init worker server)+    ,("/poll"   , withSession server poll  )+    ,("/signal" , withSession server signal)+    ]++-- | Make a new session.+newSession :: ServerState -> (URI,[(String, String)]) -> Integer -> IO Session+newSession sServerState sStartInfo sToken = do+    sSignals          <- newChan+    sInstructions     <- newChan+    sMutex            <- newMVar ()+    sEventHandlers    <- newMVar M.empty+    sElementEvents    <- newMVar M.empty+    sEventQuit        <- newEvent+    sElementIds       <- newMVar [0..]+    now               <- getCurrentTime+    sConnectedState   <- newMVar (Disconnected now)+    sThreadId         <- myThreadId+    sClosures         <- newMVar [0..]+    let session = Session {..}+    initializeElementEvents session    +    return session++-- | Make a new session and add it to the server+createSession :: (Session -> IO void) -> ServerState -> Snap Session+createSession worker server = do+    -- uri    <- snapRequestURI+    let uri = undefined -- FIXME: No URI for WebSocket requests.+    params <- snapRequestCookies+    liftIO $ modifyMVar (sSessions server) $ \sessions -> do+        let newKey = maybe 0 (+1) (lastMay (M.keys sessions))+        session <- newSession server (uri,params) newKey+        _       <- forkIO $ void $ worker session >> handleEvents session+        return (M.insert newKey session sessions, session)++-- | Respond to initialization request.+init :: (Session -> IO void) -> ServerState -> Snap ()+init worker server = do+    session <- createSession worker server+    modifyResponse . setHeader "Set-Token" . fromString . show . sToken $ session+    poll session++snapRequestURI :: Snap URI+snapRequestURI = do+    uri     <- getInput "info"+    maybe (error ("Unable to parse request URI: " ++ show uri)) return (uri >>= parseURI)++snapRequestCookies :: Snap [(String, String)]+snapRequestCookies = do+    cookies <- getsRequest rqCookies+    return $ flip map cookies $ \Cookie{..} -> (toString cookieName,toString cookieValue)+++-- | Respond to poll requests.+poll :: Session -> Snap ()+poll Session{..} = do+    let setDisconnected = do+        now <- getCurrentTime+        modifyMVar_ sConnectedState (const (return (Disconnected now)))+    +    instructions <- liftIO $ do+        modifyMVar_ sConnectedState (const (return Connected))+        threadId <- myThreadId+        forkIO $ do+            delaySeconds $ 60 * 5 -- Force kill after 5 minutes.+            killThread threadId+        E.catch (readAvailableChan sInstructions) $ \e -> do+            -- no instructions available after some time+            when (e == Control.Exception.ThreadKilled) $ setDisconnected+            E.throw e+    +    writeJson instructions++-- | Handle signals sent from the client.+signal :: Session -> Snap ()+signal Session{..} = do+    input <- getInput "signal"+    case input of+      Just signalJson -> do+        let signal = JSON.decode signalJson+        case signal of+          Ok signal -> liftIO $ writeChan sSignals signal+          Error err -> error err+      Nothing -> error $ "Unable to parse " ++ show input++{-----------------------------------------------------------------------------+    Implementation of two-way communication+    - WebSockets+------------------------------------------------------------------------------}+-- | Route the communication between JavaScript and the server+routeWebsockets :: (Session -> IO a) -> ServerState -> Routes+routeWebsockets worker server =+    [("websocket", response)]     where+    response = do+        session <- createSession worker server+        WS.runWebSocketsSnap (webSocket session)+        error "Threepenny.Internal.Core: runWebSocketsSnap should never return."+++webSocket :: Session -> WS.Request -> WS.WebSockets WS.Hybi00 ()+webSocket Session{..} req = void $ do+    WS.acceptRequest req+    -- websockets are always connected, don't let the custodian kill you.+    liftIO $ modifyMVar_ sConnectedState (const (return Connected))++    -- write data (in another thread)+    send     <- WS.getSink+    sendData <- liftIO . forkIO . forever $ do+        x <- readChan sInstructions+        WS.sendSink send . WS.textData . Text.pack . JSON.encode $ x++    -- read data+    let readData = do+            input <- WS.receiveData+            case input of+                "ping" -> liftIO . WS.sendSink send . WS.textData . Text.pack $ "pong"+                "quit" -> WS.throwWsError WS.ConnectionClosed+                input  -> case JSON.decode . Text.unpack $ input of+                    Ok signal -> liftIO $ writeChan sSignals signal+                    Error err -> WS.throwWsError . WS.ParseError $ Atto.ParseError [] err+    +    forever readData `WS.catchWsError`+        \_ -> liftIO $ do+            killThread sendData             -- kill sending thread when done+            writeChan sSignals $ Quit ()    -- signal  Quit  event++{-----------------------------------------------------------------------------+    FFI implementation on top of the communication channel+------------------------------------------------------------------------------}+-- | Atomically execute the given computation in the context of a browser window+atomic :: Window -> IO a -> IO a+atomic window@(Session{..}) m = do+  takeMVar sMutex+  ret <- m+  putMVar sMutex ()+  return ret++-- | Send an instruction and read the signal response.+call :: Session -> Instruction -> (Signal -> IO (Maybe a)) -> IO a+call session@(Session{..}) instruction withSignal = do+  takeMVar sMutex+  run session $ instruction+  newChan <- dupChan sSignals+  go sMutex newChan++  where+    go mutex newChan = do+      signal <- readChan newChan+      result <- withSignal signal+      case result of+        Just signal -> do putMVar mutex ()+                          return signal+        Nothing     -> go mutex newChan+            -- keep reading signals from the duplicated channel+            -- until the function above succeeds++-- Run the given instruction wihtout waiting for a response.+run :: Session -> Instruction -> IO ()+run (Session{..}) i = writeChan sInstructions i++-- | Call the given function with the given continuation. Doesn't block.+callDeferredFunction+  :: Window                    -- ^ Browser window+  -> String                    -- ^ The function name.+  -> [String]                  -- ^ Parameters.+  -> ([Maybe String] -> IO ()) -- ^ The continuation to call if/when the function completes.+  -> IO ()+callDeferredFunction session@(Session{..}) func params closure = do+  cid      <- modifyMVar sClosures (\(x:xs) -> return (xs,x))+  closure' <- newClosure session func (show cid) closure+  run session $ CallDeferredFunction (closure',func,params)++-- | Run the given JavaScript function and carry on. Doesn't block.+--+-- The client window uses JavaScript's @eval()@ function to run the code.+runFunction :: Window -> JSFunction () -> IO ()+runFunction session = run session . RunJSFunction . unJSCode . code++-- | Run the given JavaScript function and wait for results. Blocks.+--+-- The client window uses JavaScript's @eval()@ function to run the code.+callFunction :: Window -> JSFunction a -> IO a+callFunction window (JSFunction code marshal) = +    call window (CallJSFunction . unJSCode $ code) $ \signal ->+        case signal of+            FunctionResult v -> case marshal window v of+                Ok    a -> return $ Just a+                Error _ -> return Nothing+            _ -> return Nothing+++{-----------------------------------------------------------------------------+    Snap utilities+------------------------------------------------------------------------------}+-- Write JSON to output.+writeJson :: (MonadSnap m, JSON a) => a -> m ()+writeJson json = do+    modifyResponse $ setContentType "application/json"+    (writeText . pack . (\x -> showJSValue x "") . showJSON) json++-- Get a text input from snap.+getInput :: (MonadSnap f) => ByteString -> f (Maybe String)+getInput = fmap (fmap (unpack . decodeUtf8)) . getParam++-- Read an input from snap.+readInput :: (MonadSnap f,Read a) => ByteString -> f (Maybe a)+readInput = fmap (>>= readMay) . getInput++{-----------------------------------------------------------------------------+    Resourcse+------------------------------------------------------------------------------}+type Routes = [(ByteString, Snap ())]++routeResources :: Maybe FilePath -> Maybe FilePath -> ServerState -> Routes+routeResources customHTML staticDir server =+    fixHandlers noCache $+        static +++        [("/"                          , root)+        ,("/driver/threepenny-gui.js"  , writeText jsDriverCode )+        ,("/driver/threepenny-gui.css" , writeText cssDriverCode)+        ,("/file/:name"                ,+            withFilepath (sFiles server) (flip serveFileAs))+        ,("/dir/:name"                 ,+            withFilepath (sDirs  server) (\path _ -> serveDirectory path))+        ]+    where+    fixHandlers f routes = [(a,f b) | (a,b) <- routes]+    noCache h = modifyResponse (setHeader "Cache-Control" "no-cache") >> h+    +    static = maybe [] (\dir -> [("/static", serveDirectory dir)]) staticDir+         root = case customHTML of-        Just file -> serveFile (wwwroot </> file)+        Just file -> case staticDir of+            Just dir -> serveFile (dir </> file)+            Nothing  -> logError "Graphics.UI.Threepenny: Cannot use tpCustomHTML file without tpStatic"         Nothing   -> writeText defaultHtmlFile + -- Get a filename from a URI withFilepath :: MVar Filepaths -> (FilePath -> MimeType -> Snap a) -> Snap a withFilepath rDict cont = do     mName    <- getParam "name"-    (_,dict) <- io $ withMVar rDict return+    (_,dict) <- liftIO $ withMVar rDict return     case (\key -> M.lookup key dict) =<< mName of         Just (path,mimetype) -> cont path mimetype         Nothing              -> error $ "File not loaded: " ++ show mName@@ -202,117 +443,7 @@     key <- newAssociation (sDirs sServerState) (path,"")     return $ "/dir/" ++ key --- Initialize the session.-init :: (Session -> IO void) -> ServerState -> Snap ()-init sessionThread server = do-  uri <- getRequestURI-  params <- getRequestCookies-  key <- io $ modifyMVar (sSessions server) $ \sessions -> do-    let newKey = maybe 0 (+1) (lastMay (M.keys sessions))-    session <- newSession server (uri,params) newKey-    _ <- forkIO $ do _ <- sessionThread session; return ()-    return (M.insert newKey session sessions,newKey)-  modifyResponse $ setHeader "Set-Token" (fromString (show key))-  withGivenSession key server poll-    -  where getRequestURI = do-          uri <- getInput "info"-          maybe (error ("Unable to parse request URI: " ++ show uri)) return (uri >>= parseURI)-        getRequestCookies = do-          cookies <- getsRequest rqCookies-          return $ flip map cookies $ \Cookie{..} -> (toString cookieName,toString cookieValue) --- Make a new session.-newSession :: ServerState -> (URI,[(String, String)]) -> Integer -> IO Session-newSession server info token = do-  signals <- newChan-  instructions <- newChan-  (event, handler) <- newEventsTagged-  ids <- newMVar [0..]-  mutex <- newMVar ()-  now <- getCurrentTime-  conState <- newMVar (Disconnected now)-  threadId <- myThreadId-  closures <- newMVar [0..]-  return $ Session-    { sSignals = signals-    , sInstructions = instructions-    , sEvent        = event-    , sEventHandler = handler-    , sElementIds = ids-    , sToken = token-    , sMutex = mutex-    , sConnectedState = conState-    , sThreadId    = threadId-    , sClosures    = closures-    , sStartInfo   = info-    , sServerState = server-    }-  --- Respond to poll requests.-poll :: Session -> Snap ()-poll Session{..} = do-  let setDisconnected = do-        now <- getCurrentTime-        modifyMVar_ sConnectedState (const (return (Disconnected now)))-  io $ modifyMVar_ sConnectedState (const (return Connected))-  threadId <- io $ myThreadId-  _ <- io $ forkIO $ do-    delaySeconds $ 60 * 5 -- Force kill after 5 minutes.-    killThread threadId-  instructions <- io $ E.catch (readAvailableChan sInstructions) $ \e -> do-    when (e == E.ThreadKilled) $ do-      setDisconnected-    E.throw e-  writeJson instructions---- Write JSON to output.-writeJson :: (MonadSnap m, JSON a) => a -> m ()-writeJson json = do-    modifyResponse $ setContentType "application/json"-    (writeString . (\x -> showJSValue x "") . showJSON) json---- Write a string to output.-writeString :: (MonadSnap m) => String -> m ()-writeString = writeText . pack---- Handle signals sent from the client.-signal :: Session -> Snap ()-signal Session{..} = do-    input <- getInput "signal"-    case input of-      Just signalJson -> do-        let signal = decode signalJson-        case signal of-          Ok signal -> io $ writeChan sSignals signal-          Error err -> error err-      Nothing -> error $ "Unable to parse " ++ show input---- Get a text input from snap.-getInput :: (MonadSnap f) => ByteString -> f (Maybe String)-getInput = fmap (fmap (unpack . decodeUtf8)) . getParam---- Run a snap action with the given session.-withSession :: ServerState -> (Session -> Snap a) -> Snap a-withSession server cont = do-  token <- readInput "token"-  case token of-    Nothing    -> error $ "Invalid session token format."-    Just token -> withGivenSession token server cont-    --- Do something with the session given by its token id.-withGivenSession :: Integer -> ServerState -> (Session -> Snap a) -> Snap a-withGivenSession token ServerState{..} cont = do-  sessions <- io $ withMVar sSessions return-  case M.lookup token sessions of-    Nothing -> error $ "Nonexistant token: " ++ show token-    Just session -> cont session---- Read an input from snap.-readInput :: (MonadSnap f,Read a) => ByteString -> f (Maybe a)-readInput = fmap (>>= readMay) . getInput-- {-----------------------------------------------------------------------------     Event handling ------------------------------------------------------------------------------}@@ -329,42 +460,71 @@  -- | Handle events signalled from the client. handleEvents :: Window -> IO ()-handleEvents window = forever $ handleEvent window---- | Handle one event.-handleEvent :: Window -> IO ()-handleEvent window@(Session{..}) = do+handleEvents window@(Session{..}) = do     signal <- getSignal window     case signal of         Threepenny.Event (elid,eventType,params) -> do-            sEventHandler ((elid,eventType), EventData params)-        _ -> return ()+            handleEvent1 window ((elid,eventType),EventData params)+            handleEvents window+        Quit () -> do+            snd sEventQuit ()+            -- do not continue handling events+        _       -> do+            handleEvents window +-- | Add a new event handler for a given key+addEventHandler :: Window -> (EventKey, Handler EventData) -> IO () +addEventHandler Session{..} (key,handler) =+    modifyMVar_ sEventHandlers $ return .+        M.insertWith (\h1 h a -> h1 a >> h a) key handler        +++-- | Handle a single event+handleEvent1 :: Window -> (EventKey,EventData) -> IO ()+handleEvent1 Session{..} (key,params) = do+    handlers <- readMVar sEventHandlers+    case M.lookup key handlers of+        Just handler -> handler params+        Nothing      -> return ()+ -- Get the latest signal sent from the client. getSignal :: Window -> IO Signal getSignal (Session{..}) = readChan sSignals --- | Return an 'Event' associated to an 'Element'.+-- | Bind an event handler for a dom event to an 'Element'. bind     :: String               -- ^ The eventType, see any DOM documentation for a list of these.     -> Element              -- ^ The element to bind to.-    -> Event EventData      -- ^ The event handler.-bind eventType (Element el@(ElementId elid) session) =-    Control.Event.Event register-    where-    key        = (elid, eventType)-    register h = do-        -- register with server-        unregister <- Control.Event.register (sEvent session key) h-        -- register with client+    -> Handler EventData    -- ^ The event handler to bind.+    -> IO ()+bind eventType (Element el@(ElementId elid) session) handler = do+    let key = (elid, eventType)+    -- register with client if it has never been registered on the server+    handlers <- readMVar $ sEventHandlers session+    when (not $ key `M.member` handlers) $         run session $ Bind eventType el (Closure key)-        return unregister+    -- register with server+    addEventHandler session (key, handler) +-- | Register event handler that occurs when the client has disconnected.+disconnect :: Window -> Event ()+disconnect = fst . sEventQuit+++initializeElementEvents :: Window -> IO ()+initializeElementEvents session@(Session{..}) = do+        initEvents =<< getHead session+        initEvents =<< getBody session+    where+    initEvents el@(Element elid _) = do+        x <- newEventsNamed $ \(name,_,handler) -> bind name el handler+        modifyMVar_ sElementEvents $ return . M.insert elid x+ -- Make a uniquely numbered event handler. newClosure :: Window -> String -> String -> ([Maybe String] -> IO ()) -> IO Closure-newClosure window@(Session{..}) eventType elid thunk = do+newClosure window eventType elid thunk = do     let key = (elid, eventType)-    _ <- register (sEvent key) $ \(EventData xs) -> thunk xs+    addEventHandler window (key, \(EventData xs) -> thunk xs)     return (Closure key)  {-----------------------------------------------------------------------------@@ -437,7 +597,6 @@            -> String      -- ^ The tag name.            -> IO Element  -- ^ A tag reference. Non-blocking. newElement session@(Session{..}) tagName = do-    -- TODO: Remove the need to specify in which browser window is to be created     elid <- modifyMVar sElementIds $ \elids ->         return (tail elids,"*" ++ show (head elids) ++ ":" ++ tagName)     return (Element (ElementId elid) session)@@ -452,15 +611,7 @@     --       Implement transfer of elements across browser windows     run session $ Append parent child - {------------------------------------------------------------------------------    Oddball-------------------------------------------------------------------------------}--- | Invoke the JavaScript expression @audioElement.play();@.-audioPlay :: Element -> IO ()-audioPlay (Element el session) = runFunction session $ ffi "%1.play();" el--{-----------------------------------------------------------------------------     Querying the DOM ------------------------------------------------------------------------------} -- $querying@@ -490,6 +641,17 @@       Elements els -> return $ Just [Element el window | el <- els]       _            -> return Nothing +-- | Get a list of elements by particular class.  Blocks.+getElementsByClassName+    :: Window        -- ^ Browser window+    -> String        -- ^ The class string.+    -> IO [Element]  -- ^ Elements with given class.+getElementsByClassName window cls =+  call window (GetElementsByClassName cls) $ \signal ->+    case signal of+      Elements els -> return $ Just [Element el window | el <- els]+      _            -> return Nothing+ -- | Get the value of an input. Blocks. getValue     :: Element   -- ^ The element to get the value of.@@ -538,36 +700,6 @@     -> IO (Maybe [a]) -- ^ Maybe the read values. All or none. readValuesList = liftM (sequence . map readMay) . getValuesList --- | Atomically execute the given computation in the context of a browser window-atomic :: Window -> IO a -> IO a-atomic window@(Session{..}) m = do-  takeMVar sMutex-  ret <- m-  putMVar sMutex ()-  return ret----- Send an instruction and read the signal response.-call :: Session -> Instruction -> (Signal -> IO (Maybe a)) -> IO a-call session@(Session{..}) instruction withSignal = do-  takeMVar sMutex-  run session $ instruction-  newChan <- dupChan sSignals-  go sMutex newChan--  where-    go mutex newChan = do-      signal <- readChan newChan-      result <- withSignal signal-      case result of-        Just signal -> do putMVar mutex ()-                          return signal-        Nothing     -> go mutex newChan---- Run the given instruction.-run :: Session -> Instruction -> IO ()-run (Session{..}) i = writeChan sInstructions i- -- | Get the head of the page. getHead :: Window -> IO Element getHead session = return $ Element (ElementId "head") session@@ -597,34 +729,4 @@  -- | Clear the client's DOM. clear :: Window -> IO ()-clear window = run window $ Clear ()---- | Run the given JavaScript function and carry on. Doesn't block.------ The client window uses JavaScript's @eval()@ function to run the code.-runFunction :: Window -> JSFunction () -> IO ()-runFunction session = run session . RunJSFunction . unJSCode . code---- | Run the given JavaScript function and wait for results. Blocks.------ The client window uses JavaScript's @eval()@ function to run the code.-callFunction :: Window -> JSFunction a -> IO a-callFunction window (JSFunction code marshal) = -    call window (CallJSFunction . unJSCode $ code) $ \signal ->-        case signal of-            FunctionResult v -> case marshal window v of-                Ok    a -> return $ Just a-                Error _ -> return Nothing-            _ -> return Nothing---- | Call the given function with the given continuation. Doesn't block.-callDeferredFunction-  :: Window                    -- ^ Browser window-  -> String                    -- ^ The function name.-  -> [String]                  -- ^ Parameters.-  -> ([Maybe String] -> IO ()) -- ^ The continuation to call if/when the function completes.-  -> IO ()-callDeferredFunction session@(Session{..}) func params closure = do-  cid      <- modifyMVar sClosures (\(x:xs) -> return (xs,x))-  closure' <- newClosure session func (show cid) closure-  run session $ CallDeferredFunction (closure',func,params)+clear window = runFunction window $ ffi "$('body').contents().detach()"
+ src/Graphics/UI/Threepenny/Internal/Include.hs view
@@ -0,0 +1,21 @@+{-# LANGUAGE TemplateHaskell, CPP #-}+module Graphics.UI.Threepenny.Internal.Include (include) where+ +import Data.Functor+import qualified Language.Haskell.TH as TH+import Language.Haskell.TH.Quote++#ifdef CABAL+root = "src/"+#else+root = ""+#endif++include = QuasiQuoter+        { quoteExp  = f             -- only used as an expression,+        , quotePat  = undefined     -- hence all other use cases undefined+        , quoteType = undefined+        , quoteDec  = undefined+        }+    where+    f s = TH.LitE . TH.StringL <$> TH.runIO (readFile $ root ++ s)
src/Graphics/UI/Threepenny/Internal/Resources.hs view
@@ -3,6 +3,29 @@  import           Data.Text                               (Text) import qualified Data.Text                               as Text+import           Graphics.UI.Threepenny.Internal.Include+++jsDriverCode    :: Text+cssDriverCode   :: Text+defaultHtmlFile :: Text+++jsDriverCode = Text.unlines $ map Text.pack+    [ [include|Graphics/UI/jquery.js|]+    , [include|Graphics/UI/jquery-cookie.js|]+    , [include|Graphics/UI/driver.js|]+    ]++cssDriverCode = Text.pack [include|Graphics/UI/driver.css|]++defaultHtmlFile = Text.pack [include|Graphics/UI/index.html|]+++{-+-- Disabled code:+-- Do not embed files with executables.+ import qualified Data.Text.IO                            as Text import           System.FilePath import           System.IO.Unsafe@@ -16,11 +39,6 @@ #endif  -jsDriverCode    :: Text-cssDriverCode   :: Text-defaultHtmlFile :: Text-- jsDriverCode = unsafePerformIO $     readFiles $ words "jquery.js jquery-cookie.js driver.js"  @@ -35,20 +53,5 @@     return $ Text.unlines ys  getDataFile x = unsafePerformIO $ fmap (</> x) getDataDir---{---- Temporarily disabled code.--- Use quasiquotation to bundle resource files with executable.--jsDriverCode = Text.unlines $ map Text.pack-    [ [include|Graphics/UI/jquery.js|]-    , [include|Graphics/UI/jquery-cookie.js|]-    , [include|Graphics/UI/driver.js|]-    ]--cssDriverCode = Text.pack [include|Graphics/UI/driver.css|]--defaultHtmlFile = Text.pack [include|Graphics/UI/index.html|]  -}
src/Graphics/UI/Threepenny/Internal/Types.hs view
@@ -7,14 +7,15 @@  import Control.Applicative import Control.Concurrent-import qualified Control.Event as E-import Control.Monad.Reader-import Data.ByteString               (ByteString)-import Data.Map             (Map)+import qualified Reactive.Threepenny as E+import Data.ByteString               (ByteString, hPut)+import Data.Map                      (Map)+import Data.String                   (fromString) import Data.Time  import Network.URI import Text.JSON.Generic+import System.IO (stderr)  {-----------------------------------------------------------------------------     Public types@@ -31,7 +32,7 @@  -- | An opaque reference to an element in the DOM. data ElementId = ElementId String-  deriving (Data,Typeable,Show)+  deriving (Data,Typeable,Show,Eq,Ord)  instance JSON ElementId where   showJSON (ElementId o) = showJSON o@@ -39,34 +40,37 @@     obj <- readJSON obj     ElementId <$> valFromObj "Element" obj -  + -- | A client session. This type is opaque, you don't need to inspect it. data Session = Session   { sSignals        :: Chan Signal   , sInstructions   :: Chan Instruction-  , sEvent          :: EventKey -> E.Event EventData-  , sEventHandler   :: E.Handler (EventKey, EventData)+  , sMutex          :: MVar ()+  , sEventHandlers  :: MVar (Map EventKey (E.Handler EventData))+  , sElementEvents  :: MVar (Map ElementId ElementEvents)+  , sEventQuit      :: (E.Event (), E.Handler ())   , sClosures       :: MVar [Integer]   , sElementIds     :: MVar [Integer]   , sToken          :: Integer-  , sMutex          :: MVar ()   , sConnectedState :: MVar ConnectedState   , sThreadId       :: ThreadId   , sStartInfo      :: (URI,[(String,String)])   , sServerState    :: ServerState   } -type Sessions  = Map Integer Session-type MimeType  = ByteString-type Filepaths = (Integer, Map ByteString (FilePath, MimeType))+type Sessions      = Map Integer Session+type MimeType      = ByteString+type Filepaths     = (Integer, Map ByteString (FilePath, MimeType)) +type EventKey      = (String, String)+type ElementEvents = String -> E.Event EventData+ data ServerState = ServerState     { sSessions :: MVar Sessions     , sFiles    :: MVar Filepaths     , sDirs     :: MVar Filepaths     } -type EventKey = (String, String)  -- | The client browser window. type Window = Session@@ -83,12 +87,24 @@  -- | Record for configuring the Threepenny GUI server. data Config = Config-  { tpPort       :: Int               -- ^ Port number.-  , tpCustomHTML :: Maybe FilePath    -- ^ Custom HTML file to replace the default one.-  , tpStatic     :: FilePath          -- ^ Directory that is served under @/static@.+  { tpPort       :: Int                 -- ^ Port number.+  , tpCustomHTML :: Maybe FilePath      -- ^ Custom HTML file to replace the default one.+  , tpStatic     :: Maybe FilePath      -- ^ Directory that is served under @/static@.+  , tpLog        :: ByteString -> IO () -- ^ Print a single log message.   } +-- | Default configuration.+--+-- Port 10000, no custom HTML, no static directory, logging to stderr.+defaultConfig :: Config+defaultConfig = Config+    { tpPort       = 10000+    , tpCustomHTML = Nothing+    , tpStatic     = Nothing+    , tpLog        = \s -> hPut stderr s >> hPut stderr (fromString "\n")+    } + {-----------------------------------------------------------------------------     Communication between client and server ------------------------------------------------------------------------------}@@ -96,10 +112,8 @@ -- | An instruction that is sent to the client as JSON. data Instruction   = Debug String-  | Begin ()-  | End ()   | SetToken Integer-  | Clear ()+  | GetElementsByClassName String   | GetElementsById [String]   | GetElementsByTagName String   | SetStyle ElementId [(String,String)]@@ -111,7 +125,6 @@   | GetValue ElementId   | GetValues [ElementId]   | SetTitle String-  | GetLocation ()   | RunJSFunction String   | CallJSFunction String   | CallDeferredFunction (Closure,String,[String])@@ -125,34 +138,31 @@  -- | A signal (mostly events) that are sent from the client to the server. data Signal-  = Init ()+  = Quit ()   | Elements [ElementId]   | Event (String,String,[Maybe String])   | Value String   | Values [String]-  | Location String   | FunctionCallValues [Maybe String]   | FunctionResult JSValue-  deriving (Show)+  deriving (Typeable,Show)  instance JSON Signal where   showJSON _ = error "JSON.Signal.showJSON: No method implemented."   readJSON obj = do     obj <- readJSON obj-    let init = Init <$> valFromObj "Init" obj+    let quit     = Quit <$> valFromObj "Quit" obj         elements = Elements <$> valFromObj "Elements" obj         event = do           (cid,typ,arguments) <- valFromObj "Event" obj           args <- mapM nullable arguments           return $ Event (cid,typ,args)         value = Value <$> valFromObj "Value" obj-        location = Location <$> valFromObj "Location" obj         values = Values <$> valFromObj "Values" obj         fcallvalues = do           FunctionCallValues <$> (valFromObj "FunctionCallValues" obj >>= mapM nullable)         fresult = FunctionResult <$> valFromObj "FunctionResult" obj-    init <|> elements <|> event <|> value <|> values <|> location-         <|> fcallvalues <|> fresult+    quit <|> elements <|> event <|> value <|> values <|> fcallvalues <|> fresult  -- | Read a JSValue that may be null. nullable :: JSON a => JSValue -> Result (Maybe a)@@ -161,7 +171,7 @@  -- | An opaque reference to a closure that the event manager uses to --   trigger events signalled by the client.-data Closure = Closure (String,String)+data Closure = Closure EventKey     deriving (Typeable,Data,Show)  {-----------------------------------------------------------------------------
src/Graphics/UI/Threepenny/JQuery.hs view
@@ -1,7 +1,6 @@ {-# OPTIONS -fno-warn-wrong-do-bind #-} module Graphics.UI.Threepenny.JQuery where -import Control.Event import Control.Arrow import Data.Char import Data.Default@@ -10,6 +9,7 @@ import qualified Graphics.UI.Threepenny.Internal.Core as Core import qualified Graphics.UI.Threepenny.Internal.Types as Core import Text.JSON+import Reactive.Threepenny  data Easing = Swing | Linear   deriving (Eq,Enum,Show)
src/Graphics/UI/Threepenny/Timer.hs view
@@ -1,55 +1,63 @@-{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE RecordWildCards, DeriveDataTypeable #-}  module Graphics.UI.Threepenny.Timer (     -- * Synopsis     -- | Implementation of a simple timer which runs on the server-side.+    --+    -- NOTE: The timer may be rather wobbly unless you compile+    -- with the @-threaded@ option.          -- * Documentation     Timer, timer,     interval, running, tick, start, stop,     ) where -+import Data.Typeable import Control.Monad (when, forever, void)-import Control.Event import Control.Concurrent import Control.Concurrent.STM+import Reactive.Threepenny  import Graphics.UI.Threepenny.Core   data Timer = Timer-    { tRunning  :: TVar Bool-    , tInterval :: TVar Int      -- in ms+    { tRunning  :: GetSet Bool Bool+    , tInterval :: GetSet Int Int   -- in ms     , tTick     :: Event ()-    }+    } deriving (Typeable)  -- | Create a new timer timer :: IO Timer timer = do-    tRunning      <- newTVarIO False-    tInterval     <- newTVarIO 1000+    tvRunning     <- newTVarIO False+    tvInterval    <- newTVarIO 1000     (tTick, fire) <- newEvent          forkIO $ forever $ do-        wait <- atomically $ do-            b <- readTVar tRunning+        atomically $ do+            b <- readTVar tvRunning             when (not b) retry-            readTVar tInterval-        threadDelay (wait * 1000)+        wait <- atomically $ readTVar tvInterval         fire ()-        +        threadDelay (wait * 1000)+    +    let tRunning  = fromTVar tvRunning+        tInterval = fromTVar tvInterval +         return $ Timer {..} +-- | Timer event.+tick :: Timer -> Event () tick = tTick  -- | Timer interval in milliseconds. interval :: Attr Timer Int-interval = fromTVar tInterval+interval = fromGetSet tInterval  -- | Whether the timer is running or not. running :: Attr Timer Bool-running = fromTVar tRunning+running = fromGetSet tRunning  -- | Start the timer. start :: Timer -> IO ()@@ -59,10 +67,13 @@ stop :: Timer -> IO () stop = set' running False -fromTVar :: (x -> TVar a) -> Attr x a-fromTVar f = mkReadWriteAttr-    (atomically . readTVar . f)-    (\i x -> atomically $ writeTVar (f x) i)+fromTVar :: TVar a -> GetSet a a+fromTVar var = (atomically $ readTVar var, atomically . writeTVar var)++type GetSet i o = (IO o, i -> IO ())++fromGetSet :: (x -> GetSet i o) -> ReadWriteAttr x i o+fromGetSet f = mkReadWriteAttr (fst . f) (\i x -> snd (f x) i)   {-----------------------------------------------------------------------------
src/Graphics/UI/driver.js view
@@ -46,20 +46,34 @@   document.head = document.head || document.getElementsByTagName('head')[0];    ////////////////////////////////////////////////////////////////////////////////+  // Logging+  window.do_logging = function(x){+    $.cookie('tp_log',x.toString());+  };++  function console_log(){+    if (tp_enable_log) { window.console.log.apply(window.console,arguments); }+  }++  ////////////////////////////////////////////////////////////////////////////////   // Main entry point   $(document).ready(function(){-    setTimeout(function(){-      waitForEvents();-    })+    // initCommunicationHTTP();+    initCommunicationWebSockets();   });    ////////////////////////////////////////////////////////////////////////////////-  // Running instructions--  window.do_logging = function(x){-    $.cookie('tp_log',x.toString());-  };-    +  // Client-server communication+  // - GET and POST requests+  +  // Initialize communication via HTTP requests.+  function initCommunicationHTTP() {+    setTimeout(function(){+      waitForEvents();+    })+  }+  +  // Poll instruction from the server.   function waitForEvents(){     console_log("Polling… (%d signals so far)",signal_count);     var data = { token: sessionToken };@@ -79,8 +93,8 @@           console_log('Event list:');           runMultipleEvents(events);         } else {-          runEvent(events,function(){-            waitForEvents();+          runEvent(events, signalEvent, function(response){+            maybeReply(response, waitForEvents);           });         }       },@@ -97,12 +111,94 @@     if(events.length == 0) {       return waitForEvents();     }-    runEvent(events.shift(),function(){-      runMultipleEvents(events);+    runEvent(events.shift(), signalEvent, function(response){+      maybeReply(response, function(){+        runMultipleEvents(events);+      });     });   }+  +  // Send an event to the server.+  function signalEvent(value) {+    signal({ Event: value}, function (){});+  }+  +  // Send a reply to the server if necessary.+  function maybeReply(response, continuation) {+    if (response != undefined) { signal(response, continuation); }+    else { continuation(); }+  }+  +  // Send response back to the server.+  function signal(signal,continuation){+    signal_count++;+    console_log('Signal: %s',JSON.stringify(signal));+    $.ajax({+      dataType: 'json',+      url:'signal',+      data: { token: sessionToken, signal: JSON.stringify(signal) },+      success: function(){+        continuation();+      },+      error: function(reply){+        console_log("Error: %o",reply);+      }+    });+  }+  +  ////////////////////////////////////////////////////////////////////////////////+  // Client-server communication+  // - WebSockets+  +  // Initialize client-server communication via WebSockets.+  function initCommunicationWebSockets() {+    var url = 'ws:' + window.location.href.toString().slice(5) + 'websocket';+    var ws  = new WebSocket(url);+    +    $(window).unload( function () {+      // Make sure that the WebSocket is closed when the browser window is closed.+      ws.close();+    });+    +    var sendEvent = function (e) {+      ws.send(JSON.stringify({ Event : e}));+    }+    var reply     = function (response) {+      if (response != undefined)+        ws.send(JSON.stringify(response));+    }+    // Send ping message in regular intervals.+    // We expect pong messages in return to keep the connection alive.+    function ping(){+      ws.send("ping");+      window.setTimeout(ping,2000);+    }+    +    ws.onopen = function (e){+      ping();+      ws.onmessage = function (msg) {+        // console_log("WebSocket message: %o",msg);+        if (msg.data != "pong")+          { runEvent(JSON.parse(msg.data), sendEvent, reply); }+      }+      ws.onclose = function (e) {+        console_log("WebSocket closed: %o", e);+      }+      ws.onerror = function (e) {+        console_log("WebSocket error: %o", e);+      }+    }+  } -  function runEvent(event,continuation){++  ////////////////////////////////////////////////////////////////////////////////+  // FFI - Execute and reply to commands from the server+  +  function runEvent(event,sendEvent,reply){+    // reply();      -- Continue without replying to the server.+    // reply(value); -- Send  value  back to the server.+    // sendEvent     -- Function that sends a message { Event : value } to the server.+       console_log("Event: %s",JSON.stringify(event));     for(var key in event){       switch(key){@@ -114,7 +210,7 @@         // It is not correct to remove the child elements from the el_table         // because they may still be present on the server side.         $(el).contents().detach();-        continuation();+        reply();         break;       }       case "CallDeferredFunction": {@@ -125,39 +221,30 @@         theFunction.apply(window, params.concat(function(){           console_log(this);           var args = Array.prototype.slice.call(arguments,0);-          signal({-            Event: closure.concat([args])-          },function(){-            // No action.-          });+          sendEvent(closure.concat([args]));         }));-        continuation();+        reply();         break;       }       case "RunJSFunction": {         eval(event.RunJSFunction);-        continuation();+        reply();         break;       }       case "CallJSFunction": {         var result = eval(event.CallJSFunction);-        signal({FunctionResult : result}, continuation);+        reply({FunctionResult : result});         break;       }       case "Delete": {         event_delete(event);-        continuation();+        reply();         break;       }       case "Debug": {         if(window.console)           console.log("Server debug: %o",event.Debug);-        continuation();-        break;-      }-      case "Clear": {-        $('body').contents().detach();-        continuation();+        reply();         break;       }       case "GetElementsByTagName": {@@ -169,11 +256,7 @@             Element: elementToElid(elements[i])           });         }-        signal({-          Elements: els-        },function(){-          continuation();-        });+        reply({ Elements: els });         break;       }       case "GetElementsById": {@@ -188,13 +271,21 @@                 });             }         }-        signal({-          Elements: els-        },function(){-          continuation();-        });+        reply({ Elements: els });         break;       }+      case "GetElementsByClassName": {+        var elements = document.getElementsByClassName(event.GetElementsByClassName);+        var els = [];+        var len = elements.length;+        for(var i = 0; i < len; i++) {+          els.push({+            Element: elementToElid(elements[i])+          });+        }+        reply({ Elements: els });+        break;+      }       case "SetStyle": {         var set = event.SetStyle;         var id = set[0];@@ -204,7 +295,7 @@         for(var i = 0; i < len; i++){           el.style[style[i][0]] = style[i][1];         }-        continuation();+        reply();         break;       }       case "SetAttr": {@@ -214,26 +305,14 @@         var value = set[2];         var el    = elidToElement(id);         $(el).attr(key,value);-        continuation();+        reply();         break;       }       case "GetValue": {         var id = event.GetValue;         var el = elidToElement(id);         var value = $(el).val();-        signal({-          Value: value-        },function(){-          continuation();-        });-        break;-      }-      case "GetLocation": {-        signal({-          Location: window.location.href-        },function(){-          continuation();-        });+        reply({ Value: value });         break;       }       case "GetValues": {@@ -243,34 +322,30 @@         for(var i = 0; i < len; i++) {           values.push($(elidToElement(ids[i])).val());         }-        signal({-          Values: values-        },function(){-          continuation();-        });+        reply({ Values: values });         break;       }       case "Append": {         var append = event.Append;         $(elidToElement(append[0])).append($(elidToElement(append[1])));-        continuation();+        reply();         break;       }       case "SetText": {         var set = event.SetText;         $(elidToElement(set[0])).text(set[1]);-        continuation();+        reply();         break;       }       case "SetTitle": {         document.title = event.SetTitle;-        continuation();+        reply();         break;       }       case "SetHtml": {         var set = event.SetHtml;         $(elidToElement(set[0])).html(set[1]);-        continuation();+        reply();         break;       }       case "Bind": {@@ -281,93 +356,48 @@         console_log('event type: ' + eventType);         if(eventType == 'livechange') {           $(el).livechange(300,function(e){-            signal({-              Event: handlerGuid.concat([[$(el).val()]])-            },function(){-              // no action-            });+            sendEvent( handlerGuid.concat([[$(el).val()]]) );             return true;           });         } else if(eventType == 'sendvalue') {           $(el).sendvalue(function(x){-            signal({-              Event: handlerGuid.concat([[x]])-            },function(){});+            sendEvent( handlerGuid.concat([[x]]) );           });         } else if(eventType.match('dragstart|dragenter|dragover|dragleave|drag|drop|dragend')) {           $(el).bind(eventType,function(e){-            signal({-              Event: handlerGuid.concat([+            sendEvent( handlerGuid.concat([                 e.originalEvent.dataTransfer                     ?[e.originalEvent.dataTransfer.getData("dragData")]                     :[]])-            },function(){-              // no action-            });+              );             return true;           });         } else if(eventType.match('mousemove')) {           $(el).bind(eventType,function(e){-            signal({-              Event: handlerGuid.concat([[e.pageX.toString(), e.pageY.toString()]])-            },function(){-              // no action-            });+            sendEvent( handlerGuid.concat([[e.pageX.toString(), e.pageY.toString()]]) );             return true;           });         } else if(eventType.match('keydown|keyup')) {           $(el).bind(eventType,function(e){-            signal({-              Event: handlerGuid.concat([[e.keyCode.toString()]])-            },function(){-              // no action-            });+            sendEvent( handlerGuid.concat([[e.keyCode.toString()]]) );             return true;           });         } else {           $(el).bind(eventType,function(e){-            signal({-              Event: handlerGuid.concat([e.which?[e.which.toString()]:[]])-            },function(){-              // no action-            });+            sendEvent( handlerGuid.concat([e.which?[e.which.toString()]:[]]) );             return true;           });         }-        continuation();+        reply();         break;       }-      default: continuation();+      default: reply();       }     }   } -  function event_delete(event){-    var id = event.Delete;-    var el = elidToElement(id);-    // TODO: Think whether it is correct to remove element ids-    $(el).detach();-    deleteElementTable(id);-  }-   ////////////////////////////////////////////////////////////////////////////////-  // Signalling events--  function signal(signal,continuation){-    signal_count++;-    console_log('Signal: %s',JSON.stringify(signal));-    $.ajax({-      dataType: 'json',-      url:'signal',-      data: { token: sessionToken, signal: JSON.stringify(signal) },-      success: function(){-        continuation();-      },-      error: function(reply){-        console_log("Error: %o",reply);-      }-    });-  }+  // FFI - marshaling    // When the server creates elements, it assigns them a string "elid".     // This elidToElement function is used to sync the elids on the server with the @@ -420,17 +450,17 @@     }   } -  // A log-  function console_log(){-    if (tp_enable_log) {-      window.console.log.apply(window.console,arguments);-    }-  };--   ////////////////////////////////////////////////////////////////////////////////-  // Additional functions+  // FFI - additional primitive functions   +  function event_delete(event){+    var id = event.Delete;+    var el = elidToElement(id);+    // TODO: Think whether it is correct to remove element ids+    $(el).detach();+    deleteElementTable(id);+  }+     window.jquery_animate = function(el_id,props,duration,easing,complete){     var el = elidToElement(JSON.parse(el_id));     $(el).animate(JSON.parse(props),@@ -442,6 +472,11 @@   window.jquery_scrollToBottom = function(el){     $(el).scrollTop(el.scrollHeight);   };++  function prim_audio_stop(audio){+    audio.pause();+    audio.currentTime = 0;+  }    // see http://stackoverflow.com/a/9722502/403805   CanvasRenderingContext2D.prototype.clear = 
src/MissingDollars.hs view
@@ -1,7 +1,6 @@ {-# LANGUAGE CPP, PackageImports #-}  import Control.Monad-import Control.Monad.Extra import Safe  #ifdef CABAL@@ -19,10 +18,9 @@ main :: IO () main = do     static <- getStaticDir-    startGUI Config+    startGUI defaultConfig         { tpPort       = 10000-        , tpCustomHTML = Nothing-        , tpStatic     = static+        , tpStatic     = Just static         } setup  
+ src/Reactive/Threepenny.hs view
@@ -0,0 +1,357 @@+{-# LANGUAGE RecursiveDo  #-}+module Reactive.Threepenny (+    -- * Synopsis+    -- | Functional reactive programming.+    +    -- * Types+    -- $intro+    Event, Behavior,+    +    -- * IO+    -- | Functions to connect events to the outside world.+    Handler, newEvent, register,+    currentValue,+    +    -- * Core Combinators+    -- | Minimal set of combinators for programming with 'Event' and 'Behavior'.+    module Control.Applicative,+    never, filterJust, unionWith,+    accumE, apply, stepper,+    -- $classes+    +    -- * Derived Combinators+    -- | Additional combinators that make programming+    -- with 'Event' and 'Behavior' convenient.+    -- ** Application+    (<@>), (<@),+    -- ** Filtering+    filterE, filterApply, whenE, split,+    -- ** Union+    unions, concatenate,+    -- ** Accumulation+    -- $accumulation+    accumB, mapAccum,++    -- * Additional Notes+    -- $recursion+    +    -- * Internal+    -- | Functions reserved for special circumstances.+    -- Do not use unless you know what you're doing.+    onChange, unsafeMapIO, newEventsNamed,+    ) where++import Control.Applicative+import Control.Monad (void)+import Data.IORef+import qualified Data.Map as Map++import           Reactive.Threepenny.Memo       as Memo+import qualified Reactive.Threepenny.PulseLatch as Prim++type Pulse = Prim.Pulse+type Latch = Prim.Latch+type Map   = Map.Map++{-----------------------------------------------------------------------------+    Types+------------------------------------------------------------------------------}+{- $intro++At its core, Functional Reactive Programming (FRP) is about two+data types 'Event' and 'Behavior' and the various ways to combine them.++-}++{-| @Event a@ represents a stream of events as they occur in time.+Semantically, you can think of @Event a@ as an infinite list of values+that are tagged with their corresponding time of occurence,++> type Event a = [(Time,a)]+-}+newtype Event    a = E { unE :: Memo (Pulse a) }++{-| @Behavior a@ represents a value that varies in time. Think of it as++> type Behavior a = Time -> a+-}+data    Behavior a = B { latch :: Latch a, changes :: Event () }++{-----------------------------------------------------------------------------+    IO+------------------------------------------------------------------------------}+-- | An /event handler/ is a function that takes an+-- /event value/ and performs some computation.+type Handler a = a -> IO ()++-- | Create a new event.+-- Also returns a function that triggers an event occurrence.+newEvent :: IO (Event a, Handler a)+newEvent = do+    (p, fire) <- Prim.newPulse+    return (E $ fromPure p, fire)+++-- | Create a series of events with delayed initialization.+--+-- For each name, the initialization handler will be called+-- exactly once when the event is first "brought to life",+-- e.g. when an event handler is registered to it.+newEventsNamed :: Ord name+    => Handler (name, Event a, Handler a)   -- ^ Initialization procedure.+    -> IO (name -> Event a)                 -- ^ Series of events.+newEventsNamed init = do+    eventsRef <- newIORef Map.empty+    return $ \name -> E $ memoize $ do+        events <- readIORef eventsRef+        case Map.lookup name events of+            Just p  -> return p+            Nothing -> do+                (p, fire) <- Prim.newPulse+                writeIORef eventsRef $ Map.insert name p events+                init (name, E $ fromPure p, fire)+                return p+++-- | Register an event 'Handler' for an 'Event'.+-- All registered handlers will be called whenever the event occurs.+-- +-- When registering an event handler, you will also be given an action+-- that unregisters this handler again.+-- +-- > do unregisterMyHandler <- register event myHandler+--+-- FIXME: Unregistering event handlers does not work yet.+register :: Event a -> Handler a -> IO (IO ())+register e h = do+    p <- at (unE e)     -- evaluate the memoized action+    Prim.addHandler p h+    return $ return ()  -- FIXME++-- | Register an event 'Handler' for a 'Behavior'.+-- All registered handlers will be called whenever the behavior changes.+-- +-- However, note that this is only an approximation,+-- as behaviors may change continuously.+-- Consequently, handlers should be idempotent.+onChange :: Behavior a -> Handler a -> IO ()+onChange (B l e) h = void $ do+    -- This works because latches are updated before the handlers are being called.+    register e (\_ -> h =<< Prim.readLatch l)++-- | Read the current value of a 'Behavior'.+currentValue :: Behavior a -> IO a+currentValue (B l _) = Prim.readLatch l+++{-----------------------------------------------------------------------------+    Core Combinators+------------------------------------------------------------------------------}+instance Functor Event where+    fmap f e = E $ liftMemo1 (Prim.mapP f) (unE e)++unsafeMapIO f e   = E $ liftMemo1 (Prim.unsafeMapIOP f) (unE e)++-- | Event that never occurs.+-- Think of it as @never = []@.+never :: Event a+never = E $ fromPure Prim.neverP++-- | Return all event occurrences that are 'Just' values, discard the rest.+-- Think of it as+-- +-- > filterJust es = [(time,a) | (time,Just a) <- es]+filterJust e = E $ liftMemo1 Prim.filterJustP (unE e)++-- | Merge two event streams of the same type.+-- In case of simultaneous occurrences, the event values are combined+-- with the binary function.+-- Think of it as+--+-- > unionWith f ((timex,x):xs) ((timey,y):ys)+-- >    | timex == timey = (timex,f x y) : unionWith f xs ys+-- >    | timex <  timey = (timex,x)     : unionWith f xs ((timey,y):ys)+-- >    | timex >  timey = (timey,y)     : unionWith f ((timex,x):xs) ys+unionWith :: (a -> a -> a) -> Event a -> Event a -> Event a+unionWith f e1 e2 = E $ liftMemo2 (Prim.unionWithP f) (unE e1) (unE e2)++-- | Apply a time-varying function to a stream of events.+-- Think of it as+-- +-- > apply bf ex = [(time, bf time x) | (time, x) <- ex]+apply :: Behavior (a -> b) -> Event a -> Event b+apply  f x        = E $ liftMemo1 (\p -> Prim.applyP (latch f) p) (unE x)++infixl 4 <@>, <@++-- | Infix synonym for 'apply', similar to '<*>'.+(<@>) :: Behavior (a -> b) -> Event a -> Event b+(<@>) = apply++-- | Variant of 'apply' similar to '<*'+(<@) :: Behavior a -> Event b -> Event a+b <@ e = (const <$> b) <@> e++-- | The 'accumB' function is similar to a /strict/ left fold, 'foldl''.+-- It starts with an initial value and combines it with incoming events.+-- For example, think+--+-- > accumB "x" [(time1,(++"y")),(time2,(++"z"))]+-- >    = stepper "x" [(time1,"xy"),(time2,"xyz")]+-- +-- Note that the value of the behavior changes \"slightly after\"+-- the events occur. This allows for recursive definitions.+accumB :: a -> Event (a -> a) -> IO (Behavior a)+accumB a e = do+    (l1,p1) <- Prim.accumL a =<< at (unE e)+    p2      <- Prim.mapP (const ()) p1+    return $ B l1 (E $ fromPure p2)+++-- | Construct a time-varying function from an initial value and +-- a stream of new values. Think of it as+--+-- > stepper x0 ex = return $ \time ->+-- >     last (x0 : [x | (timex,x) <- ex, timex < time])+-- +-- Note that the smaller-than-sign in the comparision @timex < time@ means +-- that the value of the behavior changes \"slightly after\"+-- the event occurrences. This allows for recursive definitions.+stepper :: a -> Event a -> IO (Behavior a)+stepper a e = accumB a (const <$> e)++-- | The 'accumE' function accumulates a stream of events.+-- Example:+--+-- > accumE "x" [(time1,(++"y")),(time2,(++"z"))]+-- >    = return [(time1,"xy"),(time2,"xyz")]+--+-- Note that the output events are simultaneous with the input events,+-- there is no \"delay\" like in the case of 'accumB'.+accumE :: a -> Event (a -> a) -> IO (Event a)+accumE a e = do+    p <- fmap snd . Prim.accumL a =<< at (unE e)+    return $ E $ fromPure p++instance Functor Behavior where+    fmap f ~(B l e) = B (Prim.mapL f l) e++instance Applicative Behavior where+    pure a  = B (Prim.pureL a) never+    ~(B lf ef) <*> ~(B lx ex) =+        B (Prim.applyL lf lx) (unionWith const ef ex)++{- $classes++/Further combinators that Haddock can't document properly./++> instance Applicative Behavior++'Behavior' is an applicative functor. In particular, we have the following functions.++> pure :: a -> Behavior a++The constant time-varying value. Think of it as @pure x = \\time -> x@.++> (<*>) :: Behavior (a -> b) -> Behavior a -> Behavior b++Combine behaviors in applicative style.+Think of it as @bf \<*\> bx = \\time -> bf time $ bx time@.++-}++{- $recursion++Recursion in the 'IO' monad is possible, but somewhat limited.+The main rule is that the sequence of IO actions must be known+in advance, only the values may be recursive.++Good:++> mdo+>     let e2 = apply (const <$> b) e1   -- applying a behavior is not an IO action+>     b <- accumB $ (+1) <$ e2++Bad:+ +> mdo+>     b <- accumB $ (+1) <$ e2          -- actions executed here could depend ...+>     let e2 = apply (const <$> b) e1   -- ... on this value++-}++{-----------------------------------------------------------------------------+    Derived Combinators+------------------------------------------------------------------------------}+-- | Return all event occurrences that fulfill the predicate, discard the rest.+filterE :: (a -> Bool) -> Event a -> Event a+filterE p = filterJust . fmap (\a -> if p a then Just a else Nothing)++-- | Return all event occurrences that fulfill the time-varying predicate,+-- discard the rest. Generalization of 'filterE'.+filterApply :: Behavior (a -> Bool) -> Event a -> Event a+filterApply bp = fmap snd . filterE fst . apply ((\p a -> (p a,a)) <$> bp)++-- | Return event occurrences only when the behavior is 'True'.+-- Variant of 'filterApply'.+whenE :: Behavior Bool -> Event a -> Event a+whenE bf = filterApply (const <$> bf)++-- | Split event occurrences according to a tag.+-- The 'Left' values go into the left component while the 'Right' values+-- go into the right component of the result.+split :: Event (Either a b) -> (Event a, Event b)+split e = (filterJust $ fromLeft <$> e, filterJust $ fromRight <$> e)+    where+    fromLeft  (Left  a) = Just a+    fromLeft  (Right b) = Nothing+    fromRight (Left  a) = Nothing+    fromRight (Right b) = Just b++-- | Collect simultaneous event occurrences in a list.+unions :: [Event a] -> Event [a]+unions = foldr (unionWith (++)) never . map (fmap (:[]))++-- | Apply a list of functions in succession.+-- Useful in conjunction with 'unions'.+--+-- > concatenate [f,g,h] = f . g . h +concatenate :: [a -> a] -> (a -> a)+concatenate = foldr (.) id++{- $accumulation++Note: All accumulation functions are strict in the accumulated value!+acc -> (x,acc) is the order used by 'unfoldr' and 'State'.++-}++-- | Efficient combination of 'accumE' and 'accumB'.+mapAccum :: acc -> Event (acc -> (x,acc)) -> IO (Event x, Behavior acc)+mapAccum acc ef = do+    e <- accumE (undefined,acc) ((. snd) <$> ef)+    b <- stepper acc (snd <$> e)+    return (fst <$> e, b)++{-----------------------------------------------------------------------------+    Test+------------------------------------------------------------------------------}+test :: IO (Int -> IO ())+test = do+    (e1,fire) <- newEvent+    e2 <- accumE 0 $ (+) <$> e1+    _  <- register e2 print+    +    return fire+    +test_recursion1 :: IO (IO ())+test_recursion1 = mdo+    (e1, fire) <- newEvent+    let e2 :: Event Int+        e2 = apply (const <$> b) e1+    b  <- accumB 0 $ (+1) <$ e2+    _  <- register e2 print+    +    return $ fire ()++
+ src/Reactive/Threepenny/Memo.hs view
@@ -0,0 +1,43 @@+{-# LANGUAGE RecursiveDo #-}+module Reactive.Threepenny.Memo (+    Memo, fromPure, memoize, at, liftMemo1, liftMemo2,+    ) where++import Control.Monad+import Data.Functor+import Data.IORef+import System.IO.Unsafe++{-----------------------------------------------------------------------------+    Memoize time-varying values / computations+------------------------------------------------------------------------------}+data Memo a+    = Const a+    | Memoized (IORef (MemoD a))++type MemoD a = Either (IO a) a++fromPure = Const++at :: Memo a -> IO a+at (Const a)    = return a+at (Memoized r) = do+    memo <- readIORef r+    case memo of+        Right a -> return a+        Left ma -> mdo+            writeIORef r $ Right a+            a <- ma    -- allow some recursion+            return a++memoize :: IO a -> Memo a+memoize m = unsafePerformIO $ Memoized <$> newIORef (Left m)++liftMemo1 :: (a -> IO b) -> Memo a -> Memo b+liftMemo1 f ma = memoize $ f =<< at ma++liftMemo2 :: (a -> b -> IO c) -> Memo a -> Memo b -> Memo c+liftMemo2 f ma mb = memoize $ do+    a <- at ma+    b <- at mb+    f a b
+ src/Reactive/Threepenny/PulseLatch.hs view
@@ -0,0 +1,233 @@+{-# LANGUAGE RecordWildCards, RecursiveDo #-}+module Reactive.Threepenny.PulseLatch (+    Pulse, newPulse, addHandler,+    neverP, mapP, filterJustP, unionWithP, unsafeMapIOP,+    +    Latch,+    pureL, mapL, applyL, accumL, applyP,+    readLatch,+    ) where+++import Control.Applicative+import Control.Monad+import Control.Monad.Trans.Class+import Control.Monad.Trans.RWS     as Monad++import Data.Hashable+import Data.IORef+import Data.Monoid (Endo(..))++import Data.Unique.Really+import qualified Data.Vault.Lazy   as Vault+import qualified Data.HashMap.Lazy as Map++type Vault = Vault.Vault+type Map   = Map.HashMap+++type Build = IO++{-----------------------------------------------------------------------------+    Pulse+------------------------------------------------------------------------------}+type EvalP = Monad.RWST () () Vault IO++runEvalP :: Vault -> EvalP a -> IO (a, Vault)+runEvalP pulses m = do+    (a, s, w) <- Monad.runRWST m () pulses+    return (a, s)+++type Handler  = EvalP (IO ())+data Priority = DoLatch | DoIO deriving (Eq,Show,Ord,Enum)++instance Hashable Priority where hashWithSalt _ = fromEnum++data Pulse a = Pulse+    { addHandlerP :: ((Unique, Priority), Handler) -> Build ()+    , evalP       :: EvalP (Maybe a)+    }++-- Turn evaluation action into pulse that caches the value.+cacheEval :: EvalP (Maybe a) -> Build (Pulse a)+cacheEval e = do+    key <- Vault.newKey+    return $ Pulse+        { addHandlerP = \_ -> return ()+        , evalP = do+            vault <- Monad.get+            case Vault.lookup key vault of+                Just a  -> return a+                Nothing -> do+                    a <- e+                    Monad.put $ Vault.insert key a vault+                    return a+        }++-- Add a dependency to a pulse, for the sake of keeping track of dependencies.+dependOn :: Pulse a -> Pulse b -> Pulse a+dependOn p q = p { addHandlerP = \h -> addHandlerP p h >> addHandlerP q h }++-- Execute an action when the pulse occurs+whenPulse :: Pulse a -> (a -> IO ()) -> Handler+whenPulse p f = do+    ma <- evalP p+    case ma of+        Just a  -> return (f a)+        Nothing -> return $ return ()++{-----------------------------------------------------------------------------+    Latch+------------------------------------------------------------------------------}+data Latch a = Latch { readL :: IO a }++{-----------------------------------------------------------------------------+    Interface to the outside world.+------------------------------------------------------------------------------}+-- | Create a new pulse and a function to trigger it.+newPulse :: Build (Pulse a, a -> IO ())+newPulse = do+    key         <- Vault.newKey+    handlersRef <- newIORef Map.empty      -- map of handlers+    +    let+        -- add handler to map+        addHandlerP :: ((Unique, Priority), Handler) -> Build ()+        addHandlerP (uid,m) = do+            handlers <- readIORef handlersRef+            case Map.lookup uid handlers of+                Just _  -> return ()+                Nothing -> writeIORef handlersRef $ Map.insert uid m handlers+        +        -- evaluate all handlers attached to this input pulse+        fireP a = do+            let pulses = Vault.insert key (Just a) $ Vault.empty+            handlers <- readIORef handlersRef+            (ms, _)  <- runEvalP pulses $ sequence $ +                   [m | ((_,DoLatch),m) <- Map.toList handlers]+                ++ [m | ((_,DoIO   ),m) <- Map.toList handlers]  +            sequence_ ms+        +        evalP = join . Vault.lookup key <$> Monad.get++    return (Pulse {..}, fireP)++-- | Register a handler to be executed whenever a pulse occurs.+--+-- FIXME: Cannot unregister a handler again.+addHandler :: Pulse a -> (a -> IO ()) -> Build ()+addHandler p f = do+    uid <- newUnique+    addHandlerP p ((uid, DoIO), whenPulse p f)++-- | Read the value of a 'Latch' at a particular moment in Build.+readLatch :: Latch a -> Build a+readLatch = readL++{-----------------------------------------------------------------------------+    Pulse and Latch+    Public API+------------------------------------------------------------------------------}+-- | Create a new pulse that never occurs.+neverP :: Pulse a+neverP = Pulse+    { addHandlerP = const $ return ()+    , evalP       = return Nothing+    }++-- | Map a function over pulses.+mapP :: (a -> b) -> Pulse a -> Build (Pulse b)+mapP f p = (`dependOn` p) <$> cacheEval (return . fmap f =<< evalP p)++-- | Map an IO function over pulses. Is only executed once.+unsafeMapIOP :: (a -> IO b) -> Pulse a -> Build (Pulse b)+unsafeMapIOP f p = (`dependOn` p) <$> cacheEval (traverse . fmap f =<< evalP p)+    where+    traverse :: Maybe (IO a) -> EvalP (Maybe a)+    traverse Nothing  = return Nothing+    traverse (Just m) = Just <$> lift m++-- | Filter occurrences. Only keep those of the form 'Just'.+filterJustP :: Pulse (Maybe a) -> Build (Pulse a)+filterJustP p = (`dependOn` p) <$> cacheEval (return . join =<< evalP p)++-- | Pulse that occurs when either of the pulses occur.+-- Combines values with the indicated function when both occur.+unionWithP :: (a -> a -> a) -> Pulse a -> Pulse a -> Build (Pulse a)+unionWithP f p q = (`dependOn` q) . (`dependOn` p) <$> cacheEval eval+    where+    eval = do+        x <- evalP p+        y <- evalP q+        return $ case (x,y) of+            (Nothing, Nothing) -> Nothing+            (Just a , Nothing) -> Just a+            (Nothing, Just a ) -> Just a+            (Just a1, Just a2) -> Just $ f a1 a2++-- | Apply the current latch value whenever the pulse occurs.+applyP :: Latch (a -> b) -> Pulse a -> Build (Pulse b)+applyP l p = (`dependOn` p) <$> cacheEval eval+    where+    eval = do+        f <- lift $ readL l+        a <- evalP p+        return $ f <$> a++-- | Accumulate values in a latch.+accumL :: a -> Pulse (a -> a) -> Build (Latch a, Pulse a)+accumL a p1 = do+    -- IORef to hold the current latch value+    latch <- newIORef a+    let l1 = Latch { readL = readIORef latch }++    -- calculate new pulse from old value+    let l2 = mapL (flip ($)) l1+    p2 <- applyP l2 p1++    -- register handler to update latch+    uid <- newUnique+    let handler = whenPulse p2 $ (writeIORef latch $!)+    addHandlerP p2 ((uid, DoLatch), handler)+    +    return (l1,p2)++-- | Latch whose value stays constant.+pureL :: a -> Latch a+pureL a = Latch { readL = return a }++-- | Map a function over latches.+--+-- Evaluated only when needed, result is not cached.+mapL :: (a -> b) -> Latch a -> Latch b+mapL f l = Latch { readL = f <$> readL l } ++-- | Apply two current latch values+--+-- Evaluated only when needed, result is not cached.+applyL :: Latch (a -> b) -> Latch a -> Latch b+applyL l1 l2 = Latch { readL = readL l1 <*> readL l2 }++{-----------------------------------------------------------------------------+    Test+------------------------------------------------------------------------------}+test :: IO (Int -> IO ())+test = do+    (p1, fire) <- newPulse+    p2     <- mapP (+) p1+    (l1,_) <- accumL 0 p2+    let l2 =  mapL const l1+    p3     <- applyP l2 p1+    addHandler p3 print+    return fire++test_recursion1 :: IO (IO ())+test_recursion1 = mdo+    (p1, fire) <- newPulse+    p2      <- applyP l2 p1+    p3      <- mapP (const (+1)) p2+    ~(l1,_) <- accumL (0::Int) p3+    let l2  =  mapL const l1+    addHandler p2 print+    return $ fire ()
src/UseWords.hs view
@@ -11,10 +11,10 @@  #ifdef CABAL import qualified  "threepenny-gui" Graphics.UI.Threepenny as UI-import "threepenny-gui" Graphics.UI.Threepenny.Core hiding (string)+import "threepenny-gui" Graphics.UI.Threepenny.Core hiding (string, (<|>), many) #else import qualified Graphics.UI.Threepenny as UI-import Graphics.UI.Threepenny.Core hiding (string)+import Graphics.UI.Threepenny.Core hiding (string, (<|>), many) #endif import Paths import System.FilePath ((</>))@@ -25,10 +25,9 @@ main :: IO () main = do     static <- getStaticDir-    startGUI Config+    startGUI defaultConfig         { tpPort       = 10000-        , tpCustomHTML = Nothing-        , tpStatic     = static+        , tpStatic     = Just static         } setup  
threepenny-gui.cabal view
@@ -1,8 +1,8 @@ Name:                threepenny-gui-Version:             0.2.0.1-Synopsis:            Small GUI framework that uses the web browser as a display.+Version:             0.3.0.0+Synopsis:            GUI framework that uses the web browser as a display. Description:-    Threepenny-GUI is a small GUI framework that uses the web browser as a display.+    Threepenny-GUI is a GUI framework that uses the web browser as a display.     .     It's cheap and easy to install because everyone has a web browser installed.     .@@ -12,7 +12,6 @@     from your Haskell code.     .     Stability forecast: This is an experimental release! Send us your feedback!-    Basic functionality should work.     Significant API changes are likely in future versions.     .     NOTE: This library contains examples, but they are not built by default.@@ -22,6 +21,8 @@     .     Changelog:     .+    * 0.3.0.0 - Snapshot release. Browser communication with WebSockets. First stab at FRP integration.+    .     * 0.2.0.0 - Snapshot release. First stab at easy JavaScript FFI.     .     * 0.1.0.0 - Initial release.@@ -37,12 +38,10 @@  Extra-Source-Files:  src/Graphics/UI/*.html                     ,src/Graphics/UI/*.js+                    ,src/Graphics/UI/*.css  Data-dir:            .-Data-files:          src/Graphics/UI/*.js-                    ,src/Graphics/UI/*.html-                    ,src/Graphics/UI/*.css-                    ,wwwroot/css/*.css+Data-files:          wwwroot/css/*.css                     ,wwwroot/css/*.png                     ,wwwroot/*.html                     ,wwwroot/*.txt@@ -51,7 +50,7 @@  flag buildExamples     description: Build example executables.-    default:     True+    default:     False  Source-repository head     type:               git@@ -61,7 +60,7 @@ Library   Hs-source-dirs:    src   Exposed-modules:-                     Control.Event+                     Reactive.Threepenny                     ,Graphics.UI.Threepenny                     ,Graphics.UI.Threepenny.Attributes                     ,Graphics.UI.Threepenny.Core@@ -73,21 +72,26 @@                     ,Graphics.UI.Threepenny.Timer   Other-modules:                      Control.Concurrent.Chan.Extra-                    ,Control.Monad.Extra-                    ,Control.Monad.IO                     ,Control.Concurrent.Delay                     ,Graphics.UI.Threepenny.Internal.Core                     ,Graphics.UI.Threepenny.Internal.Resources+                    ,Graphics.UI.Threepenny.Internal.Include                     ,Graphics.UI.Threepenny.Internal.Types+                    ,Reactive.Threepenny.PulseLatch+                    ,Reactive.Threepenny.Memo                     ,Paths_threepenny_gui   CPP-Options:      -DCABAL   Build-depends:     base                      >= 4     && < 5                     ,snap-server                     ,snap-core-                    ,mtl+                    ,websockets+                    ,websockets-snap                     ,text                     ,safe                     ,containers+                    ,unordered-containers+                    ,hashable+                    ,vault == 0.3.*                     ,bytestring                     ,json >= 0.4.4 && < 0.6                     ,time@@ -95,8 +99,11 @@                     ,network                     ,filepath                     ,data-default+                    ,template-haskell                     ,transformers                     ,stm+                    ,attoparsec-enumerator+                    ,MonadCatchIO-transformers                      Executable threepenny-examples-bartab     if flag(buildExamples)@@ -120,6 +127,16 @@         buildable: False     main-is:           Buttons.hs     other-modules:     Paths_threepenny_gui, Paths+    hs-source-dirs:    src++Executable threepenny-examples-currencyconverter+    if flag(buildExamples)+        cpp-options:       -DCABAL+        build-depends:     base                      >= 4     && < 5+                          ,threepenny-gui+    else+        buildable: False+    main-is:           CurrencyConverter.hs     hs-source-dirs:    src  Executable threepenny-examples-dragndropexample