haskellfunctional-programmingfrpthreepenny-gui

Threenpenny gui - capturing mouse coordinates on click and using them to construct some state


What I'd like to achieve:

Each time the user clicks a canvas, take the mouse coordinates from that click to construct a Point x y and store this state in a [Point] such that at a later point in time when the user clicks a button I can use that [Point] as input for some function.

What I've done:

I have defined a Point data type, with a single value constructor like so:

data Point = Point {
  x :: Int,
  y :: Int
} deriving (Show, Eq)

I have setup a threepenny UI (monad?) to define the user interface, a simple 400 x 400 canvas and a button:

import qualified Graphics.UI.Threepenny as UI
import Graphics.UI.Threepenny.Core
import Control.Monad

canvasSize = 400

setup :: Window -> UI ()
setup window = do
  return window # set title "Haskell GUI"

  canvas <- UI.canvas
    # set UI.height canvasSize
    # set UI.width canvasSize
    # set style [("border", "solid black 1px"), ("background", "#eee")]

  button <- UI.button #+ [string "Do stuff"]

  getBody window #+
    [
    column [element canvas],
    element canvas,
    element button
    ]

  on UI.mousedown canvas $ \(x, y) -> do
    -- Need to create a point x y and add it to a list here

  on UI.click button $ const $ do
    -- Need to get the list of points here

  return ()

And then defined the function to run the UI within main:

runGui :: IO ()
runGui = startGUI defaultConfig setup

So, initially I was working on drawing points at the place where the user clicked. I achieved this fairly easily, by constructing a Point x y in the lambda argument to mousedown and drawing it to the canvas there. I am omitting that code as I have solved that and I don't believe my current problem relates to that (i.e. constructing and drawing a point in the scope of that lambda).

Instead of drawing and then throwing away the Point bound to the scope of that lambda, I would just like to store that point in a list. I would then like to be able to read that list when the user clicks the button.

I've done a bit of research into Behaviour and Event (http://hackage.haskell.org/package/threepenny-gui-0.4.2.0/docs/Reactive-Threepenny.html) for an FRP style, which I understand to be something which helps to create something like a redux pattern, but my brain is starting to melt.

Based on another StackOverflow post (Mixing Threepenny-Gui and StateT) I gather that I'm supposed to hook up to Threepenny UI events to create an event stream, then use accumB to accumulate each event from that stream into a stream of some state behaviour, then convert that state behaviour back into an event stream with apply and observe on that final stream to update the UI (simples, I think... xD)

At least that's what I gathered, I tested the code in the answer to the linked StackOverflow question and it did solve the problem posed in that particular question. But, I need to capture the x y position of the mouse on mousedown event stream (which wasn't covered in that snippet) and use it to construct a Point stream and that's where I am stuck. I tried implementing it based on modifying the accepted answers code to my purposes but ran into loads of type errors because I obviously misunderstand how the pieces fit together.

Here is my attempt at modifying the code in the accepted answer on the linked StackOverflow question:

-- This *should* be the bit that converts 
-- (x, y) click events to Point x y Event stream
let canvasClick = UI.mousedown canvas
    newPointStream = (\(x, y) -> Point x y) <$ (canvasClick)

-- This *should* be the bit that turns the 
-- Point x y Event stream into a "behaviour" stream
counter <- accumB (Point 0 0) newPointStream

Could anyone shed any light? I'm at a wall :-(


Solution

  • One of the nice things about threepenny-gui is that you don’t have to use FRP if you don’t want to. The simplest approach here would probably be to use the mutable references from Data.IORef:

    import Data.IORef
    
    setup window = do
      -- ...
    
      pointsRef <- liftIO (newIORef [] :: IO (IORef [Point]))
    
      on UI.mousedown canvas $ \(x, y) -> do
        liftIO $ modifyIORef' pointsRef ((Point x y) :)
    
      on UI.click button $ const $ do
        points <- liftIO $ readIORef pointsRef
        -- use the list of points here
    

    This creates a mutable list of points pointsRef and initialises it to [], then prepends a new point on every mousedown. When the button is clicked, the list of points is read.

    Another approach is to use FRP. let pointEv = (uncurry Point) <$> UI.mousedown canvas gives you an Event Point. Then you can do let pointPrependEv = fmap (\p -> \list -> p : list) pointEv to give an Event ([Point] -> [Point]). Next, use pointsB <- accumB [] pointsPrependEv to get a Behavior [Point] which stores the list of points at every time. Finally, use pointsB <@ UI.click button to get an Event [Point] for each button press. Now you have an event for every button press, with its value being the list of points at this time, so you can now run a computation on this event using register or any other function from threepenny-gui. The full program is:

    setup window = do
      -- ...
    
      let pointEv = (uncurry Point) <$> UI.mousedown canvas
          pointPrependEv = fmap (\p -> \list -> p : list) pointEv
      pointsB <- accumB [] pointPrependEv
      let buttonPressEv = pointsB <@ UI.click button
      -- use point list here via buttonPressEv
    

    EDIT: I just noticed that in your question, you figured out most of the above already. Your only error was in trying to do accumB [] newPointsStream. If you look at the documentation, the type is accumB :: MonadIO m => a -> Event (a -> a) -> m (Behavior a); note that this requires an Event (a -> a) rather than a simple Event a. Thus the original Event Point must be converted to an Event ([Point] -> [Point]) (which, for each new point, returns a function adding it to the input list of points) before it can be used in accumB.