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,107 @@
|
|
|
1
|
+
"""Desktop preview host (Tkinter), driven by ``pn preview``.
|
|
2
|
+
|
|
3
|
+
Placement of the root widget and the screen stack are delegated to the
|
|
4
|
+
``DesktopApp`` controller in ``pythonnative.preview`` (passed as
|
|
5
|
+
``native_instance``). The controller runs the Tk event loop on the main
|
|
6
|
+
thread and polls ``drain_desktop_scheduled_renders`` so renders
|
|
7
|
+
requested from the asyncio worker thread are applied on the main thread.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import threading
|
|
13
|
+
from typing import Any, Dict, Optional, Tuple
|
|
14
|
+
|
|
15
|
+
from .base import ScreenHost, flush_hosts
|
|
16
|
+
|
|
17
|
+
__all__ = ["DesktopScreenHost", "drain_desktop_scheduled_renders"]
|
|
18
|
+
|
|
19
|
+
_SCHEDULED: Dict[int, ScreenHost] = {}
|
|
20
|
+
_lock = threading.Lock()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def drain_desktop_scheduled_renders() -> None:
|
|
24
|
+
"""Apply renders queued from worker threads (called on the Tk main thread)."""
|
|
25
|
+
with _lock:
|
|
26
|
+
hosts = list(_SCHEDULED.values())
|
|
27
|
+
_SCHEDULED.clear()
|
|
28
|
+
flush_hosts(hosts)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class DesktopScreenHost(ScreenHost):
|
|
32
|
+
"""Host for one page of the desktop preview's screen stack."""
|
|
33
|
+
|
|
34
|
+
def __init__(self, native_instance: Any = None, component_path: str = "", component: Any = None) -> None:
|
|
35
|
+
super().__init__(native_instance, component_path, component)
|
|
36
|
+
self.container: Any = None # the Tk frame this page renders into (set by DesktopApp)
|
|
37
|
+
|
|
38
|
+
def _initial_viewport_size(self) -> Optional[Tuple[float, float]]:
|
|
39
|
+
app = self.native_instance
|
|
40
|
+
if app is None or not hasattr(app, "viewport_size"):
|
|
41
|
+
return None
|
|
42
|
+
try:
|
|
43
|
+
width, height = app.viewport_size()
|
|
44
|
+
return (float(width), float(height))
|
|
45
|
+
except Exception:
|
|
46
|
+
return None
|
|
47
|
+
|
|
48
|
+
def _schedule_render_async(self) -> bool:
|
|
49
|
+
if threading.current_thread() is threading.main_thread():
|
|
50
|
+
return False
|
|
51
|
+
if self._render_scheduled:
|
|
52
|
+
return True
|
|
53
|
+
self._render_scheduled = True
|
|
54
|
+
with _lock:
|
|
55
|
+
_SCHEDULED[id(self)] = self
|
|
56
|
+
return True
|
|
57
|
+
|
|
58
|
+
def _native_push(self, component_path: str, args: Dict[str, Any], options: Dict[str, Any]) -> None:
|
|
59
|
+
app = self.native_instance
|
|
60
|
+
if app is None or not hasattr(app, "push_screen"):
|
|
61
|
+
raise RuntimeError("desktop navigation requires a running `pn preview` session")
|
|
62
|
+
app.push_screen(component_path, args, options)
|
|
63
|
+
|
|
64
|
+
def _native_pop(self, count: int) -> None:
|
|
65
|
+
app = self.native_instance
|
|
66
|
+
if app is not None and hasattr(app, "pop_screen"):
|
|
67
|
+
for _ in range(count):
|
|
68
|
+
app.pop_screen()
|
|
69
|
+
|
|
70
|
+
def _native_pop_to_root(self) -> None:
|
|
71
|
+
app = self.native_instance
|
|
72
|
+
if app is not None and hasattr(app, "reset_to_root"):
|
|
73
|
+
app.reset_to_root()
|
|
74
|
+
|
|
75
|
+
def _native_set_options(self, options: Dict[str, Any]) -> None:
|
|
76
|
+
title = options.get("title")
|
|
77
|
+
app = self.native_instance
|
|
78
|
+
if title is not None and app is not None and hasattr(app, "set_title"):
|
|
79
|
+
try:
|
|
80
|
+
app.set_title(str(title))
|
|
81
|
+
except Exception:
|
|
82
|
+
pass
|
|
83
|
+
|
|
84
|
+
def _attach_root(self, native_view: Any) -> None:
|
|
85
|
+
from ..native_views import desktop as backend
|
|
86
|
+
|
|
87
|
+
stage = backend.get_root_container()
|
|
88
|
+
if stage is not None and native_view is not None:
|
|
89
|
+
try:
|
|
90
|
+
native_view.place(in_=stage, x=0, y=0, relwidth=1.0, relheight=1.0)
|
|
91
|
+
native_view.lift()
|
|
92
|
+
except Exception:
|
|
93
|
+
pass
|
|
94
|
+
app = self.native_instance
|
|
95
|
+
if app is not None and hasattr(app, "viewport_size"):
|
|
96
|
+
try:
|
|
97
|
+
width, height = app.viewport_size()
|
|
98
|
+
self.set_viewport_size(float(width), float(height))
|
|
99
|
+
except Exception:
|
|
100
|
+
pass
|
|
101
|
+
|
|
102
|
+
def _detach_root(self, native_view: Any) -> None:
|
|
103
|
+
if native_view is not None:
|
|
104
|
+
try:
|
|
105
|
+
native_view.place_forget()
|
|
106
|
+
except Exception:
|
|
107
|
+
pass
|
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
"""iOS screen host: one ``UIViewController`` per screen, driven by ``ViewController.swift``."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import sys
|
|
7
|
+
import threading
|
|
8
|
+
from typing import Any, Callable, Dict, Optional, Sequence, Tuple
|
|
9
|
+
|
|
10
|
+
from ..utils import IS_IOS
|
|
11
|
+
from .base import ScreenHost, flush_hosts, log_pn
|
|
12
|
+
|
|
13
|
+
__all__ = ["IOSScreenHost", "drain_ios_scheduled_renders", "forward_lifecycle"]
|
|
14
|
+
|
|
15
|
+
try:
|
|
16
|
+
from rubicon.objc import SEL, ObjCClass, ObjCInstance, objc_method
|
|
17
|
+
|
|
18
|
+
_rubicon_available = True
|
|
19
|
+
import gc as _gc
|
|
20
|
+
|
|
21
|
+
_gc.disable()
|
|
22
|
+
except ImportError: # pragma: no cover - only on hosts without the [ios] extra
|
|
23
|
+
_rubicon_available = False
|
|
24
|
+
|
|
25
|
+
# Redirect Python's stdout/stderr through fd 2 so ``print()`` output is
|
|
26
|
+
# visible via ``xcrun simctl launch --console-pty``. Runs before any user
|
|
27
|
+
# module is imported so their top-level prints are captured too.
|
|
28
|
+
if IS_IOS:
|
|
29
|
+
try:
|
|
30
|
+
from .. import _ios_log
|
|
31
|
+
|
|
32
|
+
_ios_log.install()
|
|
33
|
+
except Exception:
|
|
34
|
+
pass
|
|
35
|
+
|
|
36
|
+
_REGISTRY: Dict[int, ScreenHost] = {}
|
|
37
|
+
_SCHEDULED: Dict[int, ScreenHost] = {}
|
|
38
|
+
_timer_target: Any = None
|
|
39
|
+
_native_scheduler: Any = None
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _objc_addr(obj: Any) -> Optional[int]:
|
|
43
|
+
"""Pointer of an ``ObjCInstance`` as an int (``ptr`` varies by rubicon release)."""
|
|
44
|
+
ptr = getattr(obj, "ptr", None)
|
|
45
|
+
if ptr is None:
|
|
46
|
+
return None
|
|
47
|
+
if isinstance(ptr, (bytes, bytearray)):
|
|
48
|
+
try:
|
|
49
|
+
return int.from_bytes(ptr, byteorder=sys.byteorder, signed=False)
|
|
50
|
+
except Exception:
|
|
51
|
+
return None
|
|
52
|
+
if isinstance(ptr, int):
|
|
53
|
+
return ptr
|
|
54
|
+
value = getattr(ptr, "value", None)
|
|
55
|
+
if isinstance(value, int):
|
|
56
|
+
return value
|
|
57
|
+
try:
|
|
58
|
+
return int(ptr)
|
|
59
|
+
except Exception:
|
|
60
|
+
return None
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _publish_color_scheme() -> None:
|
|
64
|
+
try:
|
|
65
|
+
from .. import appearance
|
|
66
|
+
|
|
67
|
+
UITraitCollection = ObjCClass("UITraitCollection")
|
|
68
|
+
style_value = int(UITraitCollection.currentTraitCollection.userInterfaceStyle)
|
|
69
|
+
appearance.set_system_color_scheme("dark" if style_value == 2 else "light")
|
|
70
|
+
except Exception:
|
|
71
|
+
pass
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _flush_scheduled() -> None:
|
|
75
|
+
hosts = list(_SCHEDULED.values())
|
|
76
|
+
_SCHEDULED.clear()
|
|
77
|
+
if hosts:
|
|
78
|
+
log_pn(f"render_scheduler: flushing {len(hosts)} host(s)")
|
|
79
|
+
flush_hosts(hosts)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def drain_ios_scheduled_renders() -> None:
|
|
83
|
+
"""Entry point used by the iOS template to drain pending renders on the main thread."""
|
|
84
|
+
_flush_scheduled()
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _wake_native_drain() -> bool:
|
|
88
|
+
"""Ask the template's ``pn_schedule_render_drain`` to drain on the main thread."""
|
|
89
|
+
global _native_scheduler
|
|
90
|
+
try:
|
|
91
|
+
if _native_scheduler is None:
|
|
92
|
+
import ctypes
|
|
93
|
+
|
|
94
|
+
scheduler = ctypes.CDLL(None).pn_schedule_render_drain
|
|
95
|
+
scheduler.restype = None
|
|
96
|
+
scheduler.argtypes = []
|
|
97
|
+
_native_scheduler = scheduler
|
|
98
|
+
_native_scheduler()
|
|
99
|
+
return True
|
|
100
|
+
except Exception as exc:
|
|
101
|
+
log_pn(f"render_scheduler: native iOS wake failed: {exc!r}")
|
|
102
|
+
return False
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def forward_lifecycle(native_addr: int, event: str) -> None:
|
|
106
|
+
"""Forward a Swift ``UIViewController`` lifecycle event to its host.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
native_addr: Pointer of the view controller, as registered by
|
|
110
|
+
``create_screen``.
|
|
111
|
+
event: Host method name (``"on_resume"``, ``"on_layout"``, ...).
|
|
112
|
+
"""
|
|
113
|
+
try:
|
|
114
|
+
key = int(native_addr)
|
|
115
|
+
except Exception as exc:
|
|
116
|
+
log_pn(f"forward_lifecycle: bad native_addr={native_addr!r}: {exc!r}")
|
|
117
|
+
return
|
|
118
|
+
host = _REGISTRY.get(key)
|
|
119
|
+
if host is None:
|
|
120
|
+
log_pn(f"forward_lifecycle: no host for event={event!r} addr={key}")
|
|
121
|
+
return
|
|
122
|
+
handler = getattr(host, event, None)
|
|
123
|
+
if handler is None:
|
|
124
|
+
log_pn(f"forward_lifecycle: host has no {event!r}")
|
|
125
|
+
return
|
|
126
|
+
try:
|
|
127
|
+
handler()
|
|
128
|
+
except Exception as exc:
|
|
129
|
+
log_pn(f"forward_lifecycle: {event!r} raised: {exc!r}")
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
if _rubicon_available and IS_IOS:
|
|
133
|
+
NSObject = ObjCClass("NSObject")
|
|
134
|
+
|
|
135
|
+
class _RenderTimerTarget(NSObject): # type: ignore[misc, valid-type]
|
|
136
|
+
@objc_method
|
|
137
|
+
def onRenderTimer_(self, timer: object) -> None:
|
|
138
|
+
_flush_scheduled()
|
|
139
|
+
|
|
140
|
+
def _timer_target_instance() -> Any:
|
|
141
|
+
global _timer_target
|
|
142
|
+
if _timer_target is None:
|
|
143
|
+
target = _RenderTimerTarget.new()
|
|
144
|
+
try:
|
|
145
|
+
target.retain()
|
|
146
|
+
except Exception:
|
|
147
|
+
pass
|
|
148
|
+
_timer_target = target
|
|
149
|
+
return _timer_target
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
class IOSScreenHost(ScreenHost):
|
|
153
|
+
"""Host owned by a ``ViewController``; pushes onto its ``UINavigationController``."""
|
|
154
|
+
|
|
155
|
+
def __init__(self, native_instance: Any, component_path: str, component: Any) -> None:
|
|
156
|
+
if isinstance(native_instance, int):
|
|
157
|
+
try:
|
|
158
|
+
native_instance = ObjCInstance(native_instance)
|
|
159
|
+
except Exception:
|
|
160
|
+
native_instance = None
|
|
161
|
+
super().__init__(native_instance, component_path, component)
|
|
162
|
+
if self.native_instance is not None:
|
|
163
|
+
addr = _objc_addr(self.native_instance)
|
|
164
|
+
if addr is not None:
|
|
165
|
+
_REGISTRY[addr] = self
|
|
166
|
+
|
|
167
|
+
# -- lifecycle ------------------------------------------------------
|
|
168
|
+
|
|
169
|
+
def on_create(self) -> None:
|
|
170
|
+
"""Publish the system color scheme, then mount the root component."""
|
|
171
|
+
_publish_color_scheme()
|
|
172
|
+
super().on_create()
|
|
173
|
+
|
|
174
|
+
def on_layout(self) -> None:
|
|
175
|
+
"""Sync the root view's frame and viewport size after ``viewDidLayoutSubviews``."""
|
|
176
|
+
# ``viewDidLayoutSubviews``: safe-area insets are valid now.
|
|
177
|
+
_publish_color_scheme()
|
|
178
|
+
if self.root_native_view is not None:
|
|
179
|
+
self._sync_root_frame(self.root_native_view)
|
|
180
|
+
self._push_viewport_from_root(self.root_native_view)
|
|
181
|
+
|
|
182
|
+
def on_resume(self) -> None:
|
|
183
|
+
"""Mark the screen focused and refresh the color scheme, root frame, and viewport size."""
|
|
184
|
+
_publish_color_scheme()
|
|
185
|
+
super().on_resume()
|
|
186
|
+
if self.root_native_view is not None:
|
|
187
|
+
self._sync_root_frame(self.root_native_view)
|
|
188
|
+
self._push_viewport_from_root(self.root_native_view)
|
|
189
|
+
|
|
190
|
+
def on_destroy(self) -> None:
|
|
191
|
+
"""Drop this host from the view-controller registry, then tear down the tree."""
|
|
192
|
+
if self.native_instance is not None:
|
|
193
|
+
addr = _objc_addr(self.native_instance)
|
|
194
|
+
if addr is not None:
|
|
195
|
+
_REGISTRY.pop(addr, None)
|
|
196
|
+
super().on_destroy()
|
|
197
|
+
|
|
198
|
+
# -- platform primitives -------------------------------------------
|
|
199
|
+
|
|
200
|
+
def _initial_viewport_size(self) -> Optional[Tuple[float, float]]:
|
|
201
|
+
try:
|
|
202
|
+
if self.native_instance is not None:
|
|
203
|
+
bounds = self.native_instance.view.bounds
|
|
204
|
+
if bounds.size.width > 0 and bounds.size.height > 0:
|
|
205
|
+
return (float(bounds.size.width), float(bounds.size.height))
|
|
206
|
+
except Exception:
|
|
207
|
+
pass
|
|
208
|
+
try:
|
|
209
|
+
bounds = ObjCClass("UIScreen").mainScreen.bounds
|
|
210
|
+
return (float(bounds.size.width), float(bounds.size.height))
|
|
211
|
+
except Exception:
|
|
212
|
+
return None
|
|
213
|
+
|
|
214
|
+
def _schedule_render_async(self) -> bool:
|
|
215
|
+
if self._render_scheduled:
|
|
216
|
+
return True
|
|
217
|
+
self._render_scheduled = True
|
|
218
|
+
_SCHEDULED[id(self)] = self
|
|
219
|
+
if threading.current_thread() is not threading.main_thread():
|
|
220
|
+
if not _wake_native_drain():
|
|
221
|
+
log_pn("request_render: native iOS scheduler unavailable; render remains queued")
|
|
222
|
+
return True
|
|
223
|
+
# Main-queue GCD blocks run in the common run-loop modes, so
|
|
224
|
+
# renders land during scrolls and synthesized test touches; a
|
|
225
|
+
# default-mode NSTimer is starved for the whole gesture.
|
|
226
|
+
from ..runtime import _ensure_libdispatch_loaded, _ios_dispatch_async
|
|
227
|
+
|
|
228
|
+
if _ensure_libdispatch_loaded():
|
|
229
|
+
_ios_dispatch_async(_flush_scheduled)
|
|
230
|
+
return True
|
|
231
|
+
try:
|
|
232
|
+
NSTimer = ObjCClass("NSTimer")
|
|
233
|
+
NSTimer.scheduledTimerWithTimeInterval_target_selector_userInfo_repeats_(
|
|
234
|
+
0.0, _timer_target_instance(), SEL("onRenderTimer:"), None, False
|
|
235
|
+
)
|
|
236
|
+
return True
|
|
237
|
+
except Exception as exc:
|
|
238
|
+
self._render_scheduled = False
|
|
239
|
+
_SCHEDULED.pop(id(self), None)
|
|
240
|
+
log_pn(f"request_render: iOS defer failed ({exc!r}); rendering synchronously")
|
|
241
|
+
return False
|
|
242
|
+
|
|
243
|
+
def _view_controller_class(self) -> Any:
|
|
244
|
+
try:
|
|
245
|
+
return ObjCClass("ViewController")
|
|
246
|
+
except Exception:
|
|
247
|
+
pass
|
|
248
|
+
try:
|
|
249
|
+
bundle = ObjCClass("NSBundle").mainBundle
|
|
250
|
+
module_name = bundle.objectForInfoDictionaryKey_("CFBundleName") or bundle.objectForInfoDictionaryKey_(
|
|
251
|
+
"CFBundleExecutable"
|
|
252
|
+
)
|
|
253
|
+
if module_name:
|
|
254
|
+
return ObjCClass(f"{module_name}.ViewController")
|
|
255
|
+
except Exception:
|
|
256
|
+
pass
|
|
257
|
+
raise NameError("ViewController class not found; ensure the Swift class is ObjC-visible")
|
|
258
|
+
|
|
259
|
+
def _make_view_controller(self, component_path: str, args: Dict[str, Any], options: Dict[str, Any]) -> Any:
|
|
260
|
+
vc = self._view_controller_class().alloc().init()
|
|
261
|
+
try:
|
|
262
|
+
vc.setValue_forKey_(component_path, "requestedScreenPath")
|
|
263
|
+
if args:
|
|
264
|
+
vc.setValue_forKey_(json.dumps(args), "requestedScreenArgsJSON")
|
|
265
|
+
except Exception:
|
|
266
|
+
pass
|
|
267
|
+
title = options.get("title")
|
|
268
|
+
if title is not None:
|
|
269
|
+
try:
|
|
270
|
+
vc.setTitle_(str(title))
|
|
271
|
+
except Exception:
|
|
272
|
+
pass
|
|
273
|
+
return vc
|
|
274
|
+
|
|
275
|
+
def _run_nav_op(self, op: Callable[[Any], None]) -> None:
|
|
276
|
+
"""Run ``op(nav)`` unless a transition is mid-flight.
|
|
277
|
+
|
|
278
|
+
UIKit doesn't queue navigation calls: one issued during an
|
|
279
|
+
animation is ignored or misapplied. Dropping it keeps the stack
|
|
280
|
+
consistent; a retry lands on a settled stack.
|
|
281
|
+
"""
|
|
282
|
+
nav = getattr(self.native_instance, "navigationController", None)
|
|
283
|
+
if nav is None:
|
|
284
|
+
raise RuntimeError("No UINavigationController; the template must embed the root in one")
|
|
285
|
+
try:
|
|
286
|
+
coord = nav.transitionCoordinator
|
|
287
|
+
if callable(coord):
|
|
288
|
+
coord = coord()
|
|
289
|
+
if coord is not None:
|
|
290
|
+
log_pn("_run_nav_op: transition in flight; dropping nav op")
|
|
291
|
+
return
|
|
292
|
+
except Exception:
|
|
293
|
+
pass
|
|
294
|
+
op(nav)
|
|
295
|
+
|
|
296
|
+
def _native_push(self, component_path: str, args: Dict[str, Any], options: Dict[str, Any]) -> None:
|
|
297
|
+
vc = self._make_view_controller(component_path, args, options)
|
|
298
|
+
self._run_nav_op(lambda nav: nav.pushViewController_animated_(vc, True))
|
|
299
|
+
|
|
300
|
+
def _native_pop(self, count: int) -> None:
|
|
301
|
+
def op(nav: Any) -> None:
|
|
302
|
+
if count <= 1:
|
|
303
|
+
nav.popViewControllerAnimated_(True)
|
|
304
|
+
return
|
|
305
|
+
controllers = nav.viewControllers
|
|
306
|
+
n = int(controllers.count)
|
|
307
|
+
target_index = max(0, n - 1 - count)
|
|
308
|
+
nav.popToViewController_animated_(controllers.objectAtIndex_(target_index), True)
|
|
309
|
+
|
|
310
|
+
self._run_nav_op(op)
|
|
311
|
+
|
|
312
|
+
def _native_pop_to_root(self) -> None:
|
|
313
|
+
self._run_nav_op(lambda nav: nav.popToRootViewControllerAnimated_(True))
|
|
314
|
+
|
|
315
|
+
def _native_replace(self, component_path: str, args: Dict[str, Any], options: Dict[str, Any]) -> None:
|
|
316
|
+
vc = self._make_view_controller(component_path, args, options)
|
|
317
|
+
|
|
318
|
+
def op(nav: Any) -> None:
|
|
319
|
+
controllers = list(nav.viewControllers)
|
|
320
|
+
controllers[-1] = vc
|
|
321
|
+
nav.setViewControllers_animated_(controllers, True)
|
|
322
|
+
|
|
323
|
+
self._run_nav_op(op)
|
|
324
|
+
|
|
325
|
+
def _native_reset(self, component_path: str, screens: Sequence[Tuple[Dict[str, Any], Dict[str, Any]]]) -> None:
|
|
326
|
+
vcs = [self._make_view_controller(component_path, args, options) for args, options in screens]
|
|
327
|
+
|
|
328
|
+
def op(nav: Any) -> None:
|
|
329
|
+
root = nav.viewControllers.objectAtIndex_(0)
|
|
330
|
+
nav.setViewControllers_animated_([root, *vcs], True)
|
|
331
|
+
|
|
332
|
+
self._run_nav_op(op)
|
|
333
|
+
|
|
334
|
+
def _native_set_options(self, options: Dict[str, Any]) -> None:
|
|
335
|
+
title = options.get("title")
|
|
336
|
+
if title is None or self.native_instance is None:
|
|
337
|
+
return
|
|
338
|
+
try:
|
|
339
|
+
self.native_instance.setTitle_(str(title))
|
|
340
|
+
except Exception as exc:
|
|
341
|
+
log_pn(f"set_screen_options: setTitle failed: {exc!r}")
|
|
342
|
+
|
|
343
|
+
def _attach_root(self, native_view: Any) -> None:
|
|
344
|
+
self.native_instance.view.addSubview_(native_view)
|
|
345
|
+
# Frame-based layout for the root so the layout engine's frames
|
|
346
|
+
# aren't fought by Auto Layout.
|
|
347
|
+
try:
|
|
348
|
+
native_view.setTranslatesAutoresizingMaskIntoConstraints_(True)
|
|
349
|
+
native_view.setAutoresizingMask_(2 | 16) # FlexibleWidth | FlexibleHeight
|
|
350
|
+
except Exception:
|
|
351
|
+
pass
|
|
352
|
+
self._sync_root_frame(native_view)
|
|
353
|
+
self._push_viewport_from_root(native_view)
|
|
354
|
+
|
|
355
|
+
def _detach_root(self, native_view: Any) -> None:
|
|
356
|
+
try:
|
|
357
|
+
native_view.removeFromSuperview()
|
|
358
|
+
except Exception:
|
|
359
|
+
pass
|
|
360
|
+
|
|
361
|
+
# -- viewport plumbing ---------------------------------------------
|
|
362
|
+
|
|
363
|
+
def _sync_root_frame(self, native_view: Any) -> None:
|
|
364
|
+
"""Position the root below the top safe area, full-bleed at the bottom.
|
|
365
|
+
|
|
366
|
+
The bottom inset is published to ``platform_metrics`` instead of
|
|
367
|
+
being subtracted, so a tab bar can reach the home indicator;
|
|
368
|
+
content that needs it opts in via ``SafeAreaView``.
|
|
369
|
+
"""
|
|
370
|
+
root_view = self.native_instance.view
|
|
371
|
+
if root_view is None:
|
|
372
|
+
return
|
|
373
|
+
try:
|
|
374
|
+
bounds, insets = root_view.bounds, root_view.safeAreaInsets
|
|
375
|
+
top, left, right, bottom = float(insets.top), float(insets.left), float(insets.right), float(insets.bottom)
|
|
376
|
+
w = max(0.0, float(bounds.size.width) - left - right)
|
|
377
|
+
h = max(0.0, float(bounds.size.height) - top)
|
|
378
|
+
try:
|
|
379
|
+
from .. import platform_metrics
|
|
380
|
+
|
|
381
|
+
platform_metrics.set_safe_area_insets(0.0, left, bottom, right)
|
|
382
|
+
except Exception:
|
|
383
|
+
pass
|
|
384
|
+
if w > 0 and h > 0:
|
|
385
|
+
native_view.setFrame_(((left, top), (w, h)))
|
|
386
|
+
return
|
|
387
|
+
except Exception as exc:
|
|
388
|
+
log_pn(f"sync_root_frame: insets path failed: {exc!r}")
|
|
389
|
+
try:
|
|
390
|
+
bounds = root_view.bounds
|
|
391
|
+
native_view.setFrame_(((0, 0), (float(bounds.size.width), float(bounds.size.height))))
|
|
392
|
+
except Exception as exc:
|
|
393
|
+
log_pn(f"sync_root_frame: bounds fallback failed: {exc!r}")
|
|
394
|
+
|
|
395
|
+
def _push_viewport_from_root(self, native_view: Any) -> None:
|
|
396
|
+
try:
|
|
397
|
+
bounds = native_view.bounds
|
|
398
|
+
w, h = float(bounds.size.width), float(bounds.size.height)
|
|
399
|
+
if w <= 0 or h <= 0:
|
|
400
|
+
screen = ObjCClass("UIScreen").mainScreen.bounds
|
|
401
|
+
w, h = float(screen.size.width), float(screen.size.height)
|
|
402
|
+
self.set_viewport_size(w, h)
|
|
403
|
+
except Exception as exc:
|
|
404
|
+
log_pn(f"push_viewport: failed: {exc!r}")
|
pythonnative/hot_reload.py
CHANGED
|
@@ -433,7 +433,7 @@ class ModuleReloader:
|
|
|
433
433
|
"""Force a screen re-render after a module reload.
|
|
434
434
|
|
|
435
435
|
Args:
|
|
436
|
-
screen_instance: A `
|
|
436
|
+
screen_instance: A `ScreenHost` instance (or duck-typed
|
|
437
437
|
equivalent) that exposes a `_reconciler` attribute.
|
|
438
438
|
module_names: Optional modules that changed. Reload-aware
|
|
439
439
|
screen hosts use this to refresh imports before re-render.
|
|
@@ -442,21 +442,17 @@ class ModuleReloader:
|
|
|
442
442
|
if callable(reload_fn):
|
|
443
443
|
reload_fn(list(module_names or []))
|
|
444
444
|
return
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
if hasattr(screen_instance, "_reconciler") and screen_instance._reconciler is not None:
|
|
449
|
-
_request_render(screen_instance)
|
|
445
|
+
request = getattr(screen_instance, "request_render", None)
|
|
446
|
+
if callable(request):
|
|
447
|
+
request()
|
|
450
448
|
|
|
451
449
|
@staticmethod
|
|
452
450
|
def find_replacement_function(old_fn: Any) -> Optional[Any]:
|
|
453
451
|
"""Locate a function's post-reload counterpart by qualname.
|
|
454
452
|
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
``__qualname__`` so that the reconciler's stored
|
|
459
|
-
``element.type`` (the unwrapped function) still has the
|
|
453
|
+
[`Component`][pythonnative.Component] objects forward
|
|
454
|
+
``__module__`` / ``__qualname__`` from the render function they
|
|
455
|
+
wrap, so the reconciler's stored ``element.type`` carries the
|
|
460
456
|
information needed to re-resolve after a module reload.
|
|
461
457
|
|
|
462
458
|
Args:
|
|
@@ -486,9 +482,6 @@ class ModuleReloader:
|
|
|
486
482
|
if obj is None:
|
|
487
483
|
return None
|
|
488
484
|
|
|
489
|
-
if getattr(obj, "_pn_component", False):
|
|
490
|
-
obj = getattr(obj, "__wrapped__", obj)
|
|
491
|
-
|
|
492
485
|
if obj is old_fn:
|
|
493
486
|
return None
|
|
494
487
|
return obj
|
|
@@ -505,8 +498,7 @@ class ModuleReloader:
|
|
|
505
498
|
for its successor.
|
|
506
499
|
|
|
507
500
|
Args:
|
|
508
|
-
reconciler: The reconciler whose
|
|
509
|
-
``_tree`` should be inspected.
|
|
501
|
+
reconciler: The reconciler whose mounted ``root`` should be inspected.
|
|
510
502
|
reloaded_modules: Set of module names that were just
|
|
511
503
|
reloaded (only callables from these modules are
|
|
512
504
|
considered).
|
|
@@ -516,7 +508,7 @@ class ModuleReloader:
|
|
|
516
508
|
[`swap_components_in_tree`][pythonnative.hot_reload.ModuleReloader.swap_components_in_tree].
|
|
517
509
|
"""
|
|
518
510
|
modules: Set[str] = {m for m in reloaded_modules if m}
|
|
519
|
-
if not modules or reconciler is None or getattr(reconciler, "
|
|
511
|
+
if not modules or reconciler is None or getattr(reconciler, "root", None) is None:
|
|
520
512
|
return {}
|
|
521
513
|
|
|
522
514
|
seen: Set[int] = set()
|
|
@@ -538,7 +530,7 @@ class ModuleReloader:
|
|
|
538
530
|
for child in getattr(vnode, "children", []) or []:
|
|
539
531
|
visit(child)
|
|
540
532
|
|
|
541
|
-
visit(reconciler.
|
|
533
|
+
visit(reconciler.root)
|
|
542
534
|
return mapping
|
|
543
535
|
|
|
544
536
|
@staticmethod
|
|
@@ -547,14 +539,14 @@ class ModuleReloader:
|
|
|
547
539
|
|
|
548
540
|
Mutates ``vnode.element.type`` directly so the NEXT diff sees
|
|
549
541
|
identical types and reuses VNodes (preserving hook state).
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
542
|
+
The element lists stored on ``vnode.rendered`` are rewritten
|
|
543
|
+
too because the reconciler reads from them when comparing keys
|
|
544
|
+
across renders.
|
|
553
545
|
|
|
554
546
|
Returns:
|
|
555
547
|
The number of element type references that were rewritten.
|
|
556
548
|
"""
|
|
557
|
-
if not replacement_map or reconciler is None or getattr(reconciler, "
|
|
549
|
+
if not replacement_map or reconciler is None or getattr(reconciler, "root", None) is None:
|
|
558
550
|
return 0
|
|
559
551
|
|
|
560
552
|
rewrites = 0
|
|
@@ -575,7 +567,7 @@ class ModuleReloader:
|
|
|
575
567
|
return
|
|
576
568
|
if getattr(vnode, "element", None) is not None:
|
|
577
569
|
rewrite_element_tree(vnode.element)
|
|
578
|
-
rendered = getattr(vnode, "
|
|
570
|
+
rendered = getattr(vnode, "rendered", None)
|
|
579
571
|
if isinstance(rendered, (list, tuple)):
|
|
580
572
|
for rendered_el in rendered:
|
|
581
573
|
rewrite_element_tree(rendered_el)
|
|
@@ -584,7 +576,7 @@ class ModuleReloader:
|
|
|
584
576
|
for child in getattr(vnode, "children", []) or []:
|
|
585
577
|
visit(child)
|
|
586
578
|
|
|
587
|
-
visit(reconciler.
|
|
579
|
+
visit(reconciler.root)
|
|
588
580
|
return rewrites
|
|
589
581
|
|
|
590
582
|
@staticmethod
|