Building GUI with Dew
2020-05-19

Recently when I was building development UI for my game, I came to realize that GUI is a better expressiveness test for Dew than a mob AI.

For example, Immediate Mode Graphical User interface (IMGUI) libraries and my game share a same important problem to solve: mapping an identity in user/script code to an internal state in the library/engine. IMGUI libraries solve this problem by requiring the user code to specify an unique identifier, or a tree path of unique identifiers for more complex situations; while in my game, I can use a Dew stream to retain a handle to the internal state.

To test the expressiveness of Dew, I consider a much more complex approach of GUI than IMGUI: building GUI as a hierarchy of components, with all components being retained, and each component’s behaviours being defined locally.

React.js is one popular GUI library with such approach. In this blogpost, I’ll consider a programming model close to React.js, and have the following capabilities defined for components:

  1. There is a global state, which can be accessed by all components;

  2. Each component’s lifetime is determined by its parent;

  3. Each component receives a set of parameters from its parent, and some of the parameters can be time-varying;

  4. Each component can have its own state;

  5. Each component can send back a value to its parent component for feedback, which is often related to user inputs.

Channels

Building GUI requires a planned language feature of Dew: channels.

For the current version of Dew, I’ve always a bit concerned that data flow can be somewhat too restricted for some use cases. If wiring data through FFI is not counted, then

These restrictions are related to the language design that data mutation isn’t allowed. Data mutation across different Dew frames conflicts with the notion of Dew stream, since both of them can represent a varying value over time, and allowing both of them would lead to confusions. Another concern with data mutation is how it works work Dew streams evaluated concurrently.

To solve the problem, I’ve come up with the notion of channels recently. Channels can elegantly solve the problem of more flexible data flow across different Dew streams, while introducing minimal drawbacks.

Channels have two variants, and can be created by calling one of the following two functions:

chan: () -> (''a -> (), *''a)
dchan: () -> (''a -> (), *[''a])

When the function is called as chan () or dchan () it returns a two-element tuple (in,out), represents the two endpoints of the channel:

Note that only two primitive functions are introduced, and we did not introduce any new language constructs. Channel endpoints can be freely passed around like a normal function or stream, and can also be passed through another channel.

The channels look very powerful at this point, but I haven’t yet explained why there’re two variants of them. In our design, channel output endpoints are represented as streams. To evaluate the stream and get channel output, we’ll have to wait for all other streams, because they could all potentially hold the input endpoint, and send some values into the channel during evaluation. This introduces a lot of data dependencies, and would have a big impact on parallelizability of streams.

So instead of the naive version of channels, we split it up into two variants, each with a different restriction to keep data dependency minimal.

There are other variants of channels possible, but combination of chan and dchan seem good enough for now, and I may add more later if needed.

I expect that we can build some higher level constructs with the channels, but this is not the focus of this blogpost.

Basic GUI widgets in Dew

To implement this GUI model in Dew, we assume that a context ctx needs to be serially wired through each component. This context is internal to the GUI library, and will notably contain information necessary for layout. Then a GUI component can be represented as a Dew value of the following type:

*ctx -> (*ctx, *''a)

In this type, ''a is the type of feedback value that the component generates at each frame. A component’s lifetime starts when it’s instantiated by applying a *ctx value, and ends when the returned *ctx is no longer referenced. The component’s parameters and states can be stored as the upvalues, even though we haven’t explicitly modeled them.

The returned *ctx and *''a are two separate streams, because the feedback stream *''a only depends on the previous value of *ctx and the current value of user inputs.

We can then define a set of basic GUI widgets:

button: *string -> *ctx -> (*ctx, *bool)
label: *string -> *ctx -> (*ctx, *())
textbox: *string -> *ctx -> (*ctx, *string)
window: (num,num,num,num) -> (*ctx -> (*ctx, *''a)) -> *ctx -> (*ctx, *''a)

For buttons, the feedback value indicates whether the button is pressed; and for textboxes, the feedback value represents the next text as modified by user inputs.

button can be implemented as follows, and other widgets are similar.

button caption ctx =
   let ctx' = pre ctx0 ctx in
   let state' = pre state0 state
   and state = dew -> button_next_state @state' @ctx in
   let render = dew -> draw_button @ctx @caption @state in
   (dew -> let _ = render in layout_button @ctx @caption),
   (dew -> is_mouse_pressed @ctx')

Rendering code for the button is placed in a separate stream than ctx, so that rendering for each widget is parallelizable, while ctx has to be serially wired through each widget.

A widget’s parameter can directly depend on its feedback, as demonstrated as follows:

let textbox' ctx =
   let newctx, text = textbox text ctx in
   newctx, text

textbox' is a new widget like textbox, except that its content can only be modified by user input, and can’t be controlled programmatically.

Manipulating feedbacks

In many cases we’ll want to capture the feedback value of a component when it’s alive, and do something with it, like sending it into a channel. We define a the helper function getret for this purpose:

# getret : (''a -> ()) -> (*ctx -> (*ctx, *''a)) -> *ctx -> (*ctx, *())
getret f component ctx =
   let newctx, feedback = component ctx in
   let aux = dew' -> f @feedback in
   (dew -> let _ = aux in @newctx),
   (dew -> ())

Note that an early stream dew' -> instead of dew -> is used. This forces the feedback stream to be evaluated and the feedback value to be sent into the channel, as long as the component is retained in the last frame or has just been created. Otherwise, it could potentially introduce cyclic dependence that whether a component should be retained or not depends on its feedback, which in turn depends on the component being retained.

To change the original feedback value, we also define a mapret function:

# mapret : (''a -> ''b) -> (*ctx -> (*ctx, *''a)) -> *ctx -> (*ctx, *''b)
mapret f component ctx =
   let newctx, feedback = component ctx in
   newctx, (dew -> f @feedback)

Building up larger components

From basic widgets, we can build up larger GUI components by layout functions. We consider two simple layout functions here:

layout_row: num -> [num, *ctx -> (*ctx, *''a)] -> *ctx -> *()
layout_cat: [*ctx -> (*ctx, *''a)] -> *ctx -> *()

layout_row builds up a row of components, with the height of the row specified by the first argument, and width of each child components specified by the first tuple element in the list. layout_cat builds up a larger component from a series of rows.

These layout functions are static, and does not allow adding, removing or reordering child components dynamically. The child components passed to these layout functions will be instantiated when the larger component is instantiated.

This design is chosen because *ctx argument must be serially wired by layout functions, so a child component has to be passed as an uninstantiated form *ctx -> (*ctx, *''a), which makes it unclear what its lifetime is if it’s dynamic. It’s possible to add a unique identifier for each child component in the list to specify that, but for a simple layout function we’ll ignore that possibility.

To give a small example with static-layout components, let’s make an incrementor:

let incrementor =
   let setbtn, btn = chan () in
   let state = pre 0 state + (dew -> if @btn then 1 else 0) in
   layout_row 10 [
      30, label (tostring state);
      10, getret setbtn $ button "Increment";
   ]

Note that states of a component can be simply created by pre operator. Global states are also easy to do:

let send_globalfeedback, globalfeedback = dchan ()
let globalstates_last = pre globalstates_initial globalstates
and globalstates = dew ->
   calculate_globalstates globalstates_last globalfeedback

Dynamic layout

Now we consider how to add/remove/relayout child components in a larger component dynamically. When we do this across different Dew time frames, some of the old components should be retained, and some new components should be instantiated.

An obvious way to do this explicitly in Dew would be using state machines, which I explained in an earlier blogpost. The retained component instances can be passed among different state machine states as state parameters.

But there’s one problem. When the component among different state machine states, some of the child component instances are retained. So at the moment that a child component is instantiated, it must be given a ctx stream that could potentially come from multiple state machine states.

To solve the problem, we need to use a channel:

# instantiate : (*ctx -> (*ctx, *''a)) -> *ctx -> (*ctx, *''a)
instantiate component =
   let setctx, ctx = chan () in
   let newctx, feedback = component ctx in
   fun ctx' ->
      (dew -> setctx @ctx'; @newctx),
      feedback

The channel is created at the moment when the component is instantiated. To supply the channel, instantiate returns a fake component that intercepts ctx and send it through the channel.

Now we focus on a simple state machine with only one parameterized state to emulate the style of React.js, and make a simple Todo list GUI as the final example.

# withstate: (''s -> ((''s->''s)->()) -> *ctx -> (*ctx, *())) ->
#            ''s -> *ctx -> (*ctx, *())
withstate f =
   let component state ctx =
      let setstate, statemut = dchan () in
      let newctx, feedback = f state setstate ctx in
      (switch $ dew ->
         if @statemut == [] then @newctx\ else begin
            let newstate = foldl (fun a f -> f a) state @statemut in
            \component newstate ctx
         else),
      (dew -> ())
   in component

onclick f =
   getret (fun c -> if c then f ())

let todolist = withstate $ fun state setstate ->
   let settext, text = chan () in
   layout_cat $ lappend
      (lmapi (fun item i -> layout_row 10 [
         50, item;
         10, onclick (fun () ->
            setstate $ fun state -> lremove state i
         ) $ button "Remove Item";
      ]) state)
      [layout_row 10 [
         50, getret settext $ textbox';
         10, onclick (fun () ->
            setstate $ fun state ->
               lappend state [instantiate $ label @text]
         ) $ button "Add Item";
      ]]

The state is a list of instantiated label components, and whenever we want to modify the state, we can call setstate.

There’s a minor issue with the code, if there’re multiple click events happen at the same, the state mutation function given to setstate may not be correct. To fix this issue, we can: 1) make sure there won’t be multiple simultaneous user input events; 2) do not mutation the state if it would cause a runtime error; 3) a map instead of a list is used for state; or 4) we can make withstate accept only the first state mutation.

Conclusion

In this blogpost, I’ve described a GUI framework similar to React.js. By filling in the missing blanks, I expect that a full-featured GUI library can be built. This GUI library would introduce no unnecessary delay, and has parallelized rendering.

Comparing my framework with React.js, I see two advantages with Dew:

  1. If the goal is build the same ‘component’ abstraction, Dew do not have many advantages. However with Dew I can have much finer-grained abstractions than ‘component’. These finer-grained abstractions are so simple, that I expect much more abstractions are possible.

  2. Data flow and delay are much clearer in Dew than in React.js. In Dew, data flow and delay are explicit; while I wouldn’t expect to understand how React.js works.

It’s interesting to think that React.js turns to use fibers in newer versions, while it’s the default evaluation strategy for Dew programs.

Another popular GUI framework is the Elm language, which was first designed as a way to build web-based GUIs with functional reactive programming (FRP). However unfortunately, they later decided to abandon FRP after it was realized that streams (or signals in Elm’s terminology) does not help much. I suppose this is because the streams in Elm are much weaker in expressiveness than Dew.

By thinking how to do GUI in Dew, I learned a lot along the way:

  1. Channel is indeed a very powerful mechanism for flexible data flow! I only thought of a simpler variant similar to dchan originally, but now better variants are designed, making Dew channels even more powerful.

  2. I wasn’t sure whether early streams should be guaranteed to evaluate when they’ve just been created. Now I think it should, so that they have the property that they’ll always be evaluated when they are alive. Otherwise getret may fail to deliver the first feedback value.

  3. Polymorphic variants like those in OCaml are useful to represent feedback events.

  4. A string type can be useful for building GUIs.

For now, I don’t intend to build development UI for my game in Dew, because that would force me to expose more details of the engine to the scripting facility. But this could be interesting option to explore in the future!