Handling mutually recursive GUI widgets with reactive-banana
Asked Answered
M

1

8

I am hunting for a library to write a GUI on top of GLFW and OpenGL. I'm doing this because I am dissatisfied with the common UI library bindings which I feel are too imperative, and I would also like tight control of the look and feel of my UIs. I'd like a declarative approach to defining UI's. I am experimenting with reactive-banana (and temporarily reactive-banana-wx) to see if it meets my needs. I have a problem with defining recursive widgets. Here's my simplest test case:

  • A text widget that displays a counter.
  • A button widget that increments the counter.
  • A button widget that is inactive (so it is greyed out and does not respond to input at all) when the counter is 0 and otherwise active and resets the counter to 0.

The first and third widget have a recursive relationship. The first widget is intuitively a stepper of a union of events fed from the two buttons. However, the reset button is an fmap of the counter, and then the event stream relies on the reset button! What is to be done?

Beyond this question I have a concern about event handling: Since I want to handle device input and input focus within my code instead of relying on a framework, I see difficulties ahead in correctly dispatching events in a scalable way. Ideally I would define a data that encapsulates the hierarchical structure of a widget, a way to install event callbacks between the elements, and then write a function that traverses that data structure in order to define device input processing and graphical output. I am not sure how to take an event stream and split it as easily as event streams can be merged.

Mckinzie answered 4/11, 2012 at 1:41 Comment(0)
O
5

Recursion is allowed, as long as it is mutual recursion between a Behavior and an Event. The nice thing about Behaviors is that sampling them at the time of an update will return the old value.

For instance, your example can be expressed as follows

eClick1, eClick2 :: Event t ()

bCounter :: Behavior t Int
bCounter = accumB 0 $ mconcat [eIncrement, eReset]

eIncrement = (+1)      <$ eClick1
eReset     = (const 0) <$ whenE ((> 0) <$> bCounter) eClick2

See also the question "Can reactive-banana handle cycles in the network?"


As for your second question, you seem to be looking for the function filterE and its cousins filterApply and whenE?


As for your overall goal, I think it is quite ambitious. From what little experience I have gained so far, it seems to me that binding to an existing framework feels quite different from making an "clean-state" framework in FRP. Most likely, there are still some undiscovered (but exciting!) abstractions lurking there. I once started to write an application called BlackBoard that contains a nice abstraction about time-varying drawings.

However, if you care more about the result rather than the adventure, I would recommend a conservative approach: create the GUI toolkit in an imperative style and hook reactive-banana on top of that to get the benefits of FRP.

In case you just wish for any GUI, I am currently focussing on the web browser as a GUI. Here some preliminary experiments with Ji. The main benefit over wxHaskell is that it's a lot easier to get up and running and any API design efforts will benefit a very wide audience.

Obnubilate answered 4/11, 2012 at 11:33 Comment(6)
What if you are in the Event monad? I was concerned because it does not have MonadFix for value recursion, so that this same recursion could not be expressed in the presence of dynamic event switching. Granted I am still not entirely comfortable with all the public examples.Mckinzie
As for my second question: filterE et al. work in the wrong direction for scalability, I think. They pull from an event, and that seems cumbersome. Say, for example, we have a tree structure of gui widgets with a cursor for keyboard focus. We want to turn eDeviceInput events into events based on that cursor. With filterE, every recipient event stream would have to perform its own filter on the widget tree and also know where it is in the tree, making layout transformations and dynamic event switching cumbersome.Mckinzie
The Event type is not a monad. You probably mean the Moment monad? It's an instance of MonadFix as you would expect.Obnubilate
Well, since filterE is the only way to remove events from an event stream, you have to use it in one way or another. But you can add additional information and filter on that. For instance, you can tag each occurrence of eDeviceInput with the widget that you calculated from the tree and then have the individual widgets simply filter on the tag. This at least avoids the need to do an expensive tree calculation for each individual widget.Obnubilate
But you are right, the situation with filterE is similar to pattern matching vs pattern guards. I recommend to use it anyway for now. If it becomes a problem, I can add some sort of efficient "fanout" function, it's just that I have not found an elegant general API for this yet.Obnubilate
Ah ok. Event and moment are sort of synonyms in my head, so it explains why I mixed them up and couldn't find the monadfix instance :)Mckinzie

© 2022 - 2024 — McMap. All rights reserved.