@@ -55,9 +55,7 @@ import Control.Monad.Fix (MonadFix)
55import Control.Monad.Trans (lift)
55import Control.Monad.Trans (lift)
56import Control.Monad.Trans.Reader (ReaderT, runReaderT, asks, ask)
56import Control.Monad.Trans.Reader (ReaderT, runReaderT, asks, ask)
57import Control.Monad.Trans.Writer (WriterT, runWriterT, censor, tell)
57import Control.Monad.Trans.Writer (WriterT, runWriterT, censor, tell)
59import Data.Default
58import Data.Default
60−import Data.Maybe (fromMaybe)
61import Data.Text (Text)
59import Data.Text (Text)
62import qualified Data.Text as T
60import qualified Data.Text as T
63import Graphics.Vty (Image, Attr)
61import Graphics.Vty (Image, Attr)
@@ -65,6 +63,8 @@ import qualified Graphics.Vty as V
65import Reflex
63import Reflex
67import Reflex.Vty.Host
65import Reflex.Vty.Host
66+import Reflex.Vty.Widget.Text (TextZipper(..), DisplayLines(..))
67+import qualified Reflex.Vty.Widget.Text as TZ
69-- | The context within which a 'VtyWidget' runs
69-- | The context within which a 'VtyWidget' runs
70data VtyWidgetCtx t = VtyWidgetCtx
70data VtyWidgetCtx t = VtyWidgetCtx
@@ -448,166 +448,49 @@ wrapWithOffset maxWidth n xs =
448 let (firstLine, rest) = T.splitAt (maxWidth - n) xs
448 let (firstLine, rest) = T.splitAt (maxWidth - n) xs
449 in firstLine : (fmap (T.take maxWidth) . takeWhile (not . T.null) . iterate (T.drop maxWidth) $ rest)
449 in firstLine : (fmap (T.take maxWidth) . takeWhile (not . T.null) . iterate (T.drop maxWidth) $ rest)
452− :: Int -- ^ Maximum line length
453− -> Attr -- ^ Normal string attributes
454− -> Attr -- ^ Cursor character attributes
455− -> InputState -- ^ The input to render
457−wrapInputState maxWidth attrs cursorAttrs (InputState before after) =
458− let -- Break up the InputState into:
459− -- * A top section of lines before the line containing the cursor
460− -- * A part of the line containing the cursor that comes immediately before the cursor
461− -- * The cursor character (i.e., the character the cursor is on top of)
462− -- * The remainder of the line containing the cursor
463− -- * The bottom section of lines after the line containing the cursor
464− (top, midBefore, cursorChar, midAfter, bottom) = case (initLast before', headTail after') of
465− (Nothing, Nothing) -> ([], "", ' ', "", [])
466− (Just (top', mid), Nothing) -> (top', mid, ' ', "", [])
467− (Nothing, Just (mid, bottom')) -> case T.uncons mid of
468− Nothing -> ([], "", ' ', "", bottom')
469− Just (c, rest) -> ([], "", c, rest, bottom')
470− (Just (top', midB), Just (midA, bottom')) -> case T.uncons midA of
471− Nothing -> (top', midB, ' ', "", bottom')
472− Just (c, rest) -> (top', midB, c, rest, bottom')
473− wrappedTop = wrap top
474− wrappedBottom = wrap bottom
475− (midBeforeTop, midBefore') = fromMaybe ([], "") $ initLast $ wrap [midBefore]
476− cursorAfterEOL = T.length midBefore' == maxWidth
477− (midAfter', midAfterBottom) =
478− let offset = if cursorAfterEOL then 1 else T.length midBefore' + 1
479− in case wrapWithOffset maxWidth offset midAfter of
482− cursor = V.char cursorAttrs cursorChar
484− [ vstring <$> wrappedTop
485− , vstring <$> midBeforeTop
488− [ vstring midBefore'
496− [ vstring midBefore'
501− , vstring <$> midAfterBottom
502− , vstring <$> wrappedBottom
504− cursorY = length wrappedTop + length midBeforeTop + if cursorAfterEOL then 1 else 0
505− in (images, cursorY)
507− before' = T.split (=='\n') before
508− after' = T.split (=='\n') after
509− vstring = V.string attrs . T.unpack
510− wrap :: [Text] -> [Text]
511− wrap = concatMap (wrapWithOffset maxWidth 0)
512− headTail :: [a] -> Maybe (a, [a])
515− x:xs -> Just (x, xs)
516− initLast :: [a] -> Maybe ([a], a)
519− (x:xs) -> case initLast xs of
520− Nothing -> Just ([], x)
521− Just (ys, y) -> Just (x:ys, y)
523−data InputState = InputState
524− { _inputState_beforeCursor :: Text
525− , _inputState_afterCursor :: Text
528−inputValue :: InputState -> Text
529−inputValue (InputState a b) = a <> b
531−updateInputState :: V.Event -> InputState -> InputState
532−updateInputState ev i@(InputState before after) = case ev of
533− -- Regular characters
534− V.EvKey (V.KChar k) [] -> InputState (T.snoc before k) after
536− V.EvKey V.KBS [] -> InputState (if T.null before then T.empty else T.init before) after
537− V.EvKey V.KDel [] -> case T.uncons after of
539− Just (_, a) -> InputState before a
541− V.EvKey (V.KChar 'u') [V.MCtrl] -> InputState mempty mempty
542− V.EvKey (V.KChar 'w') [V.MCtrl] -> InputState (T.dropWhileEnd (not . isSpace) $ T.dropWhileEnd isSpace $ before) after
544− V.EvKey V.KRight [] -> case T.uncons after of
546− Just (a, as) -> InputState (T.snoc before a) as
547− V.EvKey V.KLeft [] -> case T.unsnoc before of
549− Just (bs, b) -> InputState bs (T.cons b after)
551− let (pre, post) = T.breakOnEnd "\n" before
552− (top, previousLine) = T.breakOnEnd "\n" (T.dropEnd 1 pre)
553− (previousLineBefore, previousLineAfter) = T.splitAt (T.length post) previousLine
556− else InputState (top <> previousLineBefore) (previousLineAfter <> "\n" <> post <> after)
557− V.EvKey V.KDown [] ->
558− let offset = T.length $ T.takeWhileEnd (/='\n') before
559− (pre, post) = T.breakOn "\n" after
560− (nextLine, rest) = T.breakOn "\n" (T.drop 1 post)
561− (nextLineBefore, nextLineAfter) = T.splitAt offset nextLine
564− else InputState (before <> pre <> "\n" <> nextLineBefore) (nextLineAfter <> rest)
565− V.EvKey V.KHome [] ->
566− let (pre, post) = T.breakOnEnd "\n" before
567− in InputState pre (post <> after)
568− V.EvKey V.KEnd [] ->
569− let (pre, post) = T.breakOn "\n" after
570− in InputState (before <> pre) post
573data TextInputConfig t = TextInputConfig
451data TextInputConfig t = TextInputConfig
574− { _textInputConfig_initialValue :: Text
452+ { _textInputConfig_initialValue :: TextZipper
575− , _textInputConfig_modifyInputState :: Event t (InputState -> InputState)
453+ , _textInputConfig_modify :: Event t (TextZipper -> TextZipper)
578instance Reflex t => Default (TextInputConfig t) where
456instance Reflex t => Default (TextInputConfig t) where
579− def = TextInputConfig "" never
457+ def = TextInputConfig TZ.empty never
581−textInput :: (Reflex t, MonadHold t m, MonadFix m) => TextInputConfig t -> VtyWidget t m (Dynamic t Text)
460+ :: (Reflex t, MonadHold t m, MonadFix m)
461+ => TextInputConfig t
462+ -> VtyWidget t m (Dynamic t Text)
582textInput cfg = do
463textInput cfg = do
583 i <- input
464 i <- input
584− v <- foldDyn ($) (InputState (_textInputConfig_initialValue cfg) mempty) $ mergeWith (.)
465+ v <- foldDyn ($) (_textInputConfig_initialValue cfg) $ mergeWith (.)
585− [ updateInputState <$> i
466+ [ TZ.updateTextZipper <$> i
586− , _textInputConfig_modifyInputState cfg
467+ , _textInputConfig_modify cfg
588 dw <- displayWidth
469 dw <- displayWidth
589 dh <- displayHeight
470 dh <- displayHeight
590− let img = (\w s -> wrapInputState w V.defAttr (V.withStyle V.defAttr V.reverseVideo) s) -- TODO cursor ought to blink
471+ let rows = (\w s -> TZ.displayLines w s) <$> dw <*> v
591− <$> dw
472+ img = TZ.images . _displayLines_spans <$> rows
593− y <- holdUniqDyn $ snd <$> img
473+ y <- holdUniqDyn $ _displayLines_cursorY <$> rows
594 let newScrollTop st (h, cursorY)
474 let newScrollTop st (h, cursorY)
595 | cursorY < st = cursorY
475 | cursorY < st = cursorY
596 | cursorY >= st + h = cursorY - h + 1
476 | cursorY >= st + h = cursorY - h + 1
597 | otherwise = st
477 | otherwise = st
598 rec let hy = attachWith newScrollTop (current scrollTop) $ updated $ zipDyn dh y
478 rec let hy = attachWith newScrollTop (current scrollTop) $ updated $ zipDyn dh y
599 scrollTop <- holdDyn 0 hy
479 scrollTop <- holdDyn 0 hy
600− tellImages $ (\(imgs, _) st -> (:[]) . V.vertCat $ drop st imgs) <$> current img <*> current scrollTop
480+ tellImages $ (\imgs st -> (:[]) . V.vertCat $ drop st imgs) <$> current img <*> current scrollTop
601− return $ inputValue <$> v
481+ return $ TZ.value <$> v
603−multilineTextInput :: (Reflex t, MonadHold t m, MonadFix m) => TextInputConfig t -> VtyWidget t m (Dynamic t Text)
484+ :: (Reflex t, MonadHold t m, MonadFix m)
485+ => TextInputConfig t
486+ -> VtyWidget t m (Dynamic t Text)
604multilineTextInput cfg = do
487multilineTextInput cfg = do
605 i <- input
488 i <- input
606 textInput $ cfg
489 textInput $ cfg
607− { _textInputConfig_modifyInputState = mergeWith (.)
490+ { _textInputConfig_modify = mergeWith (.)
608 [ fforMaybe i $ \case
491 [ fforMaybe i $ \case
609− V.EvKey V.KEnter [] -> Just $ \(InputState before after) -> InputState (T.snoc before '\n') after
492+ V.EvKey V.KEnter [] -> Just $ TZ.insert "\n"
610 _ -> Nothing
493 _ -> Nothing
611− , _textInputConfig_modifyInputState cfg
494+ , _textInputConfig_modify cfg