wxFruit-0.1: WXFruit.hs
-----------------------------------------------------------
-- 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