matsuri-0.0.2: Widgets/EditBox.hs
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE NamedFieldPuns #-}
-- | One-line edit box widget.
module Widgets.EditBox
( EditBox
, editBox
, defaultEditBox
, fixedWidthEditBox
, fixedWidthDefaultEditBox
, empty
, contents
, current
, defaultKeyHandler
, insert
, moveLeft
, moveRight
, moveWordLeft
, moveWordRight
, moveToHome
, moveToEnd
, backSpace
, delete
, resize
)
where
import Data.Char
import Graphics.Vty
import Graphics.Vty.Widgets.Base
import Graphics.Vty.Widgets.WrappedText
-- | Edit box can have fixed width or it expands to fill all available space.
-- | TODO: если размеры будут задаваться внешними по отношению к vty-ui методами, то разделение на fixed/free не нужно
data Size = Free {getSize :: Int} | Fixed {getSize :: Int}
data EditBox
= EditBox
{ primAttr :: Attr
, currentAttrFn :: Attr -> Attr
, width :: Size
, windowStart :: Int
, front :: String -- ^ text before cursor (in reversed order)
, back :: String -- ^ current char and text after it
}
----------------------------------------------------------------------
-- | Creates empty edit box that fills all available width.
editBox attr fn = EditBox attr fn (Free 0) 0 "" ""
defaultEditBox = editBox def_attr (`with_style` reverse_video)
-- | Creates empty edit box with fixed width.
fixedWidthEditBox attr fn w = EditBox attr fn (Fixed w) 0 "" ""
fixedWidthDefaultEditBox = fixedWidthEditBox def_attr (`with_style` reverse_video)
-- | Empties edit box.
empty t = t {windowStart = 0, front = "", back = ""}
-- | Returns text contained in edit box.
contents (EditBox{..}) = reverse front ++ back
-- | Returns current character.
current (EditBox{back})
= if null back then Nothing else Just $ head back
-- | Returns current size of edit box.
size = getSize . width
-- | Handles key events from vty
defaultKeyHandler box ev
= ($box) $ case ev of
EvKey (KASCII c) [] -> Just . insert c
EvKey KLeft [] -> Just . moveLeft
EvKey KRight [] -> Just . moveRight
EvKey KLeft [MCtrl] -> Just . moveWordLeft
EvKey KRight [MCtrl] -> Just . moveWordRight
EvKey KHome [] -> Just . moveToHome
EvKey KEnd [] -> Just . moveToEnd
EvKey KBS [] -> Just . backSpace
EvKey KDel [] -> Just . delete
_ -> const Nothing
----------------------------------------------------------------------
-- editing commands
-- TODO: more text editing commands (delete-world, delete-line and so on).
-- FIXME: tab breaks input
-- | Inserts one symbol before cursor.
insert c box = scrollWindow $ box {front = c':front box}
where -- FIXME: что делать с tab'ом в insert? | а ничего с ним не
-- надо, по табу ники перебирать же.
c' = if elem c "\r\n\t" then ' ' else c -- filter 'bad' symbols
-- | Moves cursor one position left. Scrolls text if necessary.
moveLeft box@(EditBox{front = c:front',back})
= scrollWindow $ box {front = front', back = c:back}
moveLeft box = box
-- | Moves cursor one position right. Scrolls text if necessary.
moveRight box@(EditBox{front, back = c:back'})
= scrollWindow $ box {front = c:front, back = back'}
moveRight box = box
-- | Skip all spaces then skip word.
skipWord move
= head
. dropWhile (inBounds .&&. (inWord.current))
. dropWhile (inBounds .&&. (not.inWord.current))
. iterate move
. move
where
inWord Nothing = False
inWord (Just c) = isAlphaNum c
inBounds = (not.null.front) .&&. (not.null.back)
(.&&.) f g x = f x && g x
-- | Moves cursor one word left. Scrolls if necessary.
moveWordLeft = skipWord moveLeft
-- | Moves cursor one word right. Scrolls if nesessary.
moveWordRight = skipWord moveRight
-- | Moves cursor to the very beginning of the text.
moveToHome box = scrollWindow $ box {front = "", back = contents box}
-- | Moves cursor to the end of the text.
moveToEnd box = scrollWindow $ box {front = reverse $ contents box, back = ""}
-- | Removes symbol before cursor.
backSpace box@(EditBox{front = c:front'}) = scrollWindow $ box {front = front'}
backSpace box = box
-- | Removes current symbol.
delete box@(EditBox{back = c:back'}) = scrollWindow $ box {back = back'}
delete box = box
-- | Updates internal variable that represents edit box width.
-- Used only as a response to EvResize.
-- If you wish to resize fixed size edit box, you need to creatie a new one.
resize w box@(EditBox{width})
= case width of
Free _ -> scrollWindow $ box {width = Free w}
_ -> box
-- | Scrolls text if cursor is not visible.
scrollWindow box@(EditBox{front, width, windowStart = ws})
| fLen - ws > w-1 = box {windowStart = fLen - w+1}
| fLen < ws = box {windowStart = fLen}
| otherwise = box
where
fLen = length front
w = getSize width
----------------------------------------------------------------------
-- render
instance Widget EditBox where
-- growHorizontal :: w -> Bool
growHorizontal (EditBox{width = Free _}) = True
growHorizontal _ = False
-- growVertical :: w -> Bool
growVertical _ = False
-- primaryAttribute :: w -> Attr
primaryAttribute = primAttr
-- withAttribute :: w -> Attr -> w
withAttribute w a = w {primAttr = a}
-- render :: DisplayRegion -> w -> Image
render rgn box@(EditBox{..})
= render rgn
$ text primAttr front'
<++> text (currentAttrFn primAttr) [c]
<++> text primAttr (back' ++ fill)
where
front' = drop windowStart $ reverse front
(c:back')
| null back = ' ':[]
| otherwise = take (getSize width - length front') back
fill = replicate (getSize width - length front' - length back' - 1) ' '