PandaText / Docs / Editor API

Editor API

One editor view with the same jobs on iOS and macOS, Android, the web, Flutter and React Native. Pick your platform: the setup code and every call below follow it. Or compare them all side by side.

PandaTextView on macOS (NSView) and iOS (UIScrollView). A cell names the platform where the two differ.Kotlin members of PandaTextView.Members of PandaTextView from @pandatext/textview. A constructor option has the property’s name unless the cell says otherwise. Electron uses this column.A bare name is a PandaTextView parameter, state. is PandaTextViewState, and document. is PandaTextDocument.A bare name is a prop or event, and ref. is PandaTextViewHandle. Reads through ref. return Promises. Expo uses this column.Every platform side by side. “—” means the platform has no call for that job yet.

Get started

Install the package, then load your fonts, create a view and set the text. Package sources come with early access.

iOS and macOS

Requires: iOS 18, macOS 15, Xcode with Swift 6.3.

1 Install

In Xcode: File › Add Package Dependencies, paste the package URL, and add the PandaTextView product to your app target. Or in Package.swift:

dependencies: [
    .package(url: "<package URL from your early-access email>", from: "0.1.0"),
],
targets: [
    .target(name: "App", dependencies: [
        .product(name: "PandaTextView", package: "PandaTextView"),
    ]),
]

Add your font files (.ttf or .otf) to the app target. bundledTTF names them without the extension.

2 Create the view

The view for UIKit and AppKit, as a Swift package.

import PandaTextView

let fonts = try PandaTextFontCollection(fontFamilies: [
    [.bundledTTF("NotoSans-Regular"), .bundledTTF("NotoSans-Bold")],
])
let textView = PandaTextView(frame: .zero, fontCollection: fonts, pixelSize: 17)
textView.string = "Hello PandaText"

Android

Requires: Android 8 (API 26) or later; arm64-v8a and x86_64.

1 Install

Add the repository in settings.gradle.kts:

dependencyResolutionManagement {
    repositories {
        google()
        mavenCentral()
        maven { url = uri("<repository URL from your early-access email>") }
    }
}

Then the dependency in app/build.gradle.kts:

dependencies {
    implementation("dev.pandatext:PandaTextView:0.1.0")
}

Put your font files in src/main/assets and read their bytes with assets.open(…).readBytes().

2 Create the view

The view for Android, in Kotlin.

val font = Font(fontBytes)
val family = FontFamily(listOf(font))
val collection = FontCollection(listOf(family))

val textView = PandaTextView(context)
textView.fontCollection = collection
textView.text = "Hello PandaText"

Web

Requires: Chrome, Edge, Safari and Firefox. A bundler or any server that serves files over HTTP.

1 Install

Install the two packages:

npm install @pandatext/core @pandatext/textview

With Vite, keep them out of dependency pre-bundling, so the engine finds its .wasm file:

// vite.config.ts
export default defineConfig({
  optimizeDeps: { exclude: ["@pandatext/core", "@pandatext/textview"] },
});

The engine loads pandatext_core.wasm, and word dictionaries for Chinese, Japanese and Thai only when such text appears. A Content Security Policy needs 'wasm-unsafe-eval' in script-src.

2 Create the view

The view for any page in Chrome, Safari and Firefox. It fills its container, so give #editor a height. It paints no background: set one in CSS on the container if you need it.

import { createPandaText } from "@pandatext/core";
import { PandaTextView } from "@pandatext/textview";
import "@pandatext/textview/PandaTextView.css";

const textEngine = await createPandaText();
const font = textEngine.createFontFromBytes(fontBytes);
const fontCollection = textEngine.createCollectionFromFonts([font]);

const textView = new PandaTextView({ textEngine, fontCollection, text: "Hello PandaText" });
textView.mount(document.querySelector("#editor"));

React

Requires: The web requirements above. The React example uses React 19.

1 Install

Install the web packages. React needs no wrapper package:

npm install @pandatext/core @pandatext/textview

With Vite, exclude both from optimizeDeps as in the Web setup.

2 Create the view

The web view inside a React component. No wrapper package needed.

import { useEffect, useRef } from "react";
import { createPandaText } from "@pandatext/core";
import { PandaTextView } from "@pandatext/textview";
import "@pandatext/textview/PandaTextView.css";

export function Editor({ fontBytes }: { fontBytes: Uint8Array }) {
  const ref = useRef<HTMLDivElement>(null);
  useEffect(() => {
    let view: PandaTextView | undefined;
    let unmounted = false;
    createPandaText().then((textEngine) => {
      if (unmounted || !ref.current) return;
      const fontCollection = textEngine.createCollectionFromFonts([textEngine.createFontFromBytes(fontBytes)]);
      view = new PandaTextView({ textEngine, fontCollection, text: "Hello PandaText" });
      view.mount(ref.current);
    });
    return () => {
      unmounted = true;
      const fontCollection = view?.fontCollection;
      view?.destroy();
      fontCollection?.destroy();
    };
  }, [fontBytes]);
  return <div ref={ref} style={{ height: 400 }} />;
}

Requires: tldraw 5.

1 Install

Install the shape with the web packages:

npm install @pandatext/tldraw @pandatext/textview @pandatext/core tldraw

2 Create the view

A PandaText text shape for a tldraw 5 canvas.

import { Tldraw } from "tldraw";
import { PandaTextShapeUtil, loadPandaText } from "@pandatext/tldraw";
import "@pandatext/tldraw/PandaTextShape.css";

await loadPandaText();

<Tldraw
  shapeUtils={[PandaTextShapeUtil]}
  onMount={(editor) =>
    editor.createShape({ type: "panda-text", x: 100, y: 100, props: { text: "Hello PandaText" } })
  }
/>

Electron

Requires: Electron, and the web requirements above.

1 Install

Install the web packages in the renderer’s project:

npm install @pandatext/core @pandatext/textview

Serve the renderer from a custom scheme (protocol.handle) or a bundler, not file:, since the engine fetches its .wasm by URL. Add 'wasm-unsafe-eval' to the Content Security Policy.

In the main process, let the renderer use the clipboard:

const allowed = new Set(["clipboard-read", "clipboard-sanitized-write"]);
session.defaultSession.setPermissionRequestHandler((_contents, permission, callback) =>
  callback(allowed.has(permission)),
);
session.defaultSession.setPermissionCheckHandler((_contents, permission) => allowed.has(permission));

2 Create the view

In the renderer, create the view as on the web. Edit menu items call the view.

import { createPandaText } from "@pandatext/core";
import { PandaTextView } from "@pandatext/textview";
import "@pandatext/textview/PandaTextView.css";

const textEngine = await createPandaText();
const font = textEngine.createFontFromBytes(fontBytes);
const fontCollection = textEngine.createCollectionFromFonts([font]);

const textView = new PandaTextView({ textEngine, fontCollection, text: "Hello PandaText" });
textView.mount(document.querySelector("#editor"));

// Edit menu items (Undo, Redo, Select All) go through the view:
textView.performEditAction("undo");

Flutter

Requires: Flutter 3.41 or later (Dart 3.10); macOS 15, iOS 18, Android 8, Linux and the web.

1 Install

Add the package to pubspec.yaml, and your fonts as assets:

dependencies:
  pandatext:
    git:
      url: <repository URL from your early-access email>

flutter:
  assets:
    - fonts/NotoSans-Regular.ttf
    - fonts/NotoSans-Bold.ttf

Load the bytes with rootBundle.load(…) and pass them to PandaTextFont.fromBytes.

2 Create the view

The same editor as a Flutter widget.

import 'package:pandatext/pandatext.dart';

final fonts = PandaTextFontCollection([
  PandaTextFontFamily([PandaTextFont.fromBytes(regularBytes), PandaTextFont.fromBytes(boldBytes)]),
]);

PandaTextView(fonts: fonts, text: 'Hello PandaText', fontSize: 16)

React Native

Requires: React Native 0.82 or later (the New Architecture), iOS 18, Android API 26.

1 Install

Install the package and the iOS pods:

npm install @pandatext/react-native
cd ios && pod install

The package carries the iOS view, PandaText.xcframework and the Android AAR. Add your fonts to the app as usual and name them in fonts.

2 Create the view

One component over the native iOS and Android views.

import { PandaTextView } from "@pandatext/react-native";

<PandaTextView
  style={{ flex: 1 }}
  fonts={[["NotoSans-Regular"]]}
  text="Hello PandaText"
/>

Expo

Requires: An Expo development build (not Expo Go).

1 Install

Install the package:

npx expo install @pandatext/react-native expo-font

Add its config plugin, and your fonts, to app.json:

{
  "expo": {
    "plugins": [
      "@pandatext/react-native",
      ["expo-font", { "fonts": ["./fonts/NotoSans-Regular.ttf", "./fonts/NotoSans-Bold.ttf"] }]
    ]
  }
}

Then build the app with npx expo run:ios or npx expo run:android.

2 Create the view

The same React Native component, in your Expo app.

import { PandaTextView } from "@pandatext/react-native";

<PandaTextView
  style={{ flex: 1 }}
  fonts={[["NotoSans-Regular", "NotoSans-Bold"]]}
  text="Hello PandaText"
/>

Setup

JobAppleAndroidWebFlutterReact Native
CreatePandaTextView(frame:fontCollection:pixelSize:)PandaTextView(context)new PandaTextView({ textEngine, fontCollection })PandaTextView(fonts:, text:), PandaTextView(fonts:, document:)<PandaTextView fonts={...} />
Show and releaseaddSubview(_:)addView(view)mount(container), destroy()The widget treeThe component tree
FontsfontCollection (set at init, then read-only)fontCollectionfontCollectionfontsfonts
Base font sizepixelSizepixelSizefontSize (option fontSizeCss)fontSizefontSize

Text

JobAppleAndroidWebFlutterReact Native
Set the whole textstringtexttexttexttext
Read the whole textstringtexttextstate.textref.getText()
Replace a rangereplaceText(in:with:actionName:)replaceText(start, end, text)replaceText(start, end, replacement)state.replaceText(start, end, text)ref.replaceText(start, end, text)
Reach the documentdocument (read-only)document (read-only), attachDocument(document)document (read-only)state.document, document—
Read-only modeisEditableisEditableeditable (property only)editableeditable
Hear a user's changemacOS NSText.didChangeNotification, iOS UITextView.textDidChangeNotificationonTextDidChangeinput event on elementonTextDidChangeonTextDidChange
Read what changedmakeChangeCursor(), changes(since:capacity:advance:)createChangeCursor(), changesSince(cursor, capacity, advance)createChangeCursor(), changesSince(cursor, { capacity, advance })state.createChangeCursor(), state.changesSince(cursor, capacity:, advance:)places in the onTextDidChange payload

Selection and focus

JobAppleAndroidWebFlutterReact Native
Set the selectionselectedUTF16RangesetSelectionRange(start, end)setSelection(start, end, { anchor })state.setSelection(start, end)ref.setSelection(start, end)
Read the selectionselectedUTF16RangeselectionStartOffset, selectionEndOffsetorderedSelection()state.orderedSelection(), state.selection, state.selectedTextref.getSelection()
Hear a selection changemacOS NSTextView.didChangeSelectionNotification, iOS PandaTextView.selectionDidChangeNotificationonSelectionDidChangeselectiondidchange event on elementonSelectionDidChangeonSelectionChange
Take focusmacOS window.makeFirstResponder(view), iOS becomeFirstResponder()requestFocus()focusEditor()state.focus(), autofocus, focusNoderef.focus()
Give up focusmacOS window.makeFirstResponder(nil), iOS resignFirstResponder()clearFocus()—state.blur(), focusNoderef.blur()
Hear a focus changemacOS —, iOS UITextView.textDidBeginEditingNotification, UITextView.textDidEndEditingNotificationsetOnFocusChangeListener(listener)The DOM's focus eventsfocusNodeonFocus, onBlur

Undo

JobAppleAndroidWebFlutterReact Native
Undo and redomacOS undo(_:), redo(_:), iOS undoManager?.undo(), undoManager?.redo()undo(), redo()undo(), redo()state.undo(), state.redo()ref.undo(), ref.redo()
Ask what can be undoneundoManager?.canUndo, undoManager?.canRedocanUndo(), canRedo()canUndo(), canRedo()state.canUndo, state.canRedoref.getUndoState()
Hear the undo state change————onUndoStateChange
Set the undo limitundoLevelsundoLevelsundoLevelsundoLevelsundoLevels
End the merging of editsbreakUndoCoalescing()document.breakHistoryBurst()document.breakHistoryBurst()document.breakHistoryBurst()ref.breakUndoCoalescing()
Snapshot and restoresnapshotDocument(), restoreDocument(_:selection:actionName:)snapshotDocument(), restoreDocument(snapshot)snapshotDocument(), restoreDocument(snapshot, selection)state.snapshotDocument(), state.restoreDocument(snapshot, selection:)—

Clipboard and kill ring

JobAppleAndroidWebFlutterReact Native
Cut, copy, pastecut(_:), copy(_:), paste(_:)—performEditAction("cut"), performEditAction("copy"), performEditAction("paste")state.cutSelection(), state.copySelection(), state.paste()—
Select allselectAll(_:)—performEditAction("selectAll")state.selectAll()—
YankmacOS yank(_:), iOS ——yank()state.yank()—

Styles

JobAppleAndroidWebFlutterReact Native
Set all style runsstyleRunsstyleRunsstyleRunsstyleRunsstyleRuns
Style a rangeapplyStyle(in:fields:value:actionName:)applyStyle(start, end, mask, value)applyStyle(startOffset, endOffset, style)state.applyStyle(start, end, fieldMask, value)ref.applyStyle(start, end, style)
Toggle a styletoggleBold(), toggleItalic(), toggleUnderline(), toggleStrikethrough()toggleBold(), toggleItalic(), toggleUnderline(), toggleStrikethrough()toggleBold(), toggleItalic(), toggleUnderline(), toggleStrikethrough()state.toggleBold(), state.toggleItalic(), state.toggleUnderline(), state.toggleStrikethrough()ref.toggleBold(), ref.toggleItalic(), ref.toggleUnderline(), ref.toggleStrikethrough()
Set the typing stylesetTypingStyle(fields:value:)setTypingStyle(mask, value)setTypingStyle(style)state.setTypingStyle(fieldMask, value)ref.setTypingStyle(style)
Read a stylestyleAt(_:)styleAt(offset)styleAt(offset)state.styleAt(offset)ref.styleAt(offset)

Lists and line breaks

JobAppleAndroidWebFlutterReact Native
Set list markerslistMarkers, setListMarkers(_:actionName:)listMarkers, setListMarkers(markers)listMarkers, setListMarkers(markers)listMarkers, state.setListMarkers(markers), document.setBlockMarkers(blocks, markers)listMarkers, ref.setListMarkers(markers)
Renumber markerslistMarkerTextProviderlistMarkerTextProvidersetListMarkerTextProvider(provider) (option listMarkerTextProvider)listMarkerTextProvider—
Marker gapmarkerGapmarkerGapmarkerGapmarkerGapmarkerGap
Keep a range on one linesetNoBreakRanges(_:actionName:)setNoBreakRanges(ranges)setNoBreakRanges(ranges)state.setNoBreakRanges(ranges)ref.setNoBreakRanges(ranges)

Layout and appearance

These options have the same name on every target: alignment, baseDirection, justified, lineHeight, paragraphSpacing, paragraphSpacingBefore, breakStrategy, hyphenationFrequency, localeList, lineBreakRules, underlineInkSkip, underlineSkipSpaces.

JobAppleAndroidWebFlutterReact Native
InsetstextContainerInsettextContainerInsetinsetsinsetsinsets
Selection colormacOS selectionBackgroundColor, iOS tintColorselectionColorselectionColorselectionColorselectionColor
Lazy layout thresholdlazyLayoutThresholdUnitslazyLayoutThresholdUnitslazyLayoutThresholdUnitslazyLayoutThresholdUnitslazyLayoutThresholdUnits

Scrolling

JobAppleAndroidWebFlutterReact Native
Page up and downmacOS pageUp(_:), pageDown(_:), pageUpAndModifySelection(_:), pageDownAndModifySelection(_:), iOS pageUp(extendingSelection:), pageDown(extendingSelection:)pageUp(extendingSelection), pageDown(extendingSelection)pageUp({ extendingSelection }), pageDown({ extendingSelection })state.pageUp(extendingSelection:), state.pageDown(extendingSelection:)ref.pageUp(extendingSelection), ref.pageDown(extendingSelection)
Center the selectionmacOS centerSelectionInVisibleArea(_:), iOS centerSelectionInVisibleArea()centerSelectionInVisibleArea()centerSelectionInVisibleArea()state.centerSelectionInVisibleArea()ref.centerSelectionInVisibleArea()
Scroll positionmacOS enclosingScrollView, iOS contentOffsetscrollTo(x, y), scrollYcontentScrollTop, maxContentScrollTopstate.contentScrollTop, state.maxContentScrollTop, state.scrollTo(offset), controllerref.scrollTo(offset)
Turn scrolling offmacOS —, iOS isScrollEnabledisScrollEnabled—scrollEnabledscrollEnabled, autoGrow
Content sizemacOS intrinsicContentSize, iOS contentSizecontentHeight, onContentSizeDidChange—state.contentHeight, onContentSizeDidChangeref.getContentSize(), onContentSizeChange

Editing rules

JobAppleAndroidWebFlutterReact Native
Follow a native view's editingeditingRuleeditingRuleeditingRuleeditingRuleeditingRule
Touch long press—longPresslongPresslongPresslongPress (Android only)
Finger tap—touchTaptouchTaptouchTaptouchTap (Android only)
Left and Right in mixed textarrowMovementarrowMovementarrowMovementarrowMovementarrowMovement

Keyboard shortcuts

The views on iOS, Android, the web and Flutter share one key table, so a hardware keyboard edits the same way on each. On macOS the view follows AppKit’s key bindings.

CommandmacOS and iOSWindows, Linux, Android
Undo⌘ZCtrl+Z
Redo⌘⇧ZCtrl+Shift+Z
Cut⌘XCtrl+X
Copy⌘CCtrl+C
Paste⌘VCtrl+V
Select all⌘A
Bold⌘BCtrl+B
Italic⌘ICtrl+I
Underline⌘UCtrl+U
Strikethrough⌘⇧XCtrl+Shift+X