DEV Community

Devanshu Biswas
Devanshu Biswas

Posted on

Your Marquee Selection Has Four Bugs and All of Them Keep the Rectangle Inside the Canvas

Drag a box over a canvas of items, select what it touches. Four lines of geometry, and each one is a place implementations go wrong.

Try it, with the fuzz results computed live: https://dev48.infy.uk/design/day66-marquee-select.html

1. The band is anchor-to-pointer, not a path

const band = {
  x: Math.min(anchor.x, p.x),  y: Math.min(anchor.y, p.y),
  w: Math.abs(p.x - anchor.x), h: Math.abs(p.y - anchor.y)
};
Enter fullscreen mode Exit fullscreen mode

Grow the rectangle along the pointer path instead and a drag up-and-left produces negative width and selects nothing. The symptom looks like "it only works one way".

2. Overlap, not containment

const overlaps = (a, b) => !(a.x + a.w < b.x || b.x + b.w < a.x ||
                            a.y + a.h < b.y || b.y + b.h < a.y);
Enter fullscreen mode Exit fullscreen mode

Four disjointness checks, all of which must fail. The positive version needs eight comparisons and a case analysis, and the case analysis is where the bugs live.

3. Modifiers are set algebra over a snapshot

Shift adds, Alt subtracts, Ctrl toggles — over the selection as it was at pointerdown.

The property that separates a snapshot base from a live one is not "does not flicker" (both change as the band changes). It is path independence: with the base captured once, the selection is a pure function of (base, band), so wandering the pointer away and back returns you to exactly the same selection. A live base accumulates, and a Ctrl-drag ends somewhere else entirely.

4. Content coordinates, not viewport

const toContent = (e) => ({
  x: e.clientX - rect.left + el.scrollLeft,
  y: e.clientY - rect.top  + el.scrollTop
});
Enter fullscreen mode Exit fullscreen mode

Two additions. Identical to the wrong version until the container scrolls mid-drag — after a 300px scroll the two select 18 items versus 11. Which is why it never shows up in a demo where nothing scrolls.

The measurement, and why it is the interesting part

All four fuzzed over 40,000 random drags against two independently written oracles — an interval-form overlap test and a bounded grid probe:

variant disagreements rects that left the canvas
correct 0 0
path-based band 7,244 0
containment 9,767 0
unnormalised 7,244 0

That last column is the whole point. Every broken variant keeps the rectangle inside the canvas. Every visible constraint holds. Only the invisible one breaks — so they pass review, pass a smoke test, and ship.

The technique that makes this possible: none of the four functions touch the DOM. Anchor, pointer, band and modifier in; a set of ids out. 40,000 drags run in milliseconds under node with no browser at all.

Part of a from-scratch series — one component a day, vanilla JS, one file, offline: https://dev48.infy.uk/designfromzero.php

Top comments (0)