----------------------------------------------------------- -- WXFruit: an implementation of Fruit using wxHaskell. -- Version 0.1, May 2004 -- Contributors: Antony Courtney, Bart Robinson ----------------------------------------------------------- module WXFruit where ----------------------------------------------------------- -- Module imports. import qualified Graphics.UI.WX as WX import qualified Graphics.UI.WXCore as WXCore import AFRP import AFRPUtilities import Control.Arrow import Data.IORef import System.Time ----------------------------------------------------------- -- Data types for communication between widgets and the -- system. (These are likely to change dramatically.) -- Raw input: kind of a hack... data WXRawInput = WXRawInput { wxMousePos :: WX.Point -- relative to topmost frame -- add other raw inputs here: keyboard, mouse presses? } -- Responses/Requests: This is rather clumsy, as any new -- widgets would have to add their own alternatives to -- these sums. A way to do more dynamic run-time typing -- could help with this problem. data WXWidgetResp = WXWInit -- currently not used | WXWButtonCreateResp (WX.Button ()) | WXWTextCreateResp (WX.StaticText ()) | WXWPictureCreateResp (WX.Panel ()) | WXWSliderCreateResp (WX.Slider ()) | WXWButtonCommandEvent -- button pressed | WXWSliderCommandEvent Int -- new value selected | WXWDeComp (Event WXWidgetResp,Event WXWidgetResp) data WXWidgetReq = WXWButtonCreateReq WXButtonState | WXWTextCreateReq WXTextState | WXWPictureCreateReq WXPictureState | WXWSliderCreateReq WXSliderState | WXWButtonSetReq (WX.Button ()) WXButtonState | WXWTextSetReq (WX.StaticText ()) WXTextState | WXWPictureSetReq (WX.Panel ()) WXPictureState | WXWSliderSetReq (WX.Slider ()) WXSliderState | WXWComp Orientation (Event WXWidgetReq,Event WXWidgetReq) data Orientation = Horiz | Vert deriving (Eq) ----------------------------------------------------------- -- Widgets, GUIs, and Boxes. The basic WXFruit building -- blocks. -- Widget: just a fancy signal function. type Widget a b = SF (WXRawInput,Event WXWidgetResp,a) (Event WXWidgetReq,b) -- WXGUI: conceptually the same as a Widget, but a newtype -- is used so that it can be made into an Arrow. newtype WXGUI b c = WXGUI (Widget b c) -- WXGUI functions for Arrow instance. This makes WXGUI -- correspond to the GA type in the original Fruit. I'm -- not actually not sure this is necessary, because the -- preferred idiom is to use WXBoxes, anyway. -- Lifting/lowering wxUnGUI :: WXGUI b c -> Widget b c wxUnGUI (WXGUI w) = w wxSF :: SF b c -> WXGUI b c wxSF sf = WXGUI $ proc (_,_,b) -> do c <- sf -< b returnA -< (noEvent,c) wxArr :: (b -> c) -> WXGUI b c wxArr f = wxSF (arr f) -- Composing: handles the "plumbing" involved in composing -- widgets together -- makes sure the system input/output -- streams are hooked up correctly. The ugliness this -- hides is basically the motivation for creating an Arrow -- instance for WXGUI (and WXBox). decompResp :: Event WXWidgetResp -> (Event WXWidgetResp,Event WXWidgetResp) decompResp (Event WXWInit) = (Event WXWInit,Event WXWInit) decompResp (Event (WXWDeComp (resp0,resp1))) = (resp0,resp1) decompResp _ = (noEvent,noEvent) compReq :: Orientation -> Event WXWidgetReq -> Event WXWidgetReq -> Event WXWidgetReq compReq _ NoEvent NoEvent = noEvent compReq orient req0 req1 = Event (WXWComp orient (req0,req1)) -- Composition generalized over orientations wxGComp :: Orientation -> WXGUI b c -> WXGUI c d -> WXGUI b d wxGComp orient (WXGUI w1) (WXGUI w2) = WXGUI $ proc (inp,resp,b) -> do let (resp1,resp2) = decompResp resp (req1,c) <- w1 -< (inp,resp1,b) (req2,d) <- w2 -< (inp,resp2,c) returnA -< (compReq orient req1 req2,d) -- Default layout for composed widgets is horizontal. wxComp :: WXGUI b c -> WXGUI c d -> WXGUI b d wxComp = wxGComp Horiz wxFirst :: WXGUI b c -> WXGUI (b,d) (c,d) wxFirst (WXGUI w) = WXGUI $ proc (inp,resp,(b,d)) -> do (req,c) <- w -< (inp,resp,b) returnA -< (req,(c,d)) instance Arrow WXGUI where arr = wxArr (>>>) = wxComp first = wxFirst wxLoop :: WXGUI (b,d) (c,d) -> WXGUI b c wxLoop (WXGUI w) = WXGUI $ proc (inp,resp,b) -> do rec (req,(c,d)) <- w -< (inp,resp,(b,d)) returnA -< (req,c) instance ArrowLoop WXGUI where loop = wxLoop -- WXBox: Builds upon WXGUI, adding the ability to defer -- the specification of the orientation of a collection of -- widgets. newtype WXBox b c = WXBox (Orientation -> WXGUI b c) -- Lift a GUI into a Box, keeping the GUI's internal -- orientation(s) wxBox :: WXGUI b c -> WXBox b c wxBox w = WXBox (const w) -- Compose Boxes by threading orientation to children. wxCompBox :: WXBox b c -> WXBox c d -> WXBox b d wxCompBox (WXBox b1f) (WXBox b2f) = WXBox $ \orient -> wxGComp orient (b1f orient) (b2f orient) wxFirstBox :: WXBox b c -> WXBox (b,d) (c,d) wxFirstBox (WXBox bf) = WXBox $ \orient -> first (bf orient) instance Arrow WXBox where arr = wxBox . arr (>>>) = wxCompBox first = wxFirstBox wxLoopBox :: WXBox (b,d) (c,d) -> WXBox b c wxLoopBox (WXBox bf) = WXBox $ \orient -> loop (bf orient) instance ArrowLoop WXBox where loop = wxLoopBox -- Some useful Box functions wxBoxGUI :: Widget b c -> WXBox b c wxBoxGUI = wxBox . WXGUI wxBoxSF :: SF b c -> WXBox b c wxBoxSF = wxBox . wxSF -- These two are used to surround collections of Boxes and -- make them go left-to-right or top-to-bottom. They both -- have the same effect when applied to a WXBox containing -- just a single widget. wxHBox :: WXBox b c -> WXGUI b c wxHBox (WXBox bf) = bf Horiz wxVBox :: WXBox b c -> WXGUI b c wxVBox (WXBox bf) = bf Vert ----------------------------------------------------------- -- Individual Widget definitions. These are defined as -- WXBoxes to allow easy composition. They all employ a -- similar pattern: they have a state-modifier function as -- input, and use the same internal idioms to communicate -- with the system. See Button for extensive comments. -- As more of these are added, it might be helpful to -- move them into other modules/files. (Which might be -- tricky, as they are currently closed tied up with the -- response/request data types.) -- Utility to detect when a widget's state has changed maybeChanged :: Eq a => a -> a -> Maybe () maybeChanged s s' = if s == s' then Nothing else Just () ----------------------------------------------------------- -- Button widget: has a string label, can be enabled and -- disabled, and outputs an Event () when pressed -- State data WXButtonState = WXButtonState { bsLabel :: String, bsEnabled :: Bool } deriving (Eq, Show) type WXButtonConf = WXButtonState -> WXButtonState -- Constructors to initialize and alter state. Note that -- the state of a button should not (cannot?) be modified -- other than through the use of these functions. btext :: String -> WXButtonConf btext l bs = bs {bsLabel = l} benabled :: Bool -> WXButtonConf benabled e bs = bs {bsEnabled = e} -- The button GUI wxbutton :: WXButtonConf -> WXBox WXButtonConf (Event ()) wxbutton conf0 = let -- Initial state defState = WXButtonState {bsLabel = "Default", bsEnabled = True} initState = conf0 defState -- Detect creation maybeCreate (WXWButtonCreateResp b) = Just (True,b) maybeCreate _ = Nothing -- Detect button press maybeCommand WXWButtonCommandEvent = Just () maybeCommand _ = Nothing -- Make an ordinary, atomic signal function (in order to have access -- to the response/request mechanism), then lift it into a WXBox -- with wxBoxGUI. in wxBoxGUI $ proc (_,resp,conf) -> do -- Keep track of the state. rec state <- iPre initState -< conf state -- Has the state changed? If so, generate set request. stateChanged <- edgeBy maybeChanged initState -< state -- Ensure that the system hands us back a WX.Button (). (isCreated,button) <- hold (False,undefined) -< mapFilterE maybeCreate resp -- Send a creation request if we haven't been created yet. -- WARNING: this is probably a bad way to do this. It can -- create an infinitely-dense stream of Events, which is a -- big no-no in the AFRP world (at least, conceptually). -- We used to use WXWInit for this purpose, but with the -- dynamic switching of widgets, the system wouldn't know -- when to send the WXWInit events to widgets that have -- been switched-into. let doCreate = if isCreated then noEvent else Event () -- Merge create/set requests. let req = lMerge (tag doCreate (WXWButtonCreateReq state)) (tag stateChanged (WXWButtonSetReq button state)) -- Pass button presses through. let press = mapFilterE maybeCommand resp returnA -< (req,press) ----------------------------------------------------------- -- Static text (label) widget: Displays a string, and has -- no output. -- State data WXTextState = WXTextState { tsLabel :: String } deriving (Eq, Show) type WXTextConf = WXTextState -> WXTextState -- Constructor ttext :: String -> WXTextConf ttext l ts = ts {tsLabel = l} -- The text GUI wxtext :: WXTextConf -> WXBox WXTextConf () wxtext conf0 = let -- Initial state defState = WXTextState {tsLabel = "Default"} initState = conf0 defState -- Detect creation maybeCreate (WXWTextCreateResp t) = Just (True,t) maybeCreate _ = Nothing in wxBoxGUI $ proc (_,resp,conf) -> do -- State rec state <- iPre initState -< conf state stateChanged <- edgeBy maybeChanged initState -< state -- Creation (isCreated,text) <- hold (False,undefined) -< mapFilterE maybeCreate resp let doCreate = if isCreated then noEvent else Event () -- Output let req = lMerge (tag doCreate (WXWTextCreateReq state)) (tag stateChanged (WXWTextSetReq text state)) returnA -< (req,()) ----------------------------------------------------------- -- Slider widget: a slider that the user can move along a -- bar between minimum and maximum values. Can be oriented -- horizontally or vertically (upon creation), and -- enabled/disabled dynamically. Outputs a continuous -- signal indicating the currently-selected value. -- State data WXSliderState = WXSliderState { -- These properties are determined upon creation, -- and cannot be subsequently altered. ssMin :: Int, ssMax :: Int, ssOrientation :: Orientation, ssSelection :: Int, -- user- and programmer-modifiable ssEnabled :: Bool -- programmer-modifiable } deriving (Eq) type WXSliderConf = WXSliderState -> WXSliderState -- Constructors senabled :: Bool -> WXSliderConf senabled e ss = ss {ssEnabled = e} sselection :: Int -> WXSliderConf sselection sel ss = ss {ssSelection = sel} -- The slider GUI wxslider :: Orientation -> Int -> Int -> WXSliderConf -> WXBox WXSliderConf Int wxslider orient min max conf0 = let -- Initial state defState = WXSliderState {ssMin = min, ssMax = max, ssOrientation = orient, ssSelection = 0, ssEnabled = True} initState = conf0 defState -- Change state when user moves slider changeSel state (Event (WXWSliderCommandEvent sel)) = state {ssSelection = sel} changeSel state _ = state -- Define our own maybeChanged, because we don't want to generate -- a WXWSliderSetReq if the only change in the state that occurred -- was via user input. maybeChangedInternal (_,state) (Event (WXWSliderCommandEvent _), state') = if (state {ssSelection = ssSelection state'}) == state' then Nothing else Just () maybeChangedInternal (_,state) (_,state') = maybeChanged state state' -- Detect creation maybeCreate (WXWSliderCreateResp s) = Just (True,s) maybeCreate _ = Nothing in wxBoxGUI $ proc (_,resp,conf) -> do -- State rec let newState = changeSel (conf state) resp state <- iPre initState -< newState -- Detect actionable state changes stateChanged <- edgeBy maybeChangedInternal (noEvent,initState) -< (resp,state) -- Creation (isCreated,slider) <- hold (False,undefined) -< mapFilterE maybeCreate resp let doCreate = if isCreated then noEvent else Event () -- Output let req = lMerge (tag doCreate (WXWSliderCreateReq state)) (tag stateChanged (WXWSliderSetReq slider state)) returnA -< (req,ssSelection state) ----------------------------------------------------------- -- Picture widget: a box for doing arbitrary graphics. -- Uses a WX.Panel () with a custom repaint handler. -- State data WXPictureState = WXPictureState { psSize :: WX.Size, psPicture :: WXPicture } type WXPictureConf = WXPictureState -> WXPictureState -- Constructors ppic :: WXPicture -> WXPictureConf ppic pic ps = ps {psPicture = pic} psize :: WX.Size -> WXPictureConf psize size ps = ps {psSize = size} -- The picture GUI wxpicture :: WXPictureConf -> WXBox WXPictureConf () wxpicture conf0 = let -- Initial state defState = WXPictureState {psSize = WX.sizeNull, psPicture = wxblank} initState = conf0 defState -- Detect creation maybeCreate (WXWPictureCreateResp panel) = Just (True,panel) maybeCreate _ = Nothing in wxBoxGUI $ proc (_,resp,conf) -> do -- State rec state <- iPre initState -< conf state -- Creation (isCreated,panel) <- hold (False,undefined) -< mapFilterE maybeCreate resp let doCreate = if isCreated then noEvent else Event () -- Output. Note that here we are again violating the -- rules of AFRP by sending an infinitely-dense stream -- of events, because we want a new picture at every -- sample time so that animation will work. let req = lMerge (tag doCreate (WXWPictureCreateReq state)) (Event (WXWPictureSetReq panel state)) returnA -< (req,()) ----------------------------------------------------------- -- Mouse widget: this is problematic. It just outputs the -- current mouse position, relative to the client space of -- the topmost frame. There is currently no way to -- translate this into coordinates relative to a particular -- widget. wxmouse :: WXBox () WX.Point wxmouse = wxBoxGUI $ proc (inp,_,_) -> do returnA -< (noEvent,wxMousePos inp) ----------------------------------------------------------- -- The WXFruit graphics library! (Just wrappers for -- wxHaskell functions, basically, adapting them to the -- SOE/Haven graphics idiom.) Just enough is implemented -- here to get paddleball working. -- Picture type: This corresponds to a wxHaskell repaint handler. type WXPicture = [WX.Prop (WX.DC ())] -> WX.DC () -> WX.Rect -> IO () -- Draw nothing. wxblank :: WXPicture wxblank _ _ _ = return () -- Fill the picture with a single color. wxfill :: WXPicture wxfill props dc rect = wxPicFill (wxrect rect) props dc rect -- Draw an ellipse bounded by a rectangle. wxellipse :: WX.Rect -> WXPicture wxellipse rect props dc _ = WX.ellipse dc rect props -- Draw a rectangle. wxrect :: WX.Rect -> WXPicture wxrect rect props dc _ = WX.drawRect dc rect props -- Write graphical text. Don't confuse this with wxtext; -- it's an entirely different animal. wxwrite :: String -> WX.Point -> WXPicture wxwrite str pt props dc _ = WX.drawText dc str pt props -- Make drawings use a particular color. wxWithColor :: WX.Color -> WXPicture -> WXPicture wxWithColor color pic props = pic (props ++ [WX.color WX.:= color, WX.brushColor WX.:= color]) -- Make rectangles and ellipses be filled in. wxPicFill :: WXPicture -> WXPicture wxPicFill pic props = pic (props ++ [WX.brushKind WX.:= WX.BrushSolid]) -- Combine two drawings. wxPicOver :: WXPicture -> WXPicture -> WXPicture wxPicOver pic1 pic2 props dc rect = do pic2 props dc rect pic1 props dc rect ----------------------------------------------------------- -- Data types and functions for composition of widgets. -- This is where the connection is made between the tree- -- structure of WXGUIs and WXBoxes and the actual layout -- of widgets sent to wxHaskell. This is all rather -- clumsy. -- Contents widget tree: wxFruit version of WX.Layout. data WXContents = WXCEmpty | WXCLeaf WXWidget | WXCComp Orientation WXContents WXContents data WXWidget = WXButton (WX.Button ()) | WXText (WX.StaticText ()) | WXPicture (WX.Panel ()) | WXSlider (WX.Slider ()) -- Compositional path to a widget. A True entry means -- go down the left/top branch of a WXContents tree; False -- means go down the right/bottom. type WXPath = [Bool] -- Convert contents into layout. The tricky part is -- making sure that nested WXCComps with the same -- orientation are merged into a single list (see -- c2lList). contents2layout :: WXContents -> WX.Layout contents2layout WXCEmpty = WX.empty contents2layout (WXCLeaf (WXButton b)) = WX.widget b contents2layout (WXCLeaf (WXText t)) = WX.widget t contents2layout (WXCLeaf (WXPicture p)) = WX.widget p contents2layout (WXCLeaf (WXSlider s)) = WX.widget s contents2layout comp@(WXCComp orient c1 c2) = (if orient == Horiz then WX.row else WX.column) 5 (c2lList orient comp) -- Merge similarly-orientated WXCComps together. c2lList :: Orientation -> WXContents -> [WX.Layout] c2lList orient comp@(WXCComp orient' c1 c2) = if orient == orient' then (c2lList orient c1) ++ (c2lList orient c2) else [contents2layout comp] c2lList _ WXCEmpty = [] c2lList _ c = [contents2layout c] -- Destroy the windows in a contents tree. This shouldn't -- really be necessary, except wxHaskell doesn't currently -- handle window destruction properly (IMO) -- simply -- resetting the layout doesn't get rid of any windows -- in the old layout. This is really only needed if/when -- widget-switching is implemented. removeContents :: WX.Frame () -> WXContents -> IO Bool removeContents f WXCEmpty = return True removeContents f (WXCLeaf (WXButton b)) = WXCore.windowDestroy b removeContents f (WXCLeaf (WXText t)) = WXCore.windowDestroy t removeContents f (WXCLeaf (WXSlider s)) = WXCore.windowDestroy s removeContents f (WXCLeaf (WXPicture p)) = WXCore.windowDestroy p removeContents f (WXCComp _ c0 c1) = do removeContents f c0 removeContents f c1 ----------------------------------------------------------- -- The guts of the WXFruit system. This is where the -- connection between wxHaskell and Yampa is made. -- State maintained across calls to react: the frame -- window, the time of the last sample, and the contents -- of the window (i.e. the entire widget tree). type WXGUIState = (WX.Frame (),Int,WXContents) type WXGUIRef = IORef WXGUIState type WXRHandle = ReactHandle (WXRawInput,Event WXWidgetResp,()) (Event WXWidgetReq,()) -- Start a WXGUI running in a top-level frame. startGUI :: String -> WXGUI () () -> IO () startGUI title (WXGUI g) = WX.start $ do -- Set up frame window and internal state. f <- WX.frame [WX.text WX.:= title] epoch <- getCurrentTime gsr <- newIORef (f,epoch,WXCEmpty) -- Initialize Yampa. rh <- reactInit (initSense f) (actuate gsr) g -- Set up heartbeat timer to make periodic calls to react. -- This is necessary for animation. Note that this is -- not the only place react is called from: button -- presses and other user actions may generate calls of -- their own. timer <- WX.timer f [WX.interval WX.:= 30, WX.on WX.command WX.:= respond gsr rh noEvent] return () -- Get an input sample from the OS. getRawInput :: WX.Frame () -> IO WXRawInput getRawInput f = do mouse <- WXCore.windowGetMousePosition f return WXRawInput {wxMousePos = mouse} -- The very first input sample. initSense :: WX.Frame () -> IO (WXRawInput,Event WXWidgetResp,()) initSense f = do inp <- getRawInput f return (inp,Event WXWInit,()) -- Push an input sample/widget response. respond :: WXGUIRef -> WXRHandle -> Event WXWidgetResp -> IO () respond gsr rh resp = do -- Obtain input sample. (f,prevt,c) <- readIORef gsr inp <- getRawInput f -- Make sure time's elapsed since the last call to react. -- With the timer set up in startGUI, this is probably -- unnecessary, but we'll leave it in to be safe. et <- getCurrentTime (dtf,t) <- ensureTimeElapses prevt et getCurrentTime writeIORef gsr (f,t,c) -- Let Yampa work its magic. react rh (dtf,Just (inp,resp,())) return () -- Process an output sample (i.e. a widget request). actuate :: WXGUIRef -> WXRHandle -> Bool -> (Event WXWidgetReq,()) -> IO Bool actuate gsr rh _ (wre,_) = do -- Handle requests, if any. (f,t,prevc) <- readIORef gsr (resp,c,cch) <- handleWidgetReq gsr rh f [] prevc wre -- Reset layout if contents changed. if cch then do WX.set f [WX.layout WX.:= WX.margin 5 (contents2layout c)] WX.windowReLayoutMinimal f else return () writeIORef gsr (f,t,c) -- Turn around and respond to the widgets, if necessary. -- Note that this causes a reentrant call to react. case resp of NoEvent -> return () _ -> respond gsr rh resp return False -- Handle a single widget request by making the appropriate -- calls into wxHaskell. Returns a response to the -- request (if any), the new contents of the window, and -- a boolean indicating whether the contents have changed. handleWidgetReq :: WXGUIRef -> WXRHandle -> WX.Frame () -> WXPath -> WXContents -> (Event WXWidgetReq) -> IO (Event WXWidgetResp,WXContents,Bool) -- Do nothing if no request. handleWidgetReq _ _ _ _ c NoEvent = return (noEvent,c,False) -- Handle creation by calling the wxHaskell constructor with -- properties corresponding to the given initial state, -- removing any old contents, and returning the new widget. handleWidgetReq gsr rh f path c (Event (WXWButtonCreateReq bstate)) = do let bprops = mkButtonProps gsr rh path bstate b <- WX.smallButton f bprops removeContents f c return (Event (WXWButtonCreateResp b),WXCLeaf (WXButton b),True) handleWidgetReq _ _ f path c (Event (WXWTextCreateReq tstate)) = do let tprops = mkTextProps tstate t <- WX.staticText f tprops removeContents f c return (Event (WXWTextCreateResp t),WXCLeaf (WXText t),True) handleWidgetReq _ _ f path c (Event (WXWPictureCreateReq pstate)) = do let pprops = mkPictureProps pstate p <- WX.panel f pprops removeContents f c return (Event (WXWPictureCreateResp p),WXCLeaf (WXPicture p),True) handleWidgetReq gsr rh f path c (Event (WXWSliderCreateReq sstate)) = do -- Here we create the slider before setting up its properties -- because the event handler in the properties needs a pointer -- to the slider. s <- (if ssOrientation sstate == Horiz then WX.hslider else WX.vslider) f False (ssMin sstate) (ssMax sstate) [] let sprops = mkSliderProps gsr rh path s sstate WX.set s sprops removeContents f c return (Event (WXWSliderCreateResp s),WXCLeaf (WXSlider s),True) -- Handle creation requests by resetting the properties -- of the widget. handleWidgetReq gsr rh f path c (Event (WXWButtonSetReq b bstate)) = do let bprops = mkButtonProps gsr rh path bstate WX.set b bprops return (noEvent,c,False) handleWidgetReq _ _ f path c (Event (WXWTextSetReq t tstate)) = do let tprops = mkTextProps tstate WX.set t tprops return (noEvent,c,False) handleWidgetReq _ _ f path c (Event (WXWPictureSetReq p pstate)) = do let pprops = mkPictureProps pstate WX.set p pprops WX.repaint p -- paint the new picture return (noEvent,c,False) handleWidgetReq gsr rh f path c (Event (WXWSliderSetReq s sstate)) = do -- Note that if properties other than the selection and -- enable are modified, this will not change them. let sprops = mkSliderProps gsr rh path s sstate WX.set s sprops return (noEvent,c,False) -- Pass requests down the widget tree. handleWidgetReq gsr rh f path c (Event (WXWComp orient (lreq,rreq))) = do (lresp,lc',lcch) <- handleWidgetReq gsr rh f (True:path) (subCont True c) lreq (rresp,rc',rcch) <- handleWidgetReq gsr rh f (False:path) (subCont False c) rreq -- If both children returned no response, don't compose -- them together. return (case (lresp,rresp) of (NoEvent,NoEvent) -> noEvent resp -> Event (WXWDeComp resp), WXCComp orient lc' rc',lcch || rcch) where subCont which (WXCComp _ l r) = if which then l else r subCont _ _ = WXCEmpty -- contents tree has not been set up yet -- Construct button properties list mkButtonProps :: WXGUIRef -> WXRHandle -> WXPath -> WXButtonState -> [WX.Prop (WX.Button ())] mkButtonProps gsr rh path bs = [WX.text WX.:= bsLabel bs, WX.enabled WX.:= bsEnabled bs, WX.on WX.command WX.:= mkButtonCommand gsr rh path] -- Button-press event handler. Causes a call to react. mkButtonCommand :: WXGUIRef -> WXRHandle -> WXPath -> IO () mkButtonCommand gsr rh path = do let e = pathify (Event WXWButtonCommandEvent) path respond gsr rh e -- Contruct text label properties list. mkTextProps :: WXTextState -> [WX.Prop (WX.StaticText ())] mkTextProps ts = [WX.text WX.:= tsLabel ts] -- Construct slider properties list. mkSliderProps :: WXGUIRef -> WXRHandle -> WXPath -> WX.Slider () -> WXSliderState -> [WX.Prop (WX.Slider ())] mkSliderProps gsr rh path s ss = [WX.enabled WX.:= ssEnabled ss, WX.selection WX.:= ssSelection ss, WX.on WX.command WX.:= mkSliderCommand gsr rh path s] -- Event handler called when slider is moved by user. -- Causes a call to react. mkSliderCommand :: WXGUIRef -> WXRHandle -> WXPath -> WX.Slider () -> IO () mkSliderCommand gsr rh path s = do sel <- WX.get s WX.selection let e = pathify (Event (WXWSliderCommandEvent sel)) path respond gsr rh e -- Construct picture widget properties list. mkPictureProps :: WXPictureState -> [WX.Prop (WX.Panel ())] mkPictureProps ps = [WX.clientSize WX.:= psSize ps, WX.on WX.paint WX.:= psPicture ps []] -- Construct a WXWidgetResp corresponding to a given -- path. This response will then be directed to the -- correct widget (e.g. a button-pressed event will be sent -- through the widget tree to the appropriate wxbutton). pathify :: Event WXWidgetResp -> WXPath -> Event WXWidgetResp pathify e [] = e pathify e (True:path) = pathify (Event (WXWDeComp (e,noEvent))) path pathify e (False:path) = pathify (Event (WXWDeComp (noEvent,e))) path ----------------------------------------------------------- -- Utility functions copied from Yampa. getCurrentTime :: IO Int getCurrentTime = do (TOD sec psec) <- getClockTime let usec = fromIntegral psec / 1000000 let msec = (fromIntegral ((sec - ctEpoch) * 1000)) + (usec / 1000) -- let dmsec = (fromIntegral $ msec) :: Double let imsec = round msec -- putStrLn ("getCurrentTime(): sec == " ++ show sec ++ ", usec == " ++ -- show usec) -- putStrLn ("getCurrentTime() returning: " ++ (show imsec)) return imsec where -- an arbitrary epoch, in seconds, to prevent Int wraparound: ctEpoch :: Integer ctEpoch = 1037286000 -- ensure an observable amount of time elapses by busy-waiting. -- -- arguments: -- t0 :: Int -- time of last sample -- t1 :: Int -- current time, in milliseconds -- getTime :: IO Int -- returns the current time, in milliseconds -- result: (dtf,t1) -- dtf -- the elapsed time since last sample, in seconds, as a Float -- t1 -- the current time, in millisec. -- -- We perform the floating point conversion, and perform our comparison -- with respect to that conversion here to ensure that we are using -- the same test as used by reactimate. -- ensureTimeElapses :: Int -> Int -> IO Int -> IO (Double,Int) ensureTimeElapses t0 t1 getTime = do let dt = t1 - t0 dtf = (fromIntegral dt) / 1000 if (dtf > 0) then return (dtf,t1) else do t' <- getTime ensureTimeElapses t0 t' getTime