sharkdp / purescript-flare

A special-purpose UI library for Purescript
287 stars 17 forks source link

Support situations where the state depends on input and vice versa #33

Closed KovaxG closed 4 years ago

KovaxG commented 5 years ago

I have been browsing around for creating user interfaces in purescript, and I think I like this library the most because of its simplicity, good job!

I have run into a problem that I can't resolve on my own. I have a situation in which I have a past dependent state that is modified by the input. That is no problem except the state also influences what kinds of controls appear on the screen. That is the tricky part.

For a concrete example, I want to make a program that visualizes a JSON file, by drawing shapes, and the shapes can be dragged around and stuff. This already works, I have used the signals from purescript-signal. However, I can't really do what I want.

The only way I can think of doing it is by creating a channel for the state, subscribing and creating a signal, and combining it with the input signal, and using innerFlare to hide and make elements appear. I think this would work, but I would need to perform effects inside the foldp function. I have found a function that would allow me to do what I want in Signal.Effect. It is called foldEffect, and it does exactly what I want.

I could implement it, but I need to deconstruct the UI and Flare types. The implementation would be something like

foldEffect :: forall a b. (a -> b -> Effect b) -> b -> UI a -> UI b
foldEffect f b (UI eff) = lift $ do
    (Flare _ signal) <- eff
    SE.foldEffect f b signal

Will you accept a pull request with this new feature?

If you know of a simpler way of doing what I want, please let me know. For a simpler example of my problem, consider the following simplified use case:

You have a button that says "INC" and a number. If you press "INC" the number will increase. When the number hits 10, a new button will appear that says "RESET". If you press it the number will reset to 0, and the "RESET" button will disappear.

Thanks

KovaxG commented 4 years ago

Here is the implementation for the use case the I mentioned. You have a counter and a button labelled Inc. By pressing it, you increase the counter. If the counter hits 10, the inc button is replaced by a Reset button. If you press it you are back at the begining.

Here is some framework stuff, that allows you to implement this problem, and you can implement the relevant functions to make our examples, but you can modify the input functions in any way to perform pretty much anything you might want.

framework :: forall s i r. Eq s
          => s
          -> (s -> UI i)
          -> (i -> s -> s)
          -> (s -> r)
          -> Effect (UI r)
framework s0 modIn update render = do
  stateChannel <- channel initialState
  pure $ map render
       $ foldEffect (\i -> (\s -> send stateChannel s $> s) <<< update i) s0
       $ innerFlare (wrap $ dropRepeats $ subscribe stateChannel) modIn

Basically I defined some of the functions in order to get the behavior I want:

type Input = Int
type State = Int
type Result = String

initialState :: State
initialState = 0

myModifyInput :: State -> UI Input
myModifyInput s
  | s < 10 = button "Increase" 0 1
  | otherwise = button "Reset" 0 (-s)

myUpdate :: Input -> State -> State
myUpdate input state = input + state

myRender :: State -> Result
myRender s = show s

And finally, here is how it is used:

main :: Effect Unit
main = do
  ui <- framework initialState myModifyInput myUpdate myRender
  runFlareShow "controls" "output" ui

I think it's nice, and could be useful for people. At least it is useful for me :D