Another way to handle user input
Patrick Dubroy recently posted an interesting challenge problem:
The goal is to implement a square that you can either drag and drop, or click. The code should distinguish between the two gestures: a click shouldn’t just be treated as a drop with no drag. Finally, when you’re dragging, pressing escape should abort the drag and reset the object back to its original position.
In his post, he gives three solutions. A few days later, Francisco Sant’Anna posted an additional take on the problem using Céu. Later still, Francisco wrote another post comparing and contrasting various approaches to concurrency including Céu and Martin Sústrik’s idea of Structured Concurrency.
Since Syndicate is similar both to Structured Concurrency and to synchronous languages like Céu, Francisco’s post was enough for me to get around to trying to solve Patrick’s challenge problem using Syndicate.
Drag, Click or Cancel in Syndicate/js
I’ve put a complete project up here; you can clone it with
1
git clone https://git.syndicate-lang.org/tonyg/dubroy-user-input
You can try the program out by clicking here.
The interesting bit is the part that implements the main state machine, function awaitClick
,
so in the rest of this post I’ll look into that in some detail. To see how the whole program
hangs together, visit the index.ts
file in the git
repository.
Each state is represented as a Syndicate facet. We kick things off with
1
2
3
react {
awaitClick('Waiting');
}
which enters a fresh facet and sets up its behaviour using awaitClick
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
function awaitClick(status: string) {
assert Status(status);
on message Mouse('down', $dX: number, $dY: number) => {
if (inBounds(dX - x.value, dY - y.value)) {
stop {
react {
assert Status('Intermediate');
const [oX, oY] = [x.value, y.value];
stop on message Mouse('up', _, _) => react {
awaitClick('Clicked!');
}
stop on message Key('Escape') => react {
awaitClick('Cancelled!');
}
stop on message Mouse('move', $nX: number, $nY: number) => react {
assert Status('Dragging');
stop on message Mouse('up', _, _) => react {
awaitClick('Dragged!');
}
stop on message Key('Escape') => react {
[x.value, y.value] = [oX, oY];
awaitClick('Cancelled!');
}
[x.value, y.value] = [oX + nX - dX, oY + nY - dY];
on message Mouse('move', $nX: number, $nY: number) => {
[x.value, y.value] = [oX + nX - dX, oY + nY - dY];
}
}
}
}
}
}
}
When the mouse button is pressed (line 3), if the press is inside the square (line 4), we transition (lines 5–6) to a new state (lines 7–28).
This “intermediate” state is responsible for disambiguating clicks and drags. If we get a mouse up event, we transition back to waiting for a click (lines 9–11). If we get an Escape keypress, we do the same (lines 12–14). If we get some mouse motion, however, we transition (line 15) to a third state (lines 16–27).
This “drag” state tracks the mouse as it moves (lines 24–27). If the mouse button is released,
we go back to waiting for a click (lines 17–19). If Escape is pressed (lines 20–23), we
transition back to awaitClick
state after resetting the square’s position (line 21).
Syndicate dataflow variables hook up changes to x.value
and y.value
to the position of the
square (see here in the full
program),
and the Status(...)
assertions are reflected onto the screen by a little auxiliary actor
(here in the full
program).
The idiom
1
stop on <Event> => react { ... }
seen on lines 9, 12, 15, 17, and 20, acts as a state transition: the stop
keyword causes the
active facet to terminate when the event is received, and the react
statement after =>
enters a new state.1
And that’s it! Hopefully I’ve managed to convey the idea.
I considered a few variations on this implementation as I was writing it. Originally, I omitted lines 12–14 above, because I read the specification as requiring a response to Escape only within a drag, not the intermediate state before it’s known whether a press corresponds to a click or a drag. Another variation was less state-machine-like and more statechart-like, with Escape handling in an outer state and mouse handling in a pair of inner states, but it wasn’t quite as clear as the variation above.
-
An equivalent way to write this idiom is
on <Event> => stop { react { ... } }
. I’m considering introducing new syntax,transition { ... }
for thestop { react { ... } }
part, so you could writeon <Event> => transition { ... }
. ↩