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
| Handler | Event class | Elements |
|---|---|---|
onClick, onDblClick, onContextMenu | GpuiMouseEvent | all |
onMouseDown, onMouseUp, onMouseMove | GpuiMouseEvent | all |
onMouseEnter, onMouseLeave | GpuiMouseEvent | all |
onMouseDownOutside | GpuiMouseEvent | all |
onKeyDown, onKeyUp | GpuiKeyboardEvent | all |
onFocus, onBlur | GpuiFocusEvent | all |
onScroll | GpuiScrollEvent | all |
onWheel | GpuiWheelEvent | all |
onTransitionRun, onTransitionStart, onTransitionEnd, onTransitionCancel | GpuiTransitionEvent | all |
onInput, onChange | GpuiInputEvent | input, textarea |
onLoad, onError | GpuiImageEvent | img |
All event classes are exported from retend-gpui for use in type annotations.
Event Properties
| Event class | Properties |
|---|---|
GpuiMouseEvent | clientX, clientY, button, buttons, detail, altKey, ctrlKey, metaKey, shiftKey |
GpuiKeyboardEvent | key, keyChar, repeat, altKey, ctrlKey, metaKey, shiftKey |
GpuiInputEvent | value |
GpuiScrollEvent | scrollX, scrollY |
GpuiWheelEvent | deltaX, deltaY, deltaMode, clientX, clientY, altKey, ctrlKey, metaKey, shiftKey, touchPhase |
GpuiTransitionEvent | propertyName, elapsedTime |
GpuiFocusEvent | None beyond the base event |
GpuiImageEvent | None 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:
mouseenterandmouseleavefocusandblurscrollloadanderrormousedownoutside
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():
| Event | Event class | Properties |
|---|---|---|
resize | GpuiResizeEvent | width, height |
systempreferences | GpuiSystemPreferencesEvent | theme, accentColor |
focus, blur | Event | None |
reload | Event | None |
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>
| Modifier | Effect |
|---|---|
--stop | Stops the event from propagating further. |
--prevent | Calls preventDefault() before the handler runs. |
--self | Runs the handler only when the element itself is the target. |
--once | Removes the handler after its first call. |
--passive | Marks 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>; }