Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ PythonNative is a cross-platform toolkit for building native Android and iOS app
- **Direct native bindings:** Python calls platform APIs directly through Chaquopy and rubicon-objc, with no JavaScript bridge.
- **Custom-component SDK:** Wrap any platform widget as a first-class element with type-checked props via `pythonnative.sdk` (`Props`, `@native_component`, `element_factory`). Plugins distributed on PyPI auto-register through the `pythonnative.handlers` entry-point group.
- **CLI scaffolding:** `pn init` creates a ready-to-run project; `pn run android` and `pn run ios` build and launch your app.
- **Instant desktop preview:** `pn preview` renders your app in a native desktop window via Tkinter with Fast Refresh on every save — iterate on layout, state, and navigation in milliseconds without booting a simulator or device. The reconciler, hooks, layout engine, and navigation are the same code that ships to the phone.
- **Native-backed navigation:** Declarative `Stack`, `Tab`, and `Drawer` navigators inspired by React Navigation. The root stack drives the platform's native navigation controller (`UINavigationController` on iOS, AndroidX Navigation Component on Android), so transitions, back gestures, and the hardware back button match what users expect.
- **Fast Refresh hot reload:** `pn run --hot-reload` watches `app/` and patches edits into the running app on save, preserving component state across most changes.
- **Bundled templates:** Android Gradle and iOS Xcode templates are included, so scaffolding requires no network access.
Expand Down
4 changes: 4 additions & 0 deletions docs/api/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ the documented behavior never drifts from the code.

- `pn init [name]`: scaffold a new project (creates `app/`,
`pythonnative.json`, `requirements.txt`, `.gitignore`).
- `pn preview [component]`: render the app in a desktop (Tkinter) window
with Fast Refresh — the fastest way to iterate on UI. Flags:
`--width`, `--height`, `--title`, `--no-hot-reload`. See the
[Desktop preview guide](../guides/desktop-preview.md).
- `pn run android|ios`: build and run on a connected device or
simulator. Flags: `--prepare-only`, `--hot-reload`, `--no-logs`.
- `pn clean`: remove the local `build/` directory.
Expand Down
16 changes: 13 additions & 3 deletions docs/concepts/components.md
Original file line number Diff line number Diff line change
Expand Up @@ -324,9 +324,19 @@ if pn.Platform.is_ios:
margin = 16
```

`pn.Platform.OS` is `"ios"`, `"android"`, or `"test"` (the latter
when running off-device, e.g., in unit tests). The lower-level
`utils.IS_ANDROID` / `utils.IS_IOS` constants are still available.
`pn.Platform.OS` is `"ios"`, `"android"`, `"desktop"` (the `pn preview`
backend — see the [Desktop preview guide](../guides/desktop-preview.md)),
or `"test"` (off-device, e.g. in unit tests). The lower-level
`utils.IS_ANDROID` / `utils.IS_IOS` / `utils.IS_DESKTOP` constants are
still available.

`Platform.select` matches on the exact key; a `"native"` key is shared
by iOS **and** Android (but not desktop), and a `"default"` key catches
anything unmatched:

```python
pad = pn.Platform.select({"native": 16, "desktop": 12, "default": 8})
```

## Next steps

Expand Down
10 changes: 7 additions & 3 deletions docs/concepts/native-views.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,13 @@ is a dict-like object that maps element type strings to handler
populates the registry with Chaquopy-backed handlers.
- On iOS, `pythonnative.native_views.ios.register_handlers` does the
same with rubicon-objc handlers.
- On the desktop (during tests), the registry is replaced with a mock
via [`set_registry`][pythonnative.native_views.set_registry] before
any element is rendered.
- On the desktop (`pn preview`, with `PN_PLATFORM=desktop`),
`pythonnative.native_views.desktop.register_handlers` populates the
registry with Tkinter-backed handlers. See the
[Desktop preview guide](../guides/desktop-preview.md).
- Off-device under `pytest`, the registry is replaced with a mock via
[`set_registry`][pythonnative.native_views.set_registry] before any
element is rendered.

Custom widgets follow the same pattern: register a handler under a
unique type string, then construct elements with that type and the
Expand Down
6 changes: 5 additions & 1 deletion docs/examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,16 @@ project scaffolded with `pn init`.
pn init my-app
cd my-app
# Edit app/main.py and paste any of the snippets below.
pn preview # fast desktop preview with Fast Refresh
pn run android # or: pn run ios
```

The `app/main.py` that `pn init` writes already returns a small
counter; replace it with one of the snippets to try a different
example.
example. The quickest way to iterate is
[`pn preview`](guides/desktop-preview.md), which renders the app in a
desktop window and reloads on every save; use `pn run` when you want it
on a real device or simulator.

## Snippets

Expand Down
32 changes: 32 additions & 0 deletions docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,38 @@ Key ideas:

When the root `Stack.Navigator` is rendered inside the host's first screen, `navigate(...)` and `go_back()` drive the **native** navigation controller (UINavigationController on iOS, AndroidX Navigation Component on Android). Each pushed screen runs in its own reconciler host, so state on the previous screen is preserved by the platform stack.

## Preview on your desktop

The fastest way to iterate is `pn preview`, which renders your app in a
desktop window and **Fast Refreshes on every save** — no simulator, no
device build:

```bash
pn preview
```

This opens a phone-sized window, mounts your project's `App`, and
watches `app/` for changes. Edit a component, hit save, and the window
updates in place while keeping component state (counters, form input,
scroll position). Navigation, hooks, async, and the flex layout engine
all run exactly as they do on device, because the desktop backend reuses
the same reconciler and layout engine — only the leaf widgets differ
(Tkinter instead of UIKit / Android views).

```bash
pn preview # preview the project's entry point (app/main.py → App)
pn preview app.main.Detail # preview a specific component
pn preview --width 768 --height 1024 # tablet-sized window
pn preview --no-hot-reload # disable file watching
```

The preview needs Tkinter, which ships with most Python installs. If
it's missing, install it (`brew install python-tk` on macOS,
`sudo apt-get install python3-tk` on Debian/Ubuntu). The desktop backend
is a **development** surface for layout and logic — some visual chrome is
approximated, and there's no desktop packaging. Ship to devices with
`pn run`. See the [Desktop preview guide](guides/desktop-preview.md).

## Run on a platform

```bash
Expand Down
173 changes: 173 additions & 0 deletions docs/guides/desktop-preview.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
# Desktop preview

`pn preview` renders your app in a native desktop window with **instant
Fast Refresh** on every save. It's the fastest way to build UI in
PythonNative: edit a component, hit save, and see the result in a second
— no simulator boot, no device deploy, no rebuild.

```bash
pn preview
```

The preview reuses the **same reconciler, hooks, navigation, async
runtime, and pure-Python flex layout engine** that run on device. Only
the leaf widgets differ: the desktop backend draws with
[Tkinter](https://docs.python.org/3/library/tkinter.html) instead of
UIKit / Android views. So behavior and layout match the device closely,
and your iteration loop collapses from minutes to seconds.

## Quick start

From a project directory (one created by `pn init`, with an `app/main.py`
that defines `App`):

```bash
pn preview
```

`pn preview` runs your real app code, so install your project's
dependencies in the same environment first (for example
`pip install -r requirements.txt`). If an import fails, the preview shows
the traceback in the window instead of crashing — install the missing
package or fix the code and save to recover.

This opens a phone-sized window, mounts your `App`, and starts watching
`app/` for changes. Edit any component and save — the window updates in
place while preserving component state (counters, text input, scroll
position, navigation stack).

```bash
pn preview # the project entry point (app/main.py → App)
pn preview app.screens.home # a module whose App attribute to mount
pn preview app.main.DetailScreen # a specific dotted component
pn preview --width 768 --height 1024 # tablet-sized window
pn preview --title "My App"
pn preview --no-hot-reload # mount once, don't watch files
```

## Requirements

The preview uses Tkinter, Python's standard GUI toolkit, which ships
with most Python installations. If `pn preview` reports that Tkinter is
missing:

- **macOS** (Homebrew Python): `brew install python-tk`
- **Debian / Ubuntu**: `sudo apt-get install python3-tk`
- **Windows**: re-run the Python installer and enable the
"tcl/tk and IDLE" optional feature.

No other dependencies are required — the desktop backend is pure Python.

## How it works

`pn preview` sets `PN_PLATFORM=desktop` and starts
`pythonnative.preview.run_preview`, which:

1. Opens a single Tk window with one *stage* frame.
2. Selects the Tkinter
[native-view registry][pythonnative.native_views.get_registry], so
every `pn.Text`, `pn.Button`, … maps to a Tk widget.
3. Mounts your `App` through a normal
[`Reconciler`][pythonnative.reconciler.Reconciler] and pushes the
window size in as the layout viewport.
4. Runs the Tk event loop on the main thread, polling ~60×/second to
apply renders requested from the async runtime thread and to drain
file-change reloads.
5. Watches `app/` with a
[`FileWatcher`][pythonnative.hot_reload.FileWatcher] and Fast
Refreshes on every save.

Layout is owned by the engine, not the widgets: the
[flex layout engine](../concepts/layout.md) computes an absolute frame
for every element, and the backend positions each Tk widget with that
frame. This is the same contract the iOS and Android backends follow, so
a column that lays out correctly on a phone lays out the same way in the
preview.

### Navigation

Root navigators (`create_stack_navigator`, tabs, drawer) drive a real
in-process stack of screen hosts in the preview, the same way they drive
`UINavigationController` / AndroidX Navigation on device. `navigate(...)`
pushes a new screen host (preserving the previous screen's state);
`go_back()` pops it. Each screen runs in its own reconciler host.

## Fast Refresh

Saving a `.py` file under `app/` triggers a reload. The preview prefers
**Fast Refresh**: the changed modules are reloaded and the live VNode
tree's function references are swapped in place, so the next render
reuses existing hook state. Edits to a component body keep your
counters, form values, and scroll positions. When a clean swap isn't
possible (structural edits, a raised exception), the preview falls back
to a full remount so you're never stuck with a stale tree.

If your component raises at import time (a syntax error you're mid-fix
on), the preview shows the traceback as an overlay and recovers
automatically on your next successful save — no restart needed.

See the [Hot reload guide](hot-reload.md) for the underlying mechanics;
the desktop preview shares the same Fast Refresh engine.

## Branching on the platform

When the preview is running, [`Platform.OS`][pythonnative.Platform] is
`"desktop"`:

```python
import pythonnative as pn

pad = pn.Platform.select({"desktop": 12, "ios": 16, "android": 16, "default": 12})
```

Note that `Platform.select`'s `"native"` key matches iOS and Android
only — desktop is a development surface, so use an explicit `"desktop"`
key (or `"default"`) for it. You can also check
[`Platform.is_desktop`][pythonnative.Platform] or the
`pythonnative.utils.IS_DESKTOP` flag directly.

## What's faithful, and what's approximated

The preview is a **development tool**, optimized for fidelity of layout
and logic rather than pixel-perfect chrome.

Faithful:

- Flex layout, sizing, padding, spacing, absolute positioning.
- Component lifecycle, hooks, effects, context, error boundaries.
- Navigation (stack push/pop, tabs, drawer) and per-screen state.
- The async runtime, `use_query` / `use_mutation`, timers, and
state-driven updates.
- Text wrapping and intrinsic sizing (measured with the same font the
widget renders).

Approximated or omitted (Tkinter can't express these cheaply):

- Rounded corners, shadows, gradients, and per-widget opacity.
- Overflow **clipping** — a `ScrollView`'s content renders but isn't
clipped to the viewport, and there's no interactive scrolling.
- Animations show their **end state** rather than smooth interpolation
(translations are applied; scale/rotate/opacity are skipped).
- `Image` loads local PNG/GIF files; network URLs and JPEG fall back to
a labeled placeholder. `WebView` shows a placeholder.

When the chrome matters, verify on device with `pn run`.

## When to use device builds instead

`pn preview` is for fast UI/logic iteration. Reach for `pn run` when you
need:

- Pixel-perfect native chrome and platform behaviors.
- Real device APIs (camera, location, notifications, biometrics, …).
- To test packaging, permissions, or store builds.

There is no desktop packaging target — ship to devices with
`pn run android` / `pn run ios`.

## Next steps

- Reference: [`pn` CLI](../api/cli.md).
- Mechanics shared with device hot reload: [Hot reload](hot-reload.md).
- How layout is computed: [Layout engine](../concepts/layout.md).
- Platform branching: [Platform & accessibility](platform-accessibility.md).
6 changes: 6 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ produce identical frames on both platforms.
- **Fast Refresh hot reload.** `pn run --hot-reload` watches `app/`
and patches the running app in place, preserving component state
across most edits.
- **Instant desktop preview.** `pn preview` renders your app in a
desktop window with Fast Refresh, so you can iterate on UI, state,
and navigation in milliseconds — no simulator boot required. See the
[Desktop preview guide](guides/desktop-preview.md).
- **An extension SDK.** [`pythonnative.sdk`](api/sdk.md) lets you
wrap any platform widget as a first-class element with
type-checked props, and PyPI plugins auto-register through the
Expand All @@ -63,6 +67,8 @@ produce identical frames on both platforms.
## Quick links

- New here? Start with [Getting started](getting-started.md).
- Want to see it run right now? Try the
[Desktop preview](guides/desktop-preview.md).
- Want the bigger picture? Read [Mental model](concepts/mental-model.md).
- Looking up an API? [Package overview](api/pythonnative.md).
- Wrapping a custom widget? Read
Expand Down
22 changes: 16 additions & 6 deletions docs/meta/faq.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,12 +46,22 @@ a small Python factory that returns

## Does PythonNative work on the desktop?

The core (components, hooks, reconciler) is platform-agnostic and runs
on the desktop with a [mock registry](../guides/testing.md#a-minimal-mock-registry).
That's how the test suite works. There is no built-in *desktop* widget
backend; if you need one, plug a Tk or Qt-backed
[`NativeViewRegistry`][pythonnative.native_views.NativeViewRegistry]
in.
Yes — for **previewing**. `pn preview` renders your app in a native
desktop window using a built-in Tkinter backend, with instant Fast
Refresh on every save. It's the fastest inner-loop: see your real UI
and iterate in seconds without booting a simulator or deploying to a
device. The same flex layout engine and reconciler drive it, so what
you see closely matches the device. See the
[Desktop preview guide](../guides/desktop-preview.md).

The desktop backend is a **development tool**, not a production target:
chrome like rounded corners, shadows, and overflow clipping are
approximated, and there's no app packaging for desktop. Ship to devices
with `pn run android` / `pn run ios`.

The core (components, hooks, reconciler) is also platform-agnostic and
runs headless with a [mock registry](../guides/testing.md#a-minimal-mock-registry) —
that's how the unit-test suite works.

## How do I package and distribute my app?

Expand Down
30 changes: 30 additions & 0 deletions examples/hello-world/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Hello World

The smallest PythonNative app: a counter with navigation to a detail
screen.

## Preview it on your desktop (fastest)

From this directory, install the example's dependencies (the preview
imports your real app code), then launch it:

```bash
pip install -r requirements.txt
pn preview
```

A desktop window opens running `app/main.py`'s `App`. Edit any component
under `app/`, save, and the window Fast Refreshes in place — no
simulator or device needed. See the
[Desktop preview guide](../../docs/guides/desktop-preview.md).

## Run on a device or simulator

```bash
pn run ios
# or
pn run android
```

Add `--hot-reload` to push edits to the running app without a full
rebuild.
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ nav:
- Guides:
- Android: guides/android.md
- iOS: guides/ios.md
- Desktop Preview: guides/desktop-preview.md
- Navigation: guides/navigation.md
- Styling: guides/styling.md
- Animations: guides/animations.md
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ ignore = ["D107", "D105", "D203", "D213"]
# docstrings would only add boilerplate that repeats the ABC.
"src/pythonnative/native_views/android.py" = ["D101", "D102"]
"src/pythonnative/native_views/ios.py" = ["D101", "D102"]
"src/pythonnative/native_views/desktop.py" = ["D101", "D102"]

[tool.ruff.lint.pydocstyle]
convention = "google"
Expand Down
Loading
Loading