Francisco Sant'Anna

A Toy Problem: Drag, Click, or Cancel

@_fsantanna

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

Currently, Céu lacks primitive types (e.g., strings and integers), and named fields, so some parts of the code might look strange. Anyways, here’s the complete solution with an accompanying video:

^"int.ceu"
^"pico.ceu"

-- rectangle to control
var rect: Rect = [[_10,_10],[_5,_5]]

-- tasks to behaviors: cancel, drag and drop, click
spawn {
    -- outer loop to restart after each behavior is detected
    loop {
        -- first detects the first click on the rectangle
        var dxy: Size   -- holds the offset to its center
        {
            var mouse: Point
            await evt?Mouse?Button?Down until isPointInsideRect [mouse,rect]
                where {
                    set mouse = evt!Mouse!Button!Down.pos
                }
            set dxy = [sub [rect.pos.x,mouse.x], sub [rect.pos.y,mouse.y]]
        }

        -- then either cancel, drag/drop, or click
        paror {
            -- cancel behavior: restores the original position on any key
            var orig = rect
            await evt?Key?Down until eq [evt!Key!Down.key,_SDLK_ESCAPE]
            set rect = orig
            output std _("Cancelled!"):_(char*)
        } with {
            -- drag/drop behavior: must be before click (see below)
            await evt?Mouse?Motion
            output std _("Dragging..."):_(char*)
            var mouse = evt!Mouse!Motion
            watching evt?Mouse?Button?Up {   -- terminates on mouse up
                -- tracks mouse motion to move the rectangle
                loop {
                    set rect = [pt, rect.size]
                        where {
                            var pt: Point = [add [mouse.pos.x,dxy.w], add [mouse.pos.y,dxy.h]]
                        }
                    await evt?Mouse?Motion
                    set mouse = evt!Mouse!Motion
                }
            }
            output std _("Dropped!"):_(char*)
        } with {
            -- behavior: must be the last
            -- otherwise conflicts w/ motion termination
            await evt?Mouse?Button?Up
            output std _("Clicked!"):_(char*)
        }
    }
}

-- task for redrawing
spawn {
    loop {
        await _1
        output pico Pico.Output.Clear
        output pico Pico.Output.Draw.Rect rect
        output pico Pico.Output.Present
    }
}

call pico_loop ()

Comment on @_fsantanna.