# pointer

> `onPointerEnter` / `onPointerLeave` run an action when the pointer enters or leaves an element. They are the half of hover an app can ACT on — a `Hover` variant can only change how something looks, and cannot tell the app anything. Neither carries coordinates, and neither bubbles, which is what makes "am I over this one?" answerable.

<!-- id: ui-pointer · area: ui · stability: stable · html: https://osysharp.com/reference/ui/pointer/ -->

## Summary        {#summary}
`onPointerEnter` and `onPointerLeave` run an action when the pointer enters or leaves an element.

They exist because hover had only ever been a **look**. `inside Card.Hover { Bg = Accent; }` lowers to a `:hover`
rule, so it can restyle an element and cannot tell the app anything — which left *"am I over this drop target?"*
with no spelling at all.

## Signature      {#signature}
```osy syntax
// WHICH element — no coordinates.
Box(onPointerEnter: <action>, onPointerLeave: <action>)

// WHERE on the element — each action takes a `Point`.
Canvas(onPointerDown: <action>, onPointerMove: <action>, onPointerUp: <action>)

class Point { public double X; public double Y; }   // element-relative, in CSS pixels
```

## Description    {#description}
**Neither carries coordinates, and that is the point.** The element already knows the pointer is over it — that is
what a pointer event *is*. Answering "which lane am I over?" with coordinates plus hit-testing is a much larger
surface, and it is the wrong tool for a question the DOM has already answered.

**Neither bubbles.** They lower to `pointerenter`/`pointerleave`, the non-bubbling pair, deliberately: the bubbling
alternative fires on every ancestor of whatever the pointer is really over, so a lane containing a card would report
itself entered when the pointer merely crossed the card — and telling the two apart would need the coordinates back.

**They are not activations.** They never spin a control's busy affordance and never take part in the double-submit
guard. A pointer crossing an element is not a submission, and a drop target that greyed itself out when you hovered
it would be the worse failure.

**Reach for a `Hover` variant when only the LOOK changes.** It is CSS, so it costs no round trip and no re-render,
and it keeps working when the app is busy. Reach for these when the app must *know* — a drop target that has to
record which lane is active, a row that loads a preview, a chart that reports what is under the pointer.

## Where was the pointer? — element-local coordinates   {#coordinates}
`onPointerDown` / `onPointerMove` / `onPointerUp` each hand their action a **`Point`** — where the pointer is, in
**the element's own coordinates**. `(0, 0)` is the element's top-left corner, whatever the page has done around it.

That is the space every consumer actually wants: a canvas has no elements to hit-test, so *"where in THIS canvas"* is
the only question there is, and `Draw.Circle(at.X, at.Y, 4, ink)` needs no conversion. A viewport coordinate would be the
raw browser answer and useless alone — turning it into an element coordinate needs the element's own origin, which
would then have to be kept in sync with every scroll and reflow.

**All three phases, because a gesture has three.** A brush, a zoom or a lasso anchors on `down`, stretches on `move`
and commits on `up`. `onPointerMove` alone cannot express a drag at all: there is no way to learn when it started or
that it ended.

**`onPointerMove` is coalesced to one run per animation frame**, keeping the newest position. A pointer fires at
60-120Hz while a canvas redraws once a frame, so the extra runs are waste — and the alternative default, firing
everything and expecting a `debounce:`, makes the expensive case the one you get by forgetting. `down` and `up` are
discrete and always fire immediately: a coalesced press is a lost click.

> **Reach for enter/leave when the DOM already knows the answer.** *"Am I over this lane?"* needs no coordinates, and
> answering it by hit-testing a `Point` re-derives something the browser has already computed.

> **They fire for touch and pen too**, since they are pointer events rather than mouse events. On a touch screen
> "enter" arrives with the tap, so an interaction that is only reachable by hovering is unreachable there — give it
> a tap or keyboard path as well.

## Right-click and long-press — `onContextMenu`   {#context-menu}

`onContextMenu` runs an action on the **secondary** click — right-click with a mouse, the long-press menu on touch.

```osy syntax
Pressable(onClick: Reveal, onContextMenu: Flag) { Text(face); }
```

**It suppresses the browser's own menu, and that is the point rather than a convenience.** The native menu cannot be
prevented any other way, so an element that drew its own would get both, stacked. Binding the prop *is* the statement
that this element owns its secondary action — bind it only where you mean to replace the browser's.

Like `onClick`, the **innermost** binding wins: a right-click on a card inside a row runs the card's action and not
the row's. Unlike `onClick`, it is **not an activation** — it never spins a control's busy affordance, because
opening a menu is not a submission.

⚑ **There is no `onRightClick`, `onAuxClick`, `onLongPress`, `onMouseDown` or `onDoubleClick`.** One event covers
the gesture on both pointer and touch, and the pointer props above cover everything else — the platform has no
ambient for "which button is down", so `onPointerDown` cannot tell a left press from a right one.

## Examples       {#examples}
```osy title="a drop target that knows which lane the pointer is over" test app=ui-pointer-lanes
[Page("/lanes")]
[Render(CSR)]
[AllowAnonymous]
component Lanes() {
  var over = "none";

  action EnterTodo() { over = "todo"; }
  action EnterDone() { over = "done"; }
  action Clear() { over = "none"; }

  render {
    Stack(gap: 2) {
      Text($"over={over}");
      Box(p: 3, onPointerEnter: EnterTodo, onPointerLeave: Clear) { Text("To do"); }
      Box(p: 3, onPointerEnter: EnterDone, onPointerLeave: Clear) { Text("Done"); }
    }
  }
}
```

```osy title="a canvas that paints where the pointer is" test app=ui-pointer-canvas
[Page("/paint")]
[Render(CSR)]
[AllowAnonymous]
component Paint() {
  var drawing = false;

  action Start(Point at) { drawing = true; }
  action Stroke(Point at) { if (drawing) { Draw.Circle(at.X, at.Y, 4.0, "#3b82f6"); } }
  action Stop(Point at) { drawing = false; }

  render {
    Canvas(w: 400, h: 300, onPointerDown: Start, onPointerMove: Stroke, onPointerUp: Stop);
  }
}
```

## See also       {#see-also}
- [style props](https://osysharp.com/reference/ui/styling/) — `inside X.Hover { … }`, the CSS-only form to prefer when only the look changes
- [[testing-ui#hover]] — `Ui.Hover(control)` moves a real pointer under `osy test --pixels`, which is what actually
  fires `onPointerEnter`/`onPointerLeave` in a test; plain `osy test` locates but cannot move a pointer
- [keys](https://osysharp.com/reference/ui/keys/) — the keyboard counterpart: declaring the keys an element owns, and reading held state
- [drag](https://osysharp.com/reference/ui/drag/) — dragging an element, which is a separate binding and not built on these
- [component](https://osysharp.com/reference/ui/component/) — where an `action` is declared
