Events
Perry widgets support native event handlers for user interaction. Every snippet
below is excerpted from
docs/examples/ui/events/snippets.ts —
CI compiles and runs it on every PR, so the API drawn here is the API the
runtime exposes.
Event handlers are registered as free functions that take the widget handle
as the first argument. The widget handle itself is opaque (number at the
type level); perry’s API is function-first throughout.
onClick
const greet = Button("Click me", () => {
log.set("Button clicked")
})
// Or attach a click handler to a non-button widget after creation:
const label = Text("Clickable text")
widgetSetOnClick(label, () => {
log.set("Text clicked")
})
onHover
Triggered when the cursor enters the widget.
const hoverBtn = Button("Hover me", () => {})
widgetSetOnHover(hoverBtn, () => {
log.set("hovered")
})
Note: Hover events are available on macOS, Windows, Linux, and Web. iOS and Android use touch interactions instead. The callback fires once on enter; if you need a “left” event you’ll have to track it yourself.
onDoubleClick
const dbl = Text("Double-click me")
widgetSetOnDoubleClick(dbl, () => {
log.set("double-clicked!")
})
Keyboard Shortcuts
Register in-app keyboard shortcuts (active when the app is focused):
// Cmd+N on macOS, Ctrl+N on other platforms (modifier 1 = Cmd/Ctrl).
addKeyboardShortcut("n", 1, () => {
log.set("New document")
})
// Cmd+Shift+S — modifiers add: 1 (Cmd/Ctrl) + 2 (Shift) = 3.
addKeyboardShortcut("s", 3, () => {
log.set("Save as...")
})
Modifier bits: 1 = Cmd (macOS) / Ctrl (Windows/Linux), 2 = Shift, 4 =
Option (macOS) / Alt (others), 8 = Control (macOS only). Combine by adding
— 3 = Cmd+Shift, 5 = Cmd+Option, etc.
Keyboard shortcuts are also available on menu items:
const fileMenu = menuCreate()
menuAddItem(fileMenu, "New", () => log.set("file/new"))
menuAddItem(fileMenu, "Save As", () => log.set("file/saveAs"))
Global Hotkeys
Register a hotkey that fires system-wide, even when the app is in the background:
// System-wide: fires even when the app is in the background.
// macOS: real Carbon RegisterEventHotKey. Other platforms: no-op.
registerGlobalHotkey("F5", 0, () => {
log.set("Global F5 hotkey fired")
})
// Cmd+Shift+G (modifiers: 1=Cmd + 2=Shift = 3)
registerGlobalHotkey("g", 3, () => {
log.set("Global Cmd+Shift+G fired")
})
Platform support: macOS uses Carbon RegisterEventHotKey (real
implementation). Linux, Windows, iOS, tvOS, visionOS, watchOS, and Android
log the registration and no-op — global hotkeys on those platforms require
OS-level portal / hook APIs that vary per OS.
Clipboard
// Copy to clipboard
clipboardWrite("Hello, clipboard!")
// Read from clipboard
const text = clipboardRead()
log.set(`clipboard length: ${text.length}`)
Complete Example
// demonstrates: click + hover + double-click + keyboard shortcut all wired to
// a single State-backed status label
// docs: docs/src/ui/events.md
// platforms: macos, linux, windows
// targets: ios-simulator, web, wasm
import {
App,
Text,
Button,
VStack,
State,
Spacer,
addKeyboardShortcut,
widgetSetOnHover,
widgetSetOnDoubleClick,
} from "perry/ui"
const lastEvent = State("No events yet")
// Cmd+R (modifiers: 1 = Cmd/Ctrl).
addKeyboardShortcut("r", 1, () => {
lastEvent.set("Keyboard: Cmd+R")
})
const hoverBtn = Button("Hover me", () => {})
widgetSetOnHover(hoverBtn, () => {
lastEvent.set("Hover fired")
})
const dblLabel = Text("Double-click me")
widgetSetOnDoubleClick(dblLabel, () => {
lastEvent.set("Double-clicked!")
})
App({
title: "Events Demo",
width: 400,
height: 300,
body: VStack(16, [
Text(`Last event: ${lastEvent.value}`),
Spacer(),
Button("Click me", () => {
lastEvent.set("Button clicked")
}),
hoverBtn,
dblLabel,
]),
})
Next Steps
- Menus — Menu bar and context menus with keyboard shortcuts
- Widgets — All available widgets
- State Management — Reactive state