packages feed

brillo-1.13.3: Brillo/Data/ViewState.hs

{-# LANGUAGE PatternGuards #-}

module Brillo.Data.ViewState (
  Command (..),
  CommandConfig,
  defaultCommandConfig,
  ViewState (..),
  viewStateInit,
  viewStateInitWithConfig,
  updateViewStateWithEvent,
  updateViewStateWithEventMaybe,
)
where

import Brillo.Data.Point.Arithmetic qualified as Pt
import Brillo.Data.Vector
import Brillo.Data.ViewPort
import Brillo.Geometry.Angle
import Brillo.Internals.Interface.Backend
import Brillo.Internals.Interface.Event
import Control.Monad (mplus)
import Data.Map (Map)
import Data.Map qualified as Map
import Data.Maybe


-- | 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 brillo 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 brillo 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
  { viewStateCommands :: !(Map Command [(Key, Maybe Modifiers)])
  -- ^ 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.
  , viewStateScaleStep :: !Float
  -- ^ How much to scale the world by for each step of the mouse wheel.
  , viewStateRotateFactor :: !Float
  -- ^ How many degrees to rotate the world by for each pixel of x motion.
  , viewStateScaleFactor :: !Float
  -- ^ Ratio to scale the world by for each pixel of y motion.
  , viewStateTranslateMark :: !(Maybe (Float, Float))
  -- ^ During viewport translation,
  --      where the mouse was clicked on the window to start the translate.
  , viewStateRotateMark :: !(Maybe (Float, Float))
  -- ^ During viewport rotation,
  --      where the mouse was clicked on the window to starte the rotate.
  , viewStateScaleMark :: !(Maybe (Float, Float))
  -- ^ During viewport scale,
  --      where the mouse was clicked on the window to start the scale.
  , viewStateViewPort :: ViewPort
  -- ^ The current 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