packages feed

reactivity-0.2.3.0: src/FRP/Reactivity/UI.hs

{-# LANGUAGE Trustworthy, ScopedTypeVariables, DeriveDataTypeable #-}

module FRP.Reactivity.UI (Btn(..), MouseEv(..), Message(..), Appearance(..), WndClass, isMouse, isKey, isChange, isMove, onMouse, onKey, onMove, Window(..), hwnd, event, desktop, dim, shift, inside, intersectRect, regulate, suspendUpdate, unsuspendUpdate, create, position, texts, mousePos, module FRP.Reactivity.Basic) where

import Data.XSizeable
import FRP.Reactivity.Combinators
import FRP.Reactivity.Basic
import FRP.Reactivity.Draw
import Data.IORef
import Data.Typeable
import Data.Monoid
import Data.Int
import Data.Bits hiding (shift)
import Data.Array.MArray
import qualified Data.Map as M
import Data.Char
import Foreign.Storable
import Foreign.Ptr
import Foreign.C.Types
import System.IO.Unsafe
import Unsafe.Coerce
import Graphics.Win32 hiding (fillRect)
import System.Win32
import Graphics.Win32Extras
import Graphics.Subclass
import Control.Monad
import Control.Monad.Loops
import Control.Monad.Reader
import Control.Exception
import Codec.BMP
import Prelude hiding (until)

import Data.Char

data Btn = L | R deriving (Eq, Show, Typeable)

data MouseEv = Down | Up | Dbl | Mv deriving (Eq, Show, Typeable)

data InternalMessage =
   IChange !String !HWND
 | IPush !HWND
 | IFocus !HWND
 | Destroy
 | Tick !HWND deriving (Eq, Show, Typeable)

data Message =
   Mouse !Btn !MouseEv !POINT
 | Key !Bool !VKey
 | Change !String
 | Push
 | Move !RECT
 | Close
 | Focus
 | Internal !InternalMessage deriving (Eq, Show, Typeable)

data Appearance = Appearance { draw :: !(Draw ()), text :: !String, rect :: !RECT } deriving Typeable

instance Show Appearance where
	showsPrec prec appr = showParen (prec == 11) $ ("Appearance _  "++)
		. showsPrec 11 (text appr)
		. (' ':)
		. showsPrec 11 (rect appr)

type WndClass = String

{-# INLINE isMouse #-}
isMouse (Mouse _ _ _) = True
isMouse _ = False

{-# INLINE isKey #-}
isKey (Key _ _) = True
isKey _ = False

{-# INLINE isChange #-}
isChange (Change _) = True
isChange _ = False

{-# INLINE isMove #-}
isMove (Move _) = True
isMove _ = False

{-# INLINE onMouse #-}
onMouse f ev = justE $ fmap (\m -> case m of Mouse b m p -> Just (f b m p); _ -> Nothing) ev

{-# INLINE onKey #-}
onKey f ev = justE $ fmap (\m -> case m of Key b n -> Just (f b n); _ -> Nothing) ev

{-# INLINE onMove #-}
onMove f ev = justE $ fmap (\m -> case m of Move rt -> Just (f rt); _ -> Nothing) ev

toWPARAM :: Ptr a -> WPARAM
toWPARAM = unsafeCoerce

toLPARAM :: Ptr a -> LPARAM
toLPARAM = unsafeCoerce

{-# NOINLINE ref #-}
ref :: IORef Appearance
ref = unsafePerformIO $ newIORef $ Appearance (return ()) "" (0, 0, 0, 0)

data Window = Window { hwnd :: !HWND, event :: Event Message } -- The type of windows.

-- | A window representing the desktop.
desktop = Window nullPtr mempty

-- | Dimensions of a rectangle.
dim (x1, y1, x2, y2) = (x2 - x1, y2 - y1)

-- | Shift a rectangle.
shift (x, y) (x1, y1, x2, y2) = (x1 + x, y1 + y, x2 + x, y2 + y)

-- | Membership test for rectangles.
inside (x1, y1, x2, y2) = inRange ((x1, y1), (x2, y2))

-- | Intersection of two rectangles.
intersectRect (x1, y1, x2, y2) (x1a, y1a, x2a, y2a) = (x1a `max` x1, y1a `max` y1, x2a `min` x2, y2a `min` y2)

{-# INLINE regulate #-}
-- | Under some circumstances, such as video playback, it can be undesirable to buffer
--   up many frames and play them back. In order to avoid time leaks, the program
--   can drop frames.
regulate :: Event t -> Act (Event t)
regulate e = liftM justE $ allOccs (corecA (\t1 x t -> do
	t1' <- getPOSIXTime
	if t >= fromRational (toRational (t1 - start)) then
			return (t1', Just x, t)
		else
			return (t1, Nothing, t))
	0
	e)

{-# NOINLINE suspendedUpdateWindows #-}
suspendedUpdateWindows :: IORef (M.Map HWND ())
suspendedUpdateWindows = unsafePerformIO (newIORef M.empty)

-- | The "built-in suspending mechanism" prevents interference between the reactive behavior
--   of a window and user input to that window.
--   The behavior is as follows:
--     * When the user moves/drags a window, its position stops reflecting the position
--       in its reactive behavior.
--     * When the user enters text in a text box, its text stops reflecting the text
--       of the reactive behavior.
--     * When the user moves a scroll bar, its position stops reflecting that
--       of the reactive behavior.
--
--   'suspendUpdate' suspends the update of a window. Unlike the built-in mechanism
--   for suspending, this suspends drawing on the surface of the window as well.
suspendUpdate :: HWND -> IO ()
suspendUpdate hwnd = modifyIORef' suspendedUpdateWindows (M.insert hwnd ())

-- | Undoes any suspending of the window. This affects both 'suspendUpdate' and
--   the built-in suspending mechanism.
unsuspendUpdate :: HWND -> IO ()
unsuspendUpdate hwnd = modifyIORef' suspendedUpdateWindows (M.delete hwnd)

create :: Window -> WndClass -> Behavior Appearance -> Act Window
create (Window parent parentEv) cls beh = do
	(wnd, ev) <- liftE $ return $ createWnd parent cls
	let ev' = return (Internal (Tick nullPtr)) <> if isFrame cls then
			ev
		else
			ev <> routeChildEvents wnd parentEv
	ev'' <- return (fmap (const Focus) (getEvent ticks) <> ev')
	let ups = updates cls beh ev''
	liftS nil $ fmap (\tup@((_, app), _) -> do -- Do updates
			writeIORef ref app
			update wnd cls tup)
		ups
	return (Window wnd ev') -- Return the event stream

{-# INLINE routeChildEvents #-}
routeChildEvents wnd = fmap (\msg -> case msg of
	Internal (IChange s hwnd) -> if wnd == hwnd then Change s else msg
	Internal (IPush hwnd) -> if wnd == hwnd then Push else msg
	Internal (IFocus hwnd) -> if wnd == hwnd then Focus else msg
	_ -> Internal (Tick nullPtr))

read' x = if all isDigit x then read x else unsafePerformIO (print x >> return 0)

update wnd cls ((msg, Appearance draw text (x, y, xx, yy)), suspend) = do
	mp <- readIORef suspendedUpdateWindows
	unless (M.member wnd mp) (catch (do
		let wid = xx - x
		let ht = yy - y
		-- Size the window
		(x1, y1, x2, y2) <- getWindowRect wnd
		unless (isMove msg) $ if isFrame cls then unless suspend $ do
				(_, _, clientWd, clientHt) <- getClientRect wnd
				unless (fromIntegral clientWd == wid && fromIntegral clientHt == ht) $
					setWindowPos wnd
						nullPtr
						(fromIntegral x1)
						(fromIntegral y1)
						(fromIntegral (x2 - x1 - clientWd) + fromIntegral wid)
						(fromIntegral (y2 - y1 - clientHt) + fromIntegral ht)
						sWP_NOZORDER
			else unless (x1 == fromIntegral x && y1 == fromIntegral y && x2 == fromIntegral xx && y2 == fromIntegral yy) $
				setWindowPos wnd
					nullPtr
					(fromIntegral x)
					(fromIntegral y)
					(fromIntegral wid)
					(fromIntegral ht)
					sWP_NOZORDER

		-- Set scrollbar direction
		when (map toUpper cls == "SCROLLBAR") $ void $ setWindowLong wnd gWLP_STYLE $ fromIntegral $ wS_VISIBLE .|. wS_CHILDWINDOW .|. if wid > ht then sBS_HORZ else sBS_VERT

		-- Set the text
		case map toUpper cls of
			"SCROLLBAR" -> do
				let (mx, _:rest) = break (==',') text
				let (page, _:pos) = break (==',') rest
				old <- getScrollInfo wnd (fromIntegral sB_CTL)
				let mask = sIF_RANGE .|. sIF_POS .|. if suspend then 0 else sIF_POS
				let new = SCROLLINFO mask 0 (read' mx) (read' page) (read' pos) 0
				unless (old { fMask = mask } == new) $ void $ setScrollInfo wnd (fromIntegral sB_CTL) new
			_ -> do
				prev <- getText wnd
				when (isFrame cls || not suspend && prev /= text) $ setWindowText wnd text

		-- Repaint the window
		invalidateRect (Just wnd) Nothing True
		updateWindow wnd)
		(\(_ :: IOError) -> return ()))

-- | The rectangle of a window from its message stream.
{-# INLINE position #-}
position ev = corec (\rt msg t -> let rt' = case msg of Move rt -> rt; _ -> rt in
	(rt', rt', t)) (0, 0, 0, 0) ev

{-# INLINE _texts #-}
_texts ev = corec (\s pr@(msg, _) t -> let s' = case msg of Change s -> s; _ -> s in
	(s', (pr, s'), t)) "" ev

-- | The text of a window from its message stream.
{-# INLINE texts #-}
texts ev = fmap snd $ _texts $ fmap (\msg -> (msg, Appearance (return ()) "" (0, 0, 1, 1))) ev

-- | The position of the mouse.
{-# INLINE mousePos #-}
mousePos ev = corec (\pt msg t -> let pt' = case msg of Mouse _ Mv pt -> pt; _ -> pt in
	(pt', pt', t)) (0, 0) ev

-- Rules out Change messages caused by behavior updates.
{-# INLINE behFree #-}
behFree ev = corec (\((prevMsg, _), b) (pr@(_, app), txt) t -> let pr' = (pr, text app == txt && (isChange prevMsg || b)) in
	(pr', pr', t))
	((Focus, undefined), True)
	$ _texts ev

{-# INLINE userChange #-}
userChange cls ev = if isFrame cls then
		filterE (\(msg, _) -> isMove msg) ev
	else
		fmap fst $ filterE (\((msg, _), b) -> isChange msg && b) (behFree ev)

{-# INLINE suspend #-}
suspend cls ev = flipFlop (userChange cls ev) mempty

-- Sample a behavior. This also takes care of suspending behaviors.
{-# INLINE updates #-}
updates :: WndClass -> Behavior Appearance -> Event Message -> Event ((Message, Appearance), Bool)
updates cls beh ev = snapshot samp (suspend cls samp) where
	ev' = ev `untilE` filterE (\m -> case m of Internal Destroy -> True; _ -> False) ev -- The event stream should not deliver messages after Destroy is delivered.
	samp = snapshot ev' beh

getText wnd = catch (getWindowText wnd) (\(_ :: IOError) -> return "")

createWnd parent cls = do
	-- Make event fed by the window
	Stream sink ev <- chanSource defaultFrame
	bmp :: SA Int32 CChar <- newArray_ (0, -1)
	let handler proc wnd msg wParam lParam = do
		may <- mapMessage (if isFrame cls then nullPtr else parent) wnd msg wParam lParam

		maybe
			(do
			app <- readIORef ref
			(_, _, wid, ht) <- getClientRect wnd
			let sz = 4 * wid * ht
			resize (0, sz - 1) bmp
			(_, p) <- getPtr bmp
			memset p 0 (fromIntegral sz)
			runReaderT (unDraw (fillRect defBackground (0, 0, wid, ht) >> draw app)) ((wid, ht), p, Nothing)
			if msg == wM_PAINT && isFrame cls then
				allocaPAINTSTRUCT $ \ps -> do
					hdc <- beginPaint wnd ps
					drawBitmap wnd hdc (fromIntegral wid) (fromIntegral ht) p
					endPaint wnd ps
				else if msg == wM_DRAWITEM then void $ do
					(wnd, hdc) <- catch (liftM2 (,) (peekByteOff (unsafeCoerce lParam) 20) (peekByteOff (unsafeCoerce lParam) 24)) (\(_ :: IOException) -> return (nullPtr, nullPtr))
					drawBitmap wnd hdc (fromIntegral wid) (fromIntegral ht) p
					drawText hdc (text app) (0, 0, wid, ht) (dT_VCENTER .|. dT_SINGLELINE)
				else when (msg == wM_CTLCOLORSTATIC) $ void $ setBkMode (unsafeCoerce wParam) tRANSPARENT)
			(\x -> do
				print x
				sink x
				case x of
					Mouse _ Up pt -> when (map toUpper cls == "BUTTON") $ do
						rt <- getClientRect wnd
						when (inside rt pt) (sink Push)
					_ -> return ())
			may

		-- Take care of mouse capture
		if msg `elem` [wM_LBUTTONDOWN, wM_RBUTTONDOWN] then
				void $ setCapture wnd
			else when (msg `elem` [wM_LBUTTONUP, wM_RBUTTONUP]) $ void releaseCapture

		-- An application can't be expected to respond to WM_ENDSESSION immediately,
		-- therefore it holes up in an event loop here, waiting for approval
		-- from the app to close.
		-- !!! Must do everything here that is done in event loop
		when (msg == wM_ENDSESSION) $ allocaMessage $ \msg ->
			untilM_
				(return ())
				(do
					getMessage msg Nothing
					translateMessage msg
					dispatchMessage msg
					hwnd <- peekByteOff msg 0
					msgN <- peekByteOff msg 4
					return $ hwnd == wnd && msgN == wM_DESTROY)

		if msg == wM_CLOSE || msg == wM_DRAWITEM then
			return 0
			else
			proc wnd msg wParam lParam

	-- Create the window
	hdl <- getModuleHandle Nothing
	let name = mkClassName cls
	wnd <- createWindow name "" (wS_CLIPCHILDREN .|. wS_VISIBLE .|. case map toUpper cls of
		"FRAME" -> wS_OVERLAPPEDWINDOW
		"STATIC" -> wS_CHILDWINDOW .|. sS_OWNERDRAW
		"EDIT" -> wS_CHILDWINDOW .|. eS_AUTOHSCROLL
		"SCROLLBAR" -> wS_CHILDWINDOW .|. sBS_HORZ
		_ -> wS_CHILDWINDOW) (Just 0) (Just 0) (Just 0) (Just 0) (Just parent) Nothing hdl (handler (defWindowProc . Just))
	updateWindow wnd

	if isFrame cls then void $ sendMessage wnd wM_ACTIVATE 0 0
		else void $ do
		-- Set the font
		font <- createFont 16 0 0 0 fW_NORMAL False False False dEFAULT_CHARSET 0 0 dEFAULT_QUALITY fF_DONTCARE "Tahoma"
		sendMessage wnd wM_SETFONT (toWPARAM font) 0

		subclassProc wnd handler

		-- Send a create message to the parent on this control's behalf
		postMessage parent wM_APP (toWPARAM wnd) 0

	return (wnd, ev)

isFrame cls = cls == "Frame"

mapMessage parent hwnd msg wParam lParam = if msg `elem` [wM_LBUTTONUP, wM_LBUTTONDOWN, wM_RBUTTONUP, wM_RBUTTONDOWN, wM_LBUTTONDBLCLK, wM_RBUTTONDBLCLK, wM_MOUSEMOVE] then
		return $ Just $ Mouse
			(if msg `elem` [wM_LBUTTONDOWN, wM_LBUTTONUP, wM_LBUTTONDBLCLK] then L else R)
			(if msg `elem` [wM_LBUTTONDOWN, wM_RBUTTONDOWN] then
				Down
			else if msg `elem` [wM_LBUTTONDBLCLK, wM_RBUTTONDBLCLK] then
				Dbl
			else if msg == wM_MOUSEMOVE then
				Mv
			else
				Up)
			(fromIntegral (fromIntegral (loWord lParam) :: Int16) :: Int32, hiWord lParam)
	else if msg `elem` [wM_KEYDOWN, wM_KEYUP] then
		return $ Just $ Key (msg == wM_KEYDOWN) wParam
	else if msg == wM_COMMAND && hiWord wParam `elem` [eN_UPDATE, cBN_EDITUPDATE, cBN_SELENDOK] then
		liftM (\s -> Just $ Internal $ IChange s (unsafeCoerce lParam)) $ getText $ unsafeCoerce lParam
	else if msg == wM_COMMAND && hiWord wParam == bN_CLICKED then
		return $ Just $ Internal $ IPush (unsafeCoerce lParam)
	else if msg `elem` [wM_HSCROLL, wM_VSCROLL] then do
		si <- getScrollInfo (unsafeCoerce lParam) (fromIntegral sB_CTL)
		let newY = if loWord wParam == sB_LINEDOWN then
				nPos si + 50
			else if loWord wParam == sB_LINEUP then
				nPos si - 50
			else if loWord wParam == sB_PAGEDOWN then
				nPos si + fromIntegral (nPage si)
			else if loWord wParam == sB_PAGEUP then
				nPos si - fromIntegral (nPage si)
			else if loWord wParam == sB_THUMBTRACK then
				maxBound
			else if loWord wParam == sB_THUMBPOSITION then
				nTrackPos si
			else
				nPos si
		if newY == maxBound then
			return Nothing
			else do
				setScrollInfo (unsafeCoerce lParam) (fromIntegral sB_CTL) (si { nPos = newY })
				return $ Just $ Internal $ IChange (show $ max 0 (min newY (nMax si - fromIntegral (nPage si)))) (unsafeCoerce lParam)
	else if msg == wM_WINDOWPOSCHANGED then do
		rt@(x1, y1, x2, y2) <- getWindowRect hwnd
		liftM (Just . Move) $ if parent == nullPtr then
				return rt
			else do
				(cx, cy) <- clientToScreen parent (0, 0)
				return (shift (-cx, -cy) rt)
	else if msg == wM_ACTIVATE && wParam == 1 then
		return $ Just Focus
	else if msg == wM_SETFOCUS then
		return $ Just $ Internal $ IFocus (unsafeCoerce wParam)
	else if msg == wM_APP then
		return $ Just $ Internal $ Tick (unsafeCoerce wParam)
	else if msg `elem` [wM_CLOSE, wM_ENDSESSION] then
		return $ Just Close
	else if msg == wM_DESTROY then
		return $ Just $ Internal Destroy
	else
		return Nothing