packages feed

vty-ui 1.5.1 → 1.6

raw patch · 54 files changed

+1991/−1191 lines, 54 filesdep +randomdep +textdep +vty-uidep ~QuickCheckdep ~containersdep ~directory

Dependencies added: random, text, vty-ui

Dependency ranges changed: QuickCheck, containers, directory, mtl, stm, unix, vty

Files

+ demos/ComplexDemo.hs view
@@ -0,0 +1,169 @@+{-# OPTIONS_GHC -fno-warn-missing-signatures -fno-warn-unused-do-bind #-}+{-# LANGUAGE OverloadedStrings #-}+module Main where++import System.Exit ( exitSuccess )+import System.Locale+import qualified Data.Text as T+import Control.Monad+import Control.Concurrent+import Data.Time.Clock+import Data.Time.Format+import Graphics.Vty hiding (pad)+import Graphics.Vty.Widgets.All++-- Visual attributes.+fg = white+bg = black+focAttr = black `on` yellow+headerAttr = fgColor bright_green+msgAttr = fgColor blue++-- Multi-state checkbox value type+data FrostingType = Chocolate+                  | Vanilla+                  | Lemon+                    deriving (Eq, Show)++main :: IO ()+main = do+  let msg = "- <TAB> switches input elements\n\n\+            \- ordinary keystrokes edit\n\n\+            \- <SPC> toggles radio buttons and checkboxes\n\n\+            \- <ESC> quits"++      columns = [ column (ColFixed 25) `pad` (padAll 1)+                , column ColAuto `pad` (padAll 1)+                ]++  table <- newTable columns BorderFull >>=+           withNormalAttribute (bgColor blue) >>=+           withBorderAttribute (fgColor green)++  tw <- (textWidget wrap msg) >>= withNormalAttribute msgAttr+  mainBox <- vBox table tw >>= withBoxSpacing 1++  r1 <- newCheckbox "Cake"+  r2 <- newCheckbox "Death"+  radioHeader <- plainText T.empty >>= withNormalAttribute headerAttr++  rg <- newRadioGroup+  addToRadioGroup rg r1+  addToRadioGroup rg r2++  r3 <- newMultiStateCheckbox "Frosting" [ (Chocolate, 'C')+                                         , (Vanilla, 'V')+                                         , (Lemon, 'L')+                                         ]++  edit1 <- editWidget >>= withFocusAttribute (white `on` red)+  edit2 <- multiLineEditWidget+  edit2box <- boxFixed 30 3 edit2+  setEditLineLimit edit2 $ Just 3++  edit1Header <- textWidget wrap T.empty >>= withNormalAttribute headerAttr+  edit2Header <- textWidget wrap T.empty >>= withNormalAttribute headerAttr++  lst <- newList (fgColor bright_green)++  selector <- vLimit 3 lst+  listHeader <- plainText T.empty++  rs <- vBox r1 r2++  cbHeader <- plainText T.empty+  timeText <- plainText T.empty++  prog <- newProgressBar (white `on` red) (red `on` white)+  setProgressTextAlignment prog AlignCenter++  addHeadingRow_ table headerAttr ["Column 1", "Column 2"]+  addRow table $ radioHeader .|. rs+  addRow table $ cbHeader .|. r3+  addRow table $ edit1Header .|. edit1+  addRow table $ edit2Header .|. edit2box+  addRow table $ listHeader .|. customCell selector `pad` padNone+  addRow table $ emptyCell .|. timeText+  addRow table $ emptyCell .|. prog++  rg `onRadioChange` \cb -> do+      s <- getCheckboxLabel cb+      setText radioHeader $ T.concat [s, ", please."]++  r3 `onCheckboxChange` \v ->+      setText cbHeader $ T.pack $ concat ["you chose: ", show v]++  prog `onProgressChange` \val ->+      setProgressText prog $ T.concat [ "Progress ("+                                      , T.pack $ show val+                                      , " %)"+                                      ]++  edit1 `onChange` (setText edit1Header)+  edit2 `onChange` (setText edit2Header)++  lst `onSelectionChange` \ev ->+      case ev of+        SelectionOn _ k _ -> setText listHeader $ T.pack $ "You selected: " ++ k+        SelectionOff -> return ()++  lst `onItemActivated` \(ActivateItemEvent _ s _) ->+      setText listHeader $ T.pack $ "You activated: " ++ s++  setEditText edit1 "Foo"+  setEditText edit2 "Bar"+  setCheckboxChecked r1++  setCheckboxState r3 Chocolate+  -- It would be nice if we didn't have to do this, but the+  -- setCheckboxState call above will not notify any state-change+  -- handlers because the state isn't actually changing (from its+  -- original value of Chocolate, the first value in its state list).+  setText cbHeader $ "you chose: Chocolate"++  fgr <- newFocusGroup+  fgr `onKeyPressed` \_ k _ -> do+         case k of+           KEsc -> exitSuccess+           _ -> return False++  let strs = [ "Cookies"+             , "Cupcakes"+             , "Twinkies"+             , "M&Ms"+             , "Fritos"+             , "Cheetos"+             ]++  forM_ strs $ \s ->+      (addToList lst s =<< (plainText $ T.pack s))++  addToFocusGroup fgr r1+  addToFocusGroup fgr r2+  addToFocusGroup fgr r3+  addToFocusGroup fgr edit1+  addToFocusGroup fgr edit2+  addToFocusGroup fgr lst++  ui <- centered =<< hLimit 70 mainBox++  forkIO $ forever $ do+         schedule $ do+           t <- getCurrentTime+           setText timeText $ T.pack $+                   formatTime defaultTimeLocale rfc822DateFormat t+         threadDelay $ 1 * 1000 * 1000++  forkIO $ forever $ do+         let act i = do+               threadDelay $ 1 * 1000 * 1000+               schedule $ setProgress prog (i `mod` 101)+               act $ i + 4+         act 0++  c <- newCollection+  _ <- addToCollection c ui fgr++  runUi c $ defaultContext { focusAttr = focAttr+                           , normalAttr = fg `on` bg+                           }
+ demos/DialogDemo.hs view
@@ -0,0 +1,40 @@+{-# OPTIONS_GHC -fno-warn-unused-do-bind #-}+{-# LANGUAGE OverloadedStrings #-}+module Main where++import qualified Data.Text as T+import Graphics.Vty hiding (Button)+import Graphics.Vty.Widgets.All++main :: IO ()+main = do+  e <- editWidget+  fg <- newFocusGroup+  addToFocusGroup fg e++  u <- plainText "Enter some text and press enter." <--> return e+       >>= withBoxSpacing 1++  pe <- padded u (padLeftRight 2)+  (d, dFg) <- newDialog pe "<enter text>"+  setNormalAttribute d (white `on` blue)++  c <- centered =<< withPadding (padLeftRight 10) (dialogWidget d)++  -- When the edit widget changes, set the dialog's title.+  e `onChange` setDialogTitle d++  -- When the user presses Enter in the edit widget, accept the+  -- dialog.+  e `onActivate` (const $ acceptDialog d)++  -- Exit either way.+  d `onDialogAccept` const shutdownUi+  d `onDialogCancel` const shutdownUi++  coll <- newCollection+  _ <- addToCollection coll c =<< (mergeFocusGroups fg dFg)++  runUi coll $ defaultContext { focusAttr = black `on` yellow }++  (putStrLn . ("You entered: " ++) . T.unpack) =<< getEditText e
+ demos/DirBrowserDemo.hs view
@@ -0,0 +1,17 @@+module Main where++import Graphics.Vty+import Graphics.Vty.Widgets.All++main :: IO ()+main = do+  (b, fg) <- newDirBrowser defaultBrowserSkin++  c <- newCollection+  _ <- addToCollection c (dirBrowserWidget b) fg++  b `onBrowseAccept` error+  b `onBrowseCancel` error++  runUi c $ defaultContext { focusAttr = white `on` blue+                           }
+ demos/EditDemo.hs view
@@ -0,0 +1,40 @@+{-# LANGUAGE OverloadedStrings #-}+module Main where++import Graphics.Vty hiding (Button)+import Graphics.Vty.Widgets.All++main :: IO ()+main = do+  e1 <- multiLineEditWidget+  e2 <- multiLineEditWidget+  setEditLineLimit e2 $ Just 3+  e3 <- editWidget++  fg <- newFocusGroup+  _ <- addToFocusGroup fg e1+  _ <- addToFocusGroup fg e2+  _ <- addToFocusGroup fg e3++  be1 <- bordered =<< boxFixed 40 5 e1+  be2 <- bordered =<< boxFixed 40 3 e2+  be3 <- bordered =<< boxFixed 40 1 e3++  c <- centered =<< ((plainText "Multi-Line Editor (unlimited lines):")+                         <--> (return be1)+                         <--> (plainText "Multi-Line Editor (3 lines):")+                         <--> (return be2)+                         <--> (plainText "Single-Line Editor:")+                         <--> (return be3)+                         <--> (plainText "- Esc to quit\n\n- TAB to switch editors") >>= withBoxSpacing 1+                    )++  coll <- newCollection+  _ <- addToCollection coll c fg++  fg `onKeyPressed` \_ k _ ->+      case k of+        KEsc -> shutdownUi >> return True+        _ -> return False++  runUi coll $ defaultContext { focusAttr = fgColor yellow }
+ demos/ListDemo.hs view
@@ -0,0 +1,137 @@+{-# OPTIONS_GHC -fno-warn-missing-signatures -fno-warn-unused-do-bind #-}+{-# LANGUAGE OverloadedStrings #-}+module Main where++import System.Exit ( exitSuccess )+import qualified Data.Text as T+import Graphics.Vty+import Graphics.Vty.Widgets.All++data AppElements =+    AppElements { theList :: Widget (List T.Text FormattedText)+                , theBody :: Widget FormattedText+                , theFooter :: Widget FormattedText+                , theListLimit :: Widget (VLimit (List T.Text FormattedText))+                , uis :: Collection+                }++titleAttr = bright_white `on` blue+focAttr = black `on` green+bodyAttr = white `on` black+selAttr = black `on` yellow+keyAttr = fgColor magenta++message1 :: T.Text+message1 = "This demonstration shows how list widgets behave. \n\+           \See the keystrokes below to try the demo."++message2 :: [(T.Text, Attr)]+message2 = [ ("- Press ", def_attr), ("q", keyAttr), (" to quit\n", def_attr)+           , ("- Press ", def_attr), ("+", keyAttr)+           , (" / ", def_attr), ("a", keyAttr)+           , (" to add a list item\n", def_attr)+           , ("- Press ", def_attr), ("-", keyAttr)+           , (" / ", def_attr), ("d", keyAttr)+           , (" to remove the selected list item\n", def_attr)+           , ("- Press ", def_attr)+           , ("up", keyAttr), (" / ", def_attr)+           , ("down", keyAttr), (" / ", def_attr)+           , ("page up", keyAttr), (" / ", def_attr)+           , ("page down", keyAttr)+           , (" to navigate the list\n", def_attr)+           ]++buildUi appst = do+  msg1 <- plainText message1+  setTextFormatter msg1 wrap++  msg2 <- plainTextWithAttrs message2+  setTextFormatter msg2 wrap++  mainUi <- (hBorder >>= withBorderAttribute titleAttr)+         <--> (return $ theList appst)+         <--> ((return $ theFooter appst)+               <++> (hBorder >>= withBorderAttribute titleAttr))++  centered =<< hLimit 55 =<<+             ((return msg1)+                  <--> ((vLimit 25 mainUi >>= bordered)+                        <--> return msg2 >>= withBoxSpacing 1)+                    >>= withBoxSpacing 1)++-- Construct the application statea using the message map.+mkAppElements :: IO AppElements+mkAppElements = do+  lw <- newTextList selAttr []+  b <- textWidget wrap T.empty+  ft <- plainText T.empty >>= withNormalAttribute titleAttr+  ll <- vLimit 5 lw++  c <- newCollection++  return $ AppElements { theList = lw+                       , theBody = b+                       , theFooter = ft+                       , theListLimit = ll+                       , uis = c+                       }++updateBody :: AppElements -> Int -> IO ()+updateBody st i = do+  let msg = "This is the text for list entry " ++ (show $ i + 1)+  setText (theBody st) $ T.pack msg++updateFooterNums :: AppElements -> Widget (List a b) -> IO ()+updateFooterNums st w = do+  result <- getSelected w+  sz <- getListSize w+  let msg = case result of+              Nothing -> "0/0"+              Just (i, _) -> (show $ i + 1) ++ "/" ++ (show sz)+  setText (theFooter st) $ T.pack msg++main :: IO ()+main = do+  st <- mkAppElements++  ui <- buildUi st+  fg <- newFocusGroup++  _ <- addToCollection (uis st) ui fg+  _ <- addToFocusGroup fg (theList st)++  (theList st) `onSelectionChange` \_ -> updateFooterNums st $ theList st+  (theList st) `onItemAdded` \_ -> updateFooterNums st $ theList st+  (theList st) `onItemRemoved` \_ -> updateFooterNums st $ theList st++  let removeCurrentItem = do+         result <- getSelected (theList st)+         case result of+           Nothing -> return ()+           Just (i, _) -> removeFromList (theList st) i >> return ()+      addNewItem =+          (plainText "a list item") >>=+                    addToList (theList st) "unused"++  (theList st) `onKeyPressed` \_ k _ -> do+         case k of+           (KASCII 'q') -> exitSuccess+           (KASCII '-') -> removeCurrentItem >> return True+           (KASCII 'd') -> removeCurrentItem >> return True+           (KASCII '+') -> addNewItem >> return True+           (KASCII 'a') -> addNewItem >> return True+           _ -> return False++  -- We need to call these handlers manually because while they will+  -- be called automatically as items are added to the list in the+  -- future, the items currently in the list didn't call these because+  -- they weren't registered at the time the items were added.  And+  -- that was impossible because the list was created and populated+  -- before we even got a reference to it, so we couldn't have set up+  -- event handlers.+  updateFooterNums st (theList st)++  -- Enter the event loop.+  runUi (uis st) $ defaultContext { normalAttr = bodyAttr+                                  , focusAttr = focAttr+                                  }
+ demos/PhoneInputDemo.hs view
@@ -0,0 +1,80 @@+{-# LANGUAGE OverloadedStrings #-}+module Main where++-- This demo is discussed in the vty-ui user's manual.++import Control.Monad+import qualified Data.Text as T++import Graphics.Vty hiding (pad)+import Graphics.Vty.Widgets.All++data PhoneNumber = PhoneNumber T.Text T.Text T.Text+                   deriving (Show)++-- This type isn't pretty, but we have to specify the type of the+-- complete interface.  Initially you can let the compiler tell you+-- what it is.+type T = Box (Box+              (Box (Box (HFixed Edit) FormattedText) (HFixed Edit))+              FormattedText) (HFixed Edit)++data PhoneInput =+   PhoneInput { phoneInputWidget :: Widget T+              , edit1 :: Widget Edit+              , edit2 :: Widget Edit+              , edit3 :: Widget Edit+              , activateHandlers :: Handlers PhoneNumber+              }++newPhoneInput :: IO (PhoneInput, Widget FocusGroup)+newPhoneInput = do+   ahs <- newHandlers+   e1 <- editWidget+   e2 <- editWidget+   e3 <- editWidget+   ui <- (hFixed 4 e1) <++>+         (plainText "-") <++>+         (hFixed 4 e2) <++>+         (plainText "-") <++>+         (hFixed 5 e3)++   let w = PhoneInput ui e1 e2 e3 ahs+       doFireEvent = const $ do+         num <- mkPhoneNumber+         fireEvent w (return . activateHandlers) num++       mkPhoneNumber = do+         s1 <- getEditText e1+         s2 <- getEditText e2+         s3 <- getEditText e3+         return $ PhoneNumber s1 s2 s3++   e1 `onActivate` doFireEvent+   e2 `onActivate` doFireEvent+   e3 `onActivate` doFireEvent++   e1 `onChange` \s -> when (T.length s == 3) $ focus e2+   e2 `onChange` \s -> when (T.length s == 3) $ focus e3++   fg <- newFocusGroup+   mapM_ (addToFocusGroup fg) [e1, e2, e3]+   return (w, fg)++onPhoneInputActivate :: PhoneInput+                     -> (PhoneNumber -> IO ()) -> IO ()+onPhoneInputActivate input handler =+    addHandler (return . activateHandlers) input handler++main :: IO ()+main = do+  (p, fg) <- newPhoneInput+  p `onPhoneInputActivate` (error . show)++  ui <- padded (phoneInputWidget p) (padLeftRight 5 `pad` padTopBottom 2)++  c <- newCollection+  _ <- addToCollection c ui fg++  runUi c $ defaultContext { focusAttr = white `on` blue+                           }
+ demos/ProgressBarDemo.hs view
@@ -0,0 +1,58 @@+{-# OPTIONS_GHC -fno-warn-unused-do-bind -fno-warn-missing-signatures #-}+{-# LANGUAGE OverloadedStrings #-}+module Main where++import qualified Data.Text as T+import Control.Concurrent (forkIO, threadDelay)+import Control.Monad (forever)++import System.Exit ( exitSuccess )+import Graphics.Vty+import Graphics.Vty.Widgets.All++-- Visual attributes.+focAttr = black `on` green+bodyAttr = bright_green `on` black+completeAttr = white `on` red+incompleteAttr = red `on` white++setupProgessBar :: IO (Widget ProgressBar)+setupProgessBar = do+  pb <- newProgressBar completeAttr incompleteAttr+  setProgressTextAlignment pb AlignCenter++  pb `onProgressChange` \val ->+      setProgressText pb $ T.pack $ "Progress (" ++ show val ++ " %)"++  return pb++setupProgressBarThread :: Widget ProgressBar -> IO ()+setupProgressBarThread pb = do+  forkIO $ forever $ do+    let act i = do+          threadDelay $ 1 * 1000 * 1000+          schedule $ setProgress pb (i `mod` 101)+          act $ i + 4+    act 0+  return ()++setupDialog :: (Show a) => Widget a -> IO (Dialog, Widget FocusGroup)+setupDialog ui = do+  (dlg, fg) <- newDialog ui "Progress Bar Demo"+  dlg `onDialogAccept` const exitSuccess+  dlg `onDialogCancel` const exitSuccess+  return (dlg, fg)++main :: IO ()+main = do+  pb <- setupProgessBar+  setupProgressBarThread pb++  (dlg, dlgFg) <- setupDialog pb++  c <- newCollection+  _ <- addToCollection c (dialogWidget dlg) dlgFg++  runUi c $ defaultContext { normalAttr = bodyAttr+                           , focusAttr = focAttr+                           }
doc/Makefile view
@@ -1,9 +1,9 @@ -vty-ui-users-manual.pdf: *.tex ch[1234]/*.tex+vty-ui-users-manual.pdf: *.tex ch?/*.tex 	# Run it twice so the TOC gets generated properly-	pdflatex -shell-escape vty-ui-users-manual.tex-	pdflatex -shell-escape vty-ui-users-manual.tex+	pdflatex -halt-on-error -shell-escape vty-ui-users-manual.tex+	pdflatex -halt-on-error -shell-escape vty-ui-users-manual.tex  clean: 	rm -f *~ *.dvi *.pdf *.log *.aux *.toc *.out-	rm -f ch[1234]/*.aux ch[1234]/*~+	rm -f ch?/*.aux ch?/*~
doc/ch1/api_notes.tex view
@@ -1,5 +1,7 @@ \section{Conventions and API Notes} +\subsection{Widget Types}+ When you create a widget in \vtyui, the result with almost always have a type like \fw{Widget a}.  The type variable \fw{a} represents the specific type of state the widget can carry, and therefore which@@ -15,12 +17,20 @@ \fw{Widget a} whenever you need to refer to a widget; this makes it possible to mutate widget state when events occur in your application. +\subsection{Return Values}+ Regarding return values, even if a function is of type \fw{...\ -> IO   a}, we say it is ``in the \fw{IO} monad'' and \textit{returns} \fw{a}.  We won't bother saying that a function \textit{returns \fw{IO     a}}. +\subsection{Library Modules}+ Lastly, we will refer to the many \vtyui\ library modules throughout this document.  We will almost always omit the \fw{Graphics.Vty.Widgets} module namespace prefix and will instead-refer to the modules by their short names.+refer to the modules by their short names.  In addition, many modules+in this library use \fw{Data.Text} values to represent text strings.+We assume that \fw{Data.Text} is imported under the qualified name+\fw{T}.  We also assume the use of the \fw{OverloadedStrings} compiler+language extension to avoid the repeated use of \fw{T.pack}.
doc/ch1/getting_started.tex view
@@ -8,8 +8,8 @@  import Graphics.Vty.Widgets.All \end{haskellcode} -The \fw{All} module exports everything exported by the library; if you-prefer, you may import specific modules depending on your needs.+The \fw{All} module exports almost everything exported by the library.+If you prefer, you can always import specific modules.  As a demonstration, we'll create a program which presents an editing widget in the middle of the screen.  You'll be able to provide some@@ -17,6 +17,8 @@ will print what you entered.  The code for this program is as follows:  \begin{haskellcode}+ import qualified Data.Text as T+  main :: IO ()  main = do    e <- editWidget@@ -29,7 +31,7 @@    addToCollection c ui fg     e `onActivate` \this ->-     getEditText this >>= (error . ("You entered: " ++))+     getEditText this >>= (error . ("You entered: " ++) . T.unpack)     runUi c defaultContext \end{haskellcode}@@ -106,7 +108,7 @@  \begin{haskellcode}  e `onActivate` \this -> getEditText this >>=-   (error . ("You entered: " ++))+   (error . ("You entered: " ++) . T.unpack) \end{haskellcode}  This binds an event handler to the ``activation'' of the \fw{Edit}@@ -114,7 +116,10 @@ and presses \fw{Enter}.  The handler for this event is an \fw{IO} action which takes the \fw{Edit} widget itself as its only parameter. The \fw{getEditText} function gets the current text of the \fw{Edit}-widget, and we use \fw{error} to abort the program and print the text.+widget, and we use \fw{error} to abort the program and print the+text.\footnote{In general I wouldn't recommend the use of \fw{error}+  to do this, but the \vtyui\ event loop will still cleanly shut down+  and clean up Vty in the event of any exception.}  \begin{haskellcode}  runUi c defaultContext
doc/ch3/implementing_composite_widgets.tex view
@@ -41,7 +41,7 @@ implementing \fw{WidgetImpl}'s interface.  First we provide the types:  \begin{haskellcode}- data PhoneNumber = PhoneNumber String String String+ data PhoneNumber = PhoneNumber T.Text T.Text T.Text                     deriving (Show)   -- This type isn't pretty, but we have to specify the type@@ -78,9 +78,6 @@          (plainText "-") <++>          (hFixed 5 e3) -   e1 `onChange` \s -> when (length s == 3) $ focus e2-   e2 `onChange` \s -> when (length s == 3) $ focus e3-    let w = PhoneInput ui e1 e2 e3 ahs        doFireEvent = const $ do          num <- mkPhoneNumber@@ -95,6 +92,9 @@    e1 `onActivate` doFireEvent    e2 `onActivate` doFireEvent    e3 `onActivate` doFireEvent++   e1 `onChange` \s -> when (T.length s == 3) $ focus e2+   e2 `onChange` \s -> when (T.length s == 3) $ focus e3     fg <- newFocusGroup    mapM_ (addToFocusGroup fg) [e1, e2, e3]
doc/ch3/implementing_event_handlers.tex view
@@ -68,9 +68,9 @@  We've introduced a new operator here, \fw{<\string~\string~}.  This operator takes any \fw{Widget a} and a function on its state type,-\fw{a -> b}, and runs the function and returns the value, \fw{b},-inside calling monad.  \fw{addHandler} needs a value of type-\fw{Handlers TemperatureEvent}, and to get that we must use+applies the function to the state, and returns the result.+\fw{add\-Handler} needs a value of type \fw{Handlers+  Temperature\-Event}, and to get that we must use \fw{<\string~\string~}.  The \fw{addHandler} function takes a \fw{Handlers a} and a handler of
doc/ch3/new_widget_type.tex view
@@ -8,20 +8,20 @@ As an example, consider a widget that displays a numeric counter.  The widget state will be the value of the counter.  We'll start with the following state type:\footnote{You might wonder why we don't just use-  \fw{Int}; the reason is because that's too general.  Other widgets-  might represent the temperature with an \fw{Int}, and then your-  counter API functions -- taking a widget of type \fw{Widget Int} ---  would work on their widgets, which is probably not what you want!}+  \fw{Int}, i.e., \fw{Widget Int}; the reason is because that's too+  general.  Other widgets might represent the temperature with an+  \fw{Int}, and then your counter API functions -- taking a widget of+  type \fw{Widget Int} -- would work on other types of widgets, which+  is probably not what you want!}  \begin{haskellcode}  data Counter = Counter Int \end{haskellcode}  The next step is to write a widget constructor function.  This-function will return a value of type \fw{Widget Counter}, which-indicates that it is a \fw{Widget} with state type \fw{Counter}.-We'll allow the constructor to take the counter's initial value.-Here's what the function will look like in full:+function will return a value of type \fw{Widget Counter}.  The+constructor will take the counter's initial value.  Here's the+function in full:  \begin{haskellcode}  newCounter :: Int -> IO (Widget Counter)@@ -48,15 +48,15 @@ where all of the widget logic is actually implemented.  You implement this logic by overriding the fields of the \fw{WidgetImpl} type, such as \fw{render\_}.  We call \fw{newWidget}'s result \fw{wRef} because-it is a reference to a widget object, and this helps distinguish it-from the actual widget data in the next step.+it is a reference to a widget, and this helps distinguish it from the+actual widget data in the next step.  The \fw{newWidget} function takes an initial state of the widget (of type \fw{a}) and a transformation function \fw{WidgetImpl a ->   WidgetImpl a}, creates a new \fw{WidgetImpl}, sets its \fw{state} to the initial state provided, and transforms it with the transformation function.  We do this to specify the behavior of the widget beyond the-defaults, which are specified in the \fw{newWidget} function.+defaults provided by the \fw{newWidget} function.  Here is the \fw{render\_} function which will actually construct a Vty \fw{Image} to be displayed in the terminal:@@ -65,14 +65,15 @@  render_ =    \this size ctx -> do      (Counter v) <- getState this-     let s = show v-         width = fromEnum $ region_width size - length s-         truncated = take width s-     return $ string (getNormalAttr ctx) truncated+     let s = T.pack $ show v+         width = (fromEnum $ region_width size) -+                 (fromEnum $ textWidth s)+         (truncated, _, _) = clip1d (Phys 0) (Phys width) s+     return $ string (getNormalAttr ctx) $ T.unpack truncated \end{haskellcode}  The type of \fw{render\_} is \fw{Widget a -> DisplayRegion ->-  RenderContext -> IO Image}.  The types are as follows:+  RenderContext -> IO Image}.  The arguments are as follows:  \begin{itemize} \item \fw{Widget a} - the widget being rendered, i.e., the \fw{Widget@@ -88,8 +89,8 @@   widget sizes are used to compute widget positions, so sizes must be   accurate. -  A widget may render to an \fw{Image} \textit{smaller} than the-  specified size; many do.+  A widget must render to an \fw{Image} \textit{no larger} than the+  specified size. \item \fw{RenderContext} - the rendering context passed to \fw{runUi}   as explained in Section \ref{sec:event_loop}.  In the \fw{render\_}   function, we use this to determine which screen attributes to use.@@ -114,17 +115,22 @@ the latest state value at the time \fw{render\_} is called.  \begin{haskellcode}- let s = show v-     width = fromEnum $ region_width size - length s-     truncated = take width s+ let s = T.pack $ show v+     width = (fromEnum $ region_width size) -+             (fromEnum $ textWidth s)+     (truncated, _, _) = clip1d (Phys 0) (Phys width) s \end{haskellcode}  To ensure that the \fw{Image} we generate does not exceed \fw{size} as described above, we use the width of the region to limit how many-characters we take from the string representation of the counter.+characters we take from the string representation of the counter.  We+also introduce a function to calculate the width of our counter+string, \fw{textWidth}, and a function to clip the string to the+desired width, \fw{clip1d}.  For more information on text clipping,+see Section \ref{sec:textclip}.  \begin{haskellcode}- return $ string (getNormalAttr ctx) truncated+ return $ string (getNormalAttr ctx) $ T.unpack truncated \end{haskellcode}  The \fw{string} function is a Vty library function which takes an
doc/ch3/widgetimpl_api.tex view
@@ -7,6 +7,7 @@ \begin{haskellcode}  data WidgetImpl a = WidgetImpl {        state :: a+     , visibie :: Bool      , render_ :: Widget a -> DisplayRegion -> RenderContext                -> IO Image      , growHorizontal_ :: a -> IO Bool@@ -39,6 +40,11 @@ \item \fw{focused} -- \fw{True} if this widget is focused.  As   explained in Section \ref{sec:focus}, although one widget has the   user's focus, internally many widgets may share it in a hierarchy.+\item \fw{visible} -- \fw{True} if this widget is visible.  Visible+  widgets will be rendered as usual, but invisible widgets+  automatically render to empty images and do not grow horizontally or+  vertically.  This field can be manipulated with \fw{setVisible} and+  read with \fw{getVisible}. \item \fw{currentSize} -- the ``current'' size of the widget, i.e.,   the size of the \fw{Image} \textit{after} the last time the widget   was rendered.
doc/ch4/DirBrowser.tex view
@@ -146,7 +146,9 @@  \begin{haskellcode}  browser `onBrowseAccept` \path ->-   reportBrowserError browser $ "not a valid document: " ++ path+   reportBrowserError browser $ T.concat [ "not a valid document: "+                                         , T.pack path+                                         ] \end{haskellcode}  \subsubsection{Growth Policy}
doc/ch4/Edit.tex view
@@ -77,11 +77,23 @@   \fw{setEditLineLimit \$ Just 0} is a no-op. \end{itemize} +\subsubsection{Wide Character Support}++Some characters, such as those from some Asian character sets, require+two columns instead of one when displayed in a terminal.  The+\fw{Edit} widget supports such characters with one caveat: when a wide+character straddles the left or right viewing boundary of an \fw{Edit}+widget, an indicator (\fw{\$}) will be displayed in its place to+indicate that a wide character lies on the boundary and can be+revealed by scrolling further in the appropriate direction.  Such+indicators are only visual and do not affect the underlying text, so+e.g. calls to \fw{getEditText} will return the text as expected.+ \subsubsection{Growth Policy}  Single-line \fw{Edit} widgets -- those created by \fw{editWidget} -- grow only horizontally and are always one row high.  Multi-line edit-widgets -- those created by \fw{multiLineEditWidget} -- always grow in-both dimensions.  To manage this behavior, you can use one of the-``fixed'' family of widgets to control their sizes (see Section+widgets -- those created by \fw{multi\-Line\-Edit\-Widget} -- always+grow in both dimensions.  To manage this behavior, you can use one of+the ``fixed'' family of widgets to control their sizes (see Section \ref{sec:fixed}).
doc/ch4/FormattedText.tex view
@@ -20,8 +20,8 @@ The contents of a text widget can be set in one of three ways: \begin{itemize} \item Initially, as a parameter to \fw{plainText} and \fw{textWidget}-\item As a \fw{String} parameter to \fw{setText}-\item As a list parameter of \fw{(String, Attr)} with+\item As a \fw{Text} parameter to \fw{setText}+\item As a list parameter of \fw{(Text, Attr)} with   \fw{setTextWithAttrs} \end{itemize} @@ -32,8 +32,8 @@ defaults to the ``normal'' attribute of the widget (see Section \ref{sec:attributes} for more information on attributes). -The \fw{setText} function merely takes a \fw{String}, tokenizes it,-and assigns the default attribute to all tokens.+The \fw{setText} function merely takes a \fw{Text} value, tokenizes+it, and assigns the default attribute to all tokens.  The \fw{setTextWithAttrs} function provides finer control over the initial attribute assignment to the text because it lets you specify@@ -70,7 +70,7 @@ \end{haskellcode}  In addition, the formatter for a text widget can be set at any time-with \fw{setTextFormatter}:+with \fw{set\-Text\-Formatter}:  \begin{haskellcode}  setTextFormatter t someFormatter
+ doc/ch5/TextClip.tex view
@@ -0,0 +1,47 @@+\section{Text Clipping}+\label{sec:textclip}++Most widgets in \vtyui\ render some form of text.  When we render+text, we have to reason about the amount of space -- columns -- that+the text will consume in the terminal so that we can render coherent+interfaces.  However, this is tricky because some characters use one+column of space and others use+two.\footnote{\url{http://www.unicode.org/reports/tr11/}} To account+for this possibility, any code which deals with computing the space+required for text must consider the width of \textit{each character}.++In cases where we have to consider character width, the most common+operation we're trying to perform is to \textit{clip} a text string to+ensure that it fits within a given region.  The \fw{TextClip} module+provides types and functions to do this in one and two dimensions:++\begin{itemize}+\item \fw{ClipRect} - the type of two-dimensional clipping regions.+  Allows you to specify a top-left corner, a clipping width, and a+  clipping height.+\item \fw{clip1d} - performs one-dimensional clipping on a single line+  of text.+\item \fw{clip2d} - performs two-dimensional clipping on a list of+  lines of text using a \fw{Clip\-Rect}.+\end{itemize}++The functions in the \fw{TextClip} module deal in physical values+expressed using the \fw{Phys} type.  This type designates a+\textit{physical width} as opposed to a logical one.  We use this+distinction to gain compile-time clarity about which integer values+refer to logical characters and which ones refer to terminal column+counts.++Both the \fw{clip1d} and \fw{clip2d} functions return text strings+truncated so that their characters fit into the specified physical+space.  They also return \fw{Bool} indicators which can be used to+determine whether clipping occurred in the ``middle'' of wide+characters.  The \fw{Edit} widget uses this feature to annotate+truncated strings to indicate that a wide character can be found on+either end of a truncated line of text.++In simple widgets, we could technically ignore text clipping details+if we know that we'll always be rendering strings which use+single-column characters.  However, we should get in the habit of+always using clipping functions in case we need to start showing+multi-column characters.
+ doc/ch5/TextZipper.tex view
@@ -0,0 +1,67 @@+\section{The Text Zipper}++The \fw{TextZipper} module provides a+\textit{zipper}\footnote{\url{http://www.haskell.org/haskellwiki/Zipper}}+data structure to manage the text editing process of multi-line text+buffers.  A \fw{TextZipper} stores text and a \textit{cursor position}+at which editing operations are applied.  The zipper implementation+works for any string representation type that provides implementations+of \fw{drop}, \fw{take}, \fw{length}, \fw{last}, \fw{init}, and+\fw{null}.  The module provides a default implementation (used by the+\fw{Edit} widget) based on the \fw{Data.Text.Text} type.  Its+constructor, \fw{textZipper}, takes a list of \fw{Text} values and+creates a zipper.++\begin{haskellcode}+ let z = textZipper []+\end{haskellcode}++To extract the text content of the zipper, use \fw{getText}:++\begin{haskellcode}+ let theLines = getText z+\end{haskellcode}++The module provides the following \textit{editing transformations}:++\begin{itemize}+\item \fw{moveCursor (row, col)} - Move the cursor to the specified+  cursor position.  Invalid positions will be ignored.+\item \fw{insertChar c} - Insert \fw{c} at the cursor position.+\item \fw{breakLine} - Insert a line break at the cursor position.+\item \fw{killToEOL} - Kill (delete) all text after the cursor+  position up to the end of the current line.+\item \fw{gotoEOL} - Move the cursor past the end of the current line.+\item \fw{gotoBOL} - Move the cursor to just before the beginning of+  the current line.+\item \fw{deletePrevChar} - Delete the character preceding the cursor+  position.  If the cursor is at the beginning of a line, the current+  line will be appended onto the previous line.+\item \fw{deleteChar} - Delete the character at the cursor position.+  If the cursor is at the end of a line, the following line will be+  appended onto the current line.+\item \fw{moveRight} - Move the cursor one position to the right,+  wrapping to the following line if necessary.+\item \fw{moveLeft} - Move the cursor one position to the left,+  wrapping to the preceding line if necessary.+\item \fw{moveUp} - Move the cursor up by one row.+\item \fw{moveDown} - Move the cursor down by one row.+\end{itemize}++These transformations can be composed in natural ways to create a+sequence of editing transformations.  For example:++\begin{haskellcode}+ import Control.Arrow++ doEdits :: TextZipper a -> TextZipper a+ doEdits = foldl (>>>) id [ moveRight+                          , insertChar 'x'+                          , insertChar 'z'+                          , moveLeft+                          , insertChar 'y'+                          , deletePrevChar+                          ]+ -- Later:+ let edited = doEdits $ textZipper theLines+\end{haskellcode}
+ doc/ch5/main.tex view
@@ -0,0 +1,8 @@+\chapter{Other Topics}+\label{chap:other}++This chapter contains supplementary material on various aspects of+\vtyui.++\input{ch5/TextClip}+\input{ch5/TextZipper}
doc/macros.tex view
@@ -1,6 +1,6 @@ % Custom macros. -\newcommand{\vtyuiversion}{1.5}+\newcommand{\vtyuiversion}{1.6}  \newcommand{\fw}[1]{\texttt{#1}} \newcommand{\vtyui}{\fw{vty-ui}}
doc/vty-ui-users-manual.tex view
@@ -33,5 +33,6 @@ \include{ch2/main} \include{ch3/main} \include{ch4/main}+\include{ch5/main}  \end{document}
− src/ComplexDemo.hs
@@ -1,163 +0,0 @@-{-# OPTIONS_GHC -fno-warn-missing-signatures -fno-warn-unused-do-bind #-}-module Main where--import System.Exit ( exitSuccess )-import System.Locale-import Control.Monad-import Control.Concurrent-import Data.Time.Clock-import Data.Time.Format-import Graphics.Vty hiding (pad)-import Graphics.Vty.Widgets.All---- Visual attributes.-fg = white-bg = black-focAttr = black `on` yellow-headerAttr = fgColor bright_green-msgAttr = fgColor blue---- Multi-state checkbox value type-data FrostingType = Chocolate-                  | Vanilla-                  | Lemon-                    deriving (Eq, Show)--main :: IO ()-main = do-  let msg = "- <TAB> switches input elements\n\n\-            \- ordinary keystrokes edit\n\n\-            \- <SPC> toggles radio buttons and checkboxes\n\n\-            \- <ESC> quits"--      columns = [ column (ColFixed 25) `pad` (padAll 1)-                , column ColAuto `pad` (padAll 1)-                ]--  table <- newTable columns BorderFull >>=-           withNormalAttribute (bgColor blue) >>=-           withBorderAttribute (fgColor green)--  tw <- (textWidget wrap msg) >>= withNormalAttribute msgAttr-  mainBox <- vBox table tw >>= withBoxSpacing 1--  r1 <- newCheckbox "Cake"-  r2 <- newCheckbox "Death"-  radioHeader <- plainText "" >>= withNormalAttribute headerAttr--  rg <- newRadioGroup-  addToRadioGroup rg r1-  addToRadioGroup rg r2--  r3 <- newMultiStateCheckbox "Frosting" [ (Chocolate, 'C')-                                         , (Vanilla, 'V')-                                         , (Lemon, 'L')-                                         ]--  edit1 <- editWidget >>= withFocusAttribute (white `on` red)-  edit2 <- multiLineEditWidget-  edit2box <- boxFixed 30 3 edit2-  setEditLineLimit edit2 $ Just 3--  edit1Header <- textWidget wrap [] >>= withNormalAttribute headerAttr-  edit2Header <- textWidget wrap [] >>= withNormalAttribute headerAttr--  lst <- newList (fgColor bright_green)--  selector <- vLimit 3 lst-  listHeader <- plainText ""--  rs <- vBox r1 r2--  cbHeader <- plainText ""-  timeText <- plainText ""--  prog <- newProgressBar (white `on` red) (red `on` white)-  setProgressTextAlignment prog AlignCenter--  addHeadingRow_ table headerAttr ["Column 1", "Column 2"]-  addRow table $ radioHeader .|. rs-  addRow table $ cbHeader .|. r3-  addRow table $ edit1Header .|. edit1-  addRow table $ edit2Header .|. edit2box-  addRow table $ listHeader .|. customCell selector `pad` padNone-  addRow table $ emptyCell .|. timeText-  addRow table $ emptyCell .|. prog--  rg `onRadioChange` \cb -> do-      s <- getCheckboxLabel cb-      setText radioHeader $ s ++ ", please."--  r3 `onCheckboxChange` \v ->-      setText cbHeader $ "you chose: " ++ show v--  prog `onProgressChange` \val ->-      setProgressText prog $ "Progress (" ++ show val ++ " %)"--  edit1 `onChange` (setText edit1Header)-  edit2 `onChange` (setText edit2Header)--  lst `onSelectionChange` \ev ->-      case ev of-        SelectionOn _ k _ -> setText listHeader $ "You selected: " ++ k-        SelectionOff -> return ()--  lst `onItemActivated` \(ActivateItemEvent _ s _) ->-      setText listHeader $ "You activated: " ++ s--  setEditText edit1 "Foo"-  setEditText edit2 "Bar"-  setCheckboxChecked r1--  setCheckboxState r3 Chocolate-  -- It would be nice if we didn't have to do this, but the-  -- setCheckboxState call above will not notify any state-change-  -- handlers because the state isn't actually changing (from its-  -- original value of Chocolate, the first value in its state list).-  setText cbHeader $ "you chose: Chocolate"--  fgr <- newFocusGroup-  fgr `onKeyPressed` \_ k _ -> do-         case k of-           KEsc -> exitSuccess-           _ -> return False--  let strs = [ "Cookies"-             , "Cupcakes"-             , "Twinkies"-             , "M&Ms"-             , "Fritos"-             , "Cheetos"-             ]--  forM_ strs $ \s ->-      (addToList lst s =<< plainText s)--  addToFocusGroup fgr r1-  addToFocusGroup fgr r2-  addToFocusGroup fgr r3-  addToFocusGroup fgr edit1-  addToFocusGroup fgr edit2-  addToFocusGroup fgr lst--  ui <- centered =<< hLimit 70 mainBox--  forkIO $ forever $ do-         schedule $ do-           t <- getCurrentTime-           setText timeText $ formatTime defaultTimeLocale rfc822DateFormat t-         threadDelay $ 1 * 1000 * 1000--  forkIO $ forever $ do-         let act i = do-               threadDelay $ 1 * 1000 * 1000-               schedule $ setProgress prog (i `mod` 101)-               act $ i + 4-         act 0--  c <- newCollection-  _ <- addToCollection c ui fgr--  runUi c $ defaultContext { focusAttr = focAttr-                           , normalAttr = fg `on` bg-                           }
− src/DialogDemo.hs
@@ -1,38 +0,0 @@-{-# OPTIONS_GHC -fno-warn-unused-do-bind #-}-module Main where--import Graphics.Vty hiding (Button)-import Graphics.Vty.Widgets.All--main :: IO ()-main = do-  e <- editWidget-  fg <- newFocusGroup-  addToFocusGroup fg e--  u <- plainText "Enter some text and press enter." <--> return e-       >>= withBoxSpacing 1--  pe <- padded u (padLeftRight 2)-  (d, dFg) <- newDialog pe "<enter text>"-  setNormalAttribute d (white `on` blue)--  c <- centered =<< withPadding (padLeftRight 10) (dialogWidget d)--  -- When the edit widget changes, set the dialog's title.-  e `onChange` setDialogTitle d--  -- When the user presses Enter in the edit widget, accept the-  -- dialog.-  e `onActivate` (const $ acceptDialog d)--  -- Exit either way.-  d `onDialogAccept` const shutdownUi-  d `onDialogCancel` const shutdownUi--  coll <- newCollection-  _ <- addToCollection coll c =<< (mergeFocusGroups fg dFg)--  runUi coll $ defaultContext { focusAttr = black `on` yellow }--  (putStrLn . ("You entered: " ++)) =<< getEditText e
− src/DirBrowserDemo.hs
@@ -1,17 +0,0 @@-module Main where--import Graphics.Vty-import Graphics.Vty.Widgets.All--main :: IO ()-main = do-  (b, fg) <- newDirBrowser defaultBrowserSkin--  c <- newCollection-  _ <- addToCollection c (dirBrowserWidget b) fg--  b `onBrowseAccept` error-  b `onBrowseCancel` error--  runUi c $ defaultContext { focusAttr = white `on` blue-                           }
− src/EditDemo.hs
@@ -1,39 +0,0 @@-module Main where--import Graphics.Vty hiding (Button)-import Graphics.Vty.Widgets.All--main :: IO ()-main = do-  e1 <- multiLineEditWidget-  e2 <- multiLineEditWidget-  setEditLineLimit e2 $ Just 3-  e3 <- editWidget--  fg <- newFocusGroup-  _ <- addToFocusGroup fg e1-  _ <- addToFocusGroup fg e2-  _ <- addToFocusGroup fg e3--  be1 <- bordered =<< boxFixed 40 5 e1-  be2 <- bordered =<< boxFixed 40 3 e2-  be3 <- bordered =<< boxFixed 40 1 e3--  c <- centered =<< ((plainText "Multi-Line Editor (unlimited lines):")-                         <--> (return be1)-                         <--> (plainText "Multi-Line Editor (3 lines):")-                         <--> (return be2)-                         <--> (plainText "Single-Line Editor:")-                         <--> (return be3)-                         <--> (plainText "- Esc to quit\n\n- TAB to switch editors") >>= withBoxSpacing 1-                    )--  coll <- newCollection-  _ <- addToCollection coll c fg--  fg `onKeyPressed` \_ k _ ->-      case k of-        KEsc -> shutdownUi >> return True-        _ -> return False--  runUi coll $ defaultContext { focusAttr = fgColor yellow }
src/Graphics/Vty/Widgets/All.hs view
@@ -6,6 +6,8 @@     , module Graphics.Vty.Widgets.List     , module Graphics.Vty.Widgets.Borders     , module Graphics.Vty.Widgets.Text+    , module Graphics.Vty.Widgets.TextClip+    , module Graphics.Vty.Widgets.TextZipper     , module Graphics.Vty.Widgets.Edit     , module Graphics.Vty.Widgets.Util     , module Graphics.Vty.Widgets.Table@@ -32,6 +34,8 @@ import Graphics.Vty.Widgets.List import Graphics.Vty.Widgets.Borders import Graphics.Vty.Widgets.Text+import Graphics.Vty.Widgets.TextClip+import Graphics.Vty.Widgets.TextZipper import Graphics.Vty.Widgets.Edit import Graphics.Vty.Widgets.Util import Graphics.Vty.Widgets.Table
src/Graphics/Vty/Widgets/Borders.hs view
@@ -20,6 +20,8 @@     ) where +import qualified Data.Text as T+ import Graphics.Vty import Graphics.Vty.Widgets.Core import Graphics.Vty.Widgets.Box@@ -32,7 +34,7 @@ class HasBorderAttr a where     setBorderAttribute :: a -> Attr -> IO () -data HBorder = HBorder Attr String+data HBorder = HBorder Attr T.Text                deriving (Show)  instance HasBorderAttr (Widget HBorder) where@@ -45,23 +47,23 @@  -- | Adds a label to a horizontal border.  The label will be -- horizontally centered.-withHBorderLabel :: String -> Widget HBorder -> IO (Widget HBorder)+withHBorderLabel :: T.Text -> Widget HBorder -> IO (Widget HBorder) withHBorderLabel label w = setHBorderLabel w label >> return w  -- | Adds a label to a horizontal border.  The label will be -- horizontally centered.-setHBorderLabel :: Widget HBorder -> String -> IO ()+setHBorderLabel :: Widget HBorder -> T.Text -> IO () setHBorderLabel w label =     updateWidgetState w $ \(HBorder a _) -> HBorder a label  -- | Adds a label to the top border of a bordered widget.  The label -- will be horizontally centered.-withBorderedLabel :: String -> Widget (Bordered a) -> IO (Widget (Bordered a))+withBorderedLabel :: T.Text -> Widget (Bordered a) -> IO (Widget (Bordered a)) withBorderedLabel label w = setBorderedLabel w label >> return w  -- | Adds a label to the top border of a bordered widget.  The label -- will be horizontally centered.-setBorderedLabel :: Widget (Bordered a) -> String -> IO ()+setBorderedLabel :: Widget (Bordered a) -> T.Text -> IO () setBorderedLabel w label =     updateWidgetState w $ \(Bordered a ch _) -> Bordered a ch label @@ -69,7 +71,7 @@ -- attribute and character. hBorder :: IO (Widget HBorder) hBorder = do-  let initSt = HBorder def_attr ""+  let initSt = HBorder def_attr T.empty   wRef <- newWidget initSt $ \w ->       w { growHorizontal_ = const $ return True         , render_ = renderHBorder@@ -85,22 +87,30 @@                          , attr                          , normalAttr ctx                          ]-      noTitle = char_fill attr' (skinHorizontal $ skin ctx) (region_width s) 1-  case null str of-    True -> return noTitle-    False -> do-      let title = " " ++ str ++ " "-      case (toEnum $ length title) > region_width s of-        True -> return noTitle-        False -> do-                  let remaining = region_width s - (toEnum $ length title)-                      side1 = remaining `div` 2-                      side2 = if remaining `mod` 2 == 0 then side1 else side1 + 1-                  return $ horiz_cat [ char_fill attr' (skinHorizontal $ skin ctx) side1 1-                                     , string attr' title-                                     , char_fill attr' (skinHorizontal $ skin ctx) side2 1-                                     ]+      ch = skinHorizontal $ skin ctx+      noTitle = T.pack $ replicate (fromEnum $ region_width s) ch +  wStr <- case T.null str of+            True -> return noTitle+            False -> do+              let title = T.concat [ T.pack " "+                                   , str+                                   , T.pack " "+                                   ]+              case (textWidth title) > (Phys $ fromEnum $ region_width s) of+                True -> return noTitle+                False -> do+                       let remaining = (Phys $ fromEnum $ region_width s) - (textWidth title)+                           Phys side1 = remaining `div` Phys 2+                           side2 = if remaining `mod` Phys 2 == Phys 0 then side1 else side1 + 1+                       return $ T.concat [ T.pack $ replicate side1 ch+                                         , title+                                         , T.pack $ replicate side2 ch+                                         ]++  w <- plainTextWithAttrs [(wStr, attr')]+  render w s ctx+ data VBorder = VBorder Attr                deriving (Show) @@ -125,7 +135,7 @@         }   return wRef -data Bordered a = (Show a) => Bordered Attr (Widget a) String+data Bordered a = (Show a) => Bordered Attr (Widget a) T.Text  instance Show (Bordered a) where     show (Bordered attr _ l) = concat [ "Bordered { attr = "@@ -142,7 +152,7 @@ -- |Wrap a widget in a bordering box. bordered :: (Show a) => Widget a -> IO (Widget (Bordered a)) bordered child = do-  let initSt = Bordered def_attr child ""+  let initSt = Bordered def_attr child T.empty   wRef <- newWidget initSt $ \w ->       w { growVertical_ = const $ growVertical child         , growHorizontal_ = const $ growHorizontal child@@ -189,10 +199,14 @@   let adjusted = DisplayRegion (image_width childImage + 2)                  (image_height childImage) -  tlCorner <- plainText [skinCornerTL sk] >>= withNormalAttribute attr'-  trCorner <- plainText [skinCornerTR sk] >>= withNormalAttribute attr'-  blCorner <- plainText [skinCornerBL sk] >>= withNormalAttribute attr'-  brCorner <- plainText [skinCornerBR sk] >>= withNormalAttribute attr'+  tlCorner <- plainText (T.singleton $ skinCornerTL sk)+              >>= withNormalAttribute attr'+  trCorner <- plainText (T.singleton $ skinCornerTR sk)+              >>= withNormalAttribute attr'+  blCorner <- plainText (T.singleton $ skinCornerBL sk)+              >>= withNormalAttribute attr'+  brCorner <- plainText (T.singleton $ skinCornerBR sk)+              >>= withNormalAttribute attr'    hb <- hBorder >>= withHBorderLabel label   setBorderAttribute hb attr'
src/Graphics/Vty/Widgets/Button.hs view
@@ -11,6 +11,7 @@     ) where +import qualified Data.Text as T import Graphics.Vty.Widgets.Core import Graphics.Vty.Widgets.Text import Graphics.Vty.Widgets.Padding@@ -34,7 +35,7 @@ pressButton b = fireEvent b (return . buttonPressedHandlers) b  -- |Set the text label on a button.-setButtonText :: Button -> String -> IO ()+setButtonText :: Button -> T.Text -> IO () setButtonText b s = setText (buttonText b) s  instance HasNormalAttr Button where@@ -44,7 +45,7 @@     setFocusAttribute b a = setFocusAttribute (buttonWidget b) a  -- |Create a button.  Get its underlying widget with 'buttonWidget'.-newButton :: String -> IO Button+newButton :: T.Text -> IO Button newButton msg = do   t <- plainText msg   setTextAppearFocused t True@@ -53,6 +54,8 @@        withPadding (padLeftRight 3) >>=        withNormalAttribute (white `on` black) >>=        withFocusAttribute (blue `on` white)++  updateWidget w $ \st -> st { getCursorPosition_ = const $ return Nothing }    hs <- newHandlers 
src/Graphics/Vty/Widgets/CheckBox.hs view
@@ -1,4 +1,5 @@-{-# LANGUAGE TypeSynonymInstances, FlexibleInstances, DeriveDataTypeable #-}+{-# LANGUAGE TypeSynonymInstances, FlexibleInstances, DeriveDataTypeable,+  OverloadedStrings #-} -- |This module provides ''check box'' widgets and ''radio button'' -- widgets.  In addition, this module provides a generalized -- ''multi-state'' check box type which allows you to set multiple@@ -40,11 +41,13 @@ import Data.IORef import Data.List ( findIndex ) import Data.Maybe+import qualified Data.Text as T import Control.Monad import Control.Exception import Data.Typeable import Graphics.Vty import Graphics.Vty.Widgets.Core+import Graphics.Vty.Widgets.Text import Graphics.Vty.Widgets.Events import Graphics.Vty.Widgets.Util @@ -126,9 +129,10 @@                            , rightBracketChar :: Char                            , checkboxStates :: [(a, Char)]                            , currentState :: a-                           , checkboxLabel :: String+                           , checkboxLabel :: T.Text                            , checkboxChangeHandlers :: Handlers a                            , checkboxFrozen :: Bool+                           , innerTextWidget :: Widget FormattedText                            }  instance Show a => Show (CheckBox a) where@@ -141,12 +145,12 @@                      ]  -- |Create a new checkbox with the specified text label.-newCheckbox :: String -> IO (Widget (CheckBox Bool))+newCheckbox :: T.Text -> IO (Widget (CheckBox Bool)) newCheckbox label = newMultiStateCheckbox label [(False, ' '), (True, 'x')]  -- |Create a new multi-state checkbox. newMultiStateCheckbox :: (Eq a) =>-                         String -- ^The checkbox label.+                         T.Text -- ^The checkbox label.                       -> [(a, Char)] -- ^The list of valid states that                                      -- the checkbox can be in, along                                      -- with the visual representation@@ -155,6 +159,7 @@ newMultiStateCheckbox _ [] = throw EmptyCheckboxStates newMultiStateCheckbox label states = do   cchs <- newHandlers+  t <- plainText ""   let initSt = CheckBox { checkboxLabel = label                         , checkboxChangeHandlers = cchs                         , leftBracketChar = '['@@ -162,36 +167,40 @@                         , checkboxStates = states                         , currentState = fst $ states !! 0                         , checkboxFrozen = False+                        , innerTextWidget = t                         }    wRef <- newWidget initSt $ \w ->       w { getCursorPosition_ =             \this -> do               pos <- getCurrentPosition this-              return $ Just (pos `plusWidth` 1)+              ch <- leftBracketChar <~~ this+              return $ Just (pos `plusWidth` (toEnum $ fromEnum $ chWidth ch))          , keyEventHandler = radioKeyEvent         , render_ =             \this sz ctx -> do-              f <- focused <~ this               st <- getState this--              let attr = mergeAttrs [ overrideAttr ctx-                                    , normalAttr ctx-                                    ]--                  fAttr = if f-                          then focusAttr ctx-                          else attr+              tw <- innerTextWidget <~~ this -                  v = currentState st+              let v = currentState st                   ch = fromJust $ lookup v (checkboxStates st) -                  s = [leftBracketChar st, ch, rightBracketChar st, ' '] ++-                      (checkboxLabel st)+                  s = T.concat [ T.pack [ leftBracketChar st+                                        , ch+                                        , rightBracketChar st+                                        , ' '+                                        ]+                               , checkboxLabel st+                               ] -              return $ string fAttr $ take (fromEnum $ region_width sz) s+              setText tw s+              render tw sz ctx         }++  wRef `relayFocusEvents` t+  setTextAppearFocused t True+   return wRef  modifyElem :: [a] -> Int -> (a -> a) -> [a]@@ -223,7 +232,7 @@                                      }  -- |Get a checkbox's text label.-getCheckboxLabel :: Widget (CheckBox a) -> IO String+getCheckboxLabel :: Widget (CheckBox a) -> IO T.Text getCheckboxLabel = (checkboxLabel <~~)  radioKeyEvent :: (Eq a) => Widget (CheckBox a) -> Key -> [Modifier] -> IO Bool
src/Graphics/Vty/Widgets/Core.hs view
@@ -31,6 +31,8 @@     , growHorizontal     , getCursorPosition     , showWidget+    , getVisible+    , setVisible     , (<~)     , (<~~) @@ -156,6 +158,8 @@ data WidgetImpl a = WidgetImpl {       state :: !a     -- ^The state of the widget.+    , visible :: !Bool+    -- ^Whether the widget is visible.     , render_ :: Widget a -> DisplayRegion -> RenderContext -> IO Image     -- ^The rendering routine of the widget.  Takes the widget itself,     -- a region indicating how much space the rendering process has to@@ -221,19 +225,37 @@                     , " }"                     ] +-- |Set the visibility of a widget.  Invisible widgets do not grow in+-- either direction, always render to an empty image, and never+-- declare a cursor position.+setVisible :: Widget a -> Bool -> IO ()+setVisible wRef v = updateWidget wRef $ \st -> st { visible = v }++-- |Get the visibility of a widget.+getVisible :: Widget a -> IO Bool+getVisible = (visible <~)+ -- |Does a widget grow horizontally? growHorizontal :: Widget a -> IO Bool growHorizontal w = do-  act <- growHorizontal_ <~ w-  st <- state <~ w-  act st+  v <- visible <~ w+  case v of+    True -> do+           act <- growHorizontal_ <~ w+           st <- state <~ w+           act st+    False -> return False  -- |Does a widget grow vertically? growVertical :: Widget a -> IO Bool growVertical w = do-  act <- growVertical_ <~ w-  st <- state <~ w-  act st+  v <- visible <~ w+  case v of+    True -> do+           act <- growVertical_ <~ w+           st <- state <~ w+           act st+    False -> return False  -- |Render a widget.  This function should be called by widget -- implementations, since it does more than 'render_'; this function@@ -249,23 +271,27 @@ render wRef sz ctx = do   impl <- readIORef wRef -  -- Merge the override attributes with the context.  If the overrides-  -- haven't been set (still def_attr), they will have no effect on-  -- the context attributes.-  norm <- normalAttribute <~ wRef-  foc <- focusAttribute <~ wRef-  let newCtx = ctx { normalAttr = mergeAttr norm $ normalAttr ctx-                   , focusAttr = mergeAttr foc $ focusAttr ctx-                   }+  v <- visible <~ wRef+  case v of+    False -> return empty_image+    True -> do+           -- Merge the override attributes with the context.  If the+           -- overrides haven't been set (still def_attr), they will+           -- have no effect on the context attributes.+           norm <- normalAttribute <~ wRef+           foc <- focusAttribute <~ wRef+           let newCtx = ctx { normalAttr = mergeAttr norm $ normalAttr ctx+                            , focusAttr = mergeAttr foc $ focusAttr ctx+                            } -  img <- render_ impl wRef sz newCtx-  let imgsz =  DisplayRegion (image_width img) (image_height img)+           img <- render_ impl wRef sz newCtx+           let imgsz =  DisplayRegion (image_width img) (image_height img) -  when (image_width img > region_width sz ||-        image_height img > region_height sz) $ throw $ ImageTooBig (show impl) sz imgsz+           when (image_width img > region_width sz ||+                 image_height img > region_height sz) $ throw $ ImageTooBig (show impl) sz imgsz -  setCurrentSize wRef $ DisplayRegion (image_width img) (image_height img)-  return img+           setCurrentSize wRef $ DisplayRegion (image_width img) (image_height img)+           return img  -- |Render a widget and set its position after rendering is complete. -- This is exported for internal use; widget implementations should@@ -326,6 +352,7 @@                      , currentSize = DisplayRegion 0 0                      , currentPosition = DisplayRegion 0 0                      , focused = False+                     , visible = True                      , gainFocusHandlers = gfhs                      , loseFocusHandlers = lfhs                      , keyEventHandler = \_ _ _ -> return False@@ -614,8 +641,12 @@ -- |Get the desired cursor position, if any, for a widget. getCursorPosition :: Widget a -> IO (Maybe DisplayRegion) getCursorPosition wRef = do-  ci <- getCursorPosition_ <~ wRef-  ci wRef+  v <- visible <~ wRef+  case v of+    True -> do+           ci <- getCursorPosition_ <~ wRef+           ci wRef+    False -> return Nothing  -- |Return the current focus entry of a focus group. currentEntry :: Widget FocusGroup -> IO (Widget FocusEntry)
src/Graphics/Vty/Widgets/Dialog.hs view
@@ -13,6 +13,7 @@     ) where +import qualified Data.Text as T import Graphics.Vty.Widgets.Centering import Graphics.Vty.Widgets.Button import Graphics.Vty.Widgets.Padding@@ -22,7 +23,7 @@ import Graphics.Vty.Widgets.Core  data Dialog = Dialog { dialogWidget :: Widget (Bordered Padded)-                     , setDialogTitle :: String -> IO ()+                     , setDialogTitle :: T.Text -> IO ()                      , dialogAcceptHandlers :: Handlers Dialog                      , dialogCancelHandlers :: Handlers Dialog                      }@@ -33,10 +34,10 @@ -- |Create a new dialog with the specified embedded interface and -- title.  Returns the dialog itself and the 'FocusGroup' to which its -- buttons were added, for use in your application.-newDialog :: (Show a) => Widget a -> String -> IO (Dialog, Widget FocusGroup)+newDialog :: (Show a) => Widget a -> T.Text -> IO (Dialog, Widget FocusGroup) newDialog body title = do-  okB <- newButton "OK"-  cancelB <- newButton "Cancel"+  okB <- newButton $ T.pack "OK"+  cancelB <- newButton $ T.pack "Cancel"    buttonBox <- (return $ buttonWidget okB) <++> (return $ buttonWidget cancelB)   setBoxSpacing buttonBox 4
src/Graphics/Vty/Widgets/DirBrowser.hs view
@@ -1,3 +1,4 @@+{-# LANGUAGE OverloadedStrings #-} -- |This module provides a directory browser interface widget.  For -- full details, please see the Vty-ui User's Manual. module Graphics.Vty.Widgets.DirBrowser@@ -21,6 +22,7 @@ import qualified Data.Map as Map import qualified Control.Exception as E import Control.Monad+import qualified Data.Text as T import Graphics.Vty import Graphics.Vty.Widgets.Core import Graphics.Vty.Widgets.List@@ -79,11 +81,21 @@                                -- ^Used for device entries.                                , browserSockAttr :: Attr                                -- ^Used for socket entries.+                               , browserShowHeader :: Bool+                               -- ^Whether the browser header should+                               -- be shown.+                               , browserShowFooter :: Bool+                               -- ^Whether the browser footer should+                               -- be shown.                                , browserCustomAnnotations :: [ (FilePath -> FileStatus -> Bool-                                                               , FilePath -> FileStatus -> IO String+                                                               , FilePath -> FileStatus -> IO T.Text                                                                , Attr)                                                              ]                                -- ^File annotations.+                               , browserIncludeEntry :: FilePath -> FileStatus -> Bool+                               -- ^The predicate which determines+                               -- which entries get listed in the+                               -- browser.                                }  -- |The default browser skin with (hopefully) sane attribute defaults.@@ -97,12 +109,15 @@                                  , browserNamedPipeAttr = fgColor yellow                                  , browserCharDevAttr = fgColor red                                  , browserSockAttr = fgColor magenta+                                 , browserShowHeader = True+                                 , browserShowFooter = True                                  , browserCustomAnnotations = []+                                 , browserIncludeEntry = (const . const) True                                  }  -- |Apply annotations to a browser skin. withAnnotations :: BrowserSkin-                -> [(FilePath -> FileStatus -> Bool, FilePath -> FileStatus -> IO String, Attr)]+                -> [(FilePath -> FileStatus -> Bool, FilePath -> FileStatus -> IO T.Text, Attr)]                 -> BrowserSkin withAnnotations sk as = sk { browserCustomAnnotations = browserCustomAnnotations sk ++ as } @@ -111,13 +126,15 @@ newDirBrowser :: BrowserSkin -> IO (DirBrowser, Widget FocusGroup) newDirBrowser bSkin = do   path <- getCurrentDirectory-  pathWidget <- plainText ""-  errorText <- plainText "" >>= withNormalAttribute (browserErrorAttr bSkin)-  header <- ((plainText " Path: ") <++> (return pathWidget) <++> (hFill ' ' 1))+  pathWidget <- plainText T.empty+  errorText <- plainText T.empty >>= withNormalAttribute (browserErrorAttr bSkin)+  header <- ((plainText " Path: ")+             <++> (return pathWidget) <++> (hFill ' ' 1))             >>= withNormalAttribute (browserHeaderAttr bSkin) -  fileInfo <- plainText ""-  footer <- ((plainText " ") <++> (return fileInfo) <++> (hFill ' ' 1) <++> (return errorText))+  fileInfo <- plainText T.empty+  footer <- ((plainText " ")+             <++> (return fileInfo) <++> (hFill ' ' 1) <++> (return errorText))             >>= withNormalAttribute (browserHeaderAttr bSkin)    l <- newList (browserUnfocusedSelAttr bSkin)@@ -145,8 +162,11 @@    l `onKeyPressed` handleBrowserKey b   l `onSelectionChange` (\e -> clearError b >> handleSelectionChange b e)-  b `onBrowserPathChange` setText (dirBrowserPathDisplay b)+  b `onBrowserPathChange` (setText (dirBrowserPathDisplay b) . T.pack) +  setVisible header $ browserShowHeader bSkin+  setVisible footer $ browserShowFooter bSkin+   fg <- newFocusGroup   _ <- addToFocusGroup fg ui @@ -156,11 +176,14 @@ -- |Report an error in the browser's error-reporting area.  Useful for -- reporting application-specific errors with the user's file -- selection.-reportBrowserError :: DirBrowser -> String -> IO ()-reportBrowserError b msg = setText (dirBrowserErrorWidget b) $ "Error: " ++ msg+reportBrowserError :: DirBrowser -> T.Text -> IO ()+reportBrowserError b msg = setText (dirBrowserErrorWidget b) $+                           T.concat [ "Error: "+                                    , msg+                                    ]  clearError :: DirBrowser -> IO ()-clearError b = setText (dirBrowserErrorWidget b) ""+clearError b = setText (dirBrowserErrorWidget b) T.empty  -- |Register handlers to be invoked when the user makes a selection. onBrowseAccept :: DirBrowser -> (FilePath -> IO ()) -> IO ()@@ -189,21 +212,24 @@ handleSelectionChange b ev = do   case ev of     SelectionOff -> setText (dirBrowserFileInfo b) "-"-    SelectionOn _ path _ -> setText (dirBrowserFileInfo b) =<< getFileInfo b path+    SelectionOn _ path _ -> setText (dirBrowserFileInfo b) =<<+                            (getFileInfo b path) -getFileInfo :: DirBrowser -> FilePath -> IO String+getFileInfo :: DirBrowser -> FilePath -> IO T.Text getFileInfo b path = do   cur <- getDirBrowserPath b   let newPath = cur </> path   st <- getSymbolicLinkStatus newPath   (_, mkAnn) <- fileAnnotation (dirBrowserSkin b) st cur path   ann <- mkAnn-  return $ path ++ ": " ++ ann+  return $ T.concat [ T.pack (path ++ ": ")+                    , ann+                    ] -builtInAnnotations :: FilePath -> BrowserSkin -> [(FilePath -> FileStatus -> Bool, FilePath -> FileStatus -> IO String, Attr)]+builtInAnnotations :: FilePath -> BrowserSkin -> [(FilePath -> FileStatus -> Bool, FilePath -> FileStatus -> IO T.Text, Attr)] builtInAnnotations cur sk =     [ (\_ s -> isRegularFile s-      , \_ s -> return $ "regular file, " +++      , \_ s -> return $ T.pack $ "regular file, " ++                 (show $ fileSize s) ++ " bytes"       , def_attr)     , (\_ s -> isSymbolicLink s,@@ -213,7 +239,7 @@                       else do                         linkPath <- readSymbolicLink p                         canonicalizePath $ cur </> linkPath-          return $ "symbolic link to " ++ linkDest)+          return $ T.pack $ "symbolic link to " ++ linkDest)       , browserLinkAttr sk)     , (\_ s -> isDirectory s, \_ _ -> return "directory", browserDirAttr sk)     , (\_ s -> isBlockDevice s, \_ _ -> return "block device", browserBlockDevAttr sk)@@ -222,14 +248,14 @@     , (\_ s -> isSocket s, \_ _ -> return "socket", browserSockAttr sk)     ] -fileAnnotation :: BrowserSkin -> FileStatus -> FilePath -> FilePath -> IO (Attr, IO String)+fileAnnotation :: BrowserSkin -> FileStatus -> FilePath -> FilePath -> IO (Attr, IO T.Text) fileAnnotation sk st cur shortPath = do   let fullPath = cur </> shortPath        annotation = getAnnotation' fullPath st $ (browserCustomAnnotations sk) ++                    (builtInAnnotations cur sk) -      getAnnotation' _ _ [] = (def_attr, return "")+      getAnnotation' _ _ [] = (def_attr, return T.empty)       getAnnotation' pth stat ((f,mkAnn,a):rest) =           if f pth stat           then (a, mkAnn pth stat)@@ -262,7 +288,7 @@                       entries <- getDirectoryContents cPath                       return (True, entries))                      `E.catch` \e -> do-                             reportBrowserError b (ioeGetErrorString e)+                             reportBrowserError b (T.pack $ ioeGetErrorString e)                              return (False, [])    when res $ do@@ -304,11 +330,13 @@     forM_ entries $ \entry -> do       let fullPath = cur </> entry       f <- getSymbolicLinkStatus fullPath-      (attr, _) <- fileAnnotation (dirBrowserSkin b) f cur entry-      w <- plainText " " <++> plainText entry-      addToList (dirBrowserList b) entry w-      ch <- getSecondChild w-      setNormalAttribute ch attr+      when (browserIncludeEntry (dirBrowserSkin b) fullPath f) $+           do+             (attr, _) <- fileAnnotation (dirBrowserSkin b) f cur entry+             w <- plainText " " <++> plainText (T.pack entry)+             addToList (dirBrowserList b) entry w+             ch <- getSecondChild w+             setNormalAttribute ch attr  descend :: DirBrowser -> Bool -> IO () descend b shouldSelect = do
src/Graphics/Vty/Widgets/Edit.hs view
@@ -1,4 +1,4 @@-{-# LANGUAGE TypeSynonymInstances, FlexibleInstances #-}+{-# LANGUAGE TypeSynonymInstances, FlexibleInstances, CPP #-} -- |This module provides a text-editing widget.  Edit widgets can -- operate in single- and multi-line modes. --@@ -19,6 +19,21 @@ -- * @Del@ / @Control-d@ - delete the current character -- -- * @Backspace@ - delete the previous character+--+-- Edit widgets may be configured with a line limit which limits the+-- number of lines of text the widget will store.  It does not provide+-- any limit control on the length of its lines, though.+--+-- Edit widgets support multi-column characters.  (For some+-- information, see <http://www.unicode.org/reports/tr11/>.)  When the+-- edit widget scrolling reaches a point where a wide character cannot+-- be drawn because it is bisected by the editing window's boundary,+-- it will be replaced with an indicator (\"$\") until the scrolling+-- window is moved enough to reveal the character.  This is done to+-- preserve the relative alignment of all of the rows in the widget in+-- the presence of characters of different widths.  Note that this is+-- a visual aid only and does not affect the underlying text content+-- of the widget. module Graphics.Vty.Widgets.Edit     ( Edit     , editWidget@@ -26,47 +41,44 @@     , getEditText     , getEditCurrentLine     , setEditText-    , setEditCursorPosition     , getEditCursorPosition+    , setEditCursorPosition     , setEditLineLimit     , getEditLineLimit+    , applyEdit     , onActivate     , onChange     , onCursorMove+#ifdef TESTING+    , doClipping+    , indicatorChar+#endif     ) where  import Control.Applicative ((<$>)) import Control.Monad-import Data.List+import qualified Data.Text as T import Graphics.Vty import Graphics.Vty.Widgets.Core import Graphics.Vty.Widgets.Events import Graphics.Vty.Widgets.Util+import Graphics.Vty.Widgets.TextClip+import qualified Graphics.Vty.Widgets.TextZipper as Z -data Edit = Edit { currentText :: [String]-                 , cursorRow :: Int-                 , cursorColumn :: Int-                 , displayStart :: Int-                 , displayWidth :: Int-                 , topRow :: Int-                 , visibleRows :: Int+data Edit = Edit { contents :: Z.TextZipper T.Text+                 , clipRect :: ClipRect                  , activateHandlers :: Handlers (Widget Edit)-                 , changeHandlers :: Handlers String+                 , changeHandlers :: Handlers T.Text                  , cursorMoveHandlers :: Handlers (Int, Int)                  , lineLimit :: Maybe Int                  }  instance Show Edit where     show e = concat [ "Edit { "-                    , "currentText = ", show $ currentText e-                    , ", cursorColumn = ", show $ cursorColumn e-                    , ", cursorRow = ", show $ cursorRow e-                    , ", topRow = ", show $ topRow e+                    , "contents = ", show $ contents e                     , ", lineLimit = ", show $ lineLimit e-                    , ", visibleRows = ", show $ visibleRows e-                    , ", displayStart = ", show $ displayStart e-                    , ", displayWidth = ", show $ displayWidth e+                    , ", clipRect = ", show $ clipRect e                     , " }"                     ] @@ -76,13 +88,12 @@   chs <- newHandlers   cmhs <- newHandlers -  let initSt = Edit { currentText = [""]-                    , cursorRow = 0-                    , cursorColumn = 0-                    , displayStart = 0-                    , displayWidth = 0-                    , topRow = 0-                    , visibleRows = 1+  let initSt = Edit { contents = Z.textZipper []+                    , clipRect = ClipRect { clipLeft = 0+                                          , clipWidth = 0+                                          , clipTop = 0+                                          , clipHeight = 1+                                          }                     , activateHandlers = ahs                     , changeHandlers = chs                     , cursorMoveHandlers = cmhs@@ -97,57 +108,72 @@                 Just v | v == 1 -> return False                 _ -> return True -        , getCursorPosition_ =-            \this -> do-              f <- focused <~ this-              pos <- getCurrentPosition this-              curRow <- cursorRow <~~ this-              curCol <- cursorColumn <~~ this-              startCol <- displayStart <~~ this-              startRow <- topRow <~~ this+        , getCursorPosition_ = internalGetCursorPosition+        , render_ = renderEditWidget+        , keyEventHandler = editKeyEvent+        }+  return wRef -              if f then-                  return (Just $ pos `plusWidth` (toEnum (curCol - startCol)) `plusHeight` (toEnum (curRow - startRow))) else-                  return Nothing+internalGetCursorPosition :: Widget Edit -> IO (Maybe DisplayRegion)+internalGetCursorPosition this = do+  st <- getState this+  f <- focused <~ this+  pos <- getCurrentPosition this -        , render_ =-            \this size ctx -> do-              resize this ( fromEnum $ region_height size-                          , fromEnum $ region_width size )+  let (cursorRow, _) = Z.cursorPosition (contents st)+      Phys offset = physCursorCol st - clipLeft (clipRect st)+      newPos = pos+               `withWidth` (toEnum ((fromEnum $ region_width pos) + offset))+               `plusHeight` (toEnum (cursorRow - (fromEnum $ clipTop $ clipRect st))) -              st <- getState this+  return $ if f then Just newPos else Nothing -              let truncated l = take (displayWidth st)-                                (drop (displayStart st) l)+renderEditWidget :: Widget Edit -> DisplayRegion -> RenderContext -> IO Image+renderEditWidget this size ctx = do+  resize this ( Phys $ fromEnum $ region_height size+              , Phys $ fromEnum $ region_width size ) -                  visibleLines = take (visibleRows st) $-                                 drop (topRow st) (currentText st)-                  truncatedLines = truncated <$> visibleLines+  st <- getState this+  isFocused <- focused <~ this -                  nAttr = mergeAttrs [ overrideAttr ctx-                                     , normalAttr ctx-                                     ]+  let nAttr = mergeAttrs [ overrideAttr ctx+                         , normalAttr ctx+                         ]+      attr = if isFocused then focusAttr ctx else nAttr -                  totalAllowedLines = fromEnum $ region_height size-                  numEmptyLines = lim - length truncatedLines-                      where-                        lim = case lineLimit st of-                                Just v -> min v totalAllowedLines-                                Nothing -> totalAllowedLines+      clipped = doClipping (Z.getText $ contents st) (clipRect st) -                  emptyLines = replicate numEmptyLines ""+      totalAllowedLines = fromEnum $ region_height size+      numEmptyLines = lim - length clipped+          where+            lim = case lineLimit st of+                    Just v -> min v totalAllowedLines+                    Nothing -> totalAllowedLines -              isFocused <- focused <~ this-              let attr = if isFocused then focusAttr ctx else nAttr-                  lineWidget s = string attr s-                                 <|> char_fill attr ' ' (region_width size - (toEnum $ length s)) 1+      emptyLines = replicate numEmptyLines ""+      lineWidget s = let Phys physLineLength = sum $ chWidth <$> s+                     in string attr s <|>+                        char_fill attr ' ' (region_width size - toEnum physLineLength) 1 -              return $ vert_cat $ lineWidget <$> (truncatedLines ++ emptyLines)+  return $ vert_cat $ lineWidget <$> (clipped ++ emptyLines) -        , keyEventHandler = editKeyEvent-        }-  return wRef+doClipping :: [T.Text] -> ClipRect -> [String]+doClipping ls rect =+    let sliced True = [indicatorChar]+        sliced False = ""+        truncatedLines = clip2d rect ls +    in [ sliced lslice ++ (T.unpack r) ++ sliced rslice+         | (r, lslice, rslice) <- truncatedLines ]++-- |Convert a logical column number (corresponding to a character) to+-- a physical column number (corresponding to a terminal cell).+toPhysical :: Int -> [Char] -> Phys+toPhysical col line = sum $ chWidth <$> take col line++indicatorChar :: Char+indicatorChar = '$'+ -- |Construct a text widget for editing a single line of text. -- Single-line edit widgets will send activation events when the user -- presses @Enter@ (see 'onActivate').@@ -168,9 +194,9 @@   setEditLineLimit wRef Nothing   return wRef --- |Set the limit on the number of lines for the edit widget.  Nothing--- indicates no limit, while Just indicates a limit of the specified--- number of lines.+-- |Set the limit on the number of lines for the edit widget.+-- 'Nothing' indicates no limit, while 'Just' indicates a limit of the+-- specified number of lines. setEditLineLimit :: Widget Edit -> Maybe Int -> IO () setEditLineLimit _ (Just v) | v <= 0 = return () setEditLineLimit w v = updateWidgetState w $ \st -> st { lineLimit = v }@@ -179,11 +205,34 @@ getEditLineLimit :: Widget Edit -> IO (Maybe Int) getEditLineLimit = (lineLimit <~~) -resize :: Widget Edit -> (Int, Int) -> IO ()+resize :: Widget Edit -> (Phys, Phys) -> IO () resize e (newHeight, newWidth) = do-  updateWidgetState e $ \st -> st { visibleRows = newHeight }-  setDisplayWidth e newWidth+  updateWidgetState e $ \st ->+      let newRect = (clipRect st) { clipHeight = newHeight+                                  , clipWidth = newWidth+                                  }+          (cursorRow, _) = Z.cursorPosition $ contents st+          adjusted = updateRect (Phys cursorRow, physCursorCol st) newRect+      in st { clipRect = adjusted } +  updateWidgetState e $ \s ->+      let r = clipRect s+          (_, cursorColumn) = Z.cursorPosition $ contents s+          curLine = T.unpack $ Z.currentLine $ contents s+          (_, _, ri) = clip1d (clipLeft r) (clipWidth r) (T.pack curLine)+          newCharLen = if cursorColumn >= 0 && cursorColumn < length curLine+                       then chWidth $ curLine !! cursorColumn+                       else Phys 1++          newPhysCol = toPhysical cursorColumn curLine+          extra = if ri && newPhysCol >= ((clipLeft r) + (clipWidth r) - Phys 1)+                  then newCharLen - 1+                  else 0+          newLeft = clipLeft (clipRect s) + extra+      in s { clipRect = (clipRect s) { clipLeft = newLeft+                                     }+           }+ -- |Register handlers to be invoked when the edit widget has been -- ''activated'' (when the user presses Enter while the widget is -- focused).  These handlers will only be invoked when a single-line@@ -207,322 +256,102 @@  -- |Register handlers to be invoked when the edit widget's contents -- change.  Handlers will be passed the new contents.-onChange :: Widget Edit -> (String -> IO ()) -> IO ()+onChange :: Widget Edit -> (T.Text -> IO ()) -> IO () onChange = addHandler (changeHandlers <~~)  -- |Register handlers to be invoked when the edit widget's cursor -- position changes.  Handlers will be passed the new cursor position,--- relative to the beginning of the string (position 0).+-- relative to the beginning of the text (position (0, 0)). onCursorMove :: Widget Edit -> ((Int, Int) -> IO ()) -> IO () onCursorMove = addHandler (cursorMoveHandlers <~~)  -- |Get the current contents of the edit widget.  This returns all of -- the lines of text in the widget, separated by newlines.-getEditText :: Widget Edit -> IO String-getEditText = (((intercalate "\n") . currentText) <~~)+getEditText :: Widget Edit -> IO T.Text+getEditText = (((T.intercalate (T.pack "\n")) . Z.getText . contents) <~~)  -- |Get the contents of the current line of the edit widget (the line -- on which the cursor is positioned).-getEditCurrentLine :: Widget Edit -> IO String-getEditCurrentLine e = do-  ls <- currentText <~~ e-  curL <- cursorRow <~~ e-  return $ ls !! curL--setEditCurrentLine :: Widget Edit -> String -> IO ()-setEditCurrentLine e s = do-  ls <- currentText <~~ e-  curL <- cursorRow <~~ e--  updateWidgetState e $ \st ->-      st { currentText = repl curL s ls-         }+getEditCurrentLine :: Widget Edit -> IO T.Text+getEditCurrentLine = ((Z.currentLine . contents) <~~)  -- |Set the contents of the edit widget.  Newlines will be used to -- break up the text in multiline widgets.  If the edit widget has a -- line limit, only those lines within the limit will be set.-setEditText :: Widget Edit -> String -> IO ()+setEditText :: Widget Edit -> T.Text -> IO () setEditText wRef str = do-  oldS <- currentText <~~ wRef   lim <- lineLimit <~~ wRef-  s <- case lim of-    Nothing -> return str-    Just l -> return $ intercalate "\n" $ take l $ lines str-  updateWidgetState wRef $ \st -> st { currentText = if null s-                                                     then [""]-                                                     else lines s-                                     , cursorColumn = 0-                                     , cursorRow = 0+  let ls = case lim of+             Nothing -> T.lines str+             Just l -> take l $ T.lines str+  updateWidgetState wRef $ \st -> st { contents = Z.textZipper ls                                      }-  when (oldS /= lines s) $ do-    gotoBeginning wRef-    notifyChangeHandlers wRef+  notifyChangeHandlers wRef --- |Set the current edit widget cursor position.  The tuple is (row,--- column) with each starting at zero.  Invalid cursor positions will--- be ignored.-setEditCursorPosition :: Widget Edit -> (Int, Int) -> IO ()-setEditCursorPosition wRef (newRow, newCol) = do-  ls <- currentText <~~ wRef+-- |Get the edit widget's current cursor position (row, column).+getEditCursorPosition :: Widget Edit -> IO (Int, Int)+getEditCursorPosition = ((Z.cursorPosition . contents) <~~) -  -- First, check that the row is valid-  case newRow >= 0 && newRow < (length ls) of-    False -> return ()-    True -> do-      -- Then, if the row is valid, is the column valid for that row?-      -- It's legal for the new position to be *after* the last-      -- character (i.e., in the case of go-to-end)-      case newCol >= 0 && newCol <= (length (ls !! newRow)) of-        False -> return ()-        True -> do-              (oldRow, oldCol) <- getEditCursorPosition wRef-              when ((newRow, newCol) /= (oldRow, oldCol)) $-                   do-                     st <- getState wRef+-- |Set the cursor position to the specified row and column.  Invalid+-- cursor positions will be ignored.+setEditCursorPosition :: (Int, Int) -> Widget Edit -> IO ()+setEditCursorPosition pos = applyEdit (Z.moveCursor pos) -                     let newDisplayStart = if newCol >= (displayStart st + displayWidth st)-                                           then newCol - displayWidth st + 1-                                           else if newCol < displayStart st-                                                then newCol-                                                else displayStart st-                         newTopRow = if newRow < topRow st-                                     then newRow-                                     else if newRow >= (topRow st + visibleRows st)-                                          then newRow - visibleRows st + 1-                                          else topRow st+-- |Compute the physical cursor position (column) for the cursor in a+-- given edit widget state.  The physical position is relative to the+-- beginning of the current line (i.e., zero, as opposed to the+-- displayStart and related state).+physCursorCol :: Edit -> Phys+physCursorCol s =+    let curLine = T.unpack $ Z.currentLine $ contents s+        (_, cursorColumn) = Z.cursorPosition $ contents s+    in toPhysical cursorColumn curLine -                     updateWidgetState wRef $ \s ->-                         s { displayStart = newDisplayStart-                           , topRow = newTopRow-                           }+-- |Apply an editing transformation to the edit widget's text.  If the+-- transformation modifies the text or the cursor, the appropriate+-- event handlers will be notified.  If a line limit is in effect and+-- the transformation violates it, the transformation will be ignored.+applyEdit :: (Z.TextZipper T.Text -> Z.TextZipper T.Text)+          -> Widget Edit+          -> IO ()+applyEdit f this = do+  oldC <- contents <~~ this+  updateWidgetState this $ \s ->+      let newSt = s { contents = f (contents s) }+      in case lineLimit s of+           Nothing -> newSt+           Just l -> if length (Z.getText $ contents newSt) > l+                     then s+                     else newSt -                     updateWidgetState wRef $ \s ->-                         s { cursorRow = newRow-                           , cursorColumn = newCol-                           }-                     notifyCursorMoveHandlers wRef+  newC <- contents <~~ this --- |Get the edit widget's current cursor position (row, column).-getEditCursorPosition :: Widget Edit -> IO (Int, Int)-getEditCursorPosition e = do-  r <- cursorRow <~~ e-  c <- cursorColumn <~~ e-  return (r, c)+  when (Z.getText oldC /= Z.getText newC) $+       notifyChangeHandlers this -setDisplayWidth :: Widget Edit -> Int -> IO ()-setDisplayWidth this width =-    updateWidgetState this $ \s ->-        let newDispStart = if cursorColumn s - displayStart s >= width-                           then cursorColumn s - width + 1-                           else displayStart s-        in s { displayWidth = width-             , displayStart = newDispStart-             }+  when (Z.cursorPosition oldC /= Z.cursorPosition newC) $+       notifyCursorMoveHandlers this  editKeyEvent :: Widget Edit -> Key -> [Modifier] -> IO Bool editKeyEvent this k mods = do+  let run f = applyEdit f this >> return True   case (k, mods) of-    (KASCII 'a', [MCtrl]) -> gotoBeginning this >> return True-    (KASCII 'k', [MCtrl]) -> killToEOL this >> return True-    (KASCII 'e', [MCtrl]) -> gotoEnd this >> return True-    (KASCII 'd', [MCtrl]) -> delCurrentChar this >> return True-    (KLeft, []) -> moveCursorLeft this >> return True-    (KRight, []) -> moveCursorRight this >> return True-    (KUp, []) -> moveCursorUp this >> return True-    (KDown, []) -> moveCursorDown this >> return True-    (KBS, []) -> deletePreviousChar this >> return True-    (KDel, []) -> delCurrentChar this >> return True-    (KASCII ch, []) -> insertChar this ch >> return True-    (KHome, []) -> gotoBeginning this >> return True-    (KEnd, []) -> gotoEnd this >> return True+    (KASCII 'a', [MCtrl]) -> run Z.gotoBOL+    (KASCII 'k', [MCtrl]) -> run Z.killToEOL+    (KASCII 'e', [MCtrl]) -> run Z.gotoEOL+    (KASCII 'd', [MCtrl]) -> run Z.deleteChar+    (KLeft, []) -> run Z.moveLeft+    (KRight, []) -> run Z.moveRight+    (KUp, []) -> run Z.moveUp+    (KDown, []) -> run Z.moveDown+    (KBS, []) -> run Z.deletePrevChar+    (KDel, []) -> run Z.deleteChar+    (KASCII ch, []) -> run (Z.insertChar ch)+    (KHome, []) -> run Z.gotoBOL+    (KEnd, []) -> run Z.gotoEOL     (KEnter, []) -> do                    lim <- lineLimit <~~ this                    case lim of                      Just 1 -> notifyActivateHandlers this >> return True-                     _ -> insertLineAtPoint this >> return True+                     _ -> run Z.breakLine     _ -> return False--insertLineAtPoint :: Widget Edit -> IO ()-insertLineAtPoint e = do-  -- Bail if adding a new line would violate the line limit-  lim <- lineLimit <~~ e-  numLines <- (length . currentText) <~~ e--  let continue = case lim of-                   Just v | numLines + 1 > v -> False-                   _ -> True--  when continue $-       do-         -- Get information about current line so we can break the-         -- current line-         curL <- getEditCurrentLine e-         curCol <- cursorColumn <~~ e-         curRow <- cursorRow <~~ e-         let r1 = take curCol curL-             r2 = drop curCol curL-         setEditCurrentLine e r1-         updateWidgetState e $ \st ->-             st { currentText = inject (curRow + 1) r2 (currentText st)-                }-         notifyChangeHandlers e-         setEditCursorPosition e (curRow + 1, 0)--killToEOL :: Widget Edit -> IO ()-killToEOL this = do-  -- Preserve some state since setEditText changes it.-  curCol <- cursorColumn <~~ this-  curLine <- getEditCurrentLine this-  case null curLine of-    False -> do-      setEditCurrentLine this $ take curCol curLine-      notifyChangeHandlers this-    True -> do-      curRow <- cursorRow <~~ this-      numLines <- (length . currentText) <~~ this-      if curRow == 0 && numLines == 1 then-          return () else-          do-            let newRow = if curRow == numLines - 1 && numLines > 1-                         then curRow - 1-                         else curRow-            updateWidgetState this $ \st ->-                st { currentText = remove curRow (currentText st)-                   }-            notifyChangeHandlers this-            setEditCursorPosition this (newRow, 0)--deletePreviousChar :: Widget Edit -> IO ()-deletePreviousChar this = do-  curCol <- cursorColumn <~~ this-  curRow <- cursorRow <~~ this-  case curCol == 0 of-    True ->-        if curRow == 0-        then return ()-        else do-          curLine <- getEditCurrentLine this-          ls <- currentText <~~ this-          let prevLine = ls !! (curRow - 1)-          updateWidgetState this $ \st ->-              st { currentText = repl (curRow - 1) (prevLine ++ curLine)-                                 $ remove curRow (currentText st)-                 }-          setEditCursorPosition this (curRow - 1, length prevLine)-          notifyChangeHandlers this--    False -> do-      moveCursorLeft this-      delCurrentChar this--gotoBeginning :: Widget Edit -> IO ()-gotoBeginning wRef = do-  updateWidgetState wRef $ \s -> s { displayStart = 0-                                   }-  curL <- cursorRow <~~ wRef-  setEditCursorPosition wRef (curL, 0)--gotoEnd :: Widget Edit -> IO ()-gotoEnd wRef = do-  curLine <- getEditCurrentLine wRef-  curRow <- cursorRow <~~ wRef-  setEditCursorPosition wRef (curRow, length curLine)--moveCursorUp :: Widget Edit -> IO ()-moveCursorUp wRef = do-  st <- getState wRef-  let newRow = if cursorRow st == 0-               then 0-               else cursorRow st - 1--      prevLine = currentText st !! (cursorRow st - 1)-      newCol = if cursorRow st == 0 || (cursorColumn st <= length prevLine)-               then cursorColumn st-               else length prevLine--  setEditCursorPosition wRef (newRow, newCol)--moveCursorDown :: Widget Edit -> IO ()-moveCursorDown wRef = do-  st <- getState wRef-  let newRow = if cursorRow st == (length $ currentText st) - 1-               then (length $ currentText st) - 1-               else cursorRow st + 1--      nextLine = currentText st !! (cursorRow st + 1)-      newCol = if cursorRow st == (length $ currentText st) - 1-               then cursorColumn st-               else if cursorColumn st <= length nextLine-                    then cursorColumn st-                    else length nextLine--  setEditCursorPosition wRef (newRow, newCol)--moveCursorLeft :: Widget Edit -> IO ()-moveCursorLeft wRef = do-  st <- getState wRef-  let newRow = if cursorRow st == 0-               then 0-               else if cursorColumn st == 0-                    then cursorRow st - 1-                    else cursorRow st-      prevLine = currentText st !! (cursorRow st - 1)-      newCol = if cursorColumn st == 0-               then if cursorRow st == 0-                    then 0-                    else length prevLine-               else cursorColumn st - 1-  setEditCursorPosition wRef (newRow, newCol)--moveCursorRight :: Widget Edit -> IO ()-moveCursorRight wRef = do-  st <- getState wRef-  curL <- getEditCurrentLine wRef-  let newRow = if cursorRow st == (length $ currentText st) - 1-               then cursorRow st-               else if cursorColumn st == length curL-                    then cursorRow st + 1-                    else cursorRow st-      newCol = if cursorColumn st == length curL-               then if cursorRow st == (length $ currentText st) - 1-                    then cursorColumn st-                    else 0-               else cursorColumn st + 1-  setEditCursorPosition wRef (newRow, newCol)--insertChar :: Widget Edit -> Char -> IO ()-insertChar wRef ch = do-  curLine <- getEditCurrentLine wRef-  updateWidgetState wRef $ \st ->-      let newLine = inject (cursorColumn st) ch curLine-          newViewStart =-              if cursorColumn st == displayStart st + displayWidth st - 1-              then displayStart st + 1-              else displayStart st-      in st { displayStart = newViewStart-            , currentText = repl (cursorRow st) newLine (currentText st)-            }-  moveCursorRight wRef-  notifyChangeHandlers wRef--delCurrentChar :: Widget Edit -> IO ()-delCurrentChar wRef = do-  st <- getState wRef-  curLine <- getEditCurrentLine wRef-  case cursorColumn st < (length curLine) of-    True ->-        do-          let newLine = remove (cursorColumn st) curLine-          updateWidgetState wRef $ \s -> s { currentText = repl (cursorRow st) newLine (currentText st) }-          notifyChangeHandlers wRef-    False ->-        -- If we are on the last line, do nothing, but if we aren't,-        -- combine the next line with the current one-        if cursorRow st == (length $ currentText st) - 1-        then return ()-        else do-          let nextLine = currentText st !! (cursorRow st + 1)-          updateWidgetState wRef $ \s ->-              s { currentText = remove (cursorRow s + 1) $-                                repl (cursorRow st) (curLine ++ nextLine) (currentText s)-                }
src/Graphics/Vty/Widgets/List.hs view
@@ -14,7 +14,7 @@     , SelectionEvent(..)     , ActivateItemEvent(..)     -- ** List creation-    , newStringList+    , newTextList     , newList     , addToList     , insertIntoList@@ -42,6 +42,7 @@ import Data.Typeable import Control.Exception hiding (Handler) import Control.Monad+import qualified Data.Text as T import qualified Data.Vector as V import Graphics.Vty import Graphics.Vty.Widgets.Core@@ -419,13 +420,13 @@    return $ vert_cat (visible_imgs ++ [filler]) --- |A convenience function to create a new list using 'String's as the--- internal values and 'FormattedText' widgets to represent those+-- |A convenience function to create a new list using 'Text' values as+-- the internal values and 'FormattedText' widgets to represent those -- strings.-newStringList :: Attr -- ^The attribute of the selected item-              -> [String] -- ^The list items-              -> IO (Widget (List String FormattedText))-newStringList selAttr labels = do+newTextList :: Attr -- ^The attribute of the selected item+            -> [T.Text] -- ^The list items+            -> IO (Widget (List T.Text FormattedText))+newTextList selAttr labels = do   list <- newList selAttr   forM_ labels $ \l ->       (addToList list l =<< plainText l)
src/Graphics/Vty/Widgets/ProgressBar.hs view
@@ -15,16 +15,23 @@ where  import Control.Monad+import qualified Data.Text as T import Graphics.Vty import Graphics.Vty.Widgets.Core import Graphics.Vty.Widgets.Events import Graphics.Vty.Widgets.Text import Graphics.Vty.Widgets.Alignment+import Graphics.Vty.Widgets.Util+import Graphics.Vty.Widgets.TextClip+import Text.Trans.Tokenize  data ProgressBar = ProgressBar { progressBarAmount :: Int                                , onChangeHandlers :: Handlers Int-                               , progressBarText :: String+                               , progressBarText :: T.Text                                , progressBarTextAlignment :: Alignment+                               , progCompleteAttr :: Attr+                               , progIncompleteAttr :: Attr+                               , progTextWidget :: Widget FormattedText                                }  instance Show ProgressBar where@@ -39,49 +46,72 @@ newProgressBar :: Attr -> Attr -> IO (Widget ProgressBar) newProgressBar completeAttr incompleteAttr = do   chs <- newHandlers-  t <- plainText ""-  let initSt = ProgressBar 0 chs "" AlignCenter+  t <- plainText T.empty+  let initSt = ProgressBar 0 chs T.empty AlignCenter completeAttr incompleteAttr t   wRef <- newWidget initSt $ \w ->           w { growHorizontal_ = const $ return True             , render_ =-                \this size ctx -> do-                  -- Divide the available width according to the-                  -- progress value-                  prog <- progressBarAmount <~~ this-                  txt <- progressBarText <~~ this-                  al <- progressBarTextAlignment <~~ this+                \this size ctx -> renderProgressBar size ctx =<< getState this+            } -                  let complete_width = fromEnum $ (toRational prog / toRational (100.0 :: Double)) *-                                       (toRational $ fromEnum $ region_width size)+  setProgress wRef 0+  return wRef -                      full_width = fromEnum $ region_width size-                      full_str = take full_width $ mkStr txt al+renderProgressBar :: DisplayRegion -> RenderContext -> ProgressBar -> IO Image+renderProgressBar size ctx st = do+  -- Divide the available width according to the progress value+  let prog = progressBarAmount st+      txt = progressBarText st+      al = progressBarTextAlignment st -                      mkStr s AlignLeft = s ++ replicate (full_width - length txt) ' '-                      mkStr s AlignRight = replicate (full_width - length txt) ' ' ++ s-                      mkStr s AlignCenter = concat [ half-                                                   , s-                                                   , half-                                                   , if length half * 2 < (full_width + length txt)-                                                     then " "-                                                     else ""-                                                   ]-                          where-                            half = replicate ((full_width - length txt) `div` 2) ' '+      complete_width =+          Phys $ fromEnum $ (toRational prog / toRational (100.0 :: Double)) *+                   (toRational $ fromEnum $ region_width size) -                      (complete_str, incomplete_str) = ( take complete_width full_str-                                                       , drop complete_width full_str-                                                       )+      full_width = Phys $ fromEnum $ region_width size+      full_str = truncateText full_width $ mkStr txt al -                  setTextWithAttrs t [ (complete_str, completeAttr)-                                     , (incomplete_str, incompleteAttr)-                                     ]-                  render t size ctx-            }+      mkStr s AlignLeft =+          let diff = fromEnum $ full_width - textWidth txt+          in T.concat [ s+                      , T.pack $ replicate diff ' '+                      ] -  setProgress wRef 0-  return wRef+      mkStr s AlignRight =+          let diff = fromEnum $ full_width - textWidth txt+          in T.concat [ T.pack $ replicate diff ' '+                      , s+                      ] +      mkStr s AlignCenter =+          T.concat [ half+                   , s+                   , half+                   , trailingSpc+                   ]+          where+            diff = fromEnum $ full_width - textWidth txt+            half = T.pack $ replicate (diff `div` 2) ' '+            used_width = textWidth half * 2 + textWidth txt+            trailingSpc =+                if used_width < full_width+                then T.singleton ' '+                else T.empty+++      (leftPart, _, _) = clip1d 0 complete_width full_str+      charCount = T.length leftPart++      (complete_str, incomplete_str) = ( T.take charCount full_str+                                       , T.drop charCount full_str+                                       )++  setTextWithAttrs (progTextWidget st)+                       [ (complete_str, progCompleteAttr st)+                       , (incomplete_str, progIncompleteAttr st)+                       ]+  render (progTextWidget st) size ctx+ -- |Register a handler to be invoked when the progress bar's progress -- value changes.  The handler will be passed the new progress value. onProgressChange :: Widget ProgressBar -> (Int -> IO ()) -> IO ()@@ -101,7 +131,7 @@     updateWidgetState p $ \st -> st { progressBarTextAlignment = al }  -- |Set the progress bar's text label.-setProgressText :: Widget ProgressBar -> String -> IO ()+setProgressText :: Widget ProgressBar -> T.Text -> IO () setProgressText p s =     updateWidgetState p $ \st -> st { progressBarText = s } 
src/Graphics/Vty/Widgets/Table.hs view
@@ -26,6 +26,7 @@ where  import Data.Monoid+import qualified Data.Text as T import Data.Typeable import Data.Word import Data.List@@ -207,8 +208,7 @@  -- |Create a table widget using a list of column specifications and a -- border style.-newTable :: -            [ColumnSpec]+newTable :: [ColumnSpec]          -> BorderStyle          -> IO (Widget Table) newTable specs borderSty = do@@ -454,7 +454,7 @@ -- |Add a heading row to a table.  Adds a row using the specified -- |labels and attribute.  Returns the widgets it constructed as a -- |side-effect in case you want to do something with them.-addHeadingRow :: Widget Table -> Attr -> [String] -> IO [Widget FormattedText]+addHeadingRow :: Widget Table -> Attr -> [T.Text] -> IO [Widget FormattedText] addHeadingRow tbl attr labels = do   ws <- mapM (\s -> plainText s >>= withNormalAttribute attr) labels   addRow tbl ws@@ -462,7 +462,7 @@  -- |Add a heading row to a table.  Adds a row using the specified -- |labels and attribute.-addHeadingRow_ :: Widget Table -> Attr -> [String] -> IO ()+addHeadingRow_ :: Widget Table -> Attr -> [T.Text] -> IO () addHeadingRow_ tbl attr labels = addHeadingRow tbl attr labels >> return ()  applyCellAlignment :: Alignment -> TableCell -> IO TableCell@@ -523,7 +523,7 @@  renderCell :: DisplayRegion -> TableCell -> RenderContext -> IO Image renderCell region EmptyCell ctx = do-  w <- plainText ""+  w <- plainText T.empty   render w region ctx renderCell region (TableCell w _ _) ctx =     render w region ctx
src/Graphics/Vty/Widgets/Text.hs view
@@ -1,9 +1,8 @@ {-# LANGUAGE TypeSynonymInstances, FlexibleInstances, BangPatterns #-}--- |This module provides functionality for rendering 'String's as+-- |This module provides functionality for rendering 'Text' as -- 'Widget's, including functionality to make structural and/or visual--- changes at rendering time.  To get started, turn your ordinary--- 'String' into a 'Widget' with 'plainText'; for more control, use--- 'textWidget'.+-- changes at rendering time.  To get started, use 'plainText'; for+-- more control, use 'textWidget'. module Graphics.Vty.Widgets.Text     ( FormattedText     -- *Constructing Text Widgets@@ -26,6 +25,7 @@  import Control.Applicative import Data.Monoid+import qualified Data.Text as T import Graphics.Vty import Graphics.Vty.Widgets.Core import Text.Trans.Tokenize@@ -66,16 +66,16 @@                                         , " }"                                         ] --- |Construct a Widget directly from a String.  This is recommended if--- you don't need to use a 'Formatter'.-plainText :: String -> IO (Widget FormattedText)+-- |Construct a Widget directly from a Text value.  This is+-- recommended if you don't need to use a 'Formatter'.+plainText :: T.Text -> IO (Widget FormattedText) plainText = textWidget nullFormatter  -- |Construct a Widget directly from a list of strings and their -- attributes.-plainTextWithAttrs :: [(String, Attr)] -> IO (Widget FormattedText)+plainTextWithAttrs :: [(T.Text, Attr)] -> IO (Widget FormattedText) plainTextWithAttrs pairs = do-  w <- textWidget nullFormatter ""+  w <- textWidget nullFormatter T.empty   setTextWithAttrs w pairs   return w @@ -86,13 +86,13 @@ wrap :: Formatter wrap =     Formatter $ \sz ts -> do-      let width = fromEnum $ region_width sz+      let width = Phys $ fromEnum $ region_width sz       return $ wrapStream width ts  -- |Construct a text widget formatted with the specified formatters -- and initial content.  The formatters will be applied in the order -- given here (and, depending on the formatter, order might matter).-textWidget :: Formatter -> String -> IO (Widget FormattedText)+textWidget :: Formatter -> T.Text -> IO (Widget FormattedText) textWidget format s = do   let initSt = FormattedText { text = TS []                              , formatter = format@@ -130,7 +130,7 @@  -- |Set the text value of a 'FormattedText' widget.  The specified -- string will be 'tokenize'd.-setText :: Widget FormattedText -> String -> IO ()+setText :: Widget FormattedText -> T.Text -> IO () setText wRef s = setTextWithAttrs wRef [(s, def_attr)]  -- |Set the text value of a 'FormattedText' widget directly, in case@@ -138,7 +138,7 @@ -- attributes.  The specified strings will each be 'tokenize'd, and -- tokens resulting from each 'tokenize' operation will be given the -- specified attribute in the tuple.-setTextWithAttrs :: Widget FormattedText -> [(String, Attr)] -> IO ()+setTextWithAttrs :: Widget FormattedText -> [(T.Text, Attr)] -> IO () setTextWithAttrs wRef pairs = do   let streams = map (\(s, a) -> tokenize s a) pairs       ts = concat $ map streamEntities streams@@ -172,7 +172,7 @@       emptyLineLength = min (fromEnum $ region_width sz) maxLineLength        ls = map truncLine $ map (map entityToken) $ findLines newText-      truncLine = truncateLine (fromEnum $ region_width sz)+      truncLine = truncateLine (Phys $ fromEnum $ region_width sz)        -- When building images for empty lines, it's critical that we       -- *don't* use the empty_image, because that will cause empty@@ -188,7 +188,7 @@       nullImg = string def_attr ""        mkTokenImg :: Token Attr -> Image-      mkTokenImg tok = string (finalAttr tok) (tokenStr tok)+      mkTokenImg tok = string (finalAttr tok) (T.unpack $ tokenStr tok)    return $ if region_height sz == 0            then nullImg
+ src/Graphics/Vty/Widgets/TextClip.hs view
@@ -0,0 +1,116 @@+-- |This module provides \"text clipping\" routines.  These routines+-- are responsible for ensuring that logical characters are clipped+-- properly when being laid out in a given physical region.  This is a+-- bit tricky because some Unicode characters use two terminal columns+-- and others (most) use one.  We have to take this into account when+-- truncating text to fit into rendering regions, so we concentrate+-- that logic here under the name of a \"clipping rectangle\" and+-- functions to apply it.+--+-- Clipping functionality is provided in two forms: one- and+-- two-dimensional clipping.  The former is useful for clipping a+-- single line of text at a given offset and up to a given width.  The+-- latter is useful for clipping a list of lines with respect to a 2-D+-- clipping rectangle.+module Graphics.Vty.Widgets.TextClip+    ( ClipRect(..)+    , clip1d+    , clip2d+    , updateRect+    )+where++import Control.Applicative+import Data.Maybe+import qualified Data.Text as T+import Graphics.Vty.Widgets.Util+    ( Phys(..)+    , chWidth+    )++-- |The type of clipping rectangles for 2-D clipping operations.  All+-- values are 'Phys' values to indicate that we are dealing explicitly+-- with physical column measurements rather than logical character+-- positions.+data ClipRect =+    ClipRect { clipLeft :: Phys+             -- ^The left margin of the clipping rectangle.+             , clipTop :: Phys+             -- ^The top row of the clipping rectangle.+             , clipWidth :: Phys+             -- ^The width, in columns, of the clipping rectangle.+             , clipHeight :: Phys+             -- ^The height, in rows, of the clipping rectangle.+             }+    deriving (Eq, Show)++-- |One-dimensional text clipping.  Takes the left clipping margin, a+-- clipping width, and a text string.  For example, @clip1d n w s@+-- clips the string @s@ so that the result includes characters in @s@+-- starting at position @n@ and including characters using no more+-- than @w@ columns in width.  Returns the clipped text plus 'Bool's+-- indicating whether wide characters were \"sliced\" on either side+-- (left and right, respectively) of the clipping region.  This+-- function guarantees that the text returned will always fit within+-- the specified clipping region.  Since wide characters may be sliced+-- during clipping, this may return a text string smaller than the+-- clipping region.+clip1d :: Phys -> Phys -> T.Text -> (T.Text, Bool, Bool)+clip1d _ 0 _ = (T.empty, False, False)+clip1d start len t = (T.pack result2, lSlice, rSlice)++    where+      pairs = [ (c, chWidth c) | c <- T.unpack t ]++      exploded = concat $ mkExp <$> pairs+      mkExp (a, i) = Just a : replicate (fromEnum i - 1) Nothing++      -- First clip up to the starting position.+      clip1 = drop (fromEnum start) exploded+      -- Then clip according to the width.+      clip2 = take (fromEnum len) clip1+      -- Rest is whatever was left after clipping to the width.+      rest = drop (fromEnum len) clip1++      rSlice = length rest > 0 && head rest == Nothing+      lSlice = length clip1 > 0 && head clip1 == Nothing++      result1 = catMaybes clip2+      result2 = if rSlice+                then init result1+                else result1++-- |Two-dimensional text clipping.  Returns clipping data for each+-- line as returned by 'clip1d', with the added behavior that it+-- returns at most 'clipHeight' lines of text and uses 'clipTop' as+-- the offset when clipping rows.+clip2d :: ClipRect -> [T.Text] -> [(T.Text, Bool, Bool)]+clip2d rect ls = clip1d left len <$> visibleLines+        where+          visibleLines = take (fromEnum height) $ drop (fromEnum top) ls+          left = clipLeft rect+          top = clipTop rect+          len = clipWidth rect+          height = clipHeight rect++-- |Given a physical point and a clipping rectangle, adjust the+-- clipping rectangle so that the point falls just inside the+-- rectangle.  If the point is already within the rectangle, return+-- the rectangle unmodified.  NB: this assumes that the physical+-- position given has passed whatever validation checks are relevant+-- for the user of the 'ClipRect'.  This function just performs a+-- rectangle transformation.+updateRect :: (Phys, Phys) -> ClipRect -> ClipRect+updateRect (row, col) oldRect = adjustLeft $ adjustTop oldRect+    where+      adjustLeft old+          | col < clipLeft oldRect = old { clipLeft = col }+          | col >= clipLeft oldRect + clipWidth oldRect =+              old { clipLeft = col - clipWidth old + 1 }+          | otherwise = old++      adjustTop old+          | row < clipTop oldRect = old { clipTop = row }+          | row >= clipTop oldRect + clipHeight oldRect =+              old { clipTop = row - clipHeight old + 1 }+          | otherwise = old
+ src/Graphics/Vty/Widgets/TextZipper.hs view
@@ -0,0 +1,297 @@+-- |This module provides a two-dimensional text zipper data structure.+-- This structure represents a body of text and an editing cursor+-- which can be moved throughout the text, along with a set of editing+-- transformations.+--+-- Text zippers are generalized over the set of data types that might+-- be used to store lists of characters (e.g., 'String', 'T.Text',+-- etc.).  As a result, the most general way to create a text zipper+-- is to use 'mkZipper' and provide all of the functions required to+-- manipulate the underlying text data.+--+-- A default implementation using 'T.Text' is provided and is used+-- elsewhere in this library.+module Graphics.Vty.Widgets.TextZipper+    ( TextZipper++    -- *Construction and extraction+    , mkZipper+    , textZipper+    , getText+    , currentLine+    , cursorPosition++    -- *Navigation functions+    , moveCursor+    , insertChar+    , breakLine+    , killToEOL+    , gotoEOL+    , gotoBOL+    , deletePrevChar+    , deleteChar+    , moveRight+    , moveLeft+    , moveUp+    , moveDown+    )+where++import Data.Monoid+import qualified Data.Text as T++data TextZipper a =+    TZ { toLeft :: a+       , toRight :: a+       , above :: [a]+       , below :: [a]+       , fromChar :: Char -> a+       , drop_ :: Int -> a -> a+       , take_ :: Int -> a -> a+       , length_ :: a -> Int+       , last_ :: a -> Char+       , init_ :: a -> a+       , null_ :: a -> Bool+       }++instance (Eq a) => Eq (TextZipper a) where+    a == b = and [ toLeft a == toLeft b+                 , toRight a == toRight b+                 , above a == above b+                 , below a == below b+                 ]++instance (Show a) => Show (TextZipper a) where+    show tz = concat [ "TextZipper { "+                     , "above = "+                     , show $ above tz+                     , "below = "+                     , show $ below tz+                     , "toLeft = "+                     , show $ toLeft tz+                     , "toRight = "+                     , show $ toRight tz+                     , " }"+                     ]++-- |Create a zipper using a custom text storage type.  Takes the+-- initial text as well as all of the functions necessary to+-- manipulate the underlying text values.+mkZipper :: (Monoid a) =>+            (Char -> a)+         -- ^A singleton constructor.+         -> (Int -> a -> a)+         -- ^'drop'.+         -> (Int -> a -> a)+         -- ^'take'.+         -> (a -> Int)+         -- ^'length'.+         -> (a -> Char)+         -- ^'last'.+         -> (a -> a)+         -- ^'init'.+         -> (a -> Bool)+         -- ^'null'.+         -> [a]+         -- ^The initial lines of text.+         -> TextZipper a+mkZipper fromCh drp tk lngth lst int nl ls =+    let (first, rest) = if null ls+                        then (mempty, mempty)+                        else (head ls, tail ls)+    in TZ mempty first [] rest fromCh drp tk lngth lst int nl++-- |Get the text contents of the zipper.+getText :: (Monoid a) => TextZipper a -> [a]+getText tz = concat [ above tz+                    , [currentLine tz]+                    , below tz+                    ]++-- |Get the cursor position of the zipper; returns @(row, col)@.+-- @row@ ranges from @[0..num_rows-1]@ inclusive; @col@ ranges from+-- @[0..length of current line]@ inclusive.  Column values equal to+-- line width indicate a cursor that is just past the end of a line of+-- text.+cursorPosition :: TextZipper a -> (Int, Int)+cursorPosition tz = (length $ above tz, length_ tz $ toLeft tz)++-- |Move the cursor to the specified row and column.  Invalid cursor+-- positions will be ignored.  Valid cursor positions range as+-- described for 'cursorPosition'.+moveCursor :: (Monoid a) => (Int, Int) -> TextZipper a -> TextZipper a+moveCursor (row, col) tz =+    let t = getText tz+    in if row < 0+           || row > length t+           || col < 0+           || col > length_ tz (t !! row)+       then tz+       else tz { above = take row t+               , below = drop (row + 1) t+               , toLeft = take_ tz col (t !! row)+               , toRight = drop_ tz col (t !! row)+               }++lastLine :: TextZipper a -> Bool+lastLine = (== 0) . length . below++nextLine :: TextZipper a -> a+nextLine = head . below++-- |The line of text on which the zipper's cursor currently resides.+currentLine :: (Monoid a) => TextZipper a -> a+currentLine tz = (toLeft tz) `mappend` (toRight tz)++-- |Insert a character at the current cursor position.  Move the+-- cursor one position to the right.+insertChar :: (Monoid a) => Char -> TextZipper a -> TextZipper a+insertChar ch tz = tz { toLeft = toLeft tz `mappend` (fromChar tz ch) }++-- |Insert a line break at the current cursor position.+breakLine :: (Monoid a) => TextZipper a -> TextZipper a+breakLine tz =+    tz { above = above tz ++ [toLeft tz]+       , toLeft = mempty+       }++-- |Move the cursor to the end of the current line.+gotoEOL :: (Monoid a) => TextZipper a -> TextZipper a+gotoEOL tz = tz { toLeft = currentLine tz+                , toRight = mempty+                }++-- |Remove all text from the cursor position to the end of the current+-- line.  If the cursor is at the beginning of a line and the line is+-- empty, the entire line will be removed.+killToEOL :: (Monoid a) => TextZipper a -> TextZipper a+killToEOL tz+    | (null_ tz $ toLeft tz) && (null_ tz $ toRight tz) &&+      (not $ null $ below tz) =+          tz { toRight = head $ below tz+             , below = tail $ below tz+             }+    | otherwise = tz { toRight = mempty+                     }++-- |Delete the character preceding the cursor position, and move the+-- cursor backwards by one character.+deletePrevChar :: (Eq a, Monoid a) => TextZipper a -> TextZipper a+deletePrevChar tz+    | moveLeft tz == tz = tz+    | otherwise = deleteChar $ moveLeft tz++-- |Delete the character at the cursor position.  Leaves the cursor+-- position unchanged.  If the cursor is at the end of a line of text,+-- this combines the line with the line below.+deleteChar :: (Monoid a) => TextZipper a -> TextZipper a+deleteChar tz+    -- Can we just remove a char from the current line?+    | (not $ null_ tz (toRight tz)) =+        tz { toRight = drop_ tz 1 $ toRight tz+           }+    -- Do we need to collapse the previous line onto the current one?+    | null_ tz (toRight tz) && (not $ null $ below tz) =+        tz { toRight = head $ below tz+           , below = tail $ below tz+           }+    | otherwise = tz++-- |Move the cursor to the beginning of the current line.+gotoBOL :: (Monoid a) => TextZipper a -> TextZipper a+gotoBOL tz = tz { toLeft = mempty+                , toRight = currentLine tz+                }++-- |Move the cursor right by one position.  If the cursor is at the+-- end of a line, the cursor is moved to the first position of the+-- following line (if any).+moveRight :: (Monoid a) => TextZipper a -> TextZipper a+moveRight tz+    -- Are we able to keep moving right on the current line?+    | not (null_ tz (toRight tz)) =+        tz { toLeft = toLeft tz+                      `mappend` (take_ tz 1 $ toRight tz)+           , toRight = drop_ tz 1 (toRight tz)+           }+    -- If we are going to go beyond the end of the current line, can+    -- we move to the next one?+    | not $ null (below tz) =+        tz { above = above tz ++ [toLeft tz]+           , below = tail $ below tz+           , toLeft = mempty+           , toRight = nextLine tz+           }+    | otherwise = tz++-- |Move the cursor left by one position.  If the cursor is at the+-- beginning of a line, the cursor is moved to the last position of+-- the preceding line (if any).+moveLeft :: (Monoid a) => TextZipper a -> TextZipper a+moveLeft tz+    -- Are we able to keep moving left on the current line?+    | not $ null_ tz (toLeft tz) =+        tz { toLeft = init_ tz $ toLeft tz+           , toRight = fromChar tz (last_ tz (toLeft tz))+                       `mappend` toRight tz+           }+    -- If we are going to go beyond the beginning of the current line,+    -- can we move to the end of the previous one?+    | not $ null (above tz) =+        tz { above = init $ above tz+           , below = currentLine tz : below tz+           , toLeft = last $ above tz+           , toRight = mempty+           }+    | otherwise = tz++-- |Move the cursor up by one row.  If there no are rows above the+-- current one, move to the first position of the current row.  If the+-- row above is shorter, move to the end of that row.+moveUp :: (Monoid a) => TextZipper a -> TextZipper a+moveUp tz+    -- Is there a line above at least as long as the current one?+    | (not $ null (above tz)) &&+      (length_ tz $ last $ above tz) >= length_ tz (toLeft tz) =+        tz { below = currentLine tz : below tz+           , above = init $ above tz+           , toLeft = take_ tz (length_ tz $ toLeft tz) (last $ above tz)+           , toRight = drop_ tz (length_ tz $ toLeft tz) (last $ above tz)+           }+    -- Or if there is a line above, just go to the end of it+    | (not $ null (above tz)) =+        tz { above = init $ above tz+           , below = currentLine tz : below tz+           , toLeft = last $ above tz+           , toRight = mempty+           }+    -- If nothing else, go to the beginning of the current line+    | otherwise = gotoBOL tz++-- |Move the cursor down by one row.  If there are no rows below the+-- current one, move to the last position of the current row.  If the+-- row below is shorter, move to the end of that row.+moveDown :: (Monoid a) => TextZipper a -> TextZipper a+moveDown tz+    -- Is there a line below at least as long as the current one?+    | (not $ lastLine tz) &&+      (length_ tz $ nextLine tz) >= length_ tz (toLeft tz) =+        tz { below = tail $ below tz+           , above = above tz ++ [currentLine tz]+           , toLeft = take_ tz (length_ tz $ toLeft tz) (nextLine tz)+           , toRight = drop_ tz (length_ tz $ toLeft tz) (nextLine tz)+           }+    -- Or if there is a line below, just go to the end of it+    | (not $ null (below tz)) =+        tz { above = above tz ++ [currentLine tz]+           , below = tail $ below tz+           , toLeft = nextLine tz+           , toRight = mempty+           }+    -- If nothing else, go to the end of the current line+    | otherwise = gotoEOL tz++-- |Construct a zipper from 'T.Text' values.+textZipper :: [T.Text] -> TextZipper T.Text+textZipper =+    mkZipper T.singleton T.drop T.take T.length T.last T.init T.null
src/Graphics/Vty/Widgets/Util.hs view
@@ -1,4 +1,4 @@-{-# LANGUAGE BangPatterns #-}+{-# LANGUAGE BangPatterns, GeneralizedNewtypeDeriving #-} module Graphics.Vty.Widgets.Util     ( on     , fgColor@@ -13,11 +13,50 @@     , remove     , inject     , repl+    , takeMaxText+    , takeMaxChars+    , chWidth+    , strWidth+    , textWidth+    , Phys(..)     ) where +import Control.Applicative import Data.Word+import qualified Data.Text as T import Graphics.Vty+import Graphics.Vty.Image+    ( safe_wcwidth+    )++-- A newtype to wrap physical screen coordinates, as opposed to+-- character-logical coordinates.  Used when transforming cursor+-- coordinates to screen coordinates and when computing the physical+-- width of characters and strings.+newtype Phys = Phys Int+    deriving (Num, Eq, Show, Ord, Integral, Enum, Real)++chWidth :: Char -> Phys+chWidth = Phys . fromEnum . safe_wcwidth++textWidth :: T.Text -> Phys+textWidth = strWidth . T.unpack++strWidth :: String -> Phys+strWidth = sum . (chWidth <$>)++takeMaxChars :: Phys -> [Char] -> [Char]+takeMaxChars mx xs = f' (Phys 0) xs+    where+      f' _ [] = []+      f' acc (c:cs) = let w = chWidth c+                      in if acc + w <= mx+                         then c : f' (acc + w) cs+                         else []++takeMaxText :: Phys -> T.Text -> T.Text+takeMaxText mx xs = T.pack $ takeMaxChars mx $ T.unpack xs  -- |Infix attribute constructor.  Use: foregroundColor `on` -- backgroundColor.
− src/ListDemo.hs
@@ -1,134 +0,0 @@-{-# OPTIONS_GHC -fno-warn-missing-signatures -fno-warn-unused-do-bind #-}-module Main where--import System.Exit ( exitSuccess )-import Graphics.Vty-import Graphics.Vty.Widgets.All--data AppElements =-    AppElements { theList :: Widget (List String FormattedText)-                , theBody :: Widget FormattedText-                , theFooter :: Widget FormattedText-                , theListLimit :: Widget (VLimit (List String FormattedText))-                , uis :: Collection-                }--titleAttr = bright_white `on` blue-focAttr = black `on` green-bodyAttr = white `on` black-selAttr = black `on` yellow-keyAttr = fgColor magenta--message1 :: String-message1 = "This demonstration shows how list widgets behave. \n\-           \See the keystrokes below to try the demo."--message2 :: [(String, Attr)]-message2 = [ ("- Press ", def_attr), ("q", keyAttr), (" to quit\n", def_attr)-           , ("- Press ", def_attr), ("+", keyAttr)-           , (" / ", def_attr), ("a", keyAttr)-           , (" to add a list item\n", def_attr)-           , ("- Press ", def_attr), ("-", keyAttr)-           , (" / ", def_attr), ("d", keyAttr)-           , (" to remove the selected list item\n", def_attr)-           , ("- Press ", def_attr)-           , ("up", keyAttr), (" / ", def_attr)-           , ("down", keyAttr), (" / ", def_attr)-           , ("page up", keyAttr), (" / ", def_attr)-           , ("page down", keyAttr)-           , (" to navigate the list\n", def_attr)-           ]--buildUi appst = do-  msg1 <- plainText message1-  setTextFormatter msg1 wrap--  msg2 <- plainTextWithAttrs message2-  setTextFormatter msg2 wrap--  mainUi <- (hBorder >>= withBorderAttribute titleAttr)-         <--> (return $ theList appst)-         <--> ((return $ theFooter appst)-               <++> (hBorder >>= withBorderAttribute titleAttr))--  centered =<< hLimit 55 =<<-             ((return msg1)-                  <--> ((vLimit 25 mainUi >>= bordered)-                        <--> return msg2 >>= withBoxSpacing 1)-                    >>= withBoxSpacing 1)---- Construct the application statea using the message map.-mkAppElements :: IO AppElements-mkAppElements = do-  lw <- newStringList selAttr []-  b <- textWidget wrap ""-  ft <- plainText "" >>= withNormalAttribute titleAttr-  ll <- vLimit 5 lw--  c <- newCollection--  return $ AppElements { theList = lw-                       , theBody = b-                       , theFooter = ft-                       , theListLimit = ll-                       , uis = c-                       }--updateBody :: AppElements -> Int -> IO ()-updateBody st i = do-  let msg = "This is the text for list entry " ++ (show $ i + 1)-  setText (theBody st) msg--updateFooterNums :: AppElements -> Widget (List a b) -> IO ()-updateFooterNums st w = do-  result <- getSelected w-  sz <- getListSize w-  let msg = case result of-              Nothing -> "0/0"-              Just (i, _) -> (show $ i + 1) ++ "/" ++ (show sz)-  setText (theFooter st) msg--main :: IO ()-main = do-  st <- mkAppElements--  ui <- buildUi st-  fg <- newFocusGroup--  _ <- addToCollection (uis st) ui fg-  _ <- addToFocusGroup fg (theList st)--  (theList st) `onSelectionChange` \_ -> updateFooterNums st $ theList st-  (theList st) `onItemAdded` \_ -> updateFooterNums st $ theList st-  (theList st) `onItemRemoved` \_ -> updateFooterNums st $ theList st--  let removeCurrentItem = do-         result <- getSelected (theList st)-         case result of-           Nothing -> return ()-           Just (i, _) -> removeFromList (theList st) i >> return ()-      addNewItem = do-         addToList (theList st) "unused" =<< plainText "a list item"--  (theList st) `onKeyPressed` \_ k _ -> do-         case k of-           (KASCII 'q') -> exitSuccess-           (KASCII '-') -> removeCurrentItem >> return True-           (KASCII 'd') -> removeCurrentItem >> return True-           (KASCII '+') -> addNewItem >> return True-           (KASCII 'a') -> addNewItem >> return True-           _ -> return False--  -- We need to call these handlers manually because while they will-  -- be called automatically as items are added to the list in the-  -- future, the items currently in the list didn't call these because-  -- they weren't registered at the time the items were added.  And-  -- that was impossible because the list was created and populated-  -- before we even got a reference to it, so we couldn't have set up-  -- event handlers.-  updateFooterNums st (theList st)--  -- Enter the event loop.-  runUi (uis st) $ defaultContext { normalAttr = bodyAttr-                                  , focusAttr = focAttr-                                  }
− src/PhoneInputDemo.hs
@@ -1,78 +0,0 @@-module Main where---- This demo is discussed in the vty-ui user's manual.--import Control.Monad--import Graphics.Vty hiding (pad)-import Graphics.Vty.Widgets.All--data PhoneNumber = PhoneNumber String String String-                   deriving (Show)---- This type isn't pretty, but we have to specify the type of the--- complete interface.  Initially you can let the compiler tell you--- what it is.-type T = Box (Box-              (Box (Box (HFixed Edit) FormattedText) (HFixed Edit))-              FormattedText) (HFixed Edit)--data PhoneInput =-   PhoneInput { phoneInputWidget :: Widget T-              , edit1 :: Widget Edit-              , edit2 :: Widget Edit-              , edit3 :: Widget Edit-              , activateHandlers :: Handlers PhoneNumber-              }--newPhoneInput :: IO (PhoneInput, Widget FocusGroup)-newPhoneInput = do-   ahs <- newHandlers-   e1 <- editWidget-   e2 <- editWidget-   e3 <- editWidget-   ui <- (hFixed 4 e1) <++>-         (plainText "-") <++>-         (hFixed 4 e2) <++>-         (plainText "-") <++>-         (hFixed 5 e3)--   let w = PhoneInput ui e1 e2 e3 ahs-       doFireEvent = const $ do-         num <- mkPhoneNumber-         fireEvent w (return . activateHandlers) num--       mkPhoneNumber = do-         s1 <- getEditText e1-         s2 <- getEditText e2-         s3 <- getEditText e3-         return $ PhoneNumber s1 s2 s3--   e1 `onActivate` doFireEvent-   e2 `onActivate` doFireEvent-   e3 `onActivate` doFireEvent--   e1 `onChange` \s -> when (length s == 3) $ focus e2-   e2 `onChange` \s -> when (length s == 3) $ focus e3--   fg <- newFocusGroup-   mapM_ (addToFocusGroup fg) [e1, e2, e3]-   return (w, fg)--onPhoneInputActivate :: PhoneInput-                     -> (PhoneNumber -> IO ()) -> IO ()-onPhoneInputActivate input handler =-    addHandler (return . activateHandlers) input handler--main :: IO ()-main = do-  (p, fg) <- newPhoneInput-  p `onPhoneInputActivate` (error . show)--  ui <- padded (phoneInputWidget p) (padLeftRight 5 `pad` padTopBottom 2)--  c <- newCollection-  _ <- addToCollection c ui fg--  runUi c $ defaultContext { focusAttr = white `on` blue-                           }
− src/ProgressBarDemo.hs
@@ -1,56 +0,0 @@-{-# OPTIONS_GHC -fno-warn-unused-do-bind -fno-warn-missing-signatures #-}-module Main where--import Control.Concurrent (forkIO, threadDelay)-import Control.Monad (forever)--import System.Exit ( exitSuccess )-import Graphics.Vty-import Graphics.Vty.Widgets.All---- Visual attributes.-focAttr = black `on` green-bodyAttr = bright_green `on` black-completeAttr = white `on` red-incompleteAttr = red `on` white--setupProgessBar :: IO (Widget ProgressBar)-setupProgessBar = do-  pb <- newProgressBar completeAttr incompleteAttr-  setProgressTextAlignment pb AlignCenter--  pb `onProgressChange` \val ->-      setProgressText pb $ "Progress (" ++ show val ++ " %)"--  return pb--setupProgressBarThread :: Widget ProgressBar -> IO ()-setupProgressBarThread pb = do-  forkIO $ forever $ do-    let act i = do-          threadDelay $ 1 * 1000 * 1000-          schedule $ setProgress pb (i `mod` 101)-          act $ i + 4-    act 0-  return ()--setupDialog :: (Show a) => Widget a -> IO (Dialog, Widget FocusGroup)-setupDialog ui = do-  (dlg, fg) <- newDialog ui "Progress Bar Demo"-  dlg `onDialogAccept` const exitSuccess-  dlg `onDialogCancel` const exitSuccess-  return (dlg, fg)--main :: IO ()-main = do-  pb <- setupProgessBar-  setupProgressBarThread pb--  (dlg, dlgFg) <- setupDialog pb--  c <- newCollection-  _ <- addToCollection c (dialogWidget dlg) dlgFg--  runUi c $ defaultContext { normalAttr = bodyAttr-                           , focusAttr = focAttr-                           }
src/Text/Trans/Tokenize.hs view
@@ -19,6 +19,7 @@      -- * Manipulation     , truncateLine+    , truncateText     , wrapStream     , findLines #ifdef TESTING@@ -28,22 +29,25 @@     ) where +import Control.Applicative import Data.List     ( inits     )+import qualified Data.Text as T+import Graphics.Vty.Widgets.Util  -- |The type of text tokens.  These should consist of printable -- characters and NOT presentation characters (e.g., newlines).  Each -- type of token should have as its contents a string of characters -- all of the same type.  Tokens are generalized over an attribute -- type which can be used to annotate each token.-data Token a = S { tokenStr :: !String+data Token a = S { tokenStr :: !T.Text                  -- ^The token's string.                  , tokenAttr :: !a                  -- ^The token's attribute.                  }              -- ^Non-whitespace tokens.-             | WS { tokenStr :: !String+             | WS { tokenStr :: !T.Text                   -- ^The token's string.                   , tokenAttr :: !a                   -- ^The token's attribute.@@ -90,8 +94,8 @@  -- |Get the length of a token's string. tokenLen :: Token a -> Int-tokenLen (S s _) = length s-tokenLen (WS s _) = length s+tokenLen (S s _) = T.length s+tokenLen (WS s _) = T.length s  wsChars :: [Char] wsChars = [' ', '\t']@@ -121,32 +125,41 @@  -- |Given a text stream, serialize the stream to its original textual -- representation.  This discards token attribute metadata.-serialize :: TextStream a -> String-serialize (TS es) = concat $ map serializeEntity es+serialize :: TextStream a -> T.Text+serialize (TS es) = T.concat $ serializeEntity <$> es     where-      serializeEntity NL = "\n"+      serializeEntity NL = T.pack "\n"       serializeEntity (T (WS s _)) = s       serializeEntity (T (S s _)) = s  -- |Tokenize a string and apply a default attribute to every token in -- the resulting text stream.-tokenize :: String -> a -> TextStream a+tokenize :: T.Text -> a -> TextStream a tokenize s def = TS $ findEntities s     where-      findEntities [] = []-      findEntities str@(c:_) = nextEntity : findEntities (drop nextLen str)+      findEntities str+          | T.null str = []+          | otherwise = nextEntity : findEntities (T.drop nextLen str)           where+            c = T.head str             (nextEntity, nextLen) = if isWs c-                                    then (T (WS nextWs def), length nextWs)+                                    then (T (WS nextWs def), T.length nextWs)                                     else if c == '\n'                                          then (NL, 1)-                                         else (T (S nextStr def), length nextStr)-            nextWs = takeWhile isWs str-            nextStr = takeWhile (\ch -> not $ ch `elem` ('\n':wsChars)) str+                                         else (T (S nextStr def), T.length nextStr)+            nextWs = T.takeWhile isWs str+            nextStr = T.takeWhile (\ch -> not $ ch `elem` ('\n':wsChars)) str +-- |Same as 'truncateLine' but for 'Text' values.+truncateText :: Phys -> T.Text -> T.Text+truncateText width t =+    let TS ts = tokenize t ()+        tokens = entityToken <$> ts+    in T.concat $ tokenStr <$> truncateLine width tokens+ -- |Given a list of tokens, truncate the list so that its underlying -- string representation does not exceed the specified column width.-truncateLine :: Int -> [Token a] -> [Token a]+truncateLine :: Phys -> [Token a] -> [Token a] truncateLine l _ | l < 0 = error $ "truncateLine cannot truncate at length = " ++ show l truncateLine _ [] = [] truncateLine width ts =@@ -158,14 +171,14 @@     -- If there are no passing cases (i.e., remaining is null), just     -- return 'width' characters of the first token.     if null remaining-    then [first_tok { tokenStr = take width $ tokenStr first_tok }]+    then [first_tok { tokenStr = takeMaxText width $ tokenStr first_tok }]     else if length tokens == length ts          then tokens-         else if null $ tokenStr lastToken+         else if T.null $ tokenStr lastToken               then tokens               else tokens ++ [lastToken]     where-      lengths = map (length . tokenStr) ts+      lengths = map (sum . (chWidth <$>) . T.unpack . tokenStr) ts       cases = reverse $ inits lengths       remaining = dropWhile ((> width) . sum) cases       tokens = take (length $ head remaining) ts@@ -173,28 +186,32 @@        first_tok = ts !! 0       last_tok = ts !! (length tokens)-      lastToken = last_tok { tokenStr = take (width - truncLength) $ tokenStr last_tok }+      lastToken = last_tok { tokenStr = takeMaxText (width - truncLength) $+                                        tokenStr last_tok+                           }  -- |Given a text stream and a wrapping width, return a new -- 'TextStream' with newlines inserted in appropriate places to wrap--- the text at the specified column.  This function results in text--- wrapped without leading or trailing whitespace on wrapped lines,--- although it preserves leading whitespace in the text which was not--- the cause of the wrapping transformation.-wrapStream :: (Eq a) => Int -> TextStream a -> TextStream a+-- the text at the specified column (not character position).+--+-- This function results in text wrapped without leading or trailing+-- whitespace on wrapped lines, although it preserves leading+-- whitespace in the text which was not the cause of the wrapping+-- transformation.+wrapStream :: (Eq a) => Phys -> TextStream a -> TextStream a wrapStream width (TS stream) = TS $ reverse $ dropWhile (== NL) $ reverse $ wrapAll' 0 stream     where-      wrapAll' :: Int -> [TextStreamEntity a] -> [TextStreamEntity a]+      wrapAll' :: Phys -> [TextStreamEntity a] -> [TextStreamEntity a]       wrapAll' _ [] = []       wrapAll' _ (NL:rest) = NL : wrapAll' 0 rest       wrapAll' accum (T t:rest) =-          if (length $ tokenStr t) + accum > width+          if (textWidth $ tokenStr t) + accum > width           then if isWhitespace t                then [NL] ++ wrapAll' 0 (dropWhile isWsEnt rest)-               else if accum == 0 && ((length $ tokenStr t) >= width)-                    then [T t, NL] ++ wrapAll' 0 (dropWhile isWsEnt rest)-                    else [NL, T t] ++ wrapAll' (length $ tokenStr t) rest-          else T t : wrapAll' (accum + (length $ tokenStr t)) rest+               else if accum == 0 && ((textWidth $ tokenStr t) >= width)+                    then [T t] ++ wrapAll' (textWidth $ tokenStr t) (dropWhile isWsEnt rest)+                    else [NL, T t] ++ wrapAll' (textWidth $ tokenStr t) rest+          else T t : wrapAll' (accum + (textWidth $ tokenStr t)) rest  partitions :: (a -> Bool) -> [a] -> [[a]] partitions _ [] = []
test/TestDriver.hs view
@@ -7,10 +7,16 @@  import qualified Tests.FormattedText as FormattedText import qualified Tests.Tokenize as Tokenize+import qualified Tests.Edit as Edit+import qualified Tests.TextClip as TextClip+import qualified Tests.TextZipper as TextZipper  tests :: [Property] tests = concat [ FormattedText.tests                , Tokenize.tests+               , Edit.tests+               , TextClip.tests+               , TextZipper.tests                ]  main :: IO ()
+ test/src/Tests/Edit.hs view
@@ -0,0 +1,46 @@+module Tests.Edit where++import Control.Applicative+import Data.Char ( isPrint )+import Test.QuickCheck+import Tests.Instances ()+import qualified Data.Text as T++import Graphics.Vty.Widgets.Edit+import Graphics.Vty.Widgets.Util+import Graphics.Vty.Widgets.TextClip++stringGen :: Gen String+stringGen = listOf1 charGen++textGen :: Gen T.Text+textGen = T.pack <$> (listOf $ oneof [ pure 'a'+                                     , pure ' '+                                     , pure '台'+                                     ])++charGen :: Gen Char+charGen = oneof [ arbitrary `suchThat` (\c -> isPrint c)+                , pure '台'+                ]++doClippingTest :: Property+doClippingTest =+    property $ forAll textGen $ \t ->+        forAll (choose (Phys 0, textWidth t + Phys 10)) $ \leftCrop ->+        forAll (choose (Phys 0, textWidth t + Phys 10)) $ \resWidth ->+            let rect = ClipRect { clipLeft = leftCrop+                                , clipWidth = resWidth+                                , clipTop = 0+                                , clipHeight = 1+                                }+                [(_, ls, rs)] = clip2d rect [t]+                [res] = doClipping [t] rect+            in strWidth res == 0+                   || and [ not ls || (ls && head res == indicatorChar)+                          , not rs || (rs && last res == indicatorChar)+                          ]++tests :: [Property]+tests = [ label "doClipping clips and adds indicators" doClippingTest+        ]
test/src/Tests/FormattedText.hs view
@@ -4,6 +4,7 @@ import Test.QuickCheck.Monadic  import Control.Applicative+import qualified Data.Text as T  import Graphics.Vty import Graphics.Vty.Widgets.Text@@ -42,11 +43,12 @@         img3 <- run $ render w2 sz defaultContext         return $ img1 == img3 && img1 /= img2 -textString :: Gen String-textString = listOf $ oneof [ pure 'a'-                            , pure '\n'-                            , pure ' '-                            ]+textString :: Gen T.Text+textString = T.pack <$> (listOf $ oneof [ pure 'a'+                                        , pure '\n'+                                        , pure ' '+                                        , pure '台'+                                        ])  tests :: [Property] tests = [ label "text: newlines rendered correctly" textHeight
test/src/Tests/Instances.hs view
@@ -2,9 +2,11 @@ module Tests.Instances where  import Test.QuickCheck+import System.Random import Control.Applicative ( (<*>), (<$>), pure )  import Graphics.Vty+import Graphics.Vty.Widgets.Util  instance (Show a, Arbitrary a, Eq a) => Arbitrary (MaybeDefault a) where     arbitrary = oneof [ pure Default@@ -24,3 +26,15 @@     arbitrary = DisplayRegion <$> coord <*> coord         where           coord = sized $ \n -> fromIntegral <$> choose (0, n)++instance Random Phys where+    random g =+        let (val, g') = random g+        in (Phys val, g')++    randomR (Phys a, Phys b) g =+        let (val, g') = randomR (a, b) g+        in (Phys val, g')++instance Arbitrary Phys where+    arbitrary = Phys <$> arbitrary
+ test/src/Tests/TextClip.hs view
@@ -0,0 +1,60 @@+module Tests.TextClip+    ( tests+    )+where++import Control.Applicative+import Test.QuickCheck++import Tests.Instances ()+import qualified Data.Text as T+import Graphics.Vty.Widgets.TextClip+import Graphics.Vty.Widgets.Util++rectGen :: Gen ClipRect+rectGen = ClipRect+          <$> (arbitrary `suchThat` (>= Phys 0))+          <*> (arbitrary `suchThat` (>= Phys 0))+          <*> (arbitrary `suchThat` (>= Phys 0))+          <*> (arbitrary `suchThat` (>= Phys 0))++textGen :: Gen T.Text+textGen = T.pack <$> (listOf $ oneof [ pure 'a'+                                     , pure ' '+                                     , pure '台'+                                     ])++docGen :: Gen [T.Text]+docGen = listOf1 textGen++clip1dWidth :: Property+clip1dWidth =+    forAll rectGen $ \rect ->+        forAll textGen $ \t ->+            let (r, _, _) = clip1d (clipLeft rect) (clipWidth rect) t+            in textWidth r <= clipWidth rect++clipSliceLeft :: Bool+clipSliceLeft =+    let (_, ls, _) = clip1d (Phys 1) (Phys 2) $ T.pack "台台"+    in ls++clipSliceRight :: Bool+clipSliceRight =+    let (_, _, rs) = clip1d (Phys 1) (Phys 2) $ T.pack "台台"+    in rs++clip2dBounds :: Property+clip2dBounds =+    forAll rectGen $ \rect ->+        forAll docGen $ \t ->+            let result = clip2d rect t+                check (r, _, _) = textWidth r <= clipWidth rect+            in all check result && (Phys $ length result) <= clipHeight rect++tests :: [Property]+tests = [ label "clip1d guarantees width bound" clip1dWidth+        , label "clip1d indicates left slices" $ property clipSliceLeft+        , label "clip1d indicates right slices" $ property clipSliceRight+        , label "clip2d guarantees width and height bounds" clip2dBounds+        ]
+ test/src/Tests/TextZipper.hs view
@@ -0,0 +1,43 @@+module Tests.TextZipper+    ( tests+    )+where++import Control.Applicative+import Test.QuickCheck++import Tests.Instances ()+import qualified Data.Text as T+import Graphics.Vty.Widgets.TextZipper++textGen :: Gen T.Text+textGen = T.pack <$> (listOf $ oneof [ pure 'a'+                                     , pure ' '+                                     ])++docGen :: Gen [T.Text]+docGen = listOf1 textGen++validCursorPositions :: [T.Text] -> Gen (Int, Int)+validCursorPositions ts = do+  r <- elements [0..length ts - 1]+  c <- elements [0..T.length (ts !! r)]+  return (r, c)++cursorCheck :: Property+cursorCheck =+    forAll docGen $ \doc ->+        forAll (validCursorPositions doc) $ \pos ->+            pos == cursorPosition (moveCursor pos $ textZipper doc)++insertDelCheck :: Property+insertDelCheck =+    forAll docGen $ \doc ->+        forAll (validCursorPositions doc) $ \pos ->+            let orig = moveCursor pos $ textZipper doc+            in orig == (deletePrevChar $ insertChar 'X' orig)++tests :: [Property]+tests = [ label "get/set cursor position are consistent" cursorCheck+        , label "insert/delete are inverses" insertDelCheck+        ]
test/src/Tests/Tokenize.hs view
@@ -1,40 +1,52 @@ {-# OPTIONS_GHC -fno-warn-orphans #-}+{-# LANGUAGE OverloadedStrings #-} module Tests.Tokenize where +import Control.Applicative import Data.Char ( isPrint ) import Test.QuickCheck-+import Tests.Util import Text.Trans.Tokenize+import Graphics.Vty.Widgets.Util+import qualified Data.Text as T  lineGen :: Gen [Token ()] lineGen = listOf1 $ oneof [wsgen, strgen]     where       strgen = do         s <- stringGen-        return $ S s ()+        return $ S (T.pack s) ()        wsgen = do         s <- listOf1 $ elements " \t"-        return $ WS s ()+        return $ WS (T.pack s) ()  stringGen :: Gen String stringGen = listOf1 charGen +textGen :: Gen T.Text+textGen = T.pack <$> stringGen+ charGen :: Gen Char-charGen = arbitrary `suchThat` (\c -> isPrint c)+charGen = oneof [ arbitrary `suchThat` (\c -> isPrint c)+                , pure '台'+                ]  tests :: [Property]-tests = [ label "tokenize and serialize work" $ property $ forAll stringGen $+tests = [ label "tokenize and serialize work" $+                property $ forAll textGen $                     \s -> (serialize $ tokenize s ()) == s -        , label "truncateLine leaves short lines unchanged" $ property $ forAll lineGen $-                    \ts -> ts == truncateLine (length $ serialize (TS (map T ts))) ts+        , label "truncateLine leaves short lines unchanged" $+                property $ forAll lineGen $+                    \ts -> ts == truncateLine (lineLength ts) ts          -- Bound the truncation width at twice the size of the input         -- since huge cases are silly.-        , label "truncateLine truncates long lines" $ property $ forAll lineGen $-                    \ts -> forAll (choose (0, 2 * (length $ serialize (TS (map T ts))))) $-                           \width -> length (serialize $ (TS (map T $ truncateLine width ts))) <= width+        , label "truncateLine truncates long lines" $+                property $ forAll lineGen $+                    \ts -> forAll (choose (0, 2 * (lineLength ts))) $+                           \width -> (lineLength $ truncateLine width ts) <= width          , label "wrapStream does the right thing with whitespace" $ property $                 and [ wrapStream 5 (TS [T (S "foo" ()), T (WS " " ()), T (S "bar" ())]) ==@@ -90,8 +102,9 @@         -- be broken up.         , label "wrapLine wraps long lines when possible" $                 property $ forAll lineGen $-                    \ts -> forAll (choose ((length $ tokenStr $ ts !! 0), (length $ serialize (TS $ map T ts)) - 1)) $-                           \width -> let ls = findLines new-                                         TS new = wrapStream width $ TS $ map T ts-                                     in all (\l -> (length (serialize $ TS l)) <= width || (length l == 1)) ls+                    \ts -> forAll (choose ((textWidth $ tokenStr $ ts !! 0), (lineLength ts - Phys 1))) $+                           \width -> let TS new = wrapStream width $ TS $ T <$> ts+                                         ls = findLines new+                                         check l = (lineLength $ entityToken <$> l) <= width || (length l == 1)+                                     in all check ls         ]
test/src/Tests/Util.hs view
@@ -1,7 +1,11 @@ module Tests.Util where +import Control.Applicative+import qualified Data.Text as T import Graphics.Vty import Graphics.Vty.Widgets.Core+import Graphics.Vty.Widgets.Util+import Text.Trans.Tokenize import Test.QuickCheck import Test.QuickCheck.Monadic import Tests.Instances ()@@ -14,8 +18,8 @@ count _ [] = 0 count f (a:as) = count f as + if f a then 1 else 0 -numNewlines :: String -> Int-numNewlines = count (== '\n')+numNewlines :: T.Text -> Int+numNewlines = count (== '\n') . T.unpack  sizeTest :: (Show a) => IO (Widget a) -> PropertyM IO Bool sizeTest mkWidget =@@ -26,3 +30,6 @@           return $ image_height img == 0 && image_width img == 0 else           return $ image_width img <= region_width sz &&                  image_height img <= region_height sz++lineLength :: [Token a] -> Phys+lineLength = sum . (textWidth <$>) . (tokenStr <$>)
vty-ui.cabal view
@@ -1,5 +1,5 @@ Name:                vty-ui-Version:             1.5.1+Version:             1.6 Synopsis:   An interactive terminal user interface library for Vty @@ -23,7 +23,7 @@ Build-Type:          Simple License:             BSD3 License-File:        LICENSE-Cabal-Version:       >= 1.6+Cabal-Version:       >= 1.8 Homepage:            http://jtdaugherty.github.com/vty-ui/  Data-Files:@@ -65,6 +65,9 @@     doc/ch4/Padded.tex     doc/ch4/ProgressBar.tex     doc/ch4/Table.tex+    doc/ch5/main.tex+    doc/ch5/TextClip.tex+    doc/ch5/TextZipper.tex     doc/macros.tex     doc/Makefile     doc/vty-ui-users-manual.tex@@ -75,8 +78,8 @@   type:     git   location: git://github.com/jtdaugherty/vty-ui.git -Flag testing-    Description:     Build for testing+Flag no-tests+    Description:     Skip build of testing executable     Default:         False  Flag demos@@ -86,15 +89,16 @@ Library   Build-Depends:     base >= 4 && < 5,-    vty >= 4.6 && < 4.8,-    containers >= 0.2 && < 0.5,+    vty >= 4.7.0.18,+    containers >= 0.2 && < 0.6,     regex-base >= 0.93 && < 0.94,-    directory >= 1.0 && < 1.2,+    directory >= 1.0 && < 1.3,     filepath >= 1.1 && < 1.4,-    unix >= 2.4 && < 2.6,-    mtl >= 2.0 && < 2.1,-    stm >= 2.1 && < 2.3,+    unix >= 2.4 && < 2.7,+    mtl >= 2.0 && < 2.2,+    stm >= 2.1 && < 2.5,     array >= 0.3.0.0 && < 0.5.0.0,+    text >= 0.11,     vector >= 0.9    GHC-Options:       -Wall@@ -104,6 +108,8 @@           Graphics.Vty.Widgets.All           Graphics.Vty.Widgets.Alignment           Graphics.Vty.Widgets.Text+          Graphics.Vty.Widgets.TextClip+          Graphics.Vty.Widgets.TextZipper           Graphics.Vty.Widgets.Core           Graphics.Vty.Widgets.Box           Graphics.Vty.Widgets.List@@ -129,115 +135,119 @@  Executable vty-ui-tests   Build-Depends:-    QuickCheck >= 2.4 && < 2.5+    base >= 4 && < 5,+    QuickCheck >= 2.4 && < 2.6,+    random >= 1.0,+    text,+    vty,+    vty-ui    CPP-Options: -DTESTING   GHC-Options: -Wall -  if !flag(testing)+  if flag(no-tests)     Buildable:     False   end   Hs-Source-Dirs:  src,test,test/src   Main-is:         TestDriver.hs-  if os(darwin)-    Extra-Lib-Dirs:  /usr/lib    Other-Modules:         Tests.Instances         Tests.Util+        Tests.Edit         Tests.FormattedText         Tests.Tokenize+        Tests.TextClip+        Tests.TextZipper  Executable vty-ui-list-demo-  Hs-Source-Dirs:  src+  Hs-Source-Dirs:  demos   GHC-Options:     -Wall   Main-is:         ListDemo.hs-  if os(darwin)-    Extra-Lib-Dirs:  /usr/lib   Build-Depends:     base >= 4 && < 5,-    mtl >= 2.0 && < 2.1,-    vty >= 4.6 && < 4.8+    mtl >= 2.0 && < 2.2,+    vty >= 4.7.0.18,+    text,+    vty-ui   if !flag(demos)     Buildable: False  Executable vty-ui-progressbar-demo-  Hs-Source-Dirs:  src+  Hs-Source-Dirs:  demos   GHC-Options:     -Wall   Main-is:         ProgressBarDemo.hs-  if os(darwin)-    Extra-Lib-Dirs:  /usr/lib   Build-Depends:     base >= 4 && < 5,-    mtl >= 2.0 && < 2.1,-    vty >= 4.6 && < 4.8+    mtl >= 2.0 && < 2.2,+    vty >= 4.7.0.18,+    text,+    vty-ui   if !flag(demos)     Buildable: False  Executable vty-ui-complex-demo-  Hs-Source-Dirs:  src+  Hs-Source-Dirs:  demos   GHC-Options:     -Wall   Main-is:         ComplexDemo.hs-  if os(darwin)-    Extra-Lib-Dirs:  /usr/lib   Build-Depends:     base >= 4 && < 5,-    mtl >= 2.0 && < 2.1,+    mtl >= 2.0 && < 2.2,     bytestring >= 0.9 && < 1.0,     time >= 1.1 && < 1.5,     old-locale >= 1.0 && < 1.1,-    vty >= 4.6 && < 4.8+    vty >= 4.7.0.18,+    text,+    vty-ui   if !flag(demos)     Buildable: False  Executable vty-ui-dirbrowser-demo-  Hs-Source-Dirs:  src+  Hs-Source-Dirs:  demos   GHC-Options:     -Wall   Main-is:         DirBrowserDemo.hs-  if os(darwin)-    Extra-Lib-Dirs:  /usr/lib   Build-Depends:     base >= 4 && < 5,-    mtl >= 2.0 && < 2.1,-    vty >= 4.6 && < 4.8+    mtl >= 2.0 && < 2.2,+    vty >= 4.7.0.18,+    vty-ui   if !flag(demos)     Buildable: False  Executable vty-ui-phoneinput-demo-  Hs-Source-Dirs:  src+  Hs-Source-Dirs:  demos   GHC-Options:     -Wall   Main-is:         PhoneInputDemo.hs-  if os(darwin)-    Extra-Lib-Dirs:  /usr/lib   Build-Depends:     base >= 4 && < 5,-    mtl >= 2.0 && < 2.1,-    vty >= 4.6 && < 4.8+    mtl >= 2.0 && < 2.2,+    vty >= 4.7.0.18,+    text,+    vty-ui   if !flag(demos)     Buildable: False  Executable vty-ui-dialog-demo-  Hs-Source-Dirs:  src+  Hs-Source-Dirs:  demos   GHC-Options:     -Wall   Main-is:         DialogDemo.hs-  if os(darwin)-    Extra-Lib-Dirs:  /usr/lib   Build-Depends:     base >= 4 && < 5,-    mtl >= 2.0 && < 2.1,-    vty >= 4.6 && < 4.8+    mtl >= 2.0 && < 2.2,+    vty >= 4.7.0.18,+    text,+    vty-ui   if !flag(demos)     Buildable: False  Executable vty-ui-edit-demo-  Hs-Source-Dirs:  src+  Hs-Source-Dirs:  demos   GHC-Options:     -Wall   Main-is:         EditDemo.hs-  if os(darwin)-    Extra-Lib-Dirs:  /usr/lib   Build-Depends:     base >= 4 && < 5,-    mtl >= 2.0 && < 2.1,-    vty >= 4.6 && < 4.8+    mtl >= 2.0 && < 2.2,+    vty >= 4.7.0.18,+    vty-ui   if !flag(demos)     Buildable: False