Skip to content

Commit 3228f11

Browse files
authored
feat(native_views,cli): add desktop preview backend and pn preview (#8)
1 parent 78e966c commit 3228f11

22 files changed

Lines changed: 3045 additions & 34 deletions

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ PythonNative is a cross-platform toolkit for building native Android and iOS app
4040
- **Direct native bindings:** Python calls platform APIs directly through Chaquopy and rubicon-objc, with no JavaScript bridge.
4141
- **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.
4242
- **CLI scaffolding:** `pn init` creates a ready-to-run project; `pn run android` and `pn run ios` build and launch your app.
43+
- **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.
4344
- **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.
4445
- **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.
4546
- **Bundled templates:** Android Gradle and iOS Xcode templates are included, so scaffolding requires no network access.

docs/api/cli.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ the documented behavior never drifts from the code.
88

99
- `pn init [name]`: scaffold a new project (creates `app/`,
1010
`pythonnative.json`, `requirements.txt`, `.gitignore`).
11+
- `pn preview [component]`: render the app in a desktop (Tkinter) window
12+
with Fast Refresh — the fastest way to iterate on UI. Flags:
13+
`--width`, `--height`, `--title`, `--no-hot-reload`. See the
14+
[Desktop preview guide](../guides/desktop-preview.md).
1115
- `pn run android|ios`: build and run on a connected device or
1216
simulator. Flags: `--prepare-only`, `--hot-reload`, `--no-logs`.
1317
- `pn clean`: remove the local `build/` directory.

docs/concepts/components.md

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -324,9 +324,19 @@ if pn.Platform.is_ios:
324324
margin = 16
325325
```
326326

327-
`pn.Platform.OS` is `"ios"`, `"android"`, or `"test"` (the latter
328-
when running off-device, e.g., in unit tests). The lower-level
329-
`utils.IS_ANDROID` / `utils.IS_IOS` constants are still available.
327+
`pn.Platform.OS` is `"ios"`, `"android"`, `"desktop"` (the `pn preview`
328+
backend — see the [Desktop preview guide](../guides/desktop-preview.md)),
329+
or `"test"` (off-device, e.g. in unit tests). The lower-level
330+
`utils.IS_ANDROID` / `utils.IS_IOS` / `utils.IS_DESKTOP` constants are
331+
still available.
332+
333+
`Platform.select` matches on the exact key; a `"native"` key is shared
334+
by iOS **and** Android (but not desktop), and a `"default"` key catches
335+
anything unmatched:
336+
337+
```python
338+
pad = pn.Platform.select({"native": 16, "desktop": 12, "default": 8})
339+
```
330340

331341
## Next steps
332342

docs/concepts/native-views.md

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,9 +66,13 @@ is a dict-like object that maps element type strings to handler
6666
populates the registry with Chaquopy-backed handlers.
6767
- On iOS, `pythonnative.native_views.ios.register_handlers` does the
6868
same with rubicon-objc handlers.
69-
- On the desktop (during tests), the registry is replaced with a mock
70-
via [`set_registry`][pythonnative.native_views.set_registry] before
71-
any element is rendered.
69+
- On the desktop (`pn preview`, with `PN_PLATFORM=desktop`),
70+
`pythonnative.native_views.desktop.register_handlers` populates the
71+
registry with Tkinter-backed handlers. See the
72+
[Desktop preview guide](../guides/desktop-preview.md).
73+
- Off-device under `pytest`, the registry is replaced with a mock via
74+
[`set_registry`][pythonnative.native_views.set_registry] before any
75+
element is rendered.
7276

7377
Custom widgets follow the same pattern: register a handler under a
7478
unique type string, then construct elements with that type and the

docs/examples.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,16 @@ project scaffolded with `pn init`.
2020
pn init my-app
2121
cd my-app
2222
# Edit app/main.py and paste any of the snippets below.
23+
pn preview # fast desktop preview with Fast Refresh
2324
pn run android # or: pn run ios
2425
```
2526

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

3034
## Snippets
3135

docs/getting-started.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,38 @@ Key ideas:
6666

6767
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.
6868

69+
## Preview on your desktop
70+
71+
The fastest way to iterate is `pn preview`, which renders your app in a
72+
desktop window and **Fast Refreshes on every save** — no simulator, no
73+
device build:
74+
75+
```bash
76+
pn preview
77+
```
78+
79+
This opens a phone-sized window, mounts your project's `App`, and
80+
watches `app/` for changes. Edit a component, hit save, and the window
81+
updates in place while keeping component state (counters, form input,
82+
scroll position). Navigation, hooks, async, and the flex layout engine
83+
all run exactly as they do on device, because the desktop backend reuses
84+
the same reconciler and layout engine — only the leaf widgets differ
85+
(Tkinter instead of UIKit / Android views).
86+
87+
```bash
88+
pn preview # preview the project's entry point (app/main.py → App)
89+
pn preview app.main.Detail # preview a specific component
90+
pn preview --width 768 --height 1024 # tablet-sized window
91+
pn preview --no-hot-reload # disable file watching
92+
```
93+
94+
The preview needs Tkinter, which ships with most Python installs. If
95+
it's missing, install it (`brew install python-tk` on macOS,
96+
`sudo apt-get install python3-tk` on Debian/Ubuntu). The desktop backend
97+
is a **development** surface for layout and logic — some visual chrome is
98+
approximated, and there's no desktop packaging. Ship to devices with
99+
`pn run`. See the [Desktop preview guide](guides/desktop-preview.md).
100+
69101
## Run on a platform
70102

71103
```bash

docs/guides/desktop-preview.md

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
# Desktop preview
2+
3+
`pn preview` renders your app in a native desktop window with **instant
4+
Fast Refresh** on every save. It's the fastest way to build UI in
5+
PythonNative: edit a component, hit save, and see the result in a second
6+
— no simulator boot, no device deploy, no rebuild.
7+
8+
```bash
9+
pn preview
10+
```
11+
12+
The preview reuses the **same reconciler, hooks, navigation, async
13+
runtime, and pure-Python flex layout engine** that run on device. Only
14+
the leaf widgets differ: the desktop backend draws with
15+
[Tkinter](https://docs.python.org/3/library/tkinter.html) instead of
16+
UIKit / Android views. So behavior and layout match the device closely,
17+
and your iteration loop collapses from minutes to seconds.
18+
19+
## Quick start
20+
21+
From a project directory (one created by `pn init`, with an `app/main.py`
22+
that defines `App`):
23+
24+
```bash
25+
pn preview
26+
```
27+
28+
`pn preview` runs your real app code, so install your project's
29+
dependencies in the same environment first (for example
30+
`pip install -r requirements.txt`). If an import fails, the preview shows
31+
the traceback in the window instead of crashing — install the missing
32+
package or fix the code and save to recover.
33+
34+
This opens a phone-sized window, mounts your `App`, and starts watching
35+
`app/` for changes. Edit any component and save — the window updates in
36+
place while preserving component state (counters, text input, scroll
37+
position, navigation stack).
38+
39+
```bash
40+
pn preview # the project entry point (app/main.py → App)
41+
pn preview app.screens.home # a module whose App attribute to mount
42+
pn preview app.main.DetailScreen # a specific dotted component
43+
pn preview --width 768 --height 1024 # tablet-sized window
44+
pn preview --title "My App"
45+
pn preview --no-hot-reload # mount once, don't watch files
46+
```
47+
48+
## Requirements
49+
50+
The preview uses Tkinter, Python's standard GUI toolkit, which ships
51+
with most Python installations. If `pn preview` reports that Tkinter is
52+
missing:
53+
54+
- **macOS** (Homebrew Python): `brew install python-tk`
55+
- **Debian / Ubuntu**: `sudo apt-get install python3-tk`
56+
- **Windows**: re-run the Python installer and enable the
57+
"tcl/tk and IDLE" optional feature.
58+
59+
No other dependencies are required — the desktop backend is pure Python.
60+
61+
## How it works
62+
63+
`pn preview` sets `PN_PLATFORM=desktop` and starts
64+
`pythonnative.preview.run_preview`, which:
65+
66+
1. Opens a single Tk window with one *stage* frame.
67+
2. Selects the Tkinter
68+
[native-view registry][pythonnative.native_views.get_registry], so
69+
every `pn.Text`, `pn.Button`, … maps to a Tk widget.
70+
3. Mounts your `App` through a normal
71+
[`Reconciler`][pythonnative.reconciler.Reconciler] and pushes the
72+
window size in as the layout viewport.
73+
4. Runs the Tk event loop on the main thread, polling ~60×/second to
74+
apply renders requested from the async runtime thread and to drain
75+
file-change reloads.
76+
5. Watches `app/` with a
77+
[`FileWatcher`][pythonnative.hot_reload.FileWatcher] and Fast
78+
Refreshes on every save.
79+
80+
Layout is owned by the engine, not the widgets: the
81+
[flex layout engine](../concepts/layout.md) computes an absolute frame
82+
for every element, and the backend positions each Tk widget with that
83+
frame. This is the same contract the iOS and Android backends follow, so
84+
a column that lays out correctly on a phone lays out the same way in the
85+
preview.
86+
87+
### Navigation
88+
89+
Root navigators (`create_stack_navigator`, tabs, drawer) drive a real
90+
in-process stack of screen hosts in the preview, the same way they drive
91+
`UINavigationController` / AndroidX Navigation on device. `navigate(...)`
92+
pushes a new screen host (preserving the previous screen's state);
93+
`go_back()` pops it. Each screen runs in its own reconciler host.
94+
95+
## Fast Refresh
96+
97+
Saving a `.py` file under `app/` triggers a reload. The preview prefers
98+
**Fast Refresh**: the changed modules are reloaded and the live VNode
99+
tree's function references are swapped in place, so the next render
100+
reuses existing hook state. Edits to a component body keep your
101+
counters, form values, and scroll positions. When a clean swap isn't
102+
possible (structural edits, a raised exception), the preview falls back
103+
to a full remount so you're never stuck with a stale tree.
104+
105+
If your component raises at import time (a syntax error you're mid-fix
106+
on), the preview shows the traceback as an overlay and recovers
107+
automatically on your next successful save — no restart needed.
108+
109+
See the [Hot reload guide](hot-reload.md) for the underlying mechanics;
110+
the desktop preview shares the same Fast Refresh engine.
111+
112+
## Branching on the platform
113+
114+
When the preview is running, [`Platform.OS`][pythonnative.Platform] is
115+
`"desktop"`:
116+
117+
```python
118+
import pythonnative as pn
119+
120+
pad = pn.Platform.select({"desktop": 12, "ios": 16, "android": 16, "default": 12})
121+
```
122+
123+
Note that `Platform.select`'s `"native"` key matches iOS and Android
124+
only — desktop is a development surface, so use an explicit `"desktop"`
125+
key (or `"default"`) for it. You can also check
126+
[`Platform.is_desktop`][pythonnative.Platform] or the
127+
`pythonnative.utils.IS_DESKTOP` flag directly.
128+
129+
## What's faithful, and what's approximated
130+
131+
The preview is a **development tool**, optimized for fidelity of layout
132+
and logic rather than pixel-perfect chrome.
133+
134+
Faithful:
135+
136+
- Flex layout, sizing, padding, spacing, absolute positioning.
137+
- Component lifecycle, hooks, effects, context, error boundaries.
138+
- Navigation (stack push/pop, tabs, drawer) and per-screen state.
139+
- The async runtime, `use_query` / `use_mutation`, timers, and
140+
state-driven updates.
141+
- Text wrapping and intrinsic sizing (measured with the same font the
142+
widget renders).
143+
144+
Approximated or omitted (Tkinter can't express these cheaply):
145+
146+
- Rounded corners, shadows, gradients, and per-widget opacity.
147+
- Overflow **clipping** — a `ScrollView`'s content renders but isn't
148+
clipped to the viewport, and there's no interactive scrolling.
149+
- Animations show their **end state** rather than smooth interpolation
150+
(translations are applied; scale/rotate/opacity are skipped).
151+
- `Image` loads local PNG/GIF files; network URLs and JPEG fall back to
152+
a labeled placeholder. `WebView` shows a placeholder.
153+
154+
When the chrome matters, verify on device with `pn run`.
155+
156+
## When to use device builds instead
157+
158+
`pn preview` is for fast UI/logic iteration. Reach for `pn run` when you
159+
need:
160+
161+
- Pixel-perfect native chrome and platform behaviors.
162+
- Real device APIs (camera, location, notifications, biometrics, …).
163+
- To test packaging, permissions, or store builds.
164+
165+
There is no desktop packaging target — ship to devices with
166+
`pn run android` / `pn run ios`.
167+
168+
## Next steps
169+
170+
- Reference: [`pn` CLI](../api/cli.md).
171+
- Mechanics shared with device hot reload: [Hot reload](hot-reload.md).
172+
- How layout is computed: [Layout engine](../concepts/layout.md).
173+
- Platform branching: [Platform & accessibility](platform-accessibility.md).

docs/index.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,10 @@ produce identical frames on both platforms.
5353
- **Fast Refresh hot reload.** `pn run --hot-reload` watches `app/`
5454
and patches the running app in place, preserving component state
5555
across most edits.
56+
- **Instant desktop preview.** `pn preview` renders your app in a
57+
desktop window with Fast Refresh, so you can iterate on UI, state,
58+
and navigation in milliseconds — no simulator boot required. See the
59+
[Desktop preview guide](guides/desktop-preview.md).
5660
- **An extension SDK.** [`pythonnative.sdk`](api/sdk.md) lets you
5761
wrap any platform widget as a first-class element with
5862
type-checked props, and PyPI plugins auto-register through the
@@ -63,6 +67,8 @@ produce identical frames on both platforms.
6367
## Quick links
6468

6569
- New here? Start with [Getting started](getting-started.md).
70+
- Want to see it run right now? Try the
71+
[Desktop preview](guides/desktop-preview.md).
6672
- Want the bigger picture? Read [Mental model](concepts/mental-model.md).
6773
- Looking up an API? [Package overview](api/pythonnative.md).
6874
- Wrapping a custom widget? Read

docs/meta/faq.md

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -46,12 +46,22 @@ a small Python factory that returns
4646

4747
## Does PythonNative work on the desktop?
4848

49-
The core (components, hooks, reconciler) is platform-agnostic and runs
50-
on the desktop with a [mock registry](../guides/testing.md#a-minimal-mock-registry).
51-
That's how the test suite works. There is no built-in *desktop* widget
52-
backend; if you need one, plug a Tk or Qt-backed
53-
[`NativeViewRegistry`][pythonnative.native_views.NativeViewRegistry]
54-
in.
49+
Yes — for **previewing**. `pn preview` renders your app in a native
50+
desktop window using a built-in Tkinter backend, with instant Fast
51+
Refresh on every save. It's the fastest inner-loop: see your real UI
52+
and iterate in seconds without booting a simulator or deploying to a
53+
device. The same flex layout engine and reconciler drive it, so what
54+
you see closely matches the device. See the
55+
[Desktop preview guide](../guides/desktop-preview.md).
56+
57+
The desktop backend is a **development tool**, not a production target:
58+
chrome like rounded corners, shadows, and overflow clipping are
59+
approximated, and there's no app packaging for desktop. Ship to devices
60+
with `pn run android` / `pn run ios`.
61+
62+
The core (components, hooks, reconciler) is also platform-agnostic and
63+
runs headless with a [mock registry](../guides/testing.md#a-minimal-mock-registry) —
64+
that's how the unit-test suite works.
5565

5666
## How do I package and distribute my app?
5767

examples/hello-world/README.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# Hello World
2+
3+
The smallest PythonNative app: a counter with navigation to a detail
4+
screen.
5+
6+
## Preview it on your desktop (fastest)
7+
8+
From this directory, install the example's dependencies (the preview
9+
imports your real app code), then launch it:
10+
11+
```bash
12+
pip install -r requirements.txt
13+
pn preview
14+
```
15+
16+
A desktop window opens running `app/main.py`'s `App`. Edit any component
17+
under `app/`, save, and the window Fast Refreshes in place — no
18+
simulator or device needed. See the
19+
[Desktop preview guide](../../docs/guides/desktop-preview.md).
20+
21+
## Run on a device or simulator
22+
23+
```bash
24+
pn run ios
25+
# or
26+
pn run android
27+
```
28+
29+
Add `--hot-reload` to push edits to the running app without a full
30+
rebuild.

0 commit comments

Comments
 (0)