{- ****************************************************************************** * I N V A D E R S * * * * Module: Parser * * Purpose: Parsing (mainly lexical analysis) of window event * * stream. * * Author: Henrik Nilsson * * * * Copyright (c) Yale University, 2003 * * * ****************************************************************************** -} -- Quick 'n dirty adaptation from old robot simulator. Could probably be -- done better in the new AFRP framework. module Parser ( GameInput, -- Abstract parseWinInput, -- :: SF WinInput GameInput command, -- :: SF GameInput (Event Command) cmdString, -- :: SF GameInput (Event String) ptrPos, -- :: SF GameInput Position2 lbp, -- :: SF GameInput (Event ()) lbpPos, -- :: SF GameInput (Event Position2) lbDown, -- :: SF GameInput Bool rbp, -- :: SF GameInput (Event ()) rbpPos, -- :: SF GameInput (Event Position2) rbDown, -- :: SF GameInput Bool dragStart, -- :: SF GameInput (Event ()) dragStop, -- :: SF GameInput (Event Distance2) dragStartPos, -- :: SF GameInput Position2 dragVec, -- :: SF GameInput Distance2 dragging -- :: SF GameInput Bool ) where import Data.AffineSpace (origin, (.-.)) import Data.Maybe (isNothing, isJust) import qualified Graphics.HGL as HGL (Event(..)) import Data.Char (ord, isSpace, isDigit) import FRP.Yampa import PhysicalDimensions import WorldGeometry (gPointToPosition2) import Command import Animate (WinInput) ------------------------------------------------------------------------------ -- Exported entities ------------------------------------------------------------------------------ data GameInput = GameInput { giCmdStr :: String, giCmd :: Event Command, giPDS :: PDState } parseWinInput :: SF WinInput GameInput parseWinInput = wiToCmd &&& wiToPDS >>^ \((cmdStr, cmd), pds) -> GameInput {giCmdStr = cmdStr, giCmd = cmd, giPDS = pds} -- All event sources below are defined such that they will NOT occur at local -- time 0 (immediately after a switch). Sometimes explicitly using a "notYet". -- Sometimes using through careful use of "edge" and relatives. Is this the -- right approach? -- A valid command has been read. command :: SF GameInput (Event Command) command = giCmd ^>> notYet -- Continuous parser feed back. cmdString :: SF GameInput String cmdString = arr giCmdStr ptrPos :: SF GameInput Position2 ptrPos = arr (pdsPos . giPDS) lbp :: SF GameInput (Event ()) lbp = lbpPos >>^ (`tag` ()) lbpPos :: SF GameInput (Event Position2) lbpPos = (giPDS >>> pdsLeft) ^>> edgeJust lbDown :: SF GameInput Bool lbDown = arr (giPDS >>> pdsLeft >>> isJust) rbp :: SF GameInput (Event ()) rbp = rbpPos >>^ (`tag` ()) rbpPos :: SF GameInput (Event Position2) rbpPos = (giPDS >>> pdsRight) ^>> edgeJust rbDown :: SF GameInput Bool rbDown = arr (giPDS >>> pdsRight >>> isJust) dragStart :: SF GameInput (Event ()) dragStart = (giPDS >>> pdsDrag) ^>> edgeBy detectStart (Just undefined) where detectStart Nothing (Just _) = Just () detectStart _ _ = Nothing dragStop :: SF GameInput (Event Distance2) dragStop = ((giPDS >>> pdsDrag) ^>> edgeBy detectStop Nothing) &&& dragVec >>^ \(e, dv) -> e `tag` dv where detectStop (Just _) Nothing = Just () detectStop _ _ = Nothing -- (Last) drag start position. dragStartPos :: SF GameInput Position2 dragStartPos = arr (giPDS >>> pdsDragStartPos) -- (Last) drag vector. dragVec :: SF GameInput Distance2 dragVec = arr (giPDS >>> pdsDragVec) dragging :: SF GameInput Bool dragging = arr (giPDS >>> pdsDrag >>> isJust) ------------------------------------------------------------------------------ -- Lexical analysis of character input ------------------------------------------------------------------------------ -- Currently overkill, but being able to enter multi-character commands -- could possibly be useful. wiToCmd :: SF WinInput (String, Event Command) wiToCmd = arr (mapFilterE selChar) >>> (accumBy scanChar (undefined,scanCmds) >>^ fmap fst >>^ splitE) >>> hold "" *** arr (mapFilterE id) where scanChar (_, S cont) c = cont c selChar (HGL.Char {HGL.char=c}) = Just c selChar _ = Nothing -- This ought to be redone. Kont should probably be called Tranition or -- somethinig. -- We define a continuation to be the command recognized thus far (a String -- and maybe a complete Command), and a scanner to be applied to the rest -- of the input. (I.e., there's output at every step.) type Kont = ((String, Maybe Command), Scanner) type Cont a = a -> Kont -- Since a scanner is applied to one character at a time (typically, on -- Char events), we recursively define a scanner to be a character -- continuation. newtype Scanner = S (Cont Char) -- Scan commands scanCmds :: Scanner scanCmds = scanCmd cmds where cmds = [ ("q", emitCmd scanCmds CmdQuit), -- Discard inp.? ("p", emitCmd scanCmds CmdNewGame), ("f", emitCmd scanCmds CmdFreeze), ("r", emitCmd scanCmds CmdResume) ] -- Scan one command. -- Looks for a valid command. Outputs prefix as long as the current -- prefix is valid. Starts over on first invalid character. Invokes success -- continuation on success. -- cmds ....... List of pairs of valid command and corresponding success -- continuation. scanCmd :: [(String, Cont String)] -> Scanner scanCmd cmds = scanSubCmd "" cmds -- Scan one subcommand/keyword argument. -- Looks for a valid command. Outputs prefix as long as the current -- prefix is valid. Starts over on first invalid character. Invokes success -- continuation on success. -- pfx0 ....... Initial prefix. -- cmds ....... List of pairs of valid command and corresponding success -- continuation. scanSubCmd :: String -> [(String, Cont String)] -> Scanner scanSubCmd pfx0 cmds = S (scHlp pfx0 cmds) where -- pfx ........ Command prefix. -- sfxconts ... Command suffixes paired with success continuations. -- c .......... Input character. scHlp pfx sfxconts c = case c of '\r' -> case [ cont | ("", cont) <- sfxconts ] of [] -> emitPfx (S (scHlp pfx sfxconts)) pfx (cont : _) -> cont pfx '.' -> case sfxconts of [] -> emitPfx (S (scHlp pfx0 cmds)) pfx0 [(sfx, cont)] -> cont (pfx ++ sfx) _ -> let (sfxs, conts) = unzip sfxconts cpfx = foldr1 lcp sfxs sfxs' = map (drop (length cpfx)) sfxs pfx' = pfx ++ cpfx sfxconts' = zip sfxs' conts in emitPfx (S (scHlp pfx' sfxconts')) pfx' _ -> let pfx' = pfx ++ [c] sfxconts' = [ (tail sfx, cont) | (sfx, cont) <- sfxconts, not (null sfx) && head sfx == c ] in case sfxconts' of [] -> emitPfx (S (scHlp pfx0 cmds)) pfx0 -- ("Invalid: " ++ [c]) [("", cont)] -> cont pfx' _ -> emitPfx (S (scHlp pfx' sfxconts')) pfx' -- Scan fixed-length integer argument. -- pfx0 ....... Initial prefix (command scanned thus far). -- n0 ......... Maximal number of digits. -- cont ....... Continuation: will be passed the new prefix and the -- integer value of the scanned argument. scanIntegerArg :: String -> Int -> Cont (String,Integer) -> Scanner scanIntegerArg pfx0 n0 cont | n0 > 0 = S (siaHlp (pfx0 ++ " ") n0 0) where siaHlp pfx n a c = if c == '\r' then cont (pfx, a) else if isDigit c then let a' = a * 10 + fromIntegral (ord c - ord '0') pfx' = pfx ++ [c] in if n > 1 then emitPfx (S (siaHlp pfx' (n - 1) a')) pfx' else cont (pfx', a') else emitPfx (S (siaHlp (pfx0 ++ " ") n0 0)) pfx0 -- Scan variable-length string argument. -- pfx0 ....... Initial prefix (command scanned thus far). -- cont ....... Continuation: will be passed the new prefix and the -- string value of the scanned argument. scanStringArg :: String -> Cont (String,String) -> Scanner scanStringArg pfx0 cont = S (ssaHlp (pfx0 ++ " ") "") where ssaHlp pfx a c = if c == '\r' then cont (pfx, a) else let a' = dropWhile isSpace $ a ++ [c] pfx' = pfx ++ [c] in emitPfx (S (ssaHlp pfx' a')) pfx' -- Emit command (and command string), then continue scanning. emitCmd :: Scanner -> Command -> String -> Kont emitCmd scanner cmd cmdStr = ((cmdStr, Just cmd), scanner) -- Emit current prefix, then scan next character. emitPfx :: Scanner -> String -> Kont emitPfx scanner pfx = ((pfx, Nothing), scanner) ------------------------------------------------------------------------------ -- Pointing device processing ------------------------------------------------------------------------------ -- State of the pointing device. -- The points for pdsLeft, pdsRight, and pdsDrag reflect where the button -- was initially pressed. data PDState = PDState { pdsPos :: Position2, -- Current position. pdsDragStartPos :: Position2, -- (Last) drag start position. pdsDragVec :: Distance2, -- (Latest) drag vector. pdsLeft :: Maybe Position2, -- Left button currently down. pdsRight :: Maybe Position2, -- Right button currently down. pdsDrag :: Maybe Position2 -- Currently dragging. } -- Initial state. initPDS = PDState { pdsPos = origin, pdsDragStartPos = origin, pdsDragVec = zeroVector, pdsLeft = Nothing, pdsRight = Nothing, pdsDrag = Nothing } wiToPDS :: SF WinInput PDState wiToPDS = accumHoldBy nextPDS initPDS -- Compute next pointing device state. nextPDS :: PDState -> HGL.Event -> PDState nextPDS pds (HGL.Key {}) = pds -- Currently we ignore keys. nextPDS pds (HGL.Button {HGL.pt = p, HGL.isLeft = True, HGL.isDown = True}) = -- Left button pressed. pds {pdsPos = p', pdsDragVec = dv, pdsLeft = Just p'} where p' = gPointToPosition2 p dv = maybe (pdsDragVec pds) (\dspos -> p' .-. dspos) (pdsDrag pds) nextPDS pds (HGL.Button {HGL.pt = p, HGL.isLeft = True, HGL.isDown = False}) = -- Left button released. pds {pdsPos = p', pdsDragVec = dv, pdsLeft = Nothing, pdsDrag = md} where p' = gPointToPosition2 p md = maybe Nothing (const (pdsDrag pds)) (pdsRight pds) dv = maybe (pdsDragVec pds) (\dspos -> p' .-. dspos) md nextPDS pds (HGL.Button {HGL.pt = p, HGL.isLeft = False, HGL.isDown = True}) = -- Right button pressed. pds {pdsPos = p', pdsDragVec = dv, pdsRight = Just p'} where p' = gPointToPosition2 p dv = maybe (pdsDragVec pds) (\dspos -> p' .-. dspos) (pdsDrag pds) nextPDS pds (HGL.Button {HGL.pt = p, HGL.isLeft = False, HGL.isDown = False}) = -- Right button released. pds {pdsPos = p', pdsDragVec = dv, pdsRight = Nothing, pdsDrag = md} where p' = gPointToPosition2 p md = maybe Nothing (const (pdsDrag pds)) (pdsLeft pds) dv = maybe (pdsDragVec pds) (\dspos -> p' .-. dspos) md nextPDS pds (HGL.MouseMove {HGL.pt = p}) = -- Mouse move. pds {pdsPos = p', pdsDragStartPos = dsp, pdsDragVec = dv, pdsDrag = md} where p' = gPointToPosition2 p md = case pdsLeft pds of mlp@(Just _) -> mlp Nothing -> pdsRight pds dsp = maybe (pdsDragStartPos pds) id md dv = maybe (pdsDragVec pds) (\dspos -> p' .-. dspos) md nextPDS pds _ = pds -- Ignore unknown events. ------------------------------------------------------------------------------ -- General utilities ------------------------------------------------------------------------------ -- Longest common prefix. lcp :: Eq a => [a] -> [a] -> [a] lcp [] _ = [] lcp _ [] = [] lcp (x:xs) (y:ys) | x == y = x : lcp xs ys | otherwise = []