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.
Files changed (78) hide show
  1. pythonnative/__init__.py +27 -15
  2. pythonnative/animated.py +7 -13
  3. pythonnative/cli/pn.py +1 -2
  4. pythonnative/component.py +255 -0
  5. pythonnative/components/__init__.py +106 -0
  6. pythonnative/components/_base.py +132 -0
  7. pythonnative/components/controls.py +515 -0
  8. pythonnative/components/layout.py +459 -0
  9. pythonnative/components/lists.py +809 -0
  10. pythonnative/components/media.py +204 -0
  11. pythonnative/components/overlays.py +107 -0
  12. pythonnative/components/pressable.py +252 -0
  13. pythonnative/components/structural.py +167 -0
  14. pythonnative/components/text.py +291 -0
  15. pythonnative/diagnostics.py +1 -1
  16. pythonnative/element.py +108 -29
  17. pythonnative/gestures.py +1 -1
  18. pythonnative/hooks.py +366 -671
  19. pythonnative/hosts/__init__.py +85 -0
  20. pythonnative/hosts/android.py +269 -0
  21. pythonnative/hosts/base.py +665 -0
  22. pythonnative/hosts/desktop.py +107 -0
  23. pythonnative/hosts/ios.py +404 -0
  24. pythonnative/hot_reload.py +16 -24
  25. pythonnative/layout.py +372 -66
  26. pythonnative/native_modules/__init__.py +23 -0
  27. pythonnative/native_modules/net_info.py +5 -1
  28. pythonnative/native_views/__init__.py +1 -1
  29. pythonnative/native_views/android.py +50 -6
  30. pythonnative/native_views/base.py +64 -1
  31. pythonnative/native_views/desktop.py +11 -3
  32. pythonnative/native_views/ios.py +72 -12
  33. pythonnative/navigation/__init__.py +102 -0
  34. pythonnative/navigation/container.py +154 -0
  35. pythonnative/navigation/handle.py +573 -0
  36. pythonnative/navigation/hooks.py +94 -0
  37. pythonnative/navigation/host.py +58 -0
  38. pythonnative/navigation/linking.py +200 -0
  39. pythonnative/navigation/navigators.py +637 -0
  40. pythonnative/navigation/screen.py +149 -0
  41. pythonnative/navigation/state.py +248 -0
  42. pythonnative/net.py +4 -0
  43. pythonnative/platform_metrics.py +1 -1
  44. pythonnative/preview.py +21 -19
  45. pythonnative/project/android.py +7 -1
  46. pythonnative/project/doctor.py +1 -1
  47. pythonnative/project/ios.py +1 -0
  48. pythonnative/project/runtime_assets.py +1 -1
  49. pythonnative/reconciler/__init__.py +29 -0
  50. pythonnative/reconciler/boundaries.py +365 -0
  51. pythonnative/reconciler/children.py +88 -0
  52. pythonnative/reconciler/core.py +1153 -0
  53. pythonnative/reconciler/layout_pass.py +359 -0
  54. pythonnative/reconciler/vnode.py +266 -0
  55. pythonnative/scheduler.py +159 -0
  56. pythonnative/sdk/_components.py +2 -4
  57. pythonnative/style.py +95 -13
  58. pythonnative/suspense.py +8 -12
  59. pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/MainActivity.kt +85 -1
  60. pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/ScreenFragment.kt +6 -7
  61. pythonnative/templates/android_template/app/src/main/res/navigation/nav_graph.xml +1 -1
  62. pythonnative/templates/android_template/app/src/main/res/values/strings.xml +3 -1
  63. pythonnative/templates/ios_template/ios_template/AppDelegate.swift +33 -0
  64. pythonnative/templates/ios_template/ios_template/PythonRuntime.swift +1 -1
  65. pythonnative/templates/ios_template/ios_template/ViewController.swift +12 -10
  66. pythonnative/testing/__init__.py +53 -0
  67. pythonnative/testing/backend.py +276 -0
  68. pythonnative/testing/harness.py +391 -0
  69. {pythonnative-0.32.0.dist-info → pythonnative-0.34.0.dist-info}/METADATA +2 -21
  70. {pythonnative-0.32.0.dist-info → pythonnative-0.34.0.dist-info}/RECORD +74 -43
  71. pythonnative/components.py +0 -2856
  72. pythonnative/navigation.py +0 -1031
  73. pythonnative/reconciler.py +0 -2298
  74. pythonnative/screen.py +0 -2068
  75. {pythonnative-0.32.0.dist-info → pythonnative-0.34.0.dist-info}/WHEEL +0 -0
  76. {pythonnative-0.32.0.dist-info → pythonnative-0.34.0.dist-info}/entry_points.txt +0 -0
  77. {pythonnative-0.32.0.dist-info → pythonnative-0.34.0.dist-info}/licenses/LICENSE +0 -0
  78. {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}")
@@ -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 `_ScreenHost` instance (or duck-typed
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
- from .screen import _request_render
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
- Functions decorated with [`component`][pythonnative.component]
456
- store the user's original function on the wrapper's
457
- ``__wrapped__`` attribute and forward ``__module__`` /
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, "_tree", None) is None:
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._tree)
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
- Pending ``Element`` trees stored on ``vnode._rendered`` are
551
- rewritten too because the reconciler reads from them when
552
- comparing keys across renders.
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, "_tree", None) is None:
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, "_rendered", None)
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._tree)
579
+ visit(reconciler.root)
588
580
  return rewrites
589
581
 
590
582
  @staticmethod