A board of binary switches with Brr_lwd

I’m trying to get the knacks of using Lwd by @let-def, and its backend Brr_lwd for building reactive web UIs. My first attempt at it was to create a board with a number of tiles. Each tile should reflect the state of a binary switch, and clicking on the tile should flip the switch.

I started from the only example in the repo and arrived to this. The idea is that each tile is a div whose class should reflect its current state:

open Brr
open Brr_lwd

type square = On | Off

let flip = function On -> Off | Off -> On

let class_of_state =
  function
  | On -> Jstr.v "square-on"
  | Off -> Jstr.v "square-off"

let lwd_table_row_map ~f row =
  Lwd_table.get row |> Option.iter (fun v -> Lwd_table.set row (f v))

let ui =
  let squares = Lwd_table.make () in
  let add_square () =
    let row = Lwd_table.append squares in
    Lwd_table.set row Off
  in
  for _ = 1 to 20 * 25 do
    add_square ()
  done;
  let board =
    Lwd_table.map_reduce
      (fun row state ->
         let sq =
           Elwd.div
            ~at:[ `P (At.class' (Jstr.v "square")); `P (At.class' (class_of_state state)) ] []
           in
         Lwd_seq.element @@ Lwd.map sq ~f:(fun el ->
           ignore (
             Ev.listen
               Ev.click
               (fun _ -> lwd_table_row_map row ~f:(fun state -> flip state))
               (El.as_target el)
           );
           el
         )
      )
      Lwd_seq.monoid
      squares
  in
  Elwd.div ~at:[ `P (At.class' (Jstr.v "game-board")) ] [
    `S (Lwd_seq.lift board)
  ]

let () =
  let ui = Lwd.observe ui in
  let on_invalidate _ =
    ignore @@ G.request_animation_frame @@ fun _ ->
      ignore @@ Lwd.quick_sample ui
  in
  let on_load _ =
    El.append_children (Document.body G.document) [ Lwd.quick_sample ui ];
    Lwd.set_on_invalidate ui on_invalidate
  in
  ignore @@ Ev.listen Ev.dom_content_loaded on_load (Window.as_target G.window)

With the suitable HTML page and CSS properties, I do get a board of tiles that switch colors when clicked.

The problem is that this is done by replacing the whole tile element: if I use the web inspector to put some content in the <div> and then click on it, the content disappears. I would like the attribute only to change and the contents of the <div> to remain, but I haven’t achieved it.

Maybe one way to do it would be to use `R instead of `P when specifying the class attribute in

`P (At.class' (class_of_state state))

but for that, state would have to be of type square Lwd.t rather than square, and I don’t know how to obtain an Lwd.t at this point of the code. Merely using Lwd.return doesn’t work.

While I’m at newbie questions, I wonder what Lwd.quick_sample does, how it is different from Lwd.sample, and why, unlike Lwd.sample, it does not seem to require a later call to Lwd.release.

3 Likes

I added an example with a potential fix: https://github.com/let-def/lwd/blob/master/examples/cssclasstest-brr/main.ml
I did not take the time to add proper html and css, feel free to submit an update to the example if you have improvements to share.

First, I fixed a bug and improved the Brr_lwd api to deal with event handlers.
Then I made one main change to your code to put the “reactive computation” at the intended level. We can discuss other solutions though if you aren’t happy with this one.

  1. Bug fix: support for multiple classes.
    The class attribute needs special handling: most attributes take a single value, but a DOM element accepts a set of css class. I wrote code to properly (and reactively) update this set rather than allowing only a single class.
    This is somewhat related to Allow multiple classes in At.class' by bclement-ocp · Pull Request #53 · dbuenzli/brr · GitHub .

  2. Enhancement : support declarative specification of event handlers.
    In your example, the event handler is installed by mapping over the reactive value. Because of the bridging between the imperative Dom and the reactive computation, it caused the event to be installed again after every updates, causing an ever increasing list of event handlers to be associated to the Dom element.
    This was a limitation of my bridge to Brr. I changed the way events are declared. Now every constructor takes a specification of event handlers:

Elwd.div ~at:[...] ~ev:[`P (Elwd.handler Ev.click (fun _ -> ...)]
  1. For the problem with the DOM element being recreated again and again, it has to do with the granularity of “incremental changes”. In short, I changed the code to store the state of a square in an Lwd.var. Now the table has type square Lwd.var Lwd_table.t. Flipping a square changes the variable but not the table.
    This allows to split the reaction in two levels:
  • DOM elements are associated to table cells, they are recreated only when a cell changes
  • the status of an element is associated to its variable: when the variable changes, the contents of the DOM element is updated.
    Variables and table cells are two “units” of incremental change. In your case, the status of a square can changes independently so it is worth having a separate variable for each square. On the other hand, if the table is fixed (the width, height and DOM elements associated to each cell are constants), then the Lwd_table can be skipped entirely: an array will be enough.

I hope that helps.

3 Likes

Thank you very much. I had arrived to something similar to what you suggest, since I use an array of square Lwd.var. It worked better, but in did hit the bug of multiple classes not working properly. I confirm it works better with your fix.

It will be interesting to see how hard it is to make a non-trivial UI with Lwd. Since Lwd is designed to reflect a changing “document”, I assume it won’t necessarily be beneficial to a game, especially if one day I want to add animated transitions—I have the intuition that those should probably be handled outside of Lwd for performance, but correct me if I’m wrong. I see Lwd as the connection between the M and the V in an MVC-architectured code.

I think you are right about the connection between M and V.
Using Lwd, you can expose an interface that represents a custom syntax with a built-in ability to handle values that can be updated.
The user builds its model using that interface,the implementation translates the syntax to views.
I haven’t found a nice way to handle the C, yet plain functions are working well enough so far.

On animations: I used Lwd to do some “intensive” interactive computations and Lwd was far from being a bottleneck (at least compared to rasterization on a laptop).
These were just toys, not real projects, so I can only speculate about the performance in practice.
But in general an Lwd update is very cheap and, in game dev parlance, it is equivalent to traversing a branch of the scene graph. Therefore I suspect that the overhead is comparable to the cost of maintaining a traditional scene graph.
That being said, you are right that animations can always be handled more efficiently outside of this graph. If you have a declarative notion of animation, like CSS transitions, you can use Lwd to tag the nodes of the scene graph with the intended motion and handle the animation in an outside rendering loop.

1 Like

And to answer the question about quick_sample vs sample, it has to do with resource management.

When your Lwd graph manipulates potentially expensive external resources (sockets, database connections, GPU buffers, …), you can provide hooks to be called when a resource is acquired (bound to a graph) and released (no longer reachable).
Contrary to the GC, Lwd resource management is predictable: a resource is released at the first sample where it is unreachable (using a reference counting scheme).

quick_sample is the simplest way to manage resources: it samples the graph and releases all resources immediately.
sample is a lower-level API that can be used to separate sampling from resource release. It is less relevant in a web environment where everything is managed by the javascript GC, but in native code it is convenient to efficiently interface with other runtimes (GTK widgets, OpenGL buffers, etc).

Thank you for your detailed answers, it is a bit clearer now.