File: ViewState.hs

package info (click to toggle)
haskell-gloss 1.13.2.2-4
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 344 kB
  • sloc: haskell: 2,903; makefile: 2
file content (395 lines) | stat: -rw-r--r-- 13,888 bytes parent folder | download | duplicates (5)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
{-# 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