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.

  1. An equivalent way to write this idiom is on <Event> => stop { react { ... } }. I’m considering introducing new syntax, transition { ... } for the stop { react { ... } } part, so you could write on <Event> => transition { ... }