pythonnative 0.19.0__tar.gz → 0.20.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pythonnative-0.19.0/src/pythonnative.egg-info → pythonnative-0.20.0}/PKG-INFO +2 -2
- {pythonnative-0.19.0 → pythonnative-0.20.0}/README.md +1 -1
- {pythonnative-0.19.0 → pythonnative-0.20.0}/pyproject.toml +2 -1
- {pythonnative-0.19.0 → pythonnative-0.20.0}/src/pythonnative/__init__.py +1 -1
- {pythonnative-0.19.0 → pythonnative-0.20.0}/src/pythonnative/hooks.py +30 -6
- {pythonnative-0.19.0 → pythonnative-0.20.0}/src/pythonnative/reconciler.py +285 -3
- {pythonnative-0.19.0 → pythonnative-0.20.0}/src/pythonnative/screen.py +23 -27
- {pythonnative-0.19.0 → pythonnative-0.20.0/src/pythonnative.egg-info}/PKG-INFO +2 -2
- {pythonnative-0.19.0 → pythonnative-0.20.0}/src/pythonnative.egg-info/SOURCES.txt +1 -0
- pythonnative-0.20.0/tests/test_incremental_render.py +371 -0
- {pythonnative-0.19.0 → pythonnative-0.20.0}/LICENSE +0 -0
- {pythonnative-0.19.0 → pythonnative-0.20.0}/setup.cfg +0 -0
- {pythonnative-0.19.0 → pythonnative-0.20.0}/src/pythonnative/_ios_log.py +0 -0
- {pythonnative-0.19.0 → pythonnative-0.20.0}/src/pythonnative/alerts.py +0 -0
- {pythonnative-0.19.0 → pythonnative-0.20.0}/src/pythonnative/animated.py +0 -0
- {pythonnative-0.19.0 → pythonnative-0.20.0}/src/pythonnative/cli/__init__.py +0 -0
- {pythonnative-0.19.0 → pythonnative-0.20.0}/src/pythonnative/cli/pn.py +0 -0
- {pythonnative-0.19.0 → pythonnative-0.20.0}/src/pythonnative/components.py +0 -0
- {pythonnative-0.19.0 → pythonnative-0.20.0}/src/pythonnative/element.py +0 -0
- {pythonnative-0.19.0 → pythonnative-0.20.0}/src/pythonnative/hot_reload.py +0 -0
- {pythonnative-0.19.0 → pythonnative-0.20.0}/src/pythonnative/layout.py +0 -0
- {pythonnative-0.19.0 → pythonnative-0.20.0}/src/pythonnative/native_modules/__init__.py +0 -0
- {pythonnative-0.19.0 → pythonnative-0.20.0}/src/pythonnative/native_modules/app_state.py +0 -0
- {pythonnative-0.19.0 → pythonnative-0.20.0}/src/pythonnative/native_modules/battery.py +0 -0
- {pythonnative-0.19.0 → pythonnative-0.20.0}/src/pythonnative/native_modules/biometrics.py +0 -0
- {pythonnative-0.19.0 → pythonnative-0.20.0}/src/pythonnative/native_modules/camera.py +0 -0
- {pythonnative-0.19.0 → pythonnative-0.20.0}/src/pythonnative/native_modules/clipboard.py +0 -0
- {pythonnative-0.19.0 → pythonnative-0.20.0}/src/pythonnative/native_modules/file_system.py +0 -0
- {pythonnative-0.19.0 → pythonnative-0.20.0}/src/pythonnative/native_modules/haptics.py +0 -0
- {pythonnative-0.19.0 → pythonnative-0.20.0}/src/pythonnative/native_modules/linking.py +0 -0
- {pythonnative-0.19.0 → pythonnative-0.20.0}/src/pythonnative/native_modules/location.py +0 -0
- {pythonnative-0.19.0 → pythonnative-0.20.0}/src/pythonnative/native_modules/net_info.py +0 -0
- {pythonnative-0.19.0 → pythonnative-0.20.0}/src/pythonnative/native_modules/notifications.py +0 -0
- {pythonnative-0.19.0 → pythonnative-0.20.0}/src/pythonnative/native_modules/permissions.py +0 -0
- {pythonnative-0.19.0 → pythonnative-0.20.0}/src/pythonnative/native_modules/secure_store.py +0 -0
- {pythonnative-0.19.0 → pythonnative-0.20.0}/src/pythonnative/native_modules/share.py +0 -0
- {pythonnative-0.19.0 → pythonnative-0.20.0}/src/pythonnative/native_views/__init__.py +0 -0
- {pythonnative-0.19.0 → pythonnative-0.20.0}/src/pythonnative/native_views/android.py +0 -0
- {pythonnative-0.19.0 → pythonnative-0.20.0}/src/pythonnative/native_views/base.py +0 -0
- {pythonnative-0.19.0 → pythonnative-0.20.0}/src/pythonnative/native_views/desktop.py +0 -0
- {pythonnative-0.19.0 → pythonnative-0.20.0}/src/pythonnative/native_views/ios.py +0 -0
- {pythonnative-0.19.0 → pythonnative-0.20.0}/src/pythonnative/navigation.py +0 -0
- {pythonnative-0.19.0 → pythonnative-0.20.0}/src/pythonnative/net.py +0 -0
- {pythonnative-0.19.0 → pythonnative-0.20.0}/src/pythonnative/platform.py +0 -0
- {pythonnative-0.19.0 → pythonnative-0.20.0}/src/pythonnative/platform_metrics.py +0 -0
- {pythonnative-0.19.0 → pythonnative-0.20.0}/src/pythonnative/preview.py +0 -0
- {pythonnative-0.19.0 → pythonnative-0.20.0}/src/pythonnative/runtime.py +0 -0
- {pythonnative-0.19.0 → pythonnative-0.20.0}/src/pythonnative/sdk/__init__.py +0 -0
- {pythonnative-0.19.0 → pythonnative-0.20.0}/src/pythonnative/sdk/_components.py +0 -0
- {pythonnative-0.19.0 → pythonnative-0.20.0}/src/pythonnative/storage.py +0 -0
- {pythonnative-0.19.0 → pythonnative-0.20.0}/src/pythonnative/style.py +0 -0
- {pythonnative-0.19.0 → pythonnative-0.20.0}/src/pythonnative/templates/android_template/app/build.gradle +0 -0
- {pythonnative-0.19.0 → pythonnative-0.20.0}/src/pythonnative/templates/android_template/app/proguard-rules.pro +0 -0
- {pythonnative-0.19.0 → pythonnative-0.20.0}/src/pythonnative/templates/android_template/app/src/androidTest/java/com/pythonnative/android_template/ExampleInstrumentedTest.kt +0 -0
- {pythonnative-0.19.0 → pythonnative-0.20.0}/src/pythonnative/templates/android_template/app/src/main/AndroidManifest.xml +0 -0
- {pythonnative-0.19.0 → pythonnative-0.20.0}/src/pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/MainActivity.kt +0 -0
- {pythonnative-0.19.0 → pythonnative-0.20.0}/src/pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/Navigator.kt +0 -0
- {pythonnative-0.19.0 → pythonnative-0.20.0}/src/pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/PNVirtualListView.java +0 -0
- {pythonnative-0.19.0 → pythonnative-0.20.0}/src/pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/ScreenFragment.kt +0 -0
- {pythonnative-0.19.0 → pythonnative-0.20.0}/src/pythonnative/templates/android_template/app/src/main/res/drawable/ic_launcher_background.xml +0 -0
- {pythonnative-0.19.0 → pythonnative-0.20.0}/src/pythonnative/templates/android_template/app/src/main/res/drawable-v24/ic_launcher_foreground.xml +0 -0
- {pythonnative-0.19.0 → pythonnative-0.20.0}/src/pythonnative/templates/android_template/app/src/main/res/layout/activity_main.xml +0 -0
- {pythonnative-0.19.0 → pythonnative-0.20.0}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +0 -0
- {pythonnative-0.19.0 → pythonnative-0.20.0}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +0 -0
- {pythonnative-0.19.0 → pythonnative-0.20.0}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-hdpi/ic_launcher.webp +0 -0
- {pythonnative-0.19.0 → pythonnative-0.20.0}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp +0 -0
- {pythonnative-0.19.0 → pythonnative-0.20.0}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-mdpi/ic_launcher.webp +0 -0
- {pythonnative-0.19.0 → pythonnative-0.20.0}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp +0 -0
- {pythonnative-0.19.0 → pythonnative-0.20.0}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-xhdpi/ic_launcher.webp +0 -0
- {pythonnative-0.19.0 → pythonnative-0.20.0}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp +0 -0
- {pythonnative-0.19.0 → pythonnative-0.20.0}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp +0 -0
- {pythonnative-0.19.0 → pythonnative-0.20.0}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp +0 -0
- {pythonnative-0.19.0 → pythonnative-0.20.0}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp +0 -0
- {pythonnative-0.19.0 → pythonnative-0.20.0}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp +0 -0
- {pythonnative-0.19.0 → pythonnative-0.20.0}/src/pythonnative/templates/android_template/app/src/main/res/navigation/nav_graph.xml +0 -0
- {pythonnative-0.19.0 → pythonnative-0.20.0}/src/pythonnative/templates/android_template/app/src/main/res/values/colors.xml +0 -0
- {pythonnative-0.19.0 → pythonnative-0.20.0}/src/pythonnative/templates/android_template/app/src/main/res/values/strings.xml +0 -0
- {pythonnative-0.19.0 → pythonnative-0.20.0}/src/pythonnative/templates/android_template/app/src/main/res/values/themes.xml +0 -0
- {pythonnative-0.19.0 → pythonnative-0.20.0}/src/pythonnative/templates/android_template/app/src/main/res/values-night/themes.xml +0 -0
- {pythonnative-0.19.0 → pythonnative-0.20.0}/src/pythonnative/templates/android_template/app/src/main/res/xml/backup_rules.xml +0 -0
- {pythonnative-0.19.0 → pythonnative-0.20.0}/src/pythonnative/templates/android_template/app/src/main/res/xml/data_extraction_rules.xml +0 -0
- {pythonnative-0.19.0 → pythonnative-0.20.0}/src/pythonnative/templates/android_template/app/src/test/java/com/pythonnative/android_template/ExampleUnitTest.kt +0 -0
- {pythonnative-0.19.0 → pythonnative-0.20.0}/src/pythonnative/templates/android_template/build.gradle +0 -0
- {pythonnative-0.19.0 → pythonnative-0.20.0}/src/pythonnative/templates/android_template/gradle/wrapper/gradle-wrapper.jar +0 -0
- {pythonnative-0.19.0 → pythonnative-0.20.0}/src/pythonnative/templates/android_template/gradle/wrapper/gradle-wrapper.properties +0 -0
- {pythonnative-0.19.0 → pythonnative-0.20.0}/src/pythonnative/templates/android_template/gradle.properties +0 -0
- {pythonnative-0.19.0 → pythonnative-0.20.0}/src/pythonnative/templates/android_template/gradlew +0 -0
- {pythonnative-0.19.0 → pythonnative-0.20.0}/src/pythonnative/templates/android_template/gradlew.bat +0 -0
- {pythonnative-0.19.0 → pythonnative-0.20.0}/src/pythonnative/templates/android_template/settings.gradle +0 -0
- {pythonnative-0.19.0 → pythonnative-0.20.0}/src/pythonnative/templates/ios_template/ios_template/AppDelegate.swift +0 -0
- {pythonnative-0.19.0 → pythonnative-0.20.0}/src/pythonnative/templates/ios_template/ios_template/Assets.xcassets/AccentColor.colorset/Contents.json +0 -0
- {pythonnative-0.19.0 → pythonnative-0.20.0}/src/pythonnative/templates/ios_template/ios_template/Assets.xcassets/AppIcon.appiconset/Contents.json +0 -0
- {pythonnative-0.19.0 → pythonnative-0.20.0}/src/pythonnative/templates/ios_template/ios_template/Assets.xcassets/Contents.json +0 -0
- {pythonnative-0.19.0 → pythonnative-0.20.0}/src/pythonnative/templates/ios_template/ios_template/Base.lproj/LaunchScreen.storyboard +0 -0
- {pythonnative-0.19.0 → pythonnative-0.20.0}/src/pythonnative/templates/ios_template/ios_template/Base.lproj/Main.storyboard +0 -0
- {pythonnative-0.19.0 → pythonnative-0.20.0}/src/pythonnative/templates/ios_template/ios_template/Info.plist +0 -0
- {pythonnative-0.19.0 → pythonnative-0.20.0}/src/pythonnative/templates/ios_template/ios_template/SceneDelegate.swift +0 -0
- {pythonnative-0.19.0 → pythonnative-0.20.0}/src/pythonnative/templates/ios_template/ios_template/ViewController.swift +0 -0
- {pythonnative-0.19.0 → pythonnative-0.20.0}/src/pythonnative/templates/ios_template/ios_template.xcodeproj/project.pbxproj +0 -0
- {pythonnative-0.19.0 → pythonnative-0.20.0}/src/pythonnative/templates/ios_template/ios_template.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +0 -0
- {pythonnative-0.19.0 → pythonnative-0.20.0}/src/pythonnative/templates/ios_template/ios_templateTests/ios_templateTests.swift +0 -0
- {pythonnative-0.19.0 → pythonnative-0.20.0}/src/pythonnative/templates/ios_template/ios_templateUITests/ios_templateUITests.swift +0 -0
- {pythonnative-0.19.0 → pythonnative-0.20.0}/src/pythonnative/templates/ios_template/ios_templateUITests/ios_templateUITestsLaunchTests.swift +0 -0
- {pythonnative-0.19.0 → pythonnative-0.20.0}/src/pythonnative/utils.py +0 -0
- {pythonnative-0.19.0 → pythonnative-0.20.0}/src/pythonnative.egg-info/dependency_links.txt +0 -0
- {pythonnative-0.19.0 → pythonnative-0.20.0}/src/pythonnative.egg-info/entry_points.txt +0 -0
- {pythonnative-0.19.0 → pythonnative-0.20.0}/src/pythonnative.egg-info/requires.txt +0 -0
- {pythonnative-0.19.0 → pythonnative-0.20.0}/src/pythonnative.egg-info/top_level.txt +0 -0
- {pythonnative-0.19.0 → pythonnative-0.20.0}/tests/test_alert.py +0 -0
- {pythonnative-0.19.0 → pythonnative-0.20.0}/tests/test_animated.py +0 -0
- {pythonnative-0.19.0 → pythonnative-0.20.0}/tests/test_async_hooks.py +0 -0
- {pythonnative-0.19.0 → pythonnative-0.20.0}/tests/test_cli.py +0 -0
- {pythonnative-0.19.0 → pythonnative-0.20.0}/tests/test_components.py +0 -0
- {pythonnative-0.19.0 → pythonnative-0.20.0}/tests/test_desktop_backend.py +0 -0
- {pythonnative-0.19.0 → pythonnative-0.20.0}/tests/test_element.py +0 -0
- {pythonnative-0.19.0 → pythonnative-0.20.0}/tests/test_extended_components.py +0 -0
- {pythonnative-0.19.0 → pythonnative-0.20.0}/tests/test_hooks.py +0 -0
- {pythonnative-0.19.0 → pythonnative-0.20.0}/tests/test_hot_reload.py +0 -0
- {pythonnative-0.19.0 → pythonnative-0.20.0}/tests/test_ios_log.py +0 -0
- {pythonnative-0.19.0 → pythonnative-0.20.0}/tests/test_layout.py +0 -0
- {pythonnative-0.19.0 → pythonnative-0.20.0}/tests/test_metric_hooks.py +0 -0
- {pythonnative-0.19.0 → pythonnative-0.20.0}/tests/test_native_modules.py +0 -0
- {pythonnative-0.19.0 → pythonnative-0.20.0}/tests/test_native_views.py +0 -0
- {pythonnative-0.19.0 → pythonnative-0.20.0}/tests/test_navigation.py +0 -0
- {pythonnative-0.19.0 → pythonnative-0.20.0}/tests/test_net.py +0 -0
- {pythonnative-0.19.0 → pythonnative-0.20.0}/tests/test_new_components.py +0 -0
- {pythonnative-0.19.0 → pythonnative-0.20.0}/tests/test_platform.py +0 -0
- {pythonnative-0.19.0 → pythonnative-0.20.0}/tests/test_platform_metrics.py +0 -0
- {pythonnative-0.19.0 → pythonnative-0.20.0}/tests/test_reconciler.py +0 -0
- {pythonnative-0.19.0 → pythonnative-0.20.0}/tests/test_ref.py +0 -0
- {pythonnative-0.19.0 → pythonnative-0.20.0}/tests/test_runtime.py +0 -0
- {pythonnative-0.19.0 → pythonnative-0.20.0}/tests/test_screen.py +0 -0
- {pythonnative-0.19.0 → pythonnative-0.20.0}/tests/test_sdk.py +0 -0
- {pythonnative-0.19.0 → pythonnative-0.20.0}/tests/test_smoke.py +0 -0
- {pythonnative-0.19.0 → pythonnative-0.20.0}/tests/test_storage.py +0 -0
- {pythonnative-0.19.0 → pythonnative-0.20.0}/tests/test_style.py +0 -0
- {pythonnative-0.19.0 → pythonnative-0.20.0}/tests/test_utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pythonnative
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.20.0
|
|
4
4
|
Summary: Cross-platform native UI toolkit for Android and iOS
|
|
5
5
|
Author: Owen Carey
|
|
6
6
|
License: MIT License
|
|
@@ -102,7 +102,7 @@ PythonNative is a cross-platform toolkit for building native Android and iOS app
|
|
|
102
102
|
- **Hooks and function components:** Manage state with `use_state`, side effects with `use_effect`, and navigation with `use_navigation`, all through one consistent pattern.
|
|
103
103
|
- **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`.
|
|
104
104
|
- **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.
|
|
105
|
-
- **Virtual view tree + reconciler:** Element trees are diffed and patched with minimal native mutations, similar to React's reconciliation.
|
|
105
|
+
- **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.
|
|
106
106
|
- **Direct native bindings:** Python calls platform APIs directly through Chaquopy and rubicon-objc, with no JavaScript bridge.
|
|
107
107
|
- **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.
|
|
108
108
|
- **CLI scaffolding:** `pn init` creates a ready-to-run project; `pn run android` and `pn run ios` build and launch your app.
|
|
@@ -36,7 +36,7 @@ PythonNative is a cross-platform toolkit for building native Android and iOS app
|
|
|
36
36
|
- **Hooks and function components:** Manage state with `use_state`, side effects with `use_effect`, and navigation with `use_navigation`, all through one consistent pattern.
|
|
37
37
|
- **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`.
|
|
38
38
|
- **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.
|
|
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.
|
|
40
40
|
- **Direct native bindings:** Python calls platform APIs directly through Chaquopy and rubicon-objc, with no JavaScript bridge.
|
|
41
41
|
- **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.
|
|
42
42
|
- **CLI scaffolding:** `pn init` creates a ready-to-run project; `pn run android` and `pn run ios` build and launch your app.
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "pythonnative"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.20.0"
|
|
8
8
|
description = "Cross-platform native UI toolkit for Android and iOS"
|
|
9
9
|
authors = [
|
|
10
10
|
{ name = "Owen Carey" }
|
|
@@ -72,6 +72,7 @@ Documentation = "https://docs.pythonnative.com/"
|
|
|
72
72
|
|
|
73
73
|
|
|
74
74
|
|
|
75
|
+
|
|
75
76
|
[tool.setuptools.packages.find]
|
|
76
77
|
where = ["src"]
|
|
77
78
|
|
|
@@ -76,6 +76,8 @@ class HookState:
|
|
|
76
76
|
"_trigger_render",
|
|
77
77
|
"_pending_effects",
|
|
78
78
|
"_dirty",
|
|
79
|
+
"_vnode",
|
|
80
|
+
"_reconciler",
|
|
79
81
|
)
|
|
80
82
|
|
|
81
83
|
def __init__(self) -> None:
|
|
@@ -95,6 +97,13 @@ class HookState:
|
|
|
95
97
|
# knows that a memoized component still needs to re-render even
|
|
96
98
|
# when its props didn't change.
|
|
97
99
|
self._dirty: bool = False
|
|
100
|
+
# Back-references wired by the reconciler so a state setter can
|
|
101
|
+
# mark *its own* component subtree dirty for a local re-render
|
|
102
|
+
# (instead of forcing a whole-app re-render from the root). Both
|
|
103
|
+
# stay ``None`` until the component is mounted, and are cleared
|
|
104
|
+
# again when it unmounts.
|
|
105
|
+
self._vnode: Any = None
|
|
106
|
+
self._reconciler: Any = None
|
|
98
107
|
|
|
99
108
|
def reset_index(self) -> None:
|
|
100
109
|
"""Reset every per-hook cursor to ``0``.
|
|
@@ -182,6 +191,25 @@ def _schedule_trigger(trigger: Callable[[], None]) -> None:
|
|
|
182
191
|
trigger()
|
|
183
192
|
|
|
184
193
|
|
|
194
|
+
def _notify_state_changed(ctx: "HookState") -> None:
|
|
195
|
+
"""Mark ``ctx``'s component dirty and schedule a render after a state change.
|
|
196
|
+
|
|
197
|
+
Enqueuing the owning ``VNode`` in the reconciler's dirty set is what
|
|
198
|
+
makes the subsequent render *local*: the screen host's trigger calls
|
|
199
|
+
``flush_dirty``, which re-renders only the components marked here
|
|
200
|
+
rather than the whole app. The dirty mark is eager (so several
|
|
201
|
+
setters coalesce), while the render trigger respects
|
|
202
|
+
[`batch_updates`][pythonnative.batch_updates].
|
|
203
|
+
"""
|
|
204
|
+
ctx._dirty = True
|
|
205
|
+
reconciler = ctx._reconciler
|
|
206
|
+
vnode = ctx._vnode
|
|
207
|
+
if reconciler is not None and vnode is not None:
|
|
208
|
+
reconciler.mark_dirty(vnode)
|
|
209
|
+
if ctx._trigger_render:
|
|
210
|
+
_schedule_trigger(ctx._trigger_render)
|
|
211
|
+
|
|
212
|
+
|
|
185
213
|
@contextmanager
|
|
186
214
|
def batch_updates() -> Generator[None, None, None]:
|
|
187
215
|
"""Coalesce multiple state updates into a single re-render.
|
|
@@ -272,9 +300,7 @@ def use_state(initial: Any = None) -> Tuple[Any, Callable]:
|
|
|
272
300
|
new_value = new_value(ctx.states[idx])
|
|
273
301
|
if ctx.states[idx] is not new_value and ctx.states[idx] != new_value:
|
|
274
302
|
ctx.states[idx] = new_value
|
|
275
|
-
ctx
|
|
276
|
-
if ctx._trigger_render:
|
|
277
|
-
_schedule_trigger(ctx._trigger_render)
|
|
303
|
+
_notify_state_changed(ctx)
|
|
278
304
|
|
|
279
305
|
return current, setter
|
|
280
306
|
|
|
@@ -339,9 +365,7 @@ def use_reducer(reducer: Callable[[Any, Any], Any], initial_state: Any) -> Tuple
|
|
|
339
365
|
new_state = reducer(ctx.states[idx], action)
|
|
340
366
|
if ctx.states[idx] is not new_state and ctx.states[idx] != new_state:
|
|
341
367
|
ctx.states[idx] = new_state
|
|
342
|
-
ctx
|
|
343
|
-
if ctx._trigger_render:
|
|
344
|
-
_schedule_trigger(ctx._trigger_render)
|
|
368
|
+
_notify_state_changed(ctx)
|
|
345
369
|
|
|
346
370
|
return current, dispatch
|
|
347
371
|
|
|
@@ -30,7 +30,7 @@ Supports:
|
|
|
30
30
|
|
|
31
31
|
import os
|
|
32
32
|
import sys
|
|
33
|
-
from typing import Any, List, Optional, Tuple
|
|
33
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
34
34
|
|
|
35
35
|
from .element import Element
|
|
36
36
|
from .layout import LayoutNode, calculate_layout, extract_layout_style
|
|
@@ -106,19 +106,44 @@ class VNode:
|
|
|
106
106
|
or an iOS `UIView`). May be `None` for purely virtual
|
|
107
107
|
wrappers such as providers and error boundaries.
|
|
108
108
|
children: Ordered list of child `VNode` instances.
|
|
109
|
+
parent: The owning `VNode`, or `None` for the tree root. Used
|
|
110
|
+
by local (component-scoped) re-renders to bubble a changed
|
|
111
|
+
native view up to the nearest native container.
|
|
109
112
|
hook_state: The component's
|
|
110
113
|
[`HookState`][pythonnative.hooks.HookState] when the node
|
|
111
114
|
wraps a function component, otherwise `None`.
|
|
115
|
+
mounted: `False` once the node has been destroyed, so stale
|
|
116
|
+
entries in the reconciler's dirty set are skipped.
|
|
112
117
|
"""
|
|
113
118
|
|
|
114
|
-
__slots__ = (
|
|
119
|
+
__slots__ = (
|
|
120
|
+
"element",
|
|
121
|
+
"native_view",
|
|
122
|
+
"children",
|
|
123
|
+
"parent",
|
|
124
|
+
"hook_state",
|
|
125
|
+
"mounted",
|
|
126
|
+
"_rendered",
|
|
127
|
+
"_measure_cache",
|
|
128
|
+
)
|
|
115
129
|
|
|
116
130
|
def __init__(self, element: Element, native_view: Any, children: List["VNode"]) -> None:
|
|
117
131
|
self.element = element
|
|
118
132
|
self.native_view = native_view
|
|
119
133
|
self.children = children
|
|
134
|
+
self.parent: Optional["VNode"] = None
|
|
120
135
|
self.hook_state: Any = None
|
|
136
|
+
self.mounted: bool = True
|
|
121
137
|
self._rendered: Optional[Element] = None
|
|
138
|
+
# Cache for the leaf intrinsic-size measure callback:
|
|
139
|
+
# ``(element, max_w, max_h, width, height)``. Lets the layout
|
|
140
|
+
# pass skip native ``measure_intrinsic`` calls for leaves whose
|
|
141
|
+
# element *object* (compared by identity) and constraints are
|
|
142
|
+
# unchanged since the last measure. Because untouched components
|
|
143
|
+
# keep their exact ``Element`` instances across a local
|
|
144
|
+
# re-render, this turns "re-measure everything every layout pass"
|
|
145
|
+
# into "re-measure only what actually re-rendered".
|
|
146
|
+
self._measure_cache: Optional[Tuple[Any, float, float, float, float]] = None
|
|
122
147
|
|
|
123
148
|
|
|
124
149
|
class Reconciler:
|
|
@@ -142,6 +167,11 @@ class Reconciler:
|
|
|
142
167
|
self._screen_re_render: Optional[Any] = None
|
|
143
168
|
self._viewport_size: Tuple[float, float] = (0.0, 0.0)
|
|
144
169
|
self._layout_pass = 0
|
|
170
|
+
# Function-component VNodes whose own state changed since the
|
|
171
|
+
# last flush, keyed by ``id`` to dedupe while keeping a strong
|
|
172
|
+
# reference. Drained by
|
|
173
|
+
# [`flush_dirty`][pythonnative.reconciler.Reconciler.flush_dirty].
|
|
174
|
+
self._dirty_nodes: Dict[int, VNode] = {}
|
|
145
175
|
|
|
146
176
|
# ------------------------------------------------------------------
|
|
147
177
|
# Public API
|
|
@@ -160,6 +190,7 @@ class Reconciler:
|
|
|
160
190
|
self._log_viewport(
|
|
161
191
|
f"mount: start type={self._type_label(element.type)!r} props={self._props_debug(element.props)}"
|
|
162
192
|
)
|
|
193
|
+
self._dirty_nodes.clear()
|
|
163
194
|
self._tree = self._create_tree(element)
|
|
164
195
|
self._log_viewport(f"mount: tree created root={self._node_debug(self._tree)}")
|
|
165
196
|
self._flush_effects()
|
|
@@ -181,6 +212,9 @@ class Reconciler:
|
|
|
181
212
|
f"(have_tree={self._tree is not None}) new_type={self._type_label(new_element.type)!r} "
|
|
182
213
|
f"new_props={self._props_debug(new_element.props)}"
|
|
183
214
|
)
|
|
215
|
+
# A full reconcile rebuilds the whole tree from the root, so any
|
|
216
|
+
# pending per-component dirty marks are now obsolete.
|
|
217
|
+
self._dirty_nodes.clear()
|
|
184
218
|
if self._tree is None:
|
|
185
219
|
self._tree = self._create_tree(new_element)
|
|
186
220
|
self._log_viewport(f"reconcile: created initial root={self._node_debug(self._tree)}")
|
|
@@ -195,6 +229,221 @@ class Reconciler:
|
|
|
195
229
|
self._log_viewport("reconcile: done")
|
|
196
230
|
return self._tree.native_view
|
|
197
231
|
|
|
232
|
+
def root_view(self) -> Any:
|
|
233
|
+
"""Return the current root native view, or ``None`` before mount."""
|
|
234
|
+
return self._tree.native_view if self._tree is not None else None
|
|
235
|
+
|
|
236
|
+
def mark_dirty(self, vnode: "VNode") -> None:
|
|
237
|
+
"""Queue ``vnode`` (a function component) for a local re-render.
|
|
238
|
+
|
|
239
|
+
Called by a component's ``use_state`` / ``use_reducer`` setter
|
|
240
|
+
when its own state changes. The node is re-rendered on the next
|
|
241
|
+
[`flush_dirty`][pythonnative.reconciler.Reconciler.flush_dirty]
|
|
242
|
+
pass, which the screen host schedules. Marking is idempotent and
|
|
243
|
+
cheap; the actual render is deferred so several setters (e.g.
|
|
244
|
+
inside [`batch_updates`][pythonnative.batch_updates]) coalesce
|
|
245
|
+
into a single pass.
|
|
246
|
+
"""
|
|
247
|
+
if vnode is None or vnode.hook_state is None or not vnode.mounted:
|
|
248
|
+
return
|
|
249
|
+
self._dirty_nodes[id(vnode)] = vnode
|
|
250
|
+
|
|
251
|
+
def flush_dirty(self) -> Any:
|
|
252
|
+
"""Re-render only the component subtrees marked dirty since the last pass.
|
|
253
|
+
|
|
254
|
+
This is the hot path for state-driven updates: instead of
|
|
255
|
+
re-running the whole app from the root, each dirty function
|
|
256
|
+
component re-runs its own body (reusing its
|
|
257
|
+
[`HookState`][pythonnative.hooks.HookState]) and reconciles just
|
|
258
|
+
its subtree. Nodes are processed shallowest-first so that when a
|
|
259
|
+
dirty ancestor's re-render already covers a dirty descendant, the
|
|
260
|
+
descendant is skipped (its ``_dirty`` flag is cleared by the
|
|
261
|
+
ancestor pass).
|
|
262
|
+
|
|
263
|
+
Returns:
|
|
264
|
+
The (possibly replaced) root native view, so the host can
|
|
265
|
+
re-attach it if the root changed.
|
|
266
|
+
"""
|
|
267
|
+
if self._tree is None:
|
|
268
|
+
return None
|
|
269
|
+
if not self._dirty_nodes:
|
|
270
|
+
return self._tree.native_view
|
|
271
|
+
|
|
272
|
+
pending = list(self._dirty_nodes.values())
|
|
273
|
+
self._dirty_nodes.clear()
|
|
274
|
+
pending.sort(key=self._node_depth)
|
|
275
|
+
for vnode in pending:
|
|
276
|
+
if not vnode.mounted:
|
|
277
|
+
continue
|
|
278
|
+
hook_state = vnode.hook_state
|
|
279
|
+
if hook_state is None or not hook_state._dirty:
|
|
280
|
+
# Already re-rendered as part of a dirty ancestor's pass.
|
|
281
|
+
continue
|
|
282
|
+
try:
|
|
283
|
+
self._update_component(vnode)
|
|
284
|
+
except Exception as exc:
|
|
285
|
+
# A local re-render starts below any enclosing
|
|
286
|
+
# ``ErrorBoundary``, so route the failure to the nearest
|
|
287
|
+
# boundary ancestor (re-rendering its subtree through the
|
|
288
|
+
# boundary, which mounts the fallback). With no boundary
|
|
289
|
+
# the exception propagates, matching a full render.
|
|
290
|
+
self._handle_local_render_error(vnode, exc)
|
|
291
|
+
|
|
292
|
+
self._flush_effects()
|
|
293
|
+
self._run_layout()
|
|
294
|
+
return self._tree.native_view
|
|
295
|
+
|
|
296
|
+
@staticmethod
|
|
297
|
+
def _node_depth(vnode: "VNode") -> int:
|
|
298
|
+
depth = 0
|
|
299
|
+
node = vnode.parent
|
|
300
|
+
while node is not None:
|
|
301
|
+
depth += 1
|
|
302
|
+
node = node.parent
|
|
303
|
+
return depth
|
|
304
|
+
|
|
305
|
+
def _update_component(self, vnode: "VNode") -> None:
|
|
306
|
+
"""Re-run one function component's body and reconcile its subtree in place.
|
|
307
|
+
|
|
308
|
+
Unlike a full reconcile from the root, a local update starts in
|
|
309
|
+
the *middle* of the tree, so the context stack of every
|
|
310
|
+
``__Provider__`` ancestor must be re-established before the body
|
|
311
|
+
runs (otherwise [`use_context`][pythonnative.use_context] — and
|
|
312
|
+
therefore [`use_navigation`][pythonnative.use_navigation] — would
|
|
313
|
+
read the context default instead of the provided value). Nested
|
|
314
|
+
providers *inside* this subtree are pushed/popped normally by the
|
|
315
|
+
recursive reconcile beneath us.
|
|
316
|
+
"""
|
|
317
|
+
from .hooks import _set_hook_state
|
|
318
|
+
|
|
319
|
+
new_el = vnode.element
|
|
320
|
+
if not callable(new_el.type):
|
|
321
|
+
return
|
|
322
|
+
hook_state = vnode.hook_state
|
|
323
|
+
if hook_state is None:
|
|
324
|
+
return
|
|
325
|
+
|
|
326
|
+
providers = self._ancestor_providers(vnode)
|
|
327
|
+
for context, value in providers:
|
|
328
|
+
context._stack.append(value)
|
|
329
|
+
try:
|
|
330
|
+
hook_state.reset_index()
|
|
331
|
+
hook_state._trigger_render = self._screen_re_render
|
|
332
|
+
hook_state._vnode = vnode
|
|
333
|
+
hook_state._reconciler = self
|
|
334
|
+
_set_hook_state(hook_state)
|
|
335
|
+
try:
|
|
336
|
+
rendered = new_el.type(**new_el.props)
|
|
337
|
+
finally:
|
|
338
|
+
_set_hook_state(None)
|
|
339
|
+
hook_state._dirty = False
|
|
340
|
+
|
|
341
|
+
old_native = vnode.native_view
|
|
342
|
+
if vnode.children:
|
|
343
|
+
child = self._reconcile_node(vnode.children[0], rendered)
|
|
344
|
+
else:
|
|
345
|
+
child = self._create_tree(rendered)
|
|
346
|
+
finally:
|
|
347
|
+
for context, _value in reversed(providers):
|
|
348
|
+
context._stack.pop()
|
|
349
|
+
|
|
350
|
+
child.parent = vnode
|
|
351
|
+
vnode.children = [child]
|
|
352
|
+
vnode.native_view = child.native_view
|
|
353
|
+
vnode._rendered = rendered
|
|
354
|
+
|
|
355
|
+
if child.native_view is not old_native:
|
|
356
|
+
self._bubble_native_view_change(vnode, old_native, child.native_view)
|
|
357
|
+
|
|
358
|
+
def _handle_local_render_error(self, vnode: "VNode", exc: Exception) -> None:
|
|
359
|
+
"""Route a local re-render failure to the nearest ``ErrorBoundary`` ancestor.
|
|
360
|
+
|
|
361
|
+
Re-reconciles the boundary against its own element so the throw
|
|
362
|
+
is re-triggered *inside*
|
|
363
|
+
[`_reconcile_error_boundary`][pythonnative.reconciler.Reconciler._reconcile_error_boundary],
|
|
364
|
+
which destroys the failed subtree and mounts the boundary's
|
|
365
|
+
fallback. If no boundary encloses ``vnode`` the exception
|
|
366
|
+
propagates, exactly as it would during a full render.
|
|
367
|
+
"""
|
|
368
|
+
node = vnode.parent
|
|
369
|
+
while node is not None:
|
|
370
|
+
if isinstance(node.element.type, str) and node.element.type == "__ErrorBoundary__":
|
|
371
|
+
old_native = node.native_view
|
|
372
|
+
# Like a local component update, this re-reconcile starts
|
|
373
|
+
# mid-tree, so restore the boundary's own ancestor
|
|
374
|
+
# provider context first.
|
|
375
|
+
providers = self._ancestor_providers(node)
|
|
376
|
+
for context, value in providers:
|
|
377
|
+
context._stack.append(value)
|
|
378
|
+
try:
|
|
379
|
+
self._reconcile_node(node, node.element)
|
|
380
|
+
finally:
|
|
381
|
+
for context, _value in reversed(providers):
|
|
382
|
+
context._stack.pop()
|
|
383
|
+
if node.native_view is not old_native:
|
|
384
|
+
self._bubble_native_view_change(node, old_native, node.native_view)
|
|
385
|
+
return
|
|
386
|
+
node = node.parent
|
|
387
|
+
raise exc
|
|
388
|
+
|
|
389
|
+
@staticmethod
|
|
390
|
+
def _ancestor_providers(vnode: "VNode") -> List[Tuple[Any, Any]]:
|
|
391
|
+
"""Collect ``(context, value)`` for every ``__Provider__`` above ``vnode``.
|
|
392
|
+
|
|
393
|
+
Returned outermost-first so that pushing them in order leaves the
|
|
394
|
+
nearest provider on top of each context's stack (nearest wins,
|
|
395
|
+
matching React).
|
|
396
|
+
"""
|
|
397
|
+
chain: List[Tuple[Any, Any]] = []
|
|
398
|
+
node = vnode.parent
|
|
399
|
+
while node is not None:
|
|
400
|
+
el = node.element
|
|
401
|
+
if isinstance(el.type, str) and el.type == "__Provider__":
|
|
402
|
+
chain.append((el.props["__context__"], el.props["__value__"]))
|
|
403
|
+
node = node.parent
|
|
404
|
+
chain.reverse()
|
|
405
|
+
return chain
|
|
406
|
+
|
|
407
|
+
def _bubble_native_view_change(self, vnode: "VNode", old_native: Any, new_native: Any) -> None:
|
|
408
|
+
"""Propagate a changed subtree-root native view up to its native parent.
|
|
409
|
+
|
|
410
|
+
A local re-render starts below the real native container, so when
|
|
411
|
+
the dirty component's root native view is swapped (e.g. its output
|
|
412
|
+
changed type), the change must be reflected in (a) every
|
|
413
|
+
transparent ancestor that delegated its ``native_view`` to this
|
|
414
|
+
subtree and (b) the nearest native-container ancestor's child list.
|
|
415
|
+
"""
|
|
416
|
+
child = vnode
|
|
417
|
+
node = vnode.parent
|
|
418
|
+
while node is not None:
|
|
419
|
+
if self._is_native_container(node):
|
|
420
|
+
try:
|
|
421
|
+
idx: Optional[int] = node.children.index(child)
|
|
422
|
+
except ValueError:
|
|
423
|
+
idx = None
|
|
424
|
+
if old_native is not None:
|
|
425
|
+
self.backend.remove_child(node.native_view, old_native, node.element.type)
|
|
426
|
+
if new_native is not None:
|
|
427
|
+
if idx is None:
|
|
428
|
+
self.backend.add_child(node.native_view, new_native, node.element.type)
|
|
429
|
+
else:
|
|
430
|
+
self.backend.insert_child(node.native_view, new_native, node.element.type, idx)
|
|
431
|
+
return
|
|
432
|
+
# Transparent ancestor (component / provider / error boundary /
|
|
433
|
+
# fragment) delegates its native view to this subtree.
|
|
434
|
+
if node.native_view is old_native:
|
|
435
|
+
node.native_view = new_native
|
|
436
|
+
child = node
|
|
437
|
+
node = node.parent
|
|
438
|
+
# Reached the root with no native container above: the root's
|
|
439
|
+
# ``native_view`` was already updated in the loop. The host
|
|
440
|
+
# detects the change by comparing ``root_view()`` after the flush.
|
|
441
|
+
|
|
442
|
+
@staticmethod
|
|
443
|
+
def _is_native_container(node: "VNode") -> bool:
|
|
444
|
+
t = node.element.type
|
|
445
|
+
return isinstance(t, str) and t not in ("__Provider__", "__ErrorBoundary__", "__Fragment__")
|
|
446
|
+
|
|
198
447
|
def set_viewport_size(self, width: float, height: float) -> None:
|
|
199
448
|
"""Update the viewport size and re-run layout if it changed.
|
|
200
449
|
|
|
@@ -288,12 +537,24 @@ class Reconciler:
|
|
|
288
537
|
# ------------------------------------------------------------------
|
|
289
538
|
|
|
290
539
|
def _flush_effects(self) -> None:
|
|
291
|
-
"""Walk the committed tree and flush pending effects (depth-first).
|
|
540
|
+
"""Walk the committed tree and flush pending effects (depth-first).
|
|
541
|
+
|
|
542
|
+
This post-commit walk doubles as the single source of truth for
|
|
543
|
+
``VNode.parent``: every live node's parent pointer is re-linked
|
|
544
|
+
here so that local re-renders
|
|
545
|
+
([`flush_dirty`][pythonnative.reconciler.Reconciler.flush_dirty])
|
|
546
|
+
can compute node depth and bubble native-view changes upward
|
|
547
|
+
without each reconcile path having to maintain parent links by
|
|
548
|
+
hand. The cost is folded into a walk the reconciler already runs
|
|
549
|
+
after every commit.
|
|
550
|
+
"""
|
|
292
551
|
if self._tree is not None:
|
|
552
|
+
self._tree.parent = None
|
|
293
553
|
self._flush_tree_effects(self._tree)
|
|
294
554
|
|
|
295
555
|
def _flush_tree_effects(self, node: VNode) -> None:
|
|
296
556
|
for child in node.children:
|
|
557
|
+
child.parent = node
|
|
297
558
|
self._flush_tree_effects(child)
|
|
298
559
|
if node.hook_state is not None:
|
|
299
560
|
node.hook_state.flush_pending_effects()
|
|
@@ -347,6 +608,8 @@ class Reconciler:
|
|
|
347
608
|
vnode = VNode(element, child_node.native_view, [child_node])
|
|
348
609
|
vnode.hook_state = hook_state
|
|
349
610
|
vnode._rendered = rendered
|
|
611
|
+
hook_state._vnode = vnode
|
|
612
|
+
hook_state._reconciler = self
|
|
350
613
|
return vnode
|
|
351
614
|
|
|
352
615
|
# Native element
|
|
@@ -478,6 +741,8 @@ class Reconciler:
|
|
|
478
741
|
old.element = new_el
|
|
479
742
|
old.hook_state = hook_state
|
|
480
743
|
old._rendered = rendered
|
|
744
|
+
hook_state._vnode = old
|
|
745
|
+
hook_state._reconciler = self
|
|
481
746
|
return old
|
|
482
747
|
|
|
483
748
|
# Native element
|
|
@@ -667,13 +932,25 @@ class Reconciler:
|
|
|
667
932
|
)
|
|
668
933
|
|
|
669
934
|
def _destroy_tree(self, node: VNode) -> None:
|
|
935
|
+
node.mounted = False
|
|
936
|
+
# Drop the node from the pending-render set so a setter that
|
|
937
|
+
# fired moments before unmount can't resurrect a dead subtree.
|
|
938
|
+
self._dirty_nodes.pop(id(node), None)
|
|
670
939
|
if node.hook_state is not None:
|
|
671
940
|
node.hook_state.cleanup_all_effects()
|
|
941
|
+
# Break the back-references so the unmounted component's hook
|
|
942
|
+
# state (and the closures it captured) can be freed by plain
|
|
943
|
+
# refcounting — important on iOS, where the cyclic GC is
|
|
944
|
+
# disabled.
|
|
945
|
+
node.hook_state._vnode = None
|
|
946
|
+
node.hook_state._reconciler = None
|
|
947
|
+
node.hook_state._trigger_render = None
|
|
672
948
|
if node.element is not None:
|
|
673
949
|
self._detach_ref(node.element)
|
|
674
950
|
for child in node.children:
|
|
675
951
|
self._destroy_tree(child)
|
|
676
952
|
node.children = []
|
|
953
|
+
node.parent = None
|
|
677
954
|
|
|
678
955
|
@staticmethod
|
|
679
956
|
def _strip_reconciler_props(props: dict) -> dict:
|
|
@@ -935,6 +1212,10 @@ class Reconciler:
|
|
|
935
1212
|
node_label = self._node_debug(vnode)
|
|
936
1213
|
|
|
937
1214
|
def measure(max_w: float, max_h: float) -> Tuple[float, float]:
|
|
1215
|
+
cache = vnode._measure_cache
|
|
1216
|
+
if cache is not None and cache[0] is vnode.element and cache[1] == max_w and cache[2] == max_h:
|
|
1217
|
+
self._log_viewport(f"measure: cache hit type={type_name!r} result=({cache[3]!r},{cache[4]!r})")
|
|
1218
|
+
return (cache[3], cache[4])
|
|
938
1219
|
try:
|
|
939
1220
|
self._log_viewport(
|
|
940
1221
|
"measure: before backend.measure_intrinsic " f"{node_label} max=({max_w!r},{max_h!r})"
|
|
@@ -942,6 +1223,7 @@ class Reconciler:
|
|
|
942
1223
|
w, h = backend.measure_intrinsic(view, type_name, max_w, max_h)
|
|
943
1224
|
result = (float(w), float(h))
|
|
944
1225
|
self._log_viewport(f"measure: after backend.measure_intrinsic type={type_name!r} result={result!r}")
|
|
1226
|
+
vnode._measure_cache = (vnode.element, max_w, max_h, result[0], result[1])
|
|
945
1227
|
return result
|
|
946
1228
|
except Exception as e:
|
|
947
1229
|
self._log_viewport(
|
|
@@ -322,53 +322,49 @@ def _request_render(host: Any) -> None:
|
|
|
322
322
|
|
|
323
323
|
|
|
324
324
|
def _re_render(host: Any) -> None:
|
|
325
|
-
"""Run one render pass, then drain any renders queued during it.
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
325
|
+
"""Run one *local* render pass, then drain any renders queued during it.
|
|
326
|
+
|
|
327
|
+
State setters mark only their own component subtree dirty (see
|
|
328
|
+
[`mark_dirty`][pythonnative.reconciler.Reconciler.mark_dirty]), so
|
|
329
|
+
this drains the reconciler's dirty set via
|
|
330
|
+
[`flush_dirty`][pythonnative.reconciler.Reconciler.flush_dirty]
|
|
331
|
+
instead of re-running the whole ``App`` from the root. The app's
|
|
332
|
+
element tree is only rebuilt from scratch on mount, navigation, and
|
|
333
|
+
hot reload.
|
|
334
|
+
"""
|
|
335
|
+
_log_pn("_re_render: starting local render pass")
|
|
329
336
|
host._is_rendering = True
|
|
330
337
|
try:
|
|
331
338
|
host._render_queued = False
|
|
332
|
-
|
|
333
|
-
app_element = _render_app(host)
|
|
334
|
-
provider_element = Provider(_NavigationContext, host._nav_handle, app_element)
|
|
335
|
-
|
|
336
|
-
new_root = host._reconciler.reconcile(provider_element)
|
|
337
|
-
if new_root is not host._root_native_view:
|
|
338
|
-
_log_pn(f"_re_render: ROOT VIEW CHANGED ({id(host._root_native_view)} -> {id(new_root)}); reattaching")
|
|
339
|
-
host._detach_root(host._root_native_view)
|
|
340
|
-
host._root_native_view = new_root
|
|
341
|
-
host._attach_root(new_root)
|
|
342
|
-
|
|
339
|
+
_commit_dirty(host)
|
|
343
340
|
_drain_renders(host)
|
|
344
341
|
finally:
|
|
345
342
|
host._is_rendering = False
|
|
346
343
|
_log_pn("_re_render: done")
|
|
347
344
|
|
|
348
345
|
|
|
346
|
+
def _commit_dirty(host: Any) -> None:
|
|
347
|
+
"""Flush the reconciler's dirty components and re-attach the root if it changed."""
|
|
348
|
+
new_root = host._reconciler.flush_dirty()
|
|
349
|
+
if new_root is not host._root_native_view:
|
|
350
|
+
_log_pn(f"_commit_dirty: ROOT VIEW CHANGED ({id(host._root_native_view)} -> {id(new_root)}); reattaching")
|
|
351
|
+
host._detach_root(host._root_native_view)
|
|
352
|
+
host._root_native_view = new_root
|
|
353
|
+
host._attach_root(new_root)
|
|
354
|
+
|
|
355
|
+
|
|
349
356
|
def _drain_renders(host: Any) -> None:
|
|
350
357
|
"""Flush additional renders queued by effects that set state.
|
|
351
358
|
|
|
352
359
|
Capped at `_MAX_RENDER_PASSES` to break runaway feedback loops
|
|
353
360
|
(e.g., an effect that unconditionally calls a setter).
|
|
354
361
|
"""
|
|
355
|
-
from .hooks import Provider, _NavigationContext
|
|
356
|
-
|
|
357
362
|
for i in range(_MAX_RENDER_PASSES):
|
|
358
363
|
if not host._render_queued:
|
|
359
364
|
break
|
|
360
365
|
_log_pn(f"_drain_renders: draining pass #{i + 1}")
|
|
361
366
|
host._render_queued = False
|
|
362
|
-
|
|
363
|
-
app_element = _render_app(host)
|
|
364
|
-
provider_element = Provider(_NavigationContext, host._nav_handle, app_element)
|
|
365
|
-
|
|
366
|
-
new_root = host._reconciler.reconcile(provider_element)
|
|
367
|
-
if new_root is not host._root_native_view:
|
|
368
|
-
_log_pn(f"_drain_renders: ROOT VIEW CHANGED ({id(host._root_native_view)} -> {id(new_root)}); reattaching")
|
|
369
|
-
host._detach_root(host._root_native_view)
|
|
370
|
-
host._root_native_view = new_root
|
|
371
|
-
host._attach_root(new_root)
|
|
367
|
+
_commit_dirty(host)
|
|
372
368
|
|
|
373
369
|
|
|
374
370
|
def _set_args(host: Any, args: Any) -> None:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pythonnative
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.20.0
|
|
4
4
|
Summary: Cross-platform native UI toolkit for Android and iOS
|
|
5
5
|
Author: Owen Carey
|
|
6
6
|
License: MIT License
|
|
@@ -102,7 +102,7 @@ PythonNative is a cross-platform toolkit for building native Android and iOS app
|
|
|
102
102
|
- **Hooks and function components:** Manage state with `use_state`, side effects with `use_effect`, and navigation with `use_navigation`, all through one consistent pattern.
|
|
103
103
|
- **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`.
|
|
104
104
|
- **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.
|
|
105
|
-
- **Virtual view tree + reconciler:** Element trees are diffed and patched with minimal native mutations, similar to React's reconciliation.
|
|
105
|
+
- **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.
|
|
106
106
|
- **Direct native bindings:** Python calls platform APIs directly through Chaquopy and rubicon-objc, with no JavaScript bridge.
|
|
107
107
|
- **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.
|
|
108
108
|
- **CLI scaffolding:** `pn init` creates a ready-to-run project; `pn run android` and `pn run ios` build and launch your app.
|