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 :-(
One of the nice things about
threepenny-guiis 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 fromData.IORef:This creates a mutable list of points
pointsRefand 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 canvasgives you anEvent Point. Then you can dolet pointPrependEv = fmap (\p -> \list -> p : list) pointEvto give anEvent ([Point] -> [Point]). Next, usepointsB <- accumB [] pointsPrependEvto get aBehavior [Point]which stores the list of points at every time. Finally, usepointsB <@ UI.click buttonto get anEvent [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 usingregisteror any other function fromthreepenny-gui. The full program is: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 isaccumB :: MonadIO m => a -> Event (a -> a) -> m (Behavior a); note that this requires anEvent (a -> a)rather than a simpleEvent a. Thus the originalEvent Pointmust be converted to anEvent ([Point] -> [Point])(which, for each new point, returns a function adding it to the input list of points) before it can be used inaccumB.