Widgets
Perry provides native widgets that map to each platform’s native controls.
Every example on this page is a real runnable program verified by CI
(scripts/run_doc_tests.sh) — the snippet you read is the same source that’s
compiled and launched.
The widget API is free functions, not methods. A widget is a 64-bit
opaque handle; you pass it into helpers like textSetFontSize(widget, 18)
rather than calling widget.setFontSize(18). That’s the only shape perry/ui
supports — no fluent chain, no prototype methods.
Text
Displays read-only text.
// demonstrates: Text widget styling with the real free-function API
// docs: docs/src/ui/widgets.md
// platforms: macos, linux, windows
// targets: ios-simulator, web, wasm
import { App, VStack, Text, textSetFontSize, textSetFontWeight, textSetColor, textSetFontFamily } from "perry/ui"
const label = Text("Hello, World!")
textSetFontSize(label, 18)
textSetColor(label, 0.2, 0.2, 0.2, 1.0) // RGBA in [0, 1]
textSetFontFamily(label, "Menlo")
const bold = Text("Bold")
textSetFontWeight(bold, 20, 1.0)
App({
title: "Text",
width: 400,
height: 200,
body: VStack(8, [label, bold]),
})
Color is RGBA with each channel in [0.0, 1.0] — divide a hex byte by 255
(0x33 / 255 ≈ 0.2).
Helpers: textSetString, textSetFontSize, textSetFontWeight,
textSetFontFamily, textSetColor, textSetWraps, textSetSelectable.
Text widgets inside template literals with state.value update automatically
— perry detects the state read and rewires the widget to re-render on change.
See State Management.
Button
A clickable button.
// demonstrates: Button styling with buttonSet*/widgetSet* helpers
// docs: docs/src/ui/widgets.md
// platforms: macos, linux, windows
// targets: ios-simulator, web, wasm
import {
App,
VStack,
Button,
buttonSetBordered,
widgetSetEnabled,
setCornerRadius,
} from "perry/ui"
// Note: buttonSetContentTintColor is macOS/iOS-only (maps to NSButton /
// UIButton tint). GTK4/Win32 don't have an equivalent — set
// widgetSetBackgroundColor(btn, r, g, b, a) there instead.
const primary = Button("Click Me", () => console.log("Clicked!"))
buttonSetBordered(primary, 1)
setCornerRadius(primary, 8)
const disabled = Button("Can't click me", () => {})
widgetSetEnabled(disabled, 0)
App({
title: "Button",
width: 400,
height: 200,
body: VStack(12, [primary, disabled]),
})
Helpers: buttonSetTitle, buttonSetBordered, buttonSetImage
(SF Symbol name on macOS/iOS), buttonSetImagePosition,
buttonSetContentTintColor, buttonSetTextColor, widgetSetEnabled.
TextField
An editable single-line text input.
// demonstrates: TextField + two-way binding via stateBindTextfield
// docs: docs/src/ui/widgets.md
// platforms: macos, linux, windows
// targets: ios-simulator, web, wasm
import { App, VStack, Text, TextField, State, stateBindTextfield } from "perry/ui"
const text = State("")
const field = TextField("Placeholder...", (value: string) => text.set(value))
stateBindTextfield(text, field) // programmatic text.set() also updates the field
App({
title: "TextField",
width: 400,
height: 200,
body: VStack(12, [
field,
Text(`You typed: ${text.value}`),
]),
})
TextField(placeholder, onChange) fires onChange as the user types. Pair
with stateBindTextfield(state, field) for two-way binding so programmatic
state.set(…) also updates the visible text.
Helpers: textfieldSetString, textfieldSetFontSize,
textfieldSetTextColor, textfieldSetBackgroundColor,
textfieldSetBorderless, textfieldSetOnSubmit, textfieldSetOnFocus,
textfieldSetNextKeyView.
SecureField
A password input — identical signature to TextField, but text is masked.
// demonstrates: SecureField for password input
// docs: docs/src/ui/widgets.md
// platforms: macos, linux, windows
// targets: ios-simulator, web, wasm
import { App, VStack, SecureField, State } from "perry/ui"
const password = State("")
App({
title: "SecureField",
width: 400,
height: 200,
body: VStack(12, [
SecureField("Enter password...", (value: string) => password.set(value)),
]),
})
Toggle
A boolean on/off switch.
// demonstrates: Toggle widget bound to State
// docs: docs/src/ui/widgets.md
// platforms: macos, linux, windows
// targets: ios-simulator, web, wasm
import { App, VStack, Text, Toggle, State } from "perry/ui"
const enabled = State(false)
App({
title: "Toggle",
width: 400,
height: 200,
body: VStack(12, [
Toggle("Enable notifications", (on: boolean) => enabled.set(on)),
Text(`Enabled: ${enabled.value}`),
]),
})
Slider
A numeric slider.
// demonstrates: Slider with a numeric range
// docs: docs/src/ui/widgets.md
// platforms: macos, linux, windows
// targets: ios-simulator, web, wasm
import { App, VStack, Text, Slider, State } from "perry/ui"
const value = State(50)
App({
title: "Slider",
width: 400,
height: 200,
body: VStack(12, [
Slider(0, 100, (v: number) => value.set(v)),
Text(`Value: ${value.value}`),
]),
})
Slider(min, max, onChange) — onChange fires on every drag. Use
stateBindSlider(state, slider) for two-way binding.
Picker
A dropdown selection control. Items are added with pickerAddItem.
// demonstrates: Picker with items added via pickerAddItem
// docs: docs/src/ui/widgets.md
// platforms: macos, linux, windows
// targets: ios-simulator, web, wasm
import { App, VStack, Text, Picker, State, pickerAddItem } from "perry/ui"
const selected = State(0)
const picker = Picker((index: number) => selected.set(index))
pickerAddItem(picker, "Option A")
pickerAddItem(picker, "Option B")
pickerAddItem(picker, "Option C")
App({
title: "Picker",
width: 400,
height: 200,
body: VStack(12, [
picker,
Text(`Selected index: ${selected.value}`),
]),
})
ImageFile / ImageSymbol
Two distinct constructors:
ImageFile(path)— image from a file pathImageSymbol(name)— SF Symbol glyph name (macOS/iOS only)
// demonstrates: ImageSymbol for SF Symbol glyphs (macOS/iOS)
// docs: docs/src/ui/widgets.md
// platforms: macos
// targets: ios-simulator, visionos-simulator, tvos-simulator
import { App, HStack, ImageSymbol, widgetSetWidth, widgetSetHeight } from "perry/ui"
const star = ImageSymbol("star.fill")
widgetSetWidth(star, 32)
widgetSetHeight(star, 32)
const heart = ImageSymbol("heart.fill")
const bell = ImageSymbol("bell.fill")
App({
title: "ImageSymbol",
width: 400,
height: 200,
body: HStack(12, [star, heart, bell]),
})
Use widgetSetWidth(img, N) / widgetSetHeight(img, N) to size the image.
ProgressView
An indeterminate or determinate progress indicator.
// demonstrates: ProgressView as an indeterminate spinner
// docs: docs/src/ui/widgets.md
// platforms: macos, linux, windows
// targets: ios-simulator, web, wasm
import { App, VStack, Text, ProgressView } from "perry/ui"
App({
title: "ProgressView",
width: 400,
height: 200,
body: VStack(12, [
Text("Loading..."),
ProgressView(),
]),
})
TextArea
A multi-line text input. Same (placeholder, onChange) signature as
TextField but renders as a multi-line box.
// demonstrates: TextArea for multi-line input
// docs: docs/src/ui/widgets.md
// platforms: macos, linux, windows
// targets: ios-simulator, web, wasm
import { App, VStack, Text, TextArea, State } from "perry/ui"
const content = State("")
App({
title: "TextArea",
width: 500,
height: 400,
body: VStack(12, [
TextArea("Enter multi-line text...", (value: string) => content.set(value)),
Text(`Length: ${content.value.length}`),
]),
})
Helpers: textareaSetString.
Sections
Group controls into labelled sections. Perry has no Form() widget — use a
VStack of Section(title)s and attach children via widgetAddChild.
// demonstrates: Section grouping with widgetAddChild (no Form widget in Perry)
// docs: docs/src/ui/widgets.md
// platforms: macos, linux, windows
// targets: ios-simulator, web, wasm
import {
App,
VStack,
Section,
TextField,
Toggle,
State,
widgetAddChild,
} from "perry/ui"
const name = State("")
const notifications = State(true)
const personal = Section("Personal Info")
widgetAddChild(personal, TextField("Name", (value: string) => name.set(value)))
const settings = Section("Settings")
widgetAddChild(
settings,
Toggle("Notifications", (on: boolean) => notifications.set(on)),
)
App({
title: "Sections",
width: 500,
height: 400,
body: VStack(16, [personal, settings]),
})
Platform-specific widgets
These exist only on specific platforms and aren’t verified by the cross-platform doc-tests:
Table(rows, cols, renderer)— macOS only. A data table with rows, columns, and a cell renderer.QRCode(data, size)— macOS only. Renders a QR code.Canvas(width, height, draw)— all desktop platforms. A drawing surface; see Canvas.CameraView()— iOS only (other platforms planned). See Camera.
These are linked from their own pages with platform-specific examples.
Common widget helpers
Every widget handle accepts these:
| Helper | Description |
|---|---|
widgetSetWidth(w, n) / widgetSetHeight(w, n) | Explicit size in points |
widgetSetBackgroundColor(w, r, g, b, a) | RGBA in [0, 1] |
setCornerRadius(w, r) | Rounded corners in points |
widgetSetOpacity(w, alpha) | Opacity in [0, 1] |
widgetSetEnabled(w, flag) | 0 disables, 1 enables |
widgetSetHidden(w, flag) | 0 visible, 1 hidden |
widgetSetTooltip(w, text) | Tooltip on hover (desktop only) |
widgetSetOnClick(w, cb) | Click handler |
widgetSetOnHover(w, cb) | Hover enter/leave (desktop only) |
widgetSetOnDoubleClick(w, cb) | Double-click handler |
widgetSetEdgeInsets(w, top, left, bottom, right) | Padding around contents |
widgetSetBorderColor(w, r, g, b, a) / widgetSetBorderWidth(w, n) | Border |
widgetAddChild(parent, child) | Attach a child to a container |
widgetSetContextMenu(w, menu) | Right-click menu |
See Styling and Events for deeper coverage.
Next Steps
- Layout — Arranging widgets with stacks and containers
- Styling — Colors, fonts, borders
- State Management — Reactive bindings