{-# LANGUAGE PatternGuards #-} module Graphics.Gloss.Data.ViewState ( Command (..) , CommandConfig , defaultCommandConfig , ViewState (..) , viewStateInit , viewStateInitWithConfig , updateViewStateWithEvent , updateViewStateWithEventMaybe) where import Graphics.Gloss.Data.Vector import Graphics.Gloss.Data.ViewPort import Graphics.Gloss.Geometry.Angle import Graphics.Gloss.Internals.Interface.Backend import Graphics.Gloss.Internals.Interface.Event import qualified Data.Map as Map import Data.Map (Map) import Data.Maybe import Control.Monad (mplus) import qualified Graphics.Gloss.Data.Point.Arithmetic as Pt -- | The commands suported by the view controller. data Command = CRestore | CTranslate | CRotate | CScale -- bump zoom | CBumpZoomOut | CBumpZoomIn -- bump translate | CBumpLeft | CBumpRight | CBumpUp | CBumpDown -- bump rotate | CBumpClockwise | CBumpCClockwise deriving (Show, Eq, Ord) type CommandConfig = [(Command, [(Key, Maybe Modifiers)])] -- | The default commands. Left click pans, wheel zooms, right click -- rotates, "r" key resets. defaultCommandConfig :: CommandConfig defaultCommandConfig = [ (CRestore, [ (Char 'r', Nothing) ]) , (CTranslate, [ ( MouseButton LeftButton , Just (Modifiers { shift = Up, ctrl = Up, alt = Up })) ]) , (CScale, [ ( MouseButton LeftButton , Just (Modifiers { shift = Up, ctrl = Down, alt = Up })) , ( MouseButton RightButton , Just (Modifiers { shift = Up, ctrl = Up, alt = Up })) ]) , (CRotate, [ ( MouseButton LeftButton , Just (Modifiers { shift = Up, ctrl = Up, alt = Down })) , ( MouseButton RightButton , Just (Modifiers { shift = Up, ctrl = Down, alt = Up })) ]) -- bump zoom , (CBumpZoomOut, [ (MouseButton WheelDown, Nothing) , (SpecialKey KeyPageDown, Nothing) ]) , (CBumpZoomIn, [ (MouseButton WheelUp, Nothing) , (SpecialKey KeyPageUp, Nothing)] ) -- bump translate , (CBumpLeft, [ (SpecialKey KeyLeft, Nothing) ]) , (CBumpRight, [ (SpecialKey KeyRight, Nothing) ]) , (CBumpUp, [ (SpecialKey KeyUp, Nothing) ]) , (CBumpDown, [ (SpecialKey KeyDown, Nothing) ]) -- bump rotate , (CBumpClockwise, [ (SpecialKey KeyHome, Nothing) ]) , (CBumpCClockwise, [ (SpecialKey KeyEnd, Nothing) ]) ] -- | Check if the provided key combination is some gloss viewport command. isCommand :: Map Command [(Key, Maybe Modifiers)] -> Command -> Key -> Modifiers -> Bool isCommand commands c key keyMods | Just csMatch <- Map.lookup c commands = or $ map (isCommand2 c key keyMods) csMatch | otherwise = False -- | Check if the provided key combination is some gloss viewport command. isCommand2 :: Command -> Key -> Modifiers -> (Key, Maybe Modifiers) -> Bool isCommand2 _ key keyMods cMatch | (keyC, mModsC) <- cMatch , keyC == key , case mModsC of Nothing -> True Just modsC -> modsC == keyMods = True | otherwise = False -- ViewControl State ----------------------------------------------------------- -- | State for controlling the viewport. -- These are used by the viewport control component. data ViewState = ViewState { -- | The command list for the viewport controller. -- These can be safely overwridden at any time by deleting -- or adding entries to the list. -- Entries at the front of the list take precedence. viewStateCommands :: !(Map Command [(Key, Maybe Modifiers)]) -- | How much to scale the world by for each step of the mouse wheel. , viewStateScaleStep :: !Float -- | How many degrees to rotate the world by for each pixel of x motion. , viewStateRotateFactor :: !Float -- | Ratio to scale the world by for each pixel of y motion. , viewStateScaleFactor :: !Float -- | During viewport translation, -- where the mouse was clicked on the window to start the translate. , viewStateTranslateMark :: !(Maybe (Float, Float)) -- | During viewport rotation, -- where the mouse was clicked on the window to starte the rotate. , viewStateRotateMark :: !(Maybe (Float, Float)) -- | During viewport scale, -- where the mouse was clicked on the window to start the scale. , viewStateScaleMark :: !(Maybe (Float, Float)) -- | The current viewport. , viewStateViewPort :: ViewPort } -- | The initial view state. viewStateInit :: ViewState viewStateInit = viewStateInitWithConfig defaultCommandConfig -- | Initial view state, with user defined config. viewStateInitWithConfig :: CommandConfig -> ViewState viewStateInitWithConfig commandConfig = ViewState { viewStateCommands = Map.fromList commandConfig , viewStateScaleStep = 0.85 , viewStateRotateFactor = 0.6 , viewStateScaleFactor = 0.01 , viewStateTranslateMark = Nothing , viewStateRotateMark = Nothing , viewStateScaleMark = Nothing , viewStateViewPort = viewPortInit } -- | Apply an event to a `ViewState`. updateViewStateWithEvent :: Event -> ViewState -> ViewState updateViewStateWithEvent ev viewState = fromMaybe viewState $ updateViewStateWithEventMaybe ev viewState -- | Like 'updateViewStateWithEvent', but returns 'Nothing' if no update -- was needed. updateViewStateWithEventMaybe :: Event -> ViewState -> Maybe ViewState updateViewStateWithEventMaybe (EventKey key keyState keyMods pos) viewState | isCommand commands CRestore key keyMods , keyState == Down = Just $ viewState { viewStateViewPort = viewPortInit } | isCommand commands CBumpZoomOut key keyMods , keyState == Down = Just $ controlZoomIn viewState | isCommand commands CBumpZoomIn key keyMods , keyState == Down = Just $ controlZoomOut viewState | isCommand commands CBumpLeft key keyMods , keyState == Down = Just $ viewState { viewStateViewPort = motionBump port (20, 0) } | isCommand commands CBumpRight key keyMods , keyState == Down = Just $ viewState { viewStateViewPort = motionBump port (-20, 0) } | isCommand commands CBumpUp key keyMods , keyState == Down = Just $ viewState { viewStateViewPort = motionBump port (0, -20) } | isCommand commands CBumpDown key keyMods , keyState == Down = Just $ viewState { viewStateViewPort = motionBump port (0, 20) } | isCommand commands CBumpClockwise key keyMods , keyState == Down = Just $ viewState { viewStateViewPort = port { viewPortRotate = viewPortRotate port + 5 } } | isCommand commands CBumpCClockwise key keyMods , keyState == Down = Just $ viewState { viewStateViewPort = port { viewPortRotate = viewPortRotate port - 5 } } -- Start Translation. | isCommand commands CTranslate key keyMods , keyState == Down , not $ currentlyRotating || currentlyScaling = Just $ viewState { viewStateTranslateMark = Just pos } -- Start Rotation. | isCommand commands CRotate key keyMods , keyState == Down , not $ currentlyTranslating || currentlyScaling = Just $ viewState { viewStateRotateMark = Just pos } -- Start Scale. | isCommand commands CScale key keyMods , keyState == Down , not $ currentlyTranslating || currentlyRotating = Just $ viewState { viewStateScaleMark = Just pos } -- Kill current translate/rotate/scale command when the mouse button -- is released. | keyState == Up = let killTranslate vs = vs { viewStateTranslateMark = Nothing } killRotate vs = vs { viewStateRotateMark = Nothing } killScale vs = vs { viewStateScaleMark = Nothing } in Just $ (if currentlyTranslating then killTranslate else id) $ (if currentlyRotating then killRotate else id) $ (if currentlyScaling then killScale else id) $ viewState | otherwise = Nothing where commands = viewStateCommands viewState port = viewStateViewPort viewState currentlyTranslating = isJust $ viewStateTranslateMark viewState currentlyRotating = isJust $ viewStateRotateMark viewState currentlyScaling = isJust $ viewStateScaleMark viewState -- Note that only a translation or rotation applies, not both at the same time. updateViewStateWithEventMaybe (EventMotion pos) viewState = motionScale (viewStateScaleMark viewState) pos viewState `mplus` motionTranslate (viewStateTranslateMark viewState) pos viewState `mplus` motionRotate (viewStateRotateMark viewState) pos viewState updateViewStateWithEventMaybe (EventResize _) _ = Nothing -- | Zoom in a `ViewState` by the scale step. controlZoomIn :: ViewState -> ViewState controlZoomIn viewState@ViewState { viewStateViewPort = port , viewStateScaleStep = scaleStep } = viewState { viewStateViewPort = port { viewPortScale = viewPortScale port / scaleStep } } -- | Zoom out a `ViewState` by the scale step. controlZoomOut :: ViewState -> ViewState controlZoomOut viewState@ViewState { viewStateViewPort = port , viewStateScaleStep = scaleStep } = viewState { viewStateViewPort = port { viewPortScale = viewPortScale port * scaleStep } } -- | Offset a viewport. motionBump :: ViewPort -> (Float, Float) -> ViewPort motionBump port@ViewPort { viewPortTranslate = trans , viewPortScale = scale , viewPortRotate = r } (bumpX, bumpY) = port { viewPortTranslate = trans Pt.- o } where offset = (bumpX / scale, bumpY / scale) o = rotateV (degToRad r) offset -- | Apply a translation to the `ViewState`. motionTranslate :: Maybe (Float, Float) -- Location of first mark. -> (Float, Float) -- Current position. -> ViewState -> Maybe ViewState motionTranslate Nothing _ _ = Nothing motionTranslate (Just (markX, markY)) (posX, posY) viewState = Just $ viewState { viewStateViewPort = port { viewPortTranslate = trans Pt.- o } , viewStateTranslateMark = Just (posX, posY) } where port = viewStateViewPort viewState trans = viewPortTranslate port scale = viewPortScale port r = viewPortRotate port dX = markX - posX dY = markY - posY offset = (dX / scale, dY / scale) o = rotateV (degToRad r) offset -- | Apply a rotation to the `ViewState`. motionRotate :: Maybe (Float, Float) -- Location of first mark. -> (Float, Float) -- Current position. -> ViewState -> Maybe ViewState motionRotate Nothing _ _ = Nothing motionRotate (Just (markX, _markY)) (posX, posY) viewState = Just $ viewState { viewStateViewPort = port { viewPortRotate = rotate - rotateFactor * (posX - markX) } , viewStateRotateMark = Just (posX, posY) } where port = viewStateViewPort viewState rotate = viewPortRotate port rotateFactor = viewStateRotateFactor viewState -- | Apply a scale to the `ViewState`. motionScale :: Maybe (Float, Float) -- Location of first mark. -> (Float, Float) -- Current position. -> ViewState -> Maybe ViewState motionScale Nothing _ _ = Nothing motionScale (Just (_markX, markY)) (posX, posY) viewState = Just $ viewState { viewStateViewPort = let -- Limit the amount of downward scaling so it maxes -- out at 1 percent of the original. There's not much -- point scaling down to no pixels, or going negative -- so that the image is inverted. ss = if posY > markY then scale - scale * (scaleFactor * (posY - markY)) else scale + scale * (scaleFactor * (markY - posY)) ss' = max 0.01 ss in port { viewPortScale = ss' } , viewStateScaleMark = Just (posX, posY) } where port = viewStateViewPort viewState scale = viewPortScale port scaleFactor = viewStateScaleFactor viewState