tinytools-vty-0.1.0.7: src/Potato/Reflex/Vty/Widget.hs
{-# LANGUAGE DefaultSignatures #-}
{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE FunctionalDependencies #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE RecursiveDo #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE UndecidableInstances #-}
module Potato.Reflex.Vty.Widget
(
SingleClick(..)
, singleClick
, singleClickNoDragOffSimple
, singleClickWithDownState
, DoubleClickConfig(..)
, doubleClick
, doubleClickSimple
, splitHDrag
, DragState(..)
, Drag2(..)
, drag2
) where
import Prelude
import qualified Graphics.Vty as V
import Reflex
import Reflex.Class ()
import Reflex.Vty.Widget
import Reflex.Vty.Widget.Input.Mouse
import Control.Monad.Fix
import Control.Monad.Reader
import System.Clock
-- currently only works for a SINGLE POINT
-- TODO integrate with pane2 so it reports clicks that happen on pane.
data SingleClick = SingleClick
{ _singleClick_button :: V.Button
, _singleClick_coordinates :: (Int, Int) -- ^ coordinates of down click
, _singleClick_modifiers :: [V.Modifier]
, _singleClick_didDragOff :: Bool
}
deriving (Eq, Ord, Show)
singleClick :: (Reflex t, MonadHold t m, MonadFix m, HasInput t m) => V.Button -> m (Event t SingleClick)
singleClick btn = do
let
-- TODO implement for pane2 instead
withinBounds (Drag2 (fromX, fromY) (toX, toY) _ _ _) = fromX == toX && fromY == toY
dragEv <- drag2 btn
didStayOnDyn <- foldDyn (const . withinBounds) False dragEv
return $ flip push dragEv $ \d@(Drag2 (fromX, fromY) _ _ mods ds) -> do
didStayOn <- sample . current $ didStayOnDyn
return $ if ds == DragEnd && withinBounds d
then Just $ SingleClick btn (fromX, fromY) mods (not didStayOn)
else Nothing
singleClickNoDragOffSimple :: (Reflex t, MonadHold t m, MonadFix m, HasInput t m) => V.Button -> m (Event t ())
singleClickNoDragOffSimple btn = do
ev <- singleClick btn
return $ fmapMaybe (\sc -> if _singleClick_didDragOff sc then Nothing else Just ()) ev
singleClickWithDownState :: (Reflex t, MonadHold t m, MonadFix m, HasInput t m) => V.Button -> m (Event t SingleClick, Dynamic t Bool)
singleClickWithDownState btn = do
let
-- TODO implement for pane2 instead
withinBounds (Drag2 (fromX, fromY) (toX, toY) _ _ _) = fromX == toX && fromY == toY
dragEv <- drag2 btn
downDyn <- foldDyn (\(Drag2 _ _ _ _ ds) _ -> ds /= DragEnd) False dragEv
didStayOnDyn <- foldDyn (const . withinBounds) False dragEv
let
scEv = flip push dragEv $ \d@(Drag2 (fromX, fromY) _ _ mods ds) -> do
didStayOn <- sample . current $ didStayOnDyn
return $ if ds == DragEnd && withinBounds d
then Just $ SingleClick btn (fromX, fromY) mods (not didStayOn)
else Nothing
return (scEv, downDyn)
data DoubleClickConfig = DoubleClickConfig {
-- TODO lol...
--_doubleClickConfig_spaceTolerance :: (Int, Int) -- the (x,y) mouse travel tolerance
_doubleClickConfig_timeTolerance :: Integer -- the time (ms) between click tolerance
, _dobuleClickConfig_button :: V.Button
}
doubleClick :: (MonadHold t m, MonadFix m, PerformEvent t m, MonadIO (Performable m), HasInput t m) => DoubleClickConfig -> m (Event t ())
doubleClick DoubleClickConfig {..} = do
singleClickEv <- singleClickNoDragOffSimple _dobuleClickConfig_button
singleClickTimeEv <- performEvent $ ffor singleClickEv $ \_ -> do
liftIO $ getTime Monotonic
lastClickTimeDyn <- holdDyn (-1) $ singleClickTimeEv
(fmap (fmapMaybe id)) $ performEvent $ ffor (tag (current lastClickTimeDyn) singleClickEv) $ \ns -> do
time <- liftIO $ getTime Monotonic
return $ if (toNanoSecs $ time - ns) `div` 1000000 < _doubleClickConfig_timeTolerance
then Just ()
else Nothing
doubleClickSimple :: (MonadHold t m, MonadFix m, PerformEvent t m, MonadIO (Performable m), HasInput t m) => m (Event t ())
doubleClickSimple = doubleClick DoubleClickConfig {
--_doubleClickConfig_spaceTolerance = (0,0)
_doubleClickConfig_timeTolerance = 300
, _dobuleClickConfig_button = V.BLeft
}
integralFractionalDivide :: (Integral a, Fractional b) => a -> a -> b
integralFractionalDivide n d = fromIntegral n / fromIntegral d
-- | A split of the available space into two parts with a draggable separator.
-- Starts with half the space allocated to each, and the first pane has focus.
-- Clicking in a pane switches focus.
splitHDrag :: (MonadFix m, MonadHold t m, HasDisplayRegion t m, HasInput t m, HasImageWriter t m, HasFocusReader t m)
=> Int -- ^ initial width of left panel
-> m ()
-> m a
-> m b
-> m (a,b)
splitHDrag splitter0 wS wA wB = mdo
dh <- displayHeight
dw <- displayWidth
w0 <- sample . current $ dw
dragE <- drag V.BLeft
splitterCheckpoint <- holdDyn splitter0 $ leftmost [fst <$> ffilter snd dragSplitter, resizeSplitter]
splitterPos <- holdDyn splitter0 $ leftmost [fst <$> dragSplitter, resizeSplitter]
splitterFrac <- holdDyn (integralFractionalDivide splitter0 w0) $ ffor (attach (current dw) (fst <$> dragSplitter)) $ \(w, x) ->
fromIntegral x / (max 1 (fromIntegral w))
let dragSplitter = fforMaybe (attach (current splitterCheckpoint) dragE) $
\(splitterX, Drag (fromX, _) (toX, _) _ _ end) ->
if splitterX == fromX then Just (toX, end) else Nothing
regA = Region 0 0 <$> splitterPos <*> dh
regS = Region <$> splitterPos <*> 0 <*> 1 <*> dh
regB = Region <$> (splitterPos + 1) <*> 0 <*> (dw - splitterPos - 1) <*> dh
resizeSplitter = ffor (attach (current splitterFrac) (updated dw)) $
\(frac, w) -> round (frac * fromIntegral w)
focA <- holdDyn True $ leftmost
[ True <$ mA
, False <$ mB
]
(mA, rA) <- pane regA focA $ withMouseDown wA
pane regS (pure False) wS
(mB, rB) <- pane regB (not <$> focA) $ withMouseDown wB
return (rA, rB)
where
withMouseDown x = do
m <- mouseDown V.BLeft
x' <- x
return (m, x')
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
inp <- input
let f :: Maybe Drag2 -> V.Event -> Maybe Drag2
f Nothing = \case
V.EvMouseDown x y btn' mods
| btn == btn' -> Just $ Drag2 (x,y) (x,y) btn' mods DragStart
| otherwise -> Nothing
_ -> Nothing
f (Just (Drag2 from _ _ mods st)) = \case
V.EvMouseDown x y btn' mods'
| st == DragEnd && btn == btn' -> Just $ Drag2 (x,y) (x,y) btn' mods' DragStart
| btn == btn' -> Just $ Drag2 from (x,y) btn mods' Dragging
| otherwise -> Nothing -- Ignore other buttons.
V.EvMouseUp x y (Just btn')
| st == DragEnd -> Nothing
| btn == btn' -> Just $ Drag2 from (x,y) btn mods DragEnd
| otherwise -> Nothing
V.EvMouseUp x y Nothing -- Terminal doesn't specify mouse up button,
-- assume it's the right one.
| st == DragEnd -> Nothing
| otherwise -> Just $ Drag2 from (x,y) btn mods DragEnd
_ -> Nothing
let
newDrag = attachWithMaybe f (current dragD) inp
dragD <- holdDyn Nothing $ Just <$> newDrag
return (fmapMaybe id $ updated dragD)