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.
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/textviewWith 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/textviewWith 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 tldraw2 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/textviewServe 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.ttfLoad 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 installThe 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-fontAdd 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
| Job | Apple | Android | Web | Flutter | React Native |
|---|---|---|---|---|---|
| Create | PandaTextView(frame:fontCollection:pixelSize:) | PandaTextView(context) | new PandaTextView({ textEngine, fontCollection }) | PandaTextView(fonts:, text:), PandaTextView(fonts:, document:) | <PandaTextView fonts={...} /> |
| Show and release | addSubview(_:) | addView(view) | mount(container), destroy() | The widget tree | The component tree |
| Fonts | fontCollection (set at init, then read-only) | fontCollection | fontCollection | fonts | fonts |
| Base font size | pixelSize | pixelSize | fontSize (option fontSizeCss) | fontSize | fontSize |
Text
| Job | Apple | Android | Web | Flutter | React Native |
|---|---|---|---|---|---|
| Set the whole text | string | text | text | text | text |
| Read the whole text | string | text | text | state.text | ref.getText() |
| Replace a range | replaceText(in:with:actionName:) | replaceText(start, end, text) | replaceText(start, end, replacement) | state.replaceText(start, end, text) | ref.replaceText(start, end, text) |
| Reach the document | document (read-only) | document (read-only), attachDocument(document) | document (read-only) | state.document, document | — |
| Read-only mode | isEditable | isEditable | editable (property only) | editable | editable |
| Hear a user's change | macOS NSText.didChangeNotification, iOS UITextView.textDidChangeNotification | onTextDidChange | input event on element | onTextDidChange | onTextDidChange |
| Read what changed | makeChangeCursor(), 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
| Job | Apple | Android | Web | Flutter | React Native |
|---|---|---|---|---|---|
| Set the selection | selectedUTF16Range | setSelectionRange(start, end) | setSelection(start, end, { anchor }) | state.setSelection(start, end) | ref.setSelection(start, end) |
| Read the selection | selectedUTF16Range | selectionStartOffset, selectionEndOffset | orderedSelection() | state.orderedSelection(), state.selection, state.selectedText | ref.getSelection() |
| Hear a selection change | macOS NSTextView.didChangeSelectionNotification, iOS PandaTextView.selectionDidChangeNotification | onSelectionDidChange | selectiondidchange event on element | onSelectionDidChange | onSelectionChange |
| Take focus | macOS window.makeFirstResponder(view), iOS becomeFirstResponder() | requestFocus() | focusEditor() | state.focus(), autofocus, focusNode | ref.focus() |
| Give up focus | macOS window.makeFirstResponder(nil), iOS resignFirstResponder() | clearFocus() | — | state.blur(), focusNode | ref.blur() |
| Hear a focus change | macOS —, iOS UITextView.textDidBeginEditingNotification, UITextView.textDidEndEditingNotification | setOnFocusChangeListener(listener) | The DOM's focus events | focusNode | onFocus, onBlur |
Undo
| Job | Apple | Android | Web | Flutter | React Native |
|---|---|---|---|---|---|
| Undo and redo | macOS undo(_:), redo(_:), iOS undoManager?.undo(), undoManager?.redo() | undo(), redo() | undo(), redo() | state.undo(), state.redo() | ref.undo(), ref.redo() |
| Ask what can be undone | undoManager?.canUndo, undoManager?.canRedo | canUndo(), canRedo() | canUndo(), canRedo() | state.canUndo, state.canRedo | ref.getUndoState() |
| Hear the undo state change | — | — | — | — | onUndoStateChange |
| Set the undo limit | undoLevels | undoLevels | undoLevels | undoLevels | undoLevels |
| End the merging of edits | breakUndoCoalescing() | document.breakHistoryBurst() | document.breakHistoryBurst() | document.breakHistoryBurst() | ref.breakUndoCoalescing() |
| Snapshot and restore | snapshotDocument(), restoreDocument(_:selection:actionName:) | snapshotDocument(), restoreDocument(snapshot) | snapshotDocument(), restoreDocument(snapshot, selection) | state.snapshotDocument(), state.restoreDocument(snapshot, selection:) | — |
Clipboard and kill ring
| Job | Apple | Android | Web | Flutter | React Native |
|---|---|---|---|---|---|
| Cut, copy, paste | cut(_:), copy(_:), paste(_:) | — | performEditAction("cut"), performEditAction("copy"), performEditAction("paste") | state.cutSelection(), state.copySelection(), state.paste() | — |
| Select all | selectAll(_:) | — | performEditAction("selectAll") | state.selectAll() | — |
| Yank | macOS yank(_:), iOS — | — | yank() | state.yank() | — |
Styles
| Job | Apple | Android | Web | Flutter | React Native |
|---|---|---|---|---|---|
| Set all style runs | styleRuns | styleRuns | styleRuns | styleRuns | styleRuns |
| Style a range | applyStyle(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 style | toggleBold(), 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 style | setTypingStyle(fields:value:) | setTypingStyle(mask, value) | setTypingStyle(style) | state.setTypingStyle(fieldMask, value) | ref.setTypingStyle(style) |
| Read a style | styleAt(_:) | styleAt(offset) | styleAt(offset) | state.styleAt(offset) | ref.styleAt(offset) |
Lists and line breaks
| Job | Apple | Android | Web | Flutter | React Native |
|---|---|---|---|---|---|
| Set list markers | listMarkers, setListMarkers(_:actionName:) | listMarkers, setListMarkers(markers) | listMarkers, setListMarkers(markers) | listMarkers, state.setListMarkers(markers), document.setBlockMarkers(blocks, markers) | listMarkers, ref.setListMarkers(markers) |
| Renumber markers | listMarkerTextProvider | listMarkerTextProvider | setListMarkerTextProvider(provider) (option listMarkerTextProvider) | listMarkerTextProvider | — |
| Marker gap | markerGap | markerGap | markerGap | markerGap | markerGap |
| Keep a range on one line | setNoBreakRanges(_: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.
| Job | Apple | Android | Web | Flutter | React Native |
|---|---|---|---|---|---|
| Insets | textContainerInset | textContainerInset | insets | insets | insets |
| Selection color | macOS selectionBackgroundColor, iOS tintColor | selectionColor | selectionColor | selectionColor | selectionColor |
| Lazy layout threshold | lazyLayoutThresholdUnits | lazyLayoutThresholdUnits | lazyLayoutThresholdUnits | lazyLayoutThresholdUnits | lazyLayoutThresholdUnits |
Scrolling
| Job | Apple | Android | Web | Flutter | React Native |
|---|---|---|---|---|---|
| Page up and down | macOS 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 selection | macOS centerSelectionInVisibleArea(_:), iOS centerSelectionInVisibleArea() | centerSelectionInVisibleArea() | centerSelectionInVisibleArea() | state.centerSelectionInVisibleArea() | ref.centerSelectionInVisibleArea() |
| Scroll position | macOS enclosingScrollView, iOS contentOffset | scrollTo(x, y), scrollY | contentScrollTop, maxContentScrollTop | state.contentScrollTop, state.maxContentScrollTop, state.scrollTo(offset), controller | ref.scrollTo(offset) |
| Turn scrolling off | macOS —, iOS isScrollEnabled | isScrollEnabled | — | scrollEnabled | scrollEnabled, autoGrow |
| Content size | macOS intrinsicContentSize, iOS contentSize | contentHeight, onContentSizeDidChange | — | state.contentHeight, onContentSizeDidChange | ref.getContentSize(), onContentSizeChange |
Editing rules
| Job | Apple | Android | Web | Flutter | React Native |
|---|---|---|---|---|---|
| Follow a native view's editing | editingRule | editingRule | editingRule | editingRule | editingRule |
| Touch long press | — | longPress | longPress | longPress | longPress (Android only) |
| Finger tap | — | touchTap | touchTap | touchTap | touchTap (Android only) |
| Left and Right in mixed text | arrowMovement | arrowMovement | arrowMovement | arrowMovement | arrowMovement |
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.
| Command | macOS and iOS | Windows, Linux, Android |
|---|---|---|
| Undo | ⌘Z | Ctrl+Z |
| Redo | ⌘⇧Z | Ctrl+Shift+Z |
| Cut | ⌘X | Ctrl+X |
| Copy | ⌘C | Ctrl+C |
| Paste | ⌘V | Ctrl+V |
| Select all | ⌘A | |
| Bold | ⌘B | Ctrl+B |
| Italic | ⌘I | Ctrl+I |
| Underline | ⌘U | Ctrl+U |
| Strikethrough | ⌘⇧X | Ctrl+Shift+X |