GPUI Events

GPUI elements accept the same camel-case handler props as web Retend, such as onClick. Events come from the native window system, and Retend delivers them through the logical node tree.

<div
  onClick={(event) => console.log(event.clientX, event.clientY)}
  onMouseEnter={() => console.log('hovered')}
>
  Click me
</div>

Handler Props

HandlerEvent classElements
onClick, onDblClick, onContextMenuGpuiMouseEventall
onMouseDown, onMouseUp, onMouseMoveGpuiMouseEventall
onMouseEnter, onMouseLeaveGpuiMouseEventall
onMouseDownOutsideGpuiMouseEventall
onKeyDown, onKeyUpGpuiKeyboardEventall
onFocus, onBlurGpuiFocusEventall
onScrollGpuiScrollEventall
onWheelGpuiWheelEventall
onTransitionRun, onTransitionStart, onTransitionEnd, onTransitionCancelGpuiTransitionEventall
onInput, onChangeGpuiInputEventinput, textarea
onLoad, onErrorGpuiImageEventimg

All event classes are exported from retend-gpui for use in type annotations.

Event Properties

Event classProperties
GpuiMouseEventclientX, clientY, button, buttons, detail, altKey, ctrlKey, metaKey, shiftKey
GpuiKeyboardEventkey, keyChar, repeat, altKey, ctrlKey, metaKey, shiftKey
GpuiInputEventvalue
GpuiScrollEventscrollX, scrollY
GpuiWheelEventdeltaX, deltaY, deltaMode, clientX, clientY, altKey, ctrlKey, metaKey, shiftKey, touchPhase
GpuiTransitionEventpropertyName, elapsedTime
GpuiFocusEventNone beyond the base event
GpuiImageEventNone beyond the base event

Every event also has the standard type, target, currentTarget, timeStamp, preventDefault(), and stopPropagation().

Wheel events

onWheel receives raw wheel and trackpad input even when the element has no scrollable overflow. It captures and bubbles through the logical tree. onScroll reports changes to a scroll container's absolute offset instead.

deltaMode is 0 for pixels and 1 for lines. Positive deltaX and deltaY indicate scrolling right and down. clientX and clientY are window-relative coordinates.

touchPhase preserves GPUI's started, moved, ended, or cancelled phase. Availability depends on the platform and input device. GPUI does not expose a separate momentum flag.

import { Cell } from 'retend';
import type { GpuiWheelEvent } from 'retend-gpui';

function ZoomSurface() {
  const zoom = Cell.source(1);
  const handleWheel = (event: GpuiWheelEvent) => {
    const pixels = event.deltaY * (event.deltaMode === 1 ? 16 : 1);
    zoom.set(zoom.peek() * Math.exp(-pixels * 0.002));
  };

  return (
    <div onWheel={handleWheel} style={{ width: '100%', height: '100%' }}>
      Zoom: {zoom}
    </div>
  );
}

Wheel events are non-cancelable because native scrolling runs independently of JavaScript delivery. preventDefault() and onWheel--prevent do not suppress native scrolling. Use a surface without scrollable overflow for wheel-driven zoom or panning.

Keyboard events

key is the key identifier reported by GPUI. Letter keys are lowercase, for example a. keyChar is the character the key press produced, when there is one, such as A while Shift is held.

Logging event.key once reveals the identifier for keys such as arrows or Escape.

import type { GpuiKeyboardEvent } from 'retend-gpui';

function Editor() {
  const save = (event: GpuiKeyboardEvent) => {
    if ((event.metaKey || event.ctrlKey) && event.key === 's') {
      event.preventDefault();
      console.log('Saving...');
    }
  };

  return (
    <div onKeyDown={save} tabIndex={0} style={{ padding: 16 }}>
      Editor
    </div>
  );
}

Keyboard events are delivered to the focused element and bubble up from it. A plain div needs a tabIndex to be able to take focus.

Propagation

Retend runs capture, target, and bubble phases over the logical node tree, as in the browser. Most events bubble. These do not:

  • mouseenter and mouseleave
  • focus and blur
  • scroll
  • load and error
  • mousedownoutside

mousedownoutside is specific to retend-gpui. It fires on an element when the pointer is pressed anywhere outside it, and it is delivered to that element only. It is the natural way to close menus and popovers.

Host Events

The window host (useWindow().host) dispatches its own events outside the element tree. Listen with addEventListener():

EventEvent classProperties
resizeGpuiResizeEventwidth, height
systempreferencesGpuiSystemPreferencesEventtheme, accentColor
focus, blurEventNone
reloadEventNone
import { useWindow } from 'retend-gpui';
import type { GpuiSystemPreferencesEvent } from 'retend-gpui';

function Watcher() {
  const window = useWindow();

  window.host.addEventListener('systempreferences', (event) => {
    const preferences = event as GpuiSystemPreferencesEvent;
    console.log(preferences.theme, preferences.accentColor);
  });

  return <div>Watching…</div>;
}

systempreferences fires when the OS appearance or accent color changes. In most components prefer the reactive useSystem() Cells over manual listeners.

Event Modifiers

Modifier suffixes work on GPUI handlers. They are chained with --:

<div onClick--self--stop={handleClick}>Only clicks on this element</div>
ModifierEffect
--stopStops the event from propagating further.
--preventCalls preventDefault() before the handler runs.
--selfRuns the handler only when the element itself is the target.
--onceRemoves the handler after its first call.
--passiveMarks the listener as passive.

See Event Handling for how modifiers work in Retend.

Imperative Listeners

GPUI nodes also support addEventListener(), removeEventListener(), and dispatchEvent(). This is useful for custom events and for code that has an element reference:

import { Cell, onConnected } from 'retend';
import type { GpuiDivElement } from 'retend-gpui';

function Box() {
  const box = Cell.source<GpuiDivElement | null>(null);

  onConnected(box, (element) => {
    const log = () => console.log('clicked');
    element.addEventListener('click', log);
    return () => element.removeEventListener('click', log);
  });

  return <div ref={box}>Box</div>;
}