packages feed

tinytools-vty-0.1.0.7: src/Potato/Reflex/Vty/Widget/ScrollBar.hs

{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE RecursiveDo     #-}

module Potato.Reflex.Vty.Widget.ScrollBar (
  vScrollBar
) where

import           Relude

import           Potato.Reflex.Vty.Helpers
import           Potato.Reflex.Vty.Widget


import qualified Graphics.Vty as V
import           Reflex
import           Reflex.Vty

import Data.These
import Data.Align (align)



emptyDrag2 :: Drag2
emptyDrag2 = Drag2  {
    _drag2_from       = (0,0)
    , _drag2_to        = (0,0)
    , _drag2_button    = V.BLeft
    , _drag2_modifiers = []
    , _drag2_state     = DragStart
  }

componentSub :: (Num a) => (a,a) -> (a,a) -> (a,a)
componentSub (a,b) (c,d) = (a-c,b-d)

onlyIfSimultaneous :: (Reflex t) => Event t a -> Event t b -> Event t a
onlyIfSimultaneous eva evb = fforMaybe (align eva evb) $ \case
  These a _ -> Just a
  _ -> Nothing


-- TODO you can use reflex.vty.widget.scroll for this
--makeVScrollBarWidget :: forall t m a. (MonadWidget t m, MonadLayoutWidget t m')
--  => m a -- ^ contents
--  -> Dynamic t Int -- ^ content height
--  -> m' a 


-- TODO write UTs
-- TODO reduce constraints
-- dynamically scaling vertical scroll bar
vScrollBar :: forall t m. (MonadWidget t m)
  => Int -- ^ width
  -> Dynamic t Int -- ^ content height
  -> m (Dynamic t Int) -- ^ offset
vScrollBar scrollBarWidth contentSizeDyn = mdo
  maxSizeDyn <- displayHeight
  let
    screen_over_content_dyn :: Dynamic t Float = liftA2 (\a b -> fromIntegral a / fromIntegral b ) maxSizeDyn contentSizeDyn
    maxContentSizeDiffDyn = fromIntegral . max 0 <$> liftA2 (-) contentSizeDyn maxSizeDyn
    boxHeightDyn = fmap ceiling $ liftA2 (*) screen_over_content_dyn (fromIntegral <$> maxSizeDyn)
    boxRegionDyn = Region <$> 0 <*> offsetScreenUnitDyn <*> constDyn scrollBarWidth <*> boxHeightDyn

  --innerDragEv will only fire on drag events that started on the scroll bar handle portion
  innerDragEv <- pane boxRegionDyn (constDyn True) $ do
    -- render the scroll bar handle
    fill (constant '#')
    drag2 V.BLeft

  d2ev <- drag2 V.BLeft
  let
    moveDragEv = fmapMaybe (\d2 -> if _drag2_state d2 == Dragging then Just d2 else Nothing) d2ev
  lastDrag <- holdDyn emptyDrag2 d2ev
  let
    deltaDragEv_d1' = attach (current lastDrag) moveDragEv
    deltaDragEv_d1 = fmap (\(pd,d) -> _drag2_to d `componentSub` _drag2_to pd) deltaDragEv_d1'
    -- only process the event if they are simultaneous with innerDragEv (thus meaning they started on the scroll bar handle)
    -- the reason we need to do it this way is because `pane` messes with the mouse coords so we need to get the mouse coords from outside
    deltaDragEv = onlyIfSimultaneous (fmap snd deltaDragEv_d1) innerDragEv

  let
    content_over_screen_dyn = fmap (\x -> 1 / x) screen_over_content_dyn
    dragDeltaAdjustedEv = fmap (\(x,y) -> x * fromIntegral y) (attach (current content_over_screen_dyn) deltaDragEv)

  -- TODO movement when you click on areas off the bar
  -- TODO maybe do ^ v arrows at top and bottom to click scroll through 1 at a time
  -- TODO ugg you probably need an inputCaptured event here :\ (or you could just get rid of keyboard movement...)
  -- keyboard/scroll movement
  kup <- key V.KUp
  kdown <- key V.KDown
  kpgup <- key V.KPageUp
  kpgdown <- key V.KPageDown

  -- TODO this is a mouse input so it only works if mouse is over the pane, which is not what we want. So instead you you probably need to capture moues input in the parent pane and pass it in instead....
  mscroll <- mouseScroll
  let
    requestedScroll :: Event t Float
    requestedScroll = leftmost
      [ 1 <$ kdown
      , (-1) <$ kup

      -- maybe scale to height of scroll bar?
      , 8 <$ kpgdown
      , (-8) <$ kpgup

      , ffor mscroll $ \case
          ScrollDirection_Up -> (-1)
          ScrollDirection_Down -> 1
      ]

  -- then put it all together
  let
    foldOffsetFn (maxdiff, delta) c = max 0 (min maxdiff (c+delta))
  offsetFloatDyn <- foldDyn foldOffsetFn 0 (attach (current maxContentSizeDiffDyn) (leftmost [dragDeltaAdjustedEv, requestedScroll]))

  let
    offsetScreenUnitDyn = fmap round . liftA2 (*) screen_over_content_dyn $ offsetFloatDyn

  return $ fmap floor offsetFloatDyn


{-

  data DragState = DragStart | Dragging | DragEnd deriving (Eq, Ord, Show)

  -- | Same as 'Drag' but able to track drag start case
  data Drag2 = Drag2
    { _drag2_from      :: (Int, Int) -- ^ Where the drag began
    , _drag2_to        :: (Int, Int) -- ^ Where the mouse currently is
    , _drag2_button    :: V.Button -- ^ Which mouse button is dragging
    , _drag2_modifiers :: [V.Modifier] -- ^ What modifiers are held
    , _drag2_state     :: DragState -- ^ Whether the drag ended (the mouse button was released)
    }
    deriving (Eq, Ord, Show)

  -- | Same as 'drag' but returns 'Drag2' which tracks drag start events
  drag2
    :: (Reflex t, MonadFix m, MonadHold t m, HasInput t m)
    => V.Button
    -> m (Event t Drag2)
  drag2 btn = mdo-}