gloss-1.13.2.2: Graphics/Gloss/Data/ViewState.hs
{-# 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