packages feed

vty-ui-1.6: doc/ch3/new_widget_type.tex

\section{Creating a New Widget Type}
\label{sec:new_widget_type}

The first step in creating a custom widget is deciding what kind of
state the widget will store.  This decision is based on what behaviors
the widget can have and it determines what the widget's API will be.

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}, 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}.  The
constructor will take the counter's initial value.  Here's the
function in full:

\begin{haskellcode}
 newCounter :: Int -> IO (Widget Counter)
 newCounter initialValue = do
   let st = Counter initialValue
   wRef <- newWidget st $ \w ->
     w { render_ =
         \this size ctx -> do
           (Counter v) <- getState this
           return $ string (getNormalAttr ctx) (show v)
       }
\end{haskellcode}

Now we have a constructor for a \fw{Counter} widget.  Let's go through
the code:

\begin{haskellcode}
 let st = Counter initialValue
 wRef <- newWidget st $ \w -> ...
\end{haskellcode}

The \fw{Core} module's \fw{newWidget} function creates a new
\fw{IORef} wrapping a \fw{WidgetImpl a}.  The \fw{WidgetImpl} type is
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, 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 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:

\begin{haskellcode}
 render_ =
   \this size ctx -> do
     (Counter v) <- getState this
     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 arguments are as follows:

\begin{itemize}
\item \fw{Widget a} - the widget being rendered, i.e., the \fw{Widget
  Counter} reference.  This is passed to provide access to the
  widget's state which will be used to render it.
\item \fw{DisplayRegion} - the size of the display region into which
  the widget should fit, measured in rows and columns.  The \fw{Image}
  returned by \fw{render\_} should \textit{never} be larger than this
  region, or the rendering process will raise an exception.  The
  reason is because if it were to violate the specified size, then the
  assumptions made by any other widgets about layout would fail, and
  the interface would become garbled in the terminal.  In addition,
  widget sizes are used to compute widget positions, so sizes must be
  accurate.

  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.
  We don't care about supporting a focused behavior in our
  \fw{Counter} widgets, so we just look at the ``normal'' attribute.
\item \fw{Image} - this is the type of Vty ``images'' that can be
  composed into a final terminal representation.  All widgets must be
  converted to this type during the rendering process to be composed
  into the final result.
\end{itemize}

The implementation of the \fw{render\_} function is as follows:

\begin{haskellcode}
 (Counter v) <- getState this
\end{haskellcode}

The \fw{getState} function takes a \fw{Widget a} and returns its
\fw{state} field.  In this case, it returns the \fw{Counter} value.
It's important to use \fw{getState} instead of just referring to
\fw{st} in the example above, since you'll need to make sure to get
the latest state value at the time \fw{render\_} is called.

\begin{haskellcode}
 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.  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) $ T.unpack truncated
\end{haskellcode}

The \fw{string} function is a Vty library function which takes an
attribute (\fw{Attr}) and a \fw{String} and returns an \fw{Image}.
The \fw{getNormalAttr} function returns the normal attribute from the
\fw{Render\-Context}, merged with the ``override'' attribute from the
\fw{Render\-Context}, if it is set.  For more information on the
override attribute, see Section \ref{sec:attributes}.

This concludes the basic implementation requirements for a new widget
type; to make it useful, we'll need to add some functions to manage
its state:

\begin{haskellcode}
 setCounterValue :: Widget Counter -> Int -> IO ()
 setCounterValue wRef val =
    updateWidgetState wRef $ const $ Counter val

 getCounterValue :: Widget Counter -> IO Int
 getCounterValue wRef = do
    Counter val <- getState wRef
    return val
\end{haskellcode}

The \fw{setCounterValue} function takes a \fw{Counter} widget and sets
its \fw{state} to a new counter value.  The \fw{updateWidgetState}
function takes a \fw{Widget a} and a state transformation function and
updates the \fw{state} field of the widget.  The \fw{getCounterValue}
function just reads the state and returns the counter's value.  Now
you could write a program using these functions to create, manipulate,
and display the counter.