pythonnative 0.32.0__py3-none-any.whl → 0.34.0__py3-none-any.whl
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/__init__.py +27 -15
- pythonnative/animated.py +7 -13
- pythonnative/cli/pn.py +1 -2
- pythonnative/component.py +255 -0
- pythonnative/components/__init__.py +106 -0
- pythonnative/components/_base.py +132 -0
- pythonnative/components/controls.py +515 -0
- pythonnative/components/layout.py +459 -0
- pythonnative/components/lists.py +809 -0
- pythonnative/components/media.py +204 -0
- pythonnative/components/overlays.py +107 -0
- pythonnative/components/pressable.py +252 -0
- pythonnative/components/structural.py +167 -0
- pythonnative/components/text.py +291 -0
- pythonnative/diagnostics.py +1 -1
- pythonnative/element.py +108 -29
- pythonnative/gestures.py +1 -1
- pythonnative/hooks.py +366 -671
- pythonnative/hosts/__init__.py +85 -0
- pythonnative/hosts/android.py +269 -0
- pythonnative/hosts/base.py +665 -0
- pythonnative/hosts/desktop.py +107 -0
- pythonnative/hosts/ios.py +404 -0
- pythonnative/hot_reload.py +16 -24
- pythonnative/layout.py +372 -66
- pythonnative/native_modules/__init__.py +23 -0
- pythonnative/native_modules/net_info.py +5 -1
- pythonnative/native_views/__init__.py +1 -1
- pythonnative/native_views/android.py +50 -6
- pythonnative/native_views/base.py +64 -1
- pythonnative/native_views/desktop.py +11 -3
- pythonnative/native_views/ios.py +72 -12
- pythonnative/navigation/__init__.py +102 -0
- pythonnative/navigation/container.py +154 -0
- pythonnative/navigation/handle.py +573 -0
- pythonnative/navigation/hooks.py +94 -0
- pythonnative/navigation/host.py +58 -0
- pythonnative/navigation/linking.py +200 -0
- pythonnative/navigation/navigators.py +637 -0
- pythonnative/navigation/screen.py +149 -0
- pythonnative/navigation/state.py +248 -0
- pythonnative/net.py +4 -0
- pythonnative/platform_metrics.py +1 -1
- pythonnative/preview.py +21 -19
- pythonnative/project/android.py +7 -1
- pythonnative/project/doctor.py +1 -1
- pythonnative/project/ios.py +1 -0
- pythonnative/project/runtime_assets.py +1 -1
- pythonnative/reconciler/__init__.py +29 -0
- pythonnative/reconciler/boundaries.py +365 -0
- pythonnative/reconciler/children.py +88 -0
- pythonnative/reconciler/core.py +1153 -0
- pythonnative/reconciler/layout_pass.py +359 -0
- pythonnative/reconciler/vnode.py +266 -0
- pythonnative/scheduler.py +159 -0
- pythonnative/sdk/_components.py +2 -4
- pythonnative/style.py +95 -13
- pythonnative/suspense.py +8 -12
- pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/MainActivity.kt +85 -1
- pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/ScreenFragment.kt +6 -7
- pythonnative/templates/android_template/app/src/main/res/navigation/nav_graph.xml +1 -1
- pythonnative/templates/android_template/app/src/main/res/values/strings.xml +3 -1
- pythonnative/templates/ios_template/ios_template/AppDelegate.swift +33 -0
- pythonnative/templates/ios_template/ios_template/PythonRuntime.swift +1 -1
- pythonnative/templates/ios_template/ios_template/ViewController.swift +12 -10
- pythonnative/testing/__init__.py +53 -0
- pythonnative/testing/backend.py +276 -0
- pythonnative/testing/harness.py +391 -0
- {pythonnative-0.32.0.dist-info → pythonnative-0.34.0.dist-info}/METADATA +2 -21
- {pythonnative-0.32.0.dist-info → pythonnative-0.34.0.dist-info}/RECORD +74 -43
- pythonnative/components.py +0 -2856
- pythonnative/navigation.py +0 -1031
- pythonnative/reconciler.py +0 -2298
- pythonnative/screen.py +0 -2068
- {pythonnative-0.32.0.dist-info → pythonnative-0.34.0.dist-info}/WHEEL +0 -0
- {pythonnative-0.32.0.dist-info → pythonnative-0.34.0.dist-info}/entry_points.txt +0 -0
- {pythonnative-0.32.0.dist-info → pythonnative-0.34.0.dist-info}/licenses/LICENSE +0 -0
- {pythonnative-0.32.0.dist-info → pythonnative-0.34.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""Screen hosts: the bridge between a native screen and the reconciler.
|
|
2
|
+
|
|
3
|
+
Native templates create one host per screen:
|
|
4
|
+
|
|
5
|
+
```python
|
|
6
|
+
host = pythonnative.hosts.create_screen("app.main", native_instance, args_json)
|
|
7
|
+
host.on_create()
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
and forward lifecycle events to it. The concrete class depends on the
|
|
11
|
+
runtime ([`AndroidScreenHost`][pythonnative.hosts.android.AndroidScreenHost],
|
|
12
|
+
[`IOSScreenHost`][pythonnative.hosts.ios.IOSScreenHost],
|
|
13
|
+
[`DesktopScreenHost`][pythonnative.hosts.desktop.DesktopScreenHost]);
|
|
14
|
+
the headless base [`ScreenHost`][pythonnative.hosts.ScreenHost] is used
|
|
15
|
+
in unit tests with a fake backend.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from typing import Any, Optional, Type
|
|
21
|
+
|
|
22
|
+
from ..utils import IS_ANDROID, IS_DESKTOP, IS_IOS
|
|
23
|
+
from .base import ScreenHost, import_component
|
|
24
|
+
|
|
25
|
+
__all__ = ["ScreenHost", "create_screen", "host_class", "import_component"]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def host_class() -> Type[ScreenHost]:
|
|
29
|
+
"""The host class for the current runtime."""
|
|
30
|
+
if IS_ANDROID:
|
|
31
|
+
from .android import AndroidScreenHost
|
|
32
|
+
|
|
33
|
+
return AndroidScreenHost
|
|
34
|
+
if IS_DESKTOP:
|
|
35
|
+
from .desktop import DesktopScreenHost
|
|
36
|
+
|
|
37
|
+
return DesktopScreenHost
|
|
38
|
+
if IS_IOS:
|
|
39
|
+
from .ios import IOSScreenHost
|
|
40
|
+
|
|
41
|
+
return IOSScreenHost
|
|
42
|
+
return ScreenHost
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def create_screen(component_path: str, native_instance: Any = None, args_json: Optional[str] = None) -> ScreenHost:
|
|
46
|
+
"""Create the screen host for a root component.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
component_path: ``"app.main"`` (the module's ``App`` is used) or a
|
|
50
|
+
dotted path like ``"app.main.RootScreen"``. Imported lazily
|
|
51
|
+
so the dev server can reload it.
|
|
52
|
+
native_instance: The platform object owning the screen
|
|
53
|
+
(``Activity``, ``UIViewController`` pointer, ``DesktopApp``).
|
|
54
|
+
args_json: Optional JSON launch arguments (pushed screens
|
|
55
|
+
receive their navigation history here).
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
A host ready for ``on_create`` and the other lifecycle calls.
|
|
59
|
+
"""
|
|
60
|
+
component = import_component(component_path)
|
|
61
|
+
host = host_class()(native_instance, component_path, component)
|
|
62
|
+
if args_json:
|
|
63
|
+
host.set_args(args_json)
|
|
64
|
+
return host
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def drain_ios_scheduled_renders() -> None:
|
|
68
|
+
"""Drain deferred iOS renders (called by the Swift template on the main thread)."""
|
|
69
|
+
from .ios import drain_ios_scheduled_renders as drain
|
|
70
|
+
|
|
71
|
+
drain()
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def forward_lifecycle(native_addr: int, event: str) -> None:
|
|
75
|
+
"""Forward a Swift view-controller lifecycle event to its host."""
|
|
76
|
+
from .ios import forward_lifecycle as forward
|
|
77
|
+
|
|
78
|
+
forward(native_addr, event)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def drain_desktop_scheduled_renders() -> None:
|
|
82
|
+
"""Drain deferred desktop renders (called by the preview's Tk loop)."""
|
|
83
|
+
from .desktop import drain_desktop_scheduled_renders as drain
|
|
84
|
+
|
|
85
|
+
drain()
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
"""Android screen host: a fragment inside the template's ``NavHostFragment``."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import threading
|
|
7
|
+
from typing import Any, Dict, Optional, Tuple
|
|
8
|
+
|
|
9
|
+
from java import dynamic_proxy, jclass
|
|
10
|
+
|
|
11
|
+
from ..utils import get_android_fragment_container, set_android_context
|
|
12
|
+
from .base import ScreenHost, flush_hosts
|
|
13
|
+
|
|
14
|
+
__all__ = ["AndroidScreenHost"]
|
|
15
|
+
|
|
16
|
+
_SCHEDULED: Dict[int, ScreenHost] = {}
|
|
17
|
+
_handler: Any = None
|
|
18
|
+
_runnable: Any = None
|
|
19
|
+
_main_looper: Any = None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _is_main_thread() -> bool:
|
|
23
|
+
global _main_looper
|
|
24
|
+
try:
|
|
25
|
+
Looper = jclass("android.os.Looper")
|
|
26
|
+
if _main_looper is None:
|
|
27
|
+
_main_looper = Looper.getMainLooper()
|
|
28
|
+
return Looper.myLooper() == _main_looper
|
|
29
|
+
except Exception:
|
|
30
|
+
return threading.current_thread() is threading.main_thread()
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _flush_scheduled() -> None:
|
|
34
|
+
hosts = list(_SCHEDULED.values())
|
|
35
|
+
_SCHEDULED.clear()
|
|
36
|
+
flush_hosts(hosts)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _publish_window_insets(view: Any) -> None:
|
|
40
|
+
"""Publish system-bar and IME insets from ``view`` to ``platform_metrics`` (best-effort)."""
|
|
41
|
+
try:
|
|
42
|
+
from .. import platform_metrics
|
|
43
|
+
except Exception:
|
|
44
|
+
return
|
|
45
|
+
try:
|
|
46
|
+
insets_obj = view.getRootWindowInsets()
|
|
47
|
+
if insets_obj is None:
|
|
48
|
+
return
|
|
49
|
+
density = float(view.getResources().getDisplayMetrics().density) or 1.0
|
|
50
|
+
Type: Any = None
|
|
51
|
+
try:
|
|
52
|
+
WindowInsets = jclass("android.view.WindowInsets")
|
|
53
|
+
Type = WindowInsets.Type
|
|
54
|
+
typed = insets_obj.getInsets(Type.systemBars())
|
|
55
|
+
top_px, left_px, bottom_px, right_px = int(typed.top), int(typed.left), int(typed.bottom), int(typed.right)
|
|
56
|
+
except Exception:
|
|
57
|
+
top_px = int(insets_obj.getSystemWindowInsetTop() or 0)
|
|
58
|
+
left_px = int(insets_obj.getSystemWindowInsetLeft() or 0)
|
|
59
|
+
bottom_px = int(insets_obj.getSystemWindowInsetBottom() or 0)
|
|
60
|
+
right_px = int(insets_obj.getSystemWindowInsetRight() or 0)
|
|
61
|
+
platform_metrics.set_safe_area_insets(
|
|
62
|
+
top_px / density, left_px / density, bottom_px / density, right_px / density
|
|
63
|
+
)
|
|
64
|
+
# The keyboard overlaps the navigation bar, so the visible
|
|
65
|
+
# keyboard height is the IME inset minus the bar inset.
|
|
66
|
+
try:
|
|
67
|
+
ime = insets_obj.getInsets(Type.ime())
|
|
68
|
+
platform_metrics.set_keyboard_height(max(0, int(ime.bottom) - bottom_px) / density)
|
|
69
|
+
except Exception:
|
|
70
|
+
pass
|
|
71
|
+
except Exception:
|
|
72
|
+
pass
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _publish_color_scheme(activity: Any) -> None:
|
|
76
|
+
try:
|
|
77
|
+
from .. import appearance
|
|
78
|
+
|
|
79
|
+
Configuration = jclass("android.content.res.Configuration")
|
|
80
|
+
ui_mode = int(activity.getResources().getConfiguration().uiMode)
|
|
81
|
+
night = ui_mode & int(Configuration.UI_MODE_NIGHT_MASK)
|
|
82
|
+
appearance.set_system_color_scheme("dark" if night == int(Configuration.UI_MODE_NIGHT_YES) else "light")
|
|
83
|
+
except Exception:
|
|
84
|
+
pass
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class AndroidScreenHost(ScreenHost):
|
|
88
|
+
"""Host owned by ``ScreenFragment.kt``; ``native_instance`` is the activity."""
|
|
89
|
+
|
|
90
|
+
def __init__(self, native_instance: Any, component_path: str, component: Any) -> None:
|
|
91
|
+
set_android_context(native_instance)
|
|
92
|
+
super().__init__(native_instance, component_path, component)
|
|
93
|
+
self._layout_listener: Any = None # retained to prevent GC
|
|
94
|
+
self._insets_listener: Any = None
|
|
95
|
+
|
|
96
|
+
# -- lifecycle ------------------------------------------------------
|
|
97
|
+
|
|
98
|
+
def on_create(self) -> None:
|
|
99
|
+
"""Publish the system color scheme from the activity, then mount the root component."""
|
|
100
|
+
_publish_color_scheme(self.native_instance)
|
|
101
|
+
super().on_create()
|
|
102
|
+
|
|
103
|
+
def on_resume(self) -> None:
|
|
104
|
+
"""Refresh the system color scheme, then mark the screen focused."""
|
|
105
|
+
_publish_color_scheme(self.native_instance)
|
|
106
|
+
super().on_resume()
|
|
107
|
+
|
|
108
|
+
def on_activity_result(self, request_code: int, result_code: int, data: Any) -> None:
|
|
109
|
+
"""Forward ``Activity.onActivityResult`` to the native-module dispatcher."""
|
|
110
|
+
from ..native_modules import dispatch_activity_result
|
|
111
|
+
|
|
112
|
+
dispatch_activity_result(int(request_code), int(result_code), data)
|
|
113
|
+
|
|
114
|
+
def on_request_permissions_result(self, request_code: int, permissions: Any, grant_results: Any) -> None:
|
|
115
|
+
"""Forward ``Activity.onRequestPermissionsResult`` to the native-module dispatcher."""
|
|
116
|
+
from ..native_modules import dispatch_permissions_result
|
|
117
|
+
|
|
118
|
+
dispatch_permissions_result(int(request_code), list(permissions or []), list(grant_results or []))
|
|
119
|
+
|
|
120
|
+
# -- platform primitives -------------------------------------------
|
|
121
|
+
|
|
122
|
+
def _initial_viewport_size(self) -> Optional[Tuple[float, float]]:
|
|
123
|
+
try:
|
|
124
|
+
metrics = self.native_instance.getResources().getDisplayMetrics()
|
|
125
|
+
density = float(metrics.density) or 1.0
|
|
126
|
+
return (metrics.widthPixels / density, metrics.heightPixels / density)
|
|
127
|
+
except Exception:
|
|
128
|
+
return None
|
|
129
|
+
|
|
130
|
+
def _schedule_render_async(self) -> bool:
|
|
131
|
+
global _handler, _runnable
|
|
132
|
+
if self._render_scheduled:
|
|
133
|
+
return True
|
|
134
|
+
if _is_main_thread():
|
|
135
|
+
return False
|
|
136
|
+
self._render_scheduled = True
|
|
137
|
+
_SCHEDULED[id(self)] = self
|
|
138
|
+
try:
|
|
139
|
+
if _handler is None:
|
|
140
|
+
Handler = jclass("android.os.Handler")
|
|
141
|
+
Looper = jclass("android.os.Looper")
|
|
142
|
+
Runnable = jclass("java.lang.Runnable")
|
|
143
|
+
_handler = Handler(Looper.getMainLooper())
|
|
144
|
+
|
|
145
|
+
class _RenderRunnable(dynamic_proxy(Runnable)): # type: ignore[misc]
|
|
146
|
+
def run(self) -> None:
|
|
147
|
+
_flush_scheduled()
|
|
148
|
+
|
|
149
|
+
_runnable = _RenderRunnable()
|
|
150
|
+
_handler.post(_runnable)
|
|
151
|
+
return True
|
|
152
|
+
except Exception:
|
|
153
|
+
self._render_scheduled = False
|
|
154
|
+
_SCHEDULED.pop(id(self), None)
|
|
155
|
+
return False
|
|
156
|
+
|
|
157
|
+
def _navigator(self) -> Any:
|
|
158
|
+
return jclass(f"{self.native_instance.getPackageName()}.Navigator")
|
|
159
|
+
|
|
160
|
+
def _native_push(self, component_path: str, args: Dict[str, Any], options: Dict[str, Any]) -> None:
|
|
161
|
+
self._navigator().push(self.native_instance, component_path, json.dumps(args) if args else None)
|
|
162
|
+
|
|
163
|
+
def _native_pop(self, count: int) -> None:
|
|
164
|
+
try:
|
|
165
|
+
navigator = self._navigator()
|
|
166
|
+
for _ in range(count):
|
|
167
|
+
navigator.pop(self.native_instance)
|
|
168
|
+
except Exception:
|
|
169
|
+
self.native_instance.finish()
|
|
170
|
+
|
|
171
|
+
def _native_pop_to_root(self) -> None:
|
|
172
|
+
try:
|
|
173
|
+
self._navigator().popToRoot(self.native_instance)
|
|
174
|
+
except Exception:
|
|
175
|
+
pass
|
|
176
|
+
|
|
177
|
+
def _native_set_options(self, options: Dict[str, Any]) -> None:
|
|
178
|
+
title = options.get("title")
|
|
179
|
+
try:
|
|
180
|
+
if title is not None and hasattr(self.native_instance, "setTitle"):
|
|
181
|
+
self.native_instance.setTitle(str(title))
|
|
182
|
+
except Exception:
|
|
183
|
+
pass
|
|
184
|
+
|
|
185
|
+
def _attach_root(self, native_view: Any) -> None:
|
|
186
|
+
container = None
|
|
187
|
+
try:
|
|
188
|
+
container = get_android_fragment_container()
|
|
189
|
+
try:
|
|
190
|
+
container.removeAllViews()
|
|
191
|
+
except Exception:
|
|
192
|
+
pass
|
|
193
|
+
# A root from a prior mount may still be parented under the
|
|
194
|
+
# old (destroyed) FrameLayout; addView throws otherwise.
|
|
195
|
+
try:
|
|
196
|
+
old_parent = native_view.getParent()
|
|
197
|
+
if old_parent is not None:
|
|
198
|
+
old_parent.removeView(native_view)
|
|
199
|
+
except Exception:
|
|
200
|
+
pass
|
|
201
|
+
LayoutParams = jclass("android.view.ViewGroup$LayoutParams")
|
|
202
|
+
container.addView(native_view, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT))
|
|
203
|
+
except Exception:
|
|
204
|
+
self.native_instance.setContentView(native_view)
|
|
205
|
+
container = native_view
|
|
206
|
+
if container is not None:
|
|
207
|
+
self._register_layout_listener(container)
|
|
208
|
+
self._register_insets_listener(container)
|
|
209
|
+
self._push_initial_viewport(container)
|
|
210
|
+
|
|
211
|
+
def _detach_root(self, native_view: Any) -> None:
|
|
212
|
+
# Never clear the shared fragment container: a popped fragment's
|
|
213
|
+
# detach runs after the screen below has re-attached its root.
|
|
214
|
+
if native_view is None:
|
|
215
|
+
return
|
|
216
|
+
try:
|
|
217
|
+
parent = native_view.getParent()
|
|
218
|
+
if parent is not None:
|
|
219
|
+
parent.removeView(native_view)
|
|
220
|
+
except Exception:
|
|
221
|
+
pass
|
|
222
|
+
|
|
223
|
+
# -- viewport plumbing ---------------------------------------------
|
|
224
|
+
|
|
225
|
+
def _register_layout_listener(self, view: Any) -> None:
|
|
226
|
+
host = self
|
|
227
|
+
try:
|
|
228
|
+
View = jclass("android.view.View")
|
|
229
|
+
|
|
230
|
+
class _LayoutListener(dynamic_proxy(View.OnLayoutChangeListener)): # type: ignore[misc]
|
|
231
|
+
def onLayoutChange(self, v: Any, left: int, top: int, right: int, bottom: int, *_: Any) -> None:
|
|
232
|
+
try:
|
|
233
|
+
_publish_window_insets(v)
|
|
234
|
+
density = float(v.getResources().getDisplayMetrics().density) or 1.0
|
|
235
|
+
host.set_viewport_size((right - left) / density, (bottom - top) / density)
|
|
236
|
+
except Exception:
|
|
237
|
+
pass
|
|
238
|
+
|
|
239
|
+
self._layout_listener = _LayoutListener()
|
|
240
|
+
view.addOnLayoutChangeListener(self._layout_listener)
|
|
241
|
+
except Exception:
|
|
242
|
+
pass
|
|
243
|
+
|
|
244
|
+
def _register_insets_listener(self, view: Any) -> None:
|
|
245
|
+
try:
|
|
246
|
+
View = jclass("android.view.View")
|
|
247
|
+
|
|
248
|
+
class _InsetsListener(dynamic_proxy(View.OnApplyWindowInsetsListener)): # type: ignore[misc]
|
|
249
|
+
def onApplyWindowInsets(self, v: Any, insets: Any) -> Any:
|
|
250
|
+
_publish_window_insets(v)
|
|
251
|
+
return insets
|
|
252
|
+
|
|
253
|
+
self._insets_listener = _InsetsListener()
|
|
254
|
+
view.setOnApplyWindowInsetsListener(self._insets_listener)
|
|
255
|
+
except Exception:
|
|
256
|
+
pass
|
|
257
|
+
|
|
258
|
+
def _push_initial_viewport(self, view: Any) -> None:
|
|
259
|
+
try:
|
|
260
|
+
_publish_window_insets(view)
|
|
261
|
+
metrics = view.getResources().getDisplayMetrics()
|
|
262
|
+
density = float(metrics.density) or 1.0
|
|
263
|
+
w, h = int(view.getWidth() or 0), int(view.getHeight() or 0)
|
|
264
|
+
if w > 0 and h > 0:
|
|
265
|
+
self.set_viewport_size(w / density, h / density)
|
|
266
|
+
else:
|
|
267
|
+
self.set_viewport_size(metrics.widthPixels / density, metrics.heightPixels / density)
|
|
268
|
+
except Exception:
|
|
269
|
+
pass
|