reactivity-0.3.0.0: src/FRP/Reactivity/Extras.hs
{-# LANGUAGE Trustworthy, FlexibleContexts, ScopedTypeVariables #-}
-- | Extra utilities.
module FRP.Reactivity.Extras where
import FRP.Reactivity.Hook
import FRP.Reactivity.UI
import FRP.Reactivity.Draw
import FRP.Reactivity.Combinators
import Control.Monad
import Control.Monad.Trans
import Control.Monad.Fix
import Control.Applicative
import Control.Comonad (duplicate)
import Control.Exception
import Control.Concurrent.MVar
import Data.Int
import Data.Monoid
import Data.Fixed
import Data.Maybe
import qualified Data.Map as M
import Graphics.Win32 hiding (textOut, fillRect)
import Codec.BMP
import Prelude hiding (until)
import System.IO.Unsafe
-- |
dragNDrop :: (Monoid r) => (POINT -> Appearance) -> Maybe RECT -> Hook r (Event Message) Act t -> Hook r (Event Message) Act (Either t (Window, Btn, POINT, POINT))
dragNDrop appr rect h = do
w <- window
(x, child) <- getReturns mempty id h
tellReturn child
return (Left x) <> do
-- Pick a child to follow
w2 <- lift $ liftE $ fmap return child
-- Wait for a child to be clicked
(btn, pt@(origX, origY), rest) <- lift $ liftE $ justE $ fmap (\(e, rest) -> case e of
Mouse b Down pt -> Just (return (b, pt, rest))
_ -> Nothing)
(withRest (event w2))
let pos = mousePos rest
-- Wait for the mouse to exit a "guard region" which prevents unwanted drags.
-- If the mouse is released before exiting the guard region, the drag operation is aborted.
lift $ liftE $ fmap return $ once $ filterE (\(x, y) -> abs (x - origX) > 20 && abs (y - origY) > 20) pos `untilE` filterE (\e -> case e of Mouse _ Up _ -> True; _ -> False) rest
liftIO (putStrLn "Dragging")
-- Move the drag icon in a drag operation.
liftM fst $ getReturns1 mempty id $ hCreate "STATIC" (stepper mempty $ fmap appr pos)
(do
drag <- window
lift $ liftE $ once $ justE (fmap (\e -> case e of Mouse _ Up pt2 -> Just (destroyWindow (hwnd drag) >> return (Right (w2, btn, pt, pt2))); _ -> Nothing) rest))
-- |
draggable :: (POINT -> Appearance) -> Maybe RECT -> Hook (Appearance, Event (Appearance -> Appearance)) (Event Message) Act t -> Hook (Appearance, Event (Appearance -> Appearance)) (Event Message) Act t
draggable appr rt h = do
dragEvents <- liftIO (chanSource defaultFrame)
(ei, child) <- getReturns (mempty, fmap (\(_, _, (x1, y1), (x2, y2)) a -> a { rect = shift (x2 - x1, y2 - y1) (rect a) }) (getEvent dragEvents)) id $ dragNDrop appr rt h
either
(\ei -> tellReturn child >> return ei)
(\tup -> lift (liftS dragEvents $ return $ return tup) >> mzero)
ei
dragMargin = 5
data Side = Ll | Rr | U | D | N deriving Eq
updateRect (Ll, U) (_, _, x2, y2) (x, y) = (x, y, x2, y2)
updateRect (Rr, U) (x1, _, _, y2) (x, y) = (x1, y, x, y2)
updateRect (Ll, D) (_, y1, x2, _) (x, y) = (x, y1, x2, y)
updateRect (Rr, D) (x1, y1, _, _) (x, y) = (x1, y1, x, y)
updateRect (Ll, N) (_, y1, x2, y2) (x, _) = (x, y1, x2, y2)
updateRect (Rr, N) (x1, y1, _, y2) (x, _) = (x1, y1, x, y2)
updateRect (N, U) (x1, _, x2, y2) (_, y) = (x1, y, x2, y2)
updateRect (N, D) (x1, y1, x2, _) (_, y) = (x1, y1, x2, y)
updateRect (N, N) rt _ = rt
isRight (Right _) = True
isRight _ = False
-- | Functional which makes windows from the parameter 'h' into sizeable windows with draggable edges.
sizeable :: Hook (Appearance, Event (Appearance -> Appearance)) (Event Message) Act t ->
Hook (Appearance, Event (Appearance -> Appearance)) (Event Message) Act t
sizeable h = do
w <- window
wit <- liftIO (chanSource defaultFrame)
(x, child) <- getReturns (mempty, getEvent wit) (fmap (\(_, _, _, e, _) -> e) . filterE (\(hside, vside, _, _, _) -> hside == N && vside == N) . findDragEvents) h
tellReturn child
return x <> do
-- Pick a child
w2 <- lift $ liftE $ fmap return child
-- Wait for a child to be sized
(hside, vside, rt@(left, top, _, _), _, eRest) <- lift $ liftE $ fmap return $ findDragEvents (event w2)
guard (hside /= N || vside /= N)
let resizeRects = justE (fmap (\e -> case e of
Mouse _ Mv (x, y) -> Just (updateRect (hside, vside) rt (left + x, top + y))
_ -> Nothing) eRest)
let endResizeRect = justE (fmap (\e -> case e of
Mouse _ Up (x, y) -> Just (updateRect (hside, vside) rt (left + x, top + y))
_ -> Nothing) eRest)
-- Draw selection rectangle (the four borders)
-- This illustrates how static controls can be used to draw shapes
let initAppearance = Appearance (fillRect 0 (0, 0, 65536, 65536)) "" (0, 0, 0, 0)
s1 <- hCreate' "STATIC" (initAppearance, fmap (\(x1, y1, x2, y2) a -> a { rect = (x1, y1, x2, y1 + dragMargin) }) resizeRects)
s2 <- hCreate' "STATIC" (initAppearance, fmap (\(x1, y1, x2, y2) a -> a { rect = (x1, y1 + dragMargin, x1 + dragMargin, y2 - dragMargin) }) resizeRects)
s3 <- hCreate' "STATIC" (initAppearance, fmap (\(x1, y1, x2, y2) a -> a { rect = (x2 - dragMargin, y1 + dragMargin, x2, y2 - dragMargin) }) resizeRects)
s4 <- hCreate' "STATIC" (initAppearance, fmap (\(x1, y1, x2, y2) a -> a { rect = (x1, y2 - dragMargin, x2, y2) }) resizeRects)
-- Wait for mouse cursor to release
newRt <- lift $ liftE $ fmap return $ once endResizeRect
-- Remove selection rectangle
liftIO $ destroyWindow (hwnd s1) >> destroyWindow (hwnd s2) >> destroyWindow (hwnd s3) >> destroyWindow (hwnd s4)
-- Update the rectangle
lift $ liftS wit $ return $ return $ \a -> a { rect = newRt }
mzero where
findDragEvents e = fmap (\(((e, eRest), rt@(x1, y1, x2, y2)), t) -> case e of
Mouse _ Down (x, y) ->
let
hside = if x < dragMargin then Ll else if x >= x2 - x1 - dragMargin then Rr else N
vside = if y < dragMargin then U else if y >= y2 - y1 - dragMargin then D else N in
(hside, vside, rt, e, startAt eRest (list [(t, 0.1)]))
_ -> (N, N, rt, e, startAt eRest (list [(t, 0.1)])))
$ withTime
$ holdE (withRest e) (position e) (0, 0, 0, 0)
-- | Perform an action when the window is closed.
onClose :: (HWND -> IO ()) -> Hook r x Act ()
onClose f = void $ do
w <- window
lift $ liftS nil $ fmap (\msg -> case msg of Close -> liftIO $ f (hwnd w); _ -> return ()) (event w)
-- | Causes the current window to close when the user closes it.
autoclose :: Hook r x Act ()
autoclose = onClose destroyWindow
realign (lo, hi) (mn, mx) x = if x < lo then
mn
else if x > hi then
mx
else
(x - lo) * (mx - mn) / (hi - lo) + mn
-- | Given,
-- * A behavior 'b'
-- * An event
-- * A display range 'rng'
-- * Sampling rate (number of samples per second)
-- * And a colour functional 'clrF',
-- Gives a behavior that graphs 'b' in the range 'rng'. The event indicates when to resize
-- the graph in the display window.
graphBehavior :: (RealFrac t) => Behavior t -> Event RECT -> (t, t) -> Int32 -> (t -> COLORREF) -> Behavior (Draw ())
graphBehavior b pos (nY, mY) samps clr = switcher (pure mempty)
$ fmap (\(rt@(x1, y1, x2, y2), b) -> (\t f -> graph (t - fromIntegral (x2 - x1) / fromIntegral samps, nY, t, mY) (dim rt) f clr)
<$> time
<*> history (fromIntegral $ samps * (x2 - x1)) b)
(snapshot pos (duplicate b))
vertLine x (y1, y2) = fillRect 0 (x, y1, x + 1, y2)
horzLine (x1, x2) y = fillRect 0 (x1, y, x2, y + 1)
maxOfRange :: (RealFrac t) => t -> (Int32, Fixed E6)
maxOfRange mx = (ceiling (mx / 10 ^^ n), 10 ^^ n)
where n = floor (log (fromRational (toRational mx) * 0.75) / log 10)
-- | Graphs a function in a specific range.
--
-- Parameters;
--
-- * (mX, mY) - The maximum data values of the graph.
--
-- * (x1, y1) - The dimensions of the graph, in pixels.
--
-- * f - The function to graph.
--
-- * clrF - A colour functional.
graph :: (RealFrac t, RealFrac u) => (t, u, t, u) -> POINT -> (t -> u) -> (u -> COLORREF) -> Draw ()
graph (nX, nY, mX, mY) (x1, y1) f clrF = do
-- Draw abscissa/ordinate
mapM_ (\n -> do
let x2 = 30 + (x1 - 40) * n `div` mxX
textOut (showFixed True $ fromRational (toRational nX) + fromIntegral n * incrementX) (x2 - 5, 5) defFont
vertLine x2 (20, 25)) [0..mxX]
mapM_ (\n -> do
let y2 = 25 + (y1 - 35) * n `div` mxY
textOut (showFixed True $ fromRational (toRational nY) + fromIntegral n * incrementY) (0, y2 - 5) defFont
horzLine (25, 30) y2) [0..mxY]
horzLine (30, x1) 25
vertLine 30 (25, y1)
-- Draw graph
function (31, 26, x1, y1) (\c x y -> let
v = f (realign (31, fromIntegral (x1 - 10)) (nX, mX) (fromIntegral x))
aln = realign (nY, mY) (26, fromIntegral $ y1 - 10) v in
if y > floor aln then c else clrF v)
where
(mxX, incrementX) = maxOfRange (mX - nX)
(mxY, incrementY) = maxOfRange (mY - nY)
-- | This is for anything with text. Upon double-clicking on the object, it displays
-- an edit control (which can be dismissed by hitting ENTER).
editableText :: Hook (Appearance, Event (Appearance -> Appearance)) (Event Message) Act t ->
Hook (Appearance, Event (Appearance -> Appearance)) (Event Message) Act t
editableText h = do
wnd <- window
changes <- liftIO (chanSource defaultFrame)
(x, child) <- getReturns (mempty, fmap (\t a -> a { text = t }) (getEvent changes)) id h
tellReturn child
return x <> do
((w, txt), rect) <- lift $ liftE $ child >>= \w -> fmap return $ holdE (holdE (fmap (const w) $ filterE (\e -> case e of
Mouse _ Dbl _ -> True
_ -> False) (event w)) (texts (event w)) "")
(position (event w)) (0, 0, 0, 0)
liftIO $ showWindow (hwnd w) sW_HIDE
w2 <- hCreate' "EDIT" (Appearance mempty txt rect, mempty)
let finish = fmap (\(_, txt) -> destroyWindow (hwnd w2) >> return txt) $ once $ holdE (filterE (\e -> case e of
Key True key -> key == vK_RETURN
_ -> False) (event w2))
(texts (event w2)) txt
liftIO $ showWindow (hwnd w) sW_SHOW
lift $ liftS changes finish
mzero
data Pair t u = Pair t u deriving Show
instance (Ord t, Ord u, Bounded t, Bounded u) => Monoid (Pair t u) where
mempty = Pair minBound minBound
mappend (Pair a1 b1) (Pair a2 b2) = Pair (max a1 a2) (max b1 b2)
{-# INLINE convert #-}
convert reac ev = (mempty { text = "0,0,0" },
(\(pos, (rt, mx)) _ -> let (wid, ht) = dim rt in
Appearance (return ()) (show mx
++ ',' : show pos
++ ",0") rt)
<$>
snapshot ev reac)
{-# INLINE extent #-}
extent = monoid . fmap (stepper (Pair 0 0) . justE . fmap (\msg -> case msg of
Move (_, _, x, y) -> Just (Pair x y)
Close -> Just (Pair 0 0)
_ -> Nothing))
shiftControls :: Event (Int32, Int32)
-> Hook (Appearance, Event (Appearance -> Appearance)) (Event Message) Act t
-> Hook (Appearance, Event (Appearance -> Appearance)) (Event Message) Act (t, Event Window)
shiftControls displacement h = do
-- Add a hook to move controls around
-- and a hook to make this invisible to other components
let adjust = (\((x, y), (x2, y2)) appr ->
appr { rect = shift (x2 - x, y2 - y) (rect appr) })
<$>
withPrev (cons (0, 0) 0 displacement)
getReturns (mempty, adjust) id h
{-let adjust2 ev = (\(msg, (xPos, yPos)) -> case msg of
Move rt -> Move $ shift (xPos, yPos) rt
_ -> msg)
<$>
holdE
ev
(zipE (getChanges (event hw)) 0 (getChanges (event vw)) 0)
(0, 0)-}
-- | If some child controls are out of view,
-- adds scroll bars that allow the user to access them.
scroll :: Hook (Appearance, Event (Appearance -> Appearance)) (Event Message) Act t -> Hook (Appearance, Event (Appearance -> Appearance)) (Event Message) Act t
scroll h = do
wnd <- window
adjustStream <- liftIO $ chanSource defaultFrame
~(x, children) <- shiftControls (getEvent adjustStream) h
let changes = justE $ fmap (\msg -> case msg of Move rt -> Just (dim rt); _ -> Nothing) (event wnd)
let hDim = fmap (\(wid, ht) -> (0, ht - barSize, wid - barSize, ht)) changes
let vDim = fmap (\(wid, ht) -> (wid - barSize, 0, wid, ht - barSize)) changes
let ext = extent (fmap event children)
hw <- hCreate' "HSCROLLBAR" $ convert ((,)
<$> stepper empt hDim
<*> fmap (\(Pair x _) -> x) ext)
(fmap (\(_, _, x, _) -> x) $ position (event wnd))
vw <- hCreate' "SCROLLBAR" $ convert ((,)
<$> stepper empt vDim
<*> fmap (\(Pair _ x) -> x) ext)
(fmap (\(_, _, _, y) -> y) $ position (event wnd))
lift $ liftS adjustStream $ return <$> zipE (negate <$> getChanges (event hw)) 0 (negate <$> getChanges (event vw)) 0
return x where
empt = (0, 0, 0, 0)
getChanges = justE . fmap (\msg -> case msg of Change s -> Just (read s); _ -> Nothing)
barSize :: LONG
barSize = 18
theStyle h = getReturns (fillRect 0xbebed4 (0, 0, 32767, 32767)
>> mapM_ (\y -> mapM_ (\x -> blend (fromIntegral y / 200) (x, y) 0x404092) [0..199]) [0..399])
h