Last week Patrick Dubroy (@dubroy) proposed a toy problem to handle user input:
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.
He proposed solutions using three different implementation techniques: Event listeners, Polling, and Process-oriented. For the process-oriented approach he uses his Esterel-inspired Abro.js and argues that it provides “a clear, explicit sequencing between the different states”.
Since he mentions Ceu and since I’m currently working on its upcoming
version, I felt motivated to also post a solution to the problem.
The solution in Ceu is similar to his, and uses the par-or
and awaiting
constructs to safely abort behaviors that did not complete.
A small difference worth mentioning is relying on the deterministic scheduling
semantics of Ceu to eliminate a state variable (didDrag
).
Here’s the complete solution with an accompanying video:
^["@/pico/pico.ceu"] pico-state-set-title("pico-Ceu: Click, Drag, or Cancel") ;; outer task with nested tasks to redraw, cancel, drag and drop, and click spawn { ;; rectangle to control var rect :Rect = [[10,10],[5,5]] ;; task to redraw the rectangle in the current position spawn { every :Pico.Draw { pico-state-set-color-clear([0,0,0,255]) pico-output-draw-rect(rect) } } ;; outer loop restarts after each behavior is detected loop { ;; 1. detects first click on the rectangle await :Pico.Mouse.Button.Dn, pico-point-vs-rect?(evt.pos,rect) println("> clicking...") val orig :Rect = copy(rect) val click :XY = copy(evt.pos) ;; 2. either cancel, drag/drop, or click par-or { ;; cancel task: restores the original position on key ESC await :Pico.Key.Dn, (evt.key == :Key-Escape) set rect = copy(orig) println("<<< Cancelled!") } with { ;; drag/drop task: must be before click (see below) await :Pico.Mouse.Motion println("> dragging...") awaiting :Pico.Mouse.Button.Up { ;; terminates on mouse up ;; tracks mouse motion to move the rectangle loop { set rect.pos.x = orig.pos.x + (evt.pos.x - click.x) set rect.pos.y = orig.pos.y + (evt.pos.y - click.y) await :Pico.Mouse.Motion } } println("<<< Dragged!") } with { ;; click task: must be the last ;; otherwise conflicts with motion termination await :Pico.Mouse.Button.Up println("<<< Clicked!") } } } pico-loop()
Comment on
@_fsantanna.