packages feed

vty-ui-1.0: 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}; 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!}

\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:

\begin{haskellcode}
 newCounter :: (MonadIO m) => Int -> m (Widget Counter)
 newCounter initialValue = do
   wRef <- newWidget $ \w ->
     w { state = Counter initialValue
       , 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}
 wRef <- newWidget $ \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\_} and \fw{state}.  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.

The \fw{newWidget} function takes a function \fw{WidgetImpl a ->
  WidgetImpl a} and updates the widget implementation contained in the
\fw{IORef}.  We use this to specify the behavior of the widget beyond
the defaults, which are specified in the \fw{newWidget} function.

\begin{haskellcode}
 state = Counter initialValue
\end{haskellcode}

Here we set the inital value of the counter and create the
\fw{Counter} state and store it in the \fw{WidgetImpl}.  We'll
reference this state later on in the rendering code and in any API
functions that we want to implement to mutate it.

\begin{haskellcode}
 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
\end{haskellcode}

This actually does the job of rendering the counter value into a form
that can be displayed in the terminal.  The type of \fw{render\_} is
\fw{Widget a -> DisplayRegion -> RenderContext -> IO Image}.  The
types 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 may render to an \fw{Image} \textit{smaller} than the
  specified size; many do.
\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.

\begin{haskellcode}
 let s = show v
     width = fromEnum $ region_width size - length s
     truncated = take 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.

\begin{haskellcode}
 return $ string (getNormalAttr ctx) 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 \vref{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 :: (MonadIO m) => Widget Counter -> Int -> m ()
 setCounterValue wRef val =
    updateWidgetState wRef $ const $ Counter val

 getCounterValue :: (MonadIO m) => Widget Counter -> m 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.