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-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
.