Osy#the first language built for agents
Agents firstAgentic appsWorkflowsDurable Execution — built inSecurityTestingThe editorThe UI modelOne program

Reference / UI

pointer

Box(onPointerEnter: Highlight, onPointerLeave: Clear) Canvas(onPointerDown: Start, onPointerMove: Drag, onPointerUp: Commit) // each hands the action a Point

`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.

stable2 examples compiled by CIuieventspointer

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#

// 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#

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#

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#

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

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#

[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"); }
    }
  }
}
[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#

  • style propsinside X.Hover { … }, the CSS-only form to prefer when only the look changes
  • keys — the keyboard counterpart: declaring the keys an element owns, and reading held state
  • drag — dragging an element, which is a separate binding and not built on these
  • component — where an action is declared

Related

keys

`keys:` declares that an element owns a set of keys: it becomes focusable, those keys stop scrolling the page, and…

style props

Inside a `variants` block, each `Name = value` is a style prop from a fixed vocabulary the renderer maps to CSS — paint…

component

The one archetype for all UI: a bounded reactive unit — typed props, reactive members (fields, `live`…

drag

`drag:` binds a number to a drag gesture: grabbing the element moves the value, arrow keys move the same value, and…