Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

UI Overview

Perry’s perry/ui module lets you build native desktop and mobile apps with declarative TypeScript. Your UI code compiles directly to platform-native widgets — AppKit on macOS, UIKit on iOS, GTK4 on Linux, Win32 on Windows, and DOM elements on the web.

Quick Start

// demonstrates: the smallest complete Perry UI app
// docs: docs/src/ui/overview.md
// platforms: macos, linux, windows
// targets: ios-simulator, web, wasm

import { App, Text, VStack } from "perry/ui"

App({
    title: "My App",
    width: 400,
    height: 300,
    body: VStack(16, [
        Text("Hello from Perry!"),
    ]),
})
perry app.ts -o app && ./app

Mental Model

Perry’s UI follows the same model as SwiftUI and Flutter: you compose native widgets using stack-based layout containers (VStack, HStack, ZStack), control alignment and distribution, and style widgets via free functions that take the widget handle as their first argument (textSetColor(label, r, g, b, a), widgetSetEdgeInsets(stack, ...), etc.). If you’re coming from web development, the key shift is:

  • Layout is controlled by stack alignment, distribution, and spacers — not CSS properties. See Layout.
  • Styling is applied directly to widgets — not through stylesheets. See Styling.
  • Absolute positioning uses overlays (widgetAddOverlay + widgetSetOverlayFrame) — not position: absolute/relative.
  • Design tokens come from the perry-styling package. See Theming.

App Lifecycle

Every Perry UI app starts with App():

function runAppShell(): void {
    App({
        title: "Window Title",
        width: 800,
        height: 600,
        body: VStack(16, [
            Text("Content here"),
        ]),
    })
}

App({}) accepts a config object with the following properties:

PropertyTypeDescription
titlestringWindow title
widthnumberInitial window width
heightnumberInitial window height
bodywidgetRoot widget
iconstringApp icon file path (optional)
framelessbooleanRemove title bar (optional)
levelstringWindow z-order: "floating", "statusBar", "modal" (optional)
transparentbooleanTransparent background (optional)
vibrancystringNative blur material, e.g. "sidebar" (optional)
activationPolicystring"regular", "accessory" (no dock icon), "background" (optional)

See Multi-Window for full documentation on window properties.

Lifecycle Hooks

onActivate(() => {
    console.log("App became active")
})

onTerminate(() => {
    console.log("App is closing")
})

Widget Tree

Perry UIs are built as a tree of widgets:

function runWidgetTree(): void {
    App({
        title: "Layout Demo",
        width: 400,
        height: 300,
        body: VStack(16, [
            Text("Header"),
            HStack(8, [
                Button("Left", () => console.log("left")),
                Button("Right", () => console.log("right")),
            ]),
            Text("Footer"),
        ]),
    })
}

Widgets are created by calling their constructor functions. Layout containers (VStack, HStack, ZStack) accept a spacing value (in points) followed by an array of child widgets.

Handle-Based Architecture

Under the hood, each widget is a handle — a small integer that references a native platform object. When you call Text("hello"), Perry creates a native NSTextField (macOS), UILabel (iOS), GtkLabel (Linux), or <span> (web) and returns a handle you can use to modify it.

const label = Text("Hello")
textSetFontSize(label, 18)              // Modifies the native widget
textSetColor(label, 1.0, 0.0, 0.0, 1.0) // RGBA floats in [0,1]

Imports

All UI functions are imported from perry/ui:

import {
    // App lifecycle
    App, onActivate, onTerminate,

    // Widgets
    Text, Button, TextField, SecureField, TextArea,
    Toggle, Slider, ProgressView, Picker, ImageFile, ImageSymbol,

    // Layout
    VStack, HStack, ZStack, ScrollView, Spacer, Divider,
    NavStack, TabBar, LazyVStack, Section,
    VStackWithInsets, HStackWithInsets, SplitView, splitViewAddChild,

    // Layout control
    stackSetAlignment, stackSetDistribution, stackSetDetachesHidden,
    widgetMatchParentWidth, widgetMatchParentHeight, widgetSetHugging,
    widgetAddOverlay, widgetSetOverlayFrame,

    // State
    State, ForEach,

    // Dialogs
    openFileDialog, openFolderDialog, saveFileDialog,
    alert, alertWithButtons,
    sheetCreate, sheetPresent, sheetDismiss,

    // Menus
    menuCreate, menuAddItem, menuAddSeparator, menuAddSubmenu,
    menuBarCreate, menuBarAddMenu, menuBarAttach,
    widgetSetContextMenu,

    // Window
    Window,
} from "perry/ui"

Canvas, CameraView, and the virtualized Table widget are wired through the LLVM codegen (closed via #190, #191, and #192). See each widget’s page for the platform-support matrix.

Platform Differences

The same code runs on all platforms, but the look and feel matches each platform’s native style:

FeaturemacOSiOSLinuxWindowsWeb
ButtonsNSButtonUIButtonGtkButtonHWND Button<button>
TextNSTextFieldUILabelGtkLabelStatic HWND<span>
LayoutNSStackViewUIStackViewGtkBoxManual layoutFlexbox
MenusNSMenuGMenuHMENUDOM

Platform-specific behavior is noted on each widget’s documentation page.

Next Steps