FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

feat!: overhaul native bridge, animations, gestures, and lists (#11) · pythonnative/pythonnative@b635110 · GitHub

Commit b635110

Browse files
authored
feat!: overhaul native bridge, animations, gestures, and lists (#11)
1 parent 7cab301 commit b635110

95 files changed

Lines changed: 9838 additions & 5650 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.github/workflows/e2e.yml‎

Lines changed: 36 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -33,17 +33,20 @@ jobs:
3333
# ~35th flow. Sharding the run into a few balanced groups — each on
3434
# its own freshly booted emulator — keeps every session well under
3535
# the limit, and parallelizes the Android pass as a bonus. Groups are
36-
# sized so none exceeds ~25 flows; ``components`` is the largest
37-
# single category and runs alone. ``fail-fast: false`` so one shard's
38-
# failure still lets the others report.
36+
# sized so none exceeds ~15 flows; ``components`` (28 flows after the
37+
# gesture/animated additions) crossed the ceiling at ~flow 26, so it
38+
# runs as two halves. ``fail-fast: false`` so one shard's failure
39+
# still lets the others report.
3940
strategy:
4041
fail-fast: false
4142
matrix:
4243
include:
43-
- name: components
44-
suites: components
45-
- name: hooks-nav
46-
suites: hooks navigation
44+
- name: components-a
45+
suites: components-a
46+
- name: components-b
47+
suites: components-b
48+
- name: hooks-nav-gestures
49+
suites: hooks navigation gestures
4750
- name: layout-styling-anim-misc
4851
suites: layout styling animations misc
4952

@@ -85,6 +88,18 @@ jobs:
8588
arch: x86_64
8689
script: bash -lc "./scripts/run-e2e.sh android ${{ matrix.suites }}"
8790

91+
# Maestro writes per-flow debug output (command log, view-hierarchy
92+
# dumps, failure screenshots) under ~/.maestro/tests. Surface it when
93+
# a shard fails so CI-only failures are diagnosable without guesswork.
94+
- name: Upload Maestro debug output
95+
if: failure()
96+
uses: actions/upload-artifact@v4
97+
with:
98+
name: maestro-debug-android-${{ matrix.name }}
99+
path: ~/.maestro/tests/
100+
if-no-files-found: ignore
101+
retention-days: 7
102+
88103
e2e-ios:
89104
needs: coverage
90105
runs-on: macos-latest
@@ -104,8 +119,8 @@ jobs:
104119
include:
105120
- name: components
106121
suites: components
107-
- name: hooks-nav
108-
suites: hooks navigation
122+
- name: hooks-nav-gestures
123+
suites: hooks navigation gestures
109124
- name: layout-styling-anim-misc
110125
suites: layout styling animations misc
111126

@@ -133,3 +148,15 @@ jobs:
133148
run: ./scripts/run-e2e.sh ios ${{ matrix.suites }}
134149
env:
135150
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
151+
152+
# Maestro writes per-flow debug output (command log, view-hierarchy
153+
# dumps, failure screenshots) under ~/.maestro/tests. Surface it when
154+
# a shard fails so CI-only failures are diagnosable without guesswork.
155+
- name: Upload Maestro debug output
156+
if: failure()
157+
uses: actions/upload-artifact@v4
158+
with:
159+
name: maestro-debug-ios-${{ matrix.name }}
160+
path: ~/.maestro/tests/
161+
if-no-files-found: ignore
162+
retention-days: 7

‎CONTRIBUTING.md‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,9 +104,12 @@ Recommended scopes (choose the smallest, most accurate unit; prefer module/direc
104104
- `cli` – CLI tool and `pn` command (`src/pythonnative/cli/`)
105105
- `components` – declarative element-creating functions (`components.py`)
106106
- `element` – Element descriptor class (`element.py`)
107+
- `events` – tag-based event routing between native views and Python callbacks (`events.py`)
108+
- `gestures` – gesture descriptors and the pure-Python recognition arbiter (`gestures.py`)
107109
- `hooks` – function components and hooks (`hooks.py`)
108110
- `hot_reload` – file watcher and module reloader (`hot_reload.py`)
109111
- `layout` – pure-Python flexbox engine (`layout.py`)
112+
- `mutations` – batched mutation ops between reconciler and native backends (`mutations.py`)
110113
- `native_modules` – native API modules for device capabilities (`native_modules/`)
111114
- `native_views` – platform-specific native view creation and updates (`native_views/`)
112115
- `navigation` – navigation containers and stack/tab/drawer navigators (`navigation.py`)

‎README.md‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,10 @@ PythonNative is a cross-platform toolkit for building native Android and iOS app
3636
- **Hooks and function components:** Manage state with `use_state`, side effects with `use_effect`, and navigation with `use_navigation`, all through one consistent pattern.
3737
- **Typed `style` prop:** Pass all visual and layout properties through a single `style` dict, fully described by the `pn.Style` `TypedDict` and the ergonomic `pn.style(...)` helper for IDE autocomplete and static checking. Compose reusable styles with `StyleSheet`.
3838
- **Cross-platform flexbox engine:** A pure-Python, Yoga-style layout engine computes frames once and applies them to native views, so `flex`, `padding`, `aspect_ratio`, and `position: "absolute"` produce the same geometry on Android and iOS.
39-
- **Virtual view tree + reconciler:** Element trees are diffed and patched with minimal native mutations, similar to React's reconciliation. State updates re-render **locally** — only the component whose state changed (and its subtree) re-runs, and unchanged leaves reuse cached intrinsic measurements — so deep UIs stay responsive instead of re-rendering the whole app from the root on every tap.
39+
- **Virtual view tree + reconciler:** Element trees are diffed and patched with minimal native mutations, similar to React's reconciliation. Each commit lands as **one batched transaction** of mutation ops, and event callbacks are routed through a tag-based registry so re-renders that only change closures cost zero native calls. State updates re-render **locally** — only the component whose state changed (and its subtree) re-runs, and unchanged leaves reuse cached intrinsic measurements — so deep UIs stay responsive instead of re-rendering the whole app from the root on every tap.
40+
- **Native-driven animations:** The `Animated` API (timing / spring / decay, awaitable or fire-and-forget) hands animations to Core Animation and `ViewPropertyAnimator` whenever possible, so no Python code runs per frame; a pure-Python ticker covers the rest.
41+
- **Native gesture system:** Attach `Tap`, `LongPress`, `Pan`, `Swipe`, `Pinch`, and `Rotation` recognizers to any view via the `gestures=` prop — backed by `UIGestureRecognizer` on iOS and a unit-testable pure-Python arbiter on Android and desktop.
42+
- **Virtualized lists:** `FlatList` / `SectionList` window their rows in Python over the platform scroll view — uniform, exact, or measured variable heights, grids, headers/footers, infinite scroll, and an imperative scroll controller, identical on every platform.
4043
- **Direct native bindings:** Python calls platform APIs directly through Chaquopy and rubicon-objc, with no JavaScript bridge.
4144
- **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.
4245
- **CLI scaffolding:** `pn init` creates a ready-to-run project; `pn run android` and `pn run ios` build and launch your app.

‎docs/api/gestures.md‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# Gestures
2+
3+
Native-backed gesture recognition, attached to any view-like element
4+
via the `gestures=` prop. Descriptors are frozen dataclasses; their
5+
numeric configuration crosses the bridge while callbacks are routed
6+
through the tag-based event channel. See the
7+
[Gestures guide](../guides/gestures.md) for usage patterns.
8+
9+
::: pythonnative.gestures
10+
options:
11+
show_root_heading: false
12+
show_root_toc_entry: false
13+
members_order: source
14+
filters: ["!^_"]
15+
show_if_no_docstring: true
16+
17+
## See also
18+
19+
- The [Gestures guide](../guides/gestures.md) walks through taps,
20+
drags, and gesture-driven animations.
21+
- [Animated](animated.md) pairs with `Pan` velocity for springs and
22+
decays.

‎docs/api/native_views.md‎

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
# Native views
22

33
The bridge between PythonNative's element tree and concrete native
4-
widgets. Every element type maps to a
4+
widgets. Each commit's diff is expressed as a flat list of mutation
5+
ops referencing integer tags, applied through a single
6+
[`apply_mutations`][pythonnative.native_views.NativeViewRegistry.apply_mutations]
7+
call. Every element type maps to a
58
[`ViewHandler`][pythonnative.native_views.base.ViewHandler]
69
implementation in the
710
[`NativeViewRegistry`][pythonnative.native_views.NativeViewRegistry];
@@ -15,6 +18,24 @@ the platform-specific handlers are registered lazily so importing
1518
members_order: source
1619
filters: ["!^_"]
1720

21+
## Mutation ops
22+
23+
::: pythonnative.mutations
24+
options:
25+
show_root_heading: false
26+
show_root_toc_entry: false
27+
members_order: source
28+
filters: ["!^_"]
29+
30+
## Event routing
31+
32+
::: pythonnative.events
33+
options:
34+
show_root_heading: false
35+
show_root_toc_entry: false
36+
members_order: source
37+
filters: ["!^_"]
38+
1839
## Base classes
1940

2041
::: pythonnative.native_views.base

‎docs/concepts/architecture.md‎

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,12 @@ platform APIs synchronously from Python.
1919
3. **Reconciler.** On first render, the
2020
[`Reconciler`][pythonnative.reconciler.Reconciler] walks the tree
2121
and creates real native views via the platform backend. On
22-
subsequent renders it diffs against the previous tree and applies
23-
the minimal set of native mutations. State-driven renders are
22+
subsequent renders it diffs against the previous tree and emits the
23+
minimal list of mutation ops (create / update / insert / remove /
24+
destroy / set-frame), applied as **one batched transaction** per
25+
commit through
26+
[`apply_mutations`][pythonnative.native_views.NativeViewRegistry.apply_mutations].
27+
State-driven renders are
2428
**local**: a setter marks only its own component subtree dirty, and
2529
[`flush_dirty`][pythonnative.reconciler.Reconciler.flush_dirty]
2630
re-runs just those components instead of the whole app from the
@@ -95,11 +99,14 @@ and a new one is created.
9599
so [`use_context`][pythonnative.use_context] still resolves. Hooks
96100
record state reads, queue effects, and register memos. No native
97101
mutations happen yet.
98-
3. **Commit phase**: the reconciler applies the diff for each
99-
re-rendered subtree to native views, creating, updating, and
100-
removing views as needed, and bubbles any swapped subtree-root view
101-
up to its nearest native container.
102-
4. **Layout phase**: a layout pass recomputes frames. Leaves whose
102+
3. **Commit phase**: the reconciler turns the diff for each
103+
re-rendered subtree into mutation ops referencing view tags, and
104+
flushes them all in a single `apply_mutations` transaction. Event
105+
callbacks are routed to the Python-side
106+
[`EventRegistry`][pythonnative.events.EventRegistry] instead of
107+
crossing the bridge, so callback-identity churn costs nothing.
108+
4. **Layout phase**: a layout pass recomputes frames and emits
109+
`SetFrameOp`s only for frames that changed. Leaves whose
103110
`Element` is unchanged reuse a cached intrinsic measurement, so
104111
untouched subtrees skip native `measure_intrinsic` calls.
105112
5. **Effect phase**: pending effects are flushed in depth-first order
@@ -256,9 +263,18 @@ Each handler class maps an element type name (e.g., `"Text"`,
256263
`"Button"`) to platform-native widget creation, property updates, and
257264
child management. The
258265
[`NativeViewRegistry`][pythonnative.native_views.NativeViewRegistry]
266+
owns the tag-to-view table, applies each commit's mutation list, and
259267
lazily imports only the relevant platform module at runtime, so the
260268
package can be imported on any platform for testing.
261269

270+
Native events flow back through a single channel: handlers wire their
271+
platform listeners once at view creation and call
272+
[`dispatch_event`][pythonnative.events.dispatch_event] with the view's
273+
tag; the [`EventRegistry`][pythonnative.events.EventRegistry] resolves
274+
the current Python callback. Gestures
275+
([`pythonnative.gestures`](../api/gestures.md)) and natively-driven
276+
animations (the `Animated` API) ride the same tag infrastructure.
277+
262278
## Comparisons
263279

264280
!!! note "Versus React Native"

‎docs/concepts/components.md‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -84,9 +84,9 @@ pn.Column(
8484
**Lists:**
8585

8686
- [`FlatList(data, render_item, key_extractor, item_height, ...)`][pythonnative.FlatList]:
87-
scrollable data list. Pass `item_height=` to enable native
88-
virtualization (`UITableView` / `RecyclerView`); rows are mounted
89-
lazily as they scroll into view.
87+
virtualized scrollable data list. Rows are mounted lazily as they
88+
scroll into view; pass `item_height=` (or `get_item_height=`) for
89+
exact extents, or let rows be measured on screen.
9090
- [`SectionList(sections, render_item, render_section_header, item_height, ...)`][pythonnative.SectionList]:
9191
virtualized list with section headers.
9292

0 commit comments

Comments
 (0)

Back | FazBrowse Home | New Git URL