Francisco Sant'Anna

A Toy Problem: Drag, Click, or Cancel

@fsantanna

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 Céu and since I’m currently working on its upcoming version Atmos, I felt motivated to also post a solution to the problem. The solution in Atmos is similar to his, and uses the par-or and watching constructs to safely abort behaviors that did not complete. A small difference worth mentioning is relying on the deterministic scheduling semantics of Atmos to eliminate a state variable (didDrag). Here’s the solution with an accompanying video:

;; skip some SDL initialization

;; rectangle to control and text to display

var rect = @{ x=108,y=108, w=40,h=40 }
var text = " "

;; task to redraw the rectangle in the current position
spawn {
    every :sdl.draw {
        REN::setDrawColor(0x000000)
        REN::clear()
        REN::setDrawColor(0xFFFFFF)
        REN::fillRect(rect)
        sdl.write(FNT, text, @{x=256/2, y=200})
        REN::present()
    }
}

;; outer loop restarts after each behavior is detected
loop {
    ;; 1. detects first click on the rectangle
    val click = await(SDL.event.MouseButtonDown, \{point_vs_rect(it, rect)})
    val orig = @{x=rect.x, y=rect.y, w=rect.w, h=rect.h}
    set text = "... clicking ..."

    ;; 2. either cancel, drag/drop, or click
    par_or {
        ;; cancel: restores the original position on key :Escape
        await(SDL.event.KeyDown, :Escape)
        set rect = orig
        set text = "!!! CANCELLED !!!"
    } with {
        par_or {
            ;; drag/drop task: must be before click (see below)
            await(SDL.event.MouseMotion)
            set text = "... dragging ..."
            await(SDL.event.MouseButtonUp)
            set text = "!!! DRAGGED !!!"
        } with {
            ;; tracks mouse motion to move the rectangle
            every SDL.event.MouseMotion \{
                set rect.x = orig.x + (it.x - click.x)
                set rect.y = orig.y + (it.y - click.y)
            }
        }
    } with {
        ;; click task: must be the last
        ;; otherwise conflicts with motion termination
        await(SDL.event.MouseButtonUp)
        set text = "!!! CLICKED !!!"
    }
}

Comment on @fsantanna.