tinytools-vty-0.1.0.0: src/Potato/Flow/Vty/Common.hs
-- TODO move to widget folder
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE RecursiveDo #-}
module Potato.Flow.Vty.Common (
ffilterButtonIndex
, oneLineButton
, buttonList
, radioList
, radioListSimple
, checkBox
) where
import Relude
import qualified Relude.Unsafe as Unsafe
import Potato.Flow.Vty.Attrs
import Potato.Reflex.Vty.Widget
import Control.Monad.Fix
import Control.Monad.NodeId
import qualified Data.List.Index as L
import qualified Data.Text as T
import Data.Tuple.Extra
import qualified Graphics.Vty as V
import Reflex
import Reflex.Vty
ffilterButtonIndex :: (Reflex t) => Int -> Event t Int -> Event t ()
ffilterButtonIndex i = fmapMaybe (\i' -> if i == i' then Just () else Nothing)
maximumlist :: [Int] -> Int
maximumlist = foldr (\x y ->if x >= y then x else y) (-1)
simpleDrag :: (Reflex t, MonadHold t m, MonadFix m, HasInput t m) => V.Button -> m (Event t ((Int, Int), (Int, Int)))
simpleDrag btn = do
dragEv <- drag2 btn
return $ flip push dragEv $ \(Drag2 (fromX, fromY) (toX, toY) _ _ ds) -> do
return $ if ds == DragEnd
then Just $ ((fromX, fromY), (toX, toY))
else Nothing
makeOneLineButtonImage :: V.Attr -> V.Attr -> ((Int,Int,Int), Text, Bool) -> V.Image
makeOneLineButtonImage defAttr downAttr ((x,y,_), t, downclickTODO) = V.translate x y $ V.text' attr ("["<>t<>"]") where
attr = if downclickTODO then downAttr else defAttr
oneLineButton :: forall t m. (MonadFix m, MonadHold t m, HasDisplayRegion t m, HasImageWriter t m, HasInput t m)
=> Behavior t (V.Attr, V.Attr)
-> Dynamic t Text -- ^ button content
-> m (Event t ()) -- ^ event when button is clicked
oneLineButton attrBeh buttonDyn = do
dw <- displayWidth
clickEv <- simpleDrag V.BLeft
let
selectEv = flip push clickEv $ \((px,py),(ex,ey)) -> do
t <- sample . current $ buttonDyn
let l = T.length t + 2
return $ if py == 0 && ey == 0 && px >= 0 && ex >= 0 && px < l && ex < l
then Just ()
else Nothing
(defAttr, downAttr) <- sample attrBeh
let
-- ((x,y,length), contents, downClickTODO)
buttonDyn' :: Dynamic t ((Int,Int,Int), Text, Bool)
buttonDyn' = ffor2 dw buttonDyn $ \w t -> ((0,0, T.length t + 2), t, False)
-- TODO pass correct theme based on style
tellImages $ fmap (\b -> [makeOneLineButtonImage defAttr downAttr b]) $ current buttonDyn'
return $ selectEv
-- TODO pass in sel and default attrs
-- | option to pass in height is a hack to work around circular dependency issues as when using Layout, displayWidth may be dependent on returned dynamic height
buttonList :: forall t m. (MonadFix m, MonadHold t m, HasDisplayRegion t m, HasImageWriter t m, HasInput t m, HasTheme t m)
=> Dynamic t [Text] -- ^ list of button contents
-> Maybe (Dynamic t Int) -- ^ optional width (displayWidth is used if Nothing)
-> m (Event t Int, Dynamic t Int) -- ^ (event when button is clicked, height)
buttonList buttonsDyn mWidthDyn = do
dw <- case mWidthDyn of
Nothing -> displayWidth
Just widthDyn -> return widthDyn
clickEv <- simpleDrag V.BLeft
-- TODO the better version of this highlights button on mouse down and "clicks" so long as you don't drag off the button
--dragPosDyn
--isDraggingDyn
let
-- ((x,y,length), contents, downclickTODO)
buttons :: Dynamic t [((Int,Int,Int), Text, Bool)]
buttons = ffor2 dw buttonsDyn $ fn where
fn w bs = r where
mapaccumfn (x,y) t = ((nextx, ny), ((nx,ny,buttonl),t, False)) where
buttonl = T.length t + 2
nextx' = x + buttonl
(nx,ny,nextx) = if nextx' > w then (0,y+1, buttonl) else (x,y, nextx')
(_,r) = mapAccumL mapaccumfn (0, 0) bs
-- TODO replace with makeOneLineButtonImage
makeImage :: ((Int,Int,Int), Text, Bool) -> V.Image
makeImage ((x,y,_), t, downclickTODO) = V.translate x y $ V.text' attr ("["<>t<>"]") where
attr = if downclickTODO then lg_layer_selected else lg_default
heightDyn = fmap ((+1) . maximumlist . fmap (snd3 . fst3)) buttons
selectEv = flip push clickEv $ \((px,py),(ex,ey)) -> do
bs <- sample . current $ buttons
return $ L.ifindIndex (\_ ((x,y,l),_,_) -> py == y && ey == y && px >= x && ex >= x && px < x+l && ex < x+l) bs
tellImages $ fmap (fmap makeImage) $ current buttons
return $ (selectEv, heightDyn)
-- TODO pass in sel and default attrs
-- | option to pass in height is a hack to work around circular dependency issues as when using Layout, displayWidth may be dependent on returned dynamic height
-- override style: does not modify state internally, instead state must be passed back in
radioList :: forall t m. (Reflex t, MonadNodeId m, HasDisplayRegion t m, HasImageWriter t m, HasInput t m, HasTheme t m)
=> Dynamic t [Text] -- ^ list of button contents
-> Dynamic t [Int] -- ^ which buttons are "active"
-> Maybe (Dynamic t Int) -- ^ optional width (displayWidth is used if Nothing)
-> m (Event t Int, Dynamic t Int) -- ^ (event when button is clicked, height)
radioList buttonsDyn activeDyn mWidthDyn = do
dw <- case mWidthDyn of
Nothing -> displayWidth
Just widthDyn -> return widthDyn
mouseDownEv <- mouseDown V.BLeft
let
-- ((x,y,length), contents)
buttons' :: Dynamic t [((Int,Int,Int), Text)]
buttons' = ffor2 dw buttonsDyn $ fn where
fn w bs = r where
mapaccumfn (x,y) t = ((nextx, ny), ((nx,ny,buttonl),t)) where
buttonl = T.length t + 2
nextx' = x + buttonl
(nx,ny,nextx) = if nextx' > w then (0,y+1, buttonl) else (x,y, nextx')
(_,r) = mapAccumL mapaccumfn (0, 0) bs
buttons :: Dynamic t [((Int,Int,Int), Text, Bool)]
buttons = ffor2 buttons' activeDyn $ fn where
fn bs actives' = r where
actives = reverse $ sort actives'
ifoldrfn _ (l,t) (output, []) = ((l,t,False):output, [])
ifoldrfn i (l,t) (output, a:as) = if i == a
then ((l,t,True):output, as)
else ((l,t,False):output, a:as)
(r,_) = L.ifoldr ifoldrfn ([],actives) bs
makeImage :: ((Int,Int,Int), Text, Bool) -> V.Image
makeImage ((x,y,_), t, selected) = V.translate x y $ V.text' attr c where
attr = if selected then lg_layer_selected else lg_default
--c = if selected then "[" <> t <> "]" else " " <> t <> " "
c = "["<>t<>"]"
heightDyn = fmap ((+1) . maximumlist . fmap (snd3 . fst3)) buttons
selectEv = flip push mouseDownEv $ \(MouseDown _ (px,py) _) -> do
bs <- sample . current $ buttons
return $ L.ifindIndex (\_ ((x,y,l),_,_) -> py == y && px >= x && px < x+l) bs
tellImages $ fmap (fmap makeImage) $ current buttons
return $ (selectEv, heightDyn)
-- TODO pass in sel and default attrs
radioListSimple :: forall t m. (Reflex t, MonadFix m, MonadHold t m, MonadNodeId m, HasDisplayRegion t m, HasImageWriter t m, HasInput t m, HasTheme t m)
=> Int -- ^ initial choice
-> [Text] -- ^ list of button contents (must be at least one)
-> m (Dynamic t Int) -- ^ which radio is selected
radioListSimple initial buttons = mdo
(radioEvs,_) <- radioList (constDyn buttons) radioDyn Nothing
radioDyn <- holdDyn [initial] $ fmap (\x->[x]) radioEvs
return $ fmap (Unsafe.head) radioDyn
-- TODO focus + enter to select via keyboard
-- | creates a check box "[x]" in upper left corner of region
-- override style: does not modify state internally, instead state must be passed back in
checkBox
:: forall t m. (Reflex t, MonadFix m, HasDisplayRegion t m, HasImageWriter t m, HasInput t m, HasTheme t m)
=> Dynamic t Bool
-> m (Event t Bool)
checkBox stateDyn = do
text (ffor (current stateDyn) (\s -> if s then "[x]" else "[ ]"))
mouseDownEv <- mouseDown V.BLeft
let toggleEv = fforMaybe mouseDownEv $ \(MouseDown _ (px,py) _) -> if px >= 0 && px < 3 && py == 0 then Just () else Nothing
return $ tag (current (fmap not stateDyn)) toggleEv