Tutorial: Roguelike with effect handlers

Sorry about the late reply, I was busy actually verifying that my concept works out. Thankfully it does :smile:

The UI framework is inspired by Concur which means that every widget listens for some set of events and suspends computation until one of these events occurs. Once it does, it continues execution until it encounter the next await at which point it will suspend once more. Once a widget has fulfilled its purpose it terminates with some return value (e.g. text input is confirmed with enter → return with a string).
Complex UIs are then built by composing simpler widgets. A more detailed explanation can be found in the link above.

I’ve implemented this concept using an await function that takes a list of triggers and a handler for each possible event:

effect Await : Event.t list -> Event.t
let rec await triggers handler =
  handler (EffectHandlers.perform (Await triggers))

let rec check_box checked  =
  (* display check box *)
  ...;
  await [Mouse_press; Key_press] (function
  | Mouse_press ->
    print_endline "I've been (un-)checked!";
    check_box (not checked)
  | Key_press -> (* Terminate if any key is pressed *) checked)

Every widget can then be implemented as a function which displays the widget and performs an Await triggers which is resumed by passing an event from triggers, for example the check box above.

The most complex widget I’ve implemented so far is a single line text input. It can be clicked or selected with tab. Moving the mouse while holding the button down changes the selection. As an automaton:

image

Obviously, this is not a directed acyclic graph and therefore not a perfect fit for the implicit state stored in the continuation. Specifically, Pressed has an edge to one of its multiple parents. We can extract the Pressed state into its own function and therefore avoid this issue by ‘duplicating’ this state. Now Pressed no longer has multiple parents:

image

Some cycles remain and we can’t remove them because they are essential to the functionality. Instead we throw an exception Repeat that returns us to a parent node (explicitly shown for Focused → Pressed → Released → Focused). To do that we modify await:

let rec await triggers handler =
  try handler (EffectHandlers.perform (Await triggers)) with
  | Repeat -> await triggers handler

In the end this results in this main method for the text input, with only minor simplifications:

method execute =
  (* Represent the Pressed state.
     We await the Mouse_release and handle Mouse_motion while we wait. *)
  let pressed (x,_) =
    selection <- Point x;
    await [`Mouse_release; `Mouse_motion] @@ function
    | `Mouse_release (_, LMB) ->
      ()
    | `Mouse_motion (x,_) ->
      self#select x;
      raise Repeat (* This restarts the await function *)
    | _ ->
      raise Repeat
  in

  (* We start in the Unfocused state *)
  begin
    await [`Mouse_press; `Key_press] @@ function
    | `Mouse_press (pos, LMB) ->
       (* We have registered the press, but only when it is released
          will we be focused. *)
       pressed pos
    | `Key_press Tab ->
      selection <- Area (0, List.length keys)
    | _ -> raise Repeat
  end;

  (* We move into the Focused state *)
  begin
    await [`Codepoint; `Key_press; `Mouse_press] @@ function
    | `Key_press Tab | `Key_press Return ->
      () (* The only path without raising Repeat.
            Therefore we only leave this await when a tab or return occurs *)
    | `Mouse_press (pos, LMB) ->
      pressed pos;
      raise Repeat
    | `Key_press c ->
      self#insert c;
      raise Repeat
    | _ -> raise Repeat
  end;
  (* We have reached the finished state. We can now return the entered text. *)
  self#text

I think that this method captures the automaton above quite nicely and can be relatively easily understood (hopefully even when one is unfamiliar with the framework and accepts that some magic is happening in the background (: ). Implementing automatons in terms of effect handlers seems to work quite well, at least for games and UIs. What these automatons have in common is that they can be thought of as flows, starting at some state and ending at one of multiple final states and only have few edges that don’t fit this scheme, turning them into ‘directed almost acyclic graphs’.

There is obviously a lot more necessary for a UI framework (e.g. resizing the window/widgets, delegating the events to the correct widget, composing widgets, drawing on the screen etc.) and I plan to write about it at some point in the future. But for that I will first need to actually solve these problems as right now their implementation is quite barebones. The code can be found here for those interested (still very early in development!): GitHub - Willenbrink/bogue: GUI library for ocaml based on SDL2

4 Likes