pythonnative 0.7.0__py3-none-any.whl → 0.8.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 +22 -1
- pythonnative/components.py +78 -21
- pythonnative/hooks.py +135 -29
- pythonnative/hot_reload.py +2 -2
- pythonnative/native_views/__init__.py +87 -0
- pythonnative/native_views/android.py +832 -0
- pythonnative/native_views/base.py +150 -0
- pythonnative/native_views/ios.py +777 -0
- pythonnative/navigation.py +571 -0
- pythonnative/page.py +61 -16
- pythonnative/reconciler.py +89 -1
- {pythonnative-0.7.0.dist-info → pythonnative-0.8.0.dist-info}/METADATA +1 -1
- {pythonnative-0.7.0.dist-info → pythonnative-0.8.0.dist-info}/RECORD +17 -13
- pythonnative/native_views.py +0 -1404
- {pythonnative-0.7.0.dist-info → pythonnative-0.8.0.dist-info}/WHEEL +0 -0
- {pythonnative-0.7.0.dist-info → pythonnative-0.8.0.dist-info}/entry_points.txt +0 -0
- {pythonnative-0.7.0.dist-info → pythonnative-0.8.0.dist-info}/licenses/LICENSE +0 -0
- {pythonnative-0.7.0.dist-info → pythonnative-0.8.0.dist-info}/top_level.txt +0 -0
pythonnative/__init__.py
CHANGED
|
@@ -14,12 +14,13 @@ Public API::
|
|
|
14
14
|
)
|
|
15
15
|
"""
|
|
16
16
|
|
|
17
|
-
__version__ = "0.
|
|
17
|
+
__version__ = "0.8.0"
|
|
18
18
|
|
|
19
19
|
from .components import (
|
|
20
20
|
ActivityIndicator,
|
|
21
21
|
Button,
|
|
22
22
|
Column,
|
|
23
|
+
ErrorBoundary,
|
|
23
24
|
FlatList,
|
|
24
25
|
Image,
|
|
25
26
|
Modal,
|
|
@@ -39,6 +40,7 @@ from .components import (
|
|
|
39
40
|
from .element import Element
|
|
40
41
|
from .hooks import (
|
|
41
42
|
Provider,
|
|
43
|
+
batch_updates,
|
|
42
44
|
component,
|
|
43
45
|
create_context,
|
|
44
46
|
use_callback,
|
|
@@ -46,9 +48,18 @@ from .hooks import (
|
|
|
46
48
|
use_effect,
|
|
47
49
|
use_memo,
|
|
48
50
|
use_navigation,
|
|
51
|
+
use_reducer,
|
|
49
52
|
use_ref,
|
|
50
53
|
use_state,
|
|
51
54
|
)
|
|
55
|
+
from .navigation import (
|
|
56
|
+
NavigationContainer,
|
|
57
|
+
create_drawer_navigator,
|
|
58
|
+
create_stack_navigator,
|
|
59
|
+
create_tab_navigator,
|
|
60
|
+
use_focus_effect,
|
|
61
|
+
use_route,
|
|
62
|
+
)
|
|
52
63
|
from .page import create_page
|
|
53
64
|
from .style import StyleSheet, ThemeContext
|
|
54
65
|
|
|
@@ -57,6 +68,7 @@ __all__ = [
|
|
|
57
68
|
"ActivityIndicator",
|
|
58
69
|
"Button",
|
|
59
70
|
"Column",
|
|
71
|
+
"ErrorBoundary",
|
|
60
72
|
"FlatList",
|
|
61
73
|
"Image",
|
|
62
74
|
"Modal",
|
|
@@ -76,16 +88,25 @@ __all__ = [
|
|
|
76
88
|
"Element",
|
|
77
89
|
"create_page",
|
|
78
90
|
# Hooks
|
|
91
|
+
"batch_updates",
|
|
79
92
|
"component",
|
|
80
93
|
"create_context",
|
|
81
94
|
"use_callback",
|
|
82
95
|
"use_context",
|
|
83
96
|
"use_effect",
|
|
97
|
+
"use_focus_effect",
|
|
84
98
|
"use_memo",
|
|
85
99
|
"use_navigation",
|
|
100
|
+
"use_reducer",
|
|
86
101
|
"use_ref",
|
|
102
|
+
"use_route",
|
|
87
103
|
"use_state",
|
|
88
104
|
"Provider",
|
|
105
|
+
# Navigation
|
|
106
|
+
"NavigationContainer",
|
|
107
|
+
"create_drawer_navigator",
|
|
108
|
+
"create_stack_navigator",
|
|
109
|
+
"create_tab_navigator",
|
|
89
110
|
# Styling
|
|
90
111
|
"StyleSheet",
|
|
91
112
|
"ThemeContext",
|
pythonnative/components.py
CHANGED
|
@@ -9,12 +9,18 @@ which accepts a dict or a list of dicts (later entries override earlier).
|
|
|
9
9
|
|
|
10
10
|
Layout properties supported by all components::
|
|
11
11
|
|
|
12
|
-
width, height, flex,
|
|
13
|
-
max_height, align_self
|
|
12
|
+
width, height, flex, flex_grow, flex_shrink, margin,
|
|
13
|
+
min_width, max_width, min_height, max_height, align_self
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
Flex container properties (View / Column / Row)::
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
flex_direction, justify_content, align_items, overflow,
|
|
18
|
+
spacing, padding
|
|
19
|
+
|
|
20
|
+
``View`` is the universal flex container (like React Native's ``View``).
|
|
21
|
+
It defaults to ``flex_direction: "column"``. ``Column`` and ``Row``
|
|
22
|
+
are convenience wrappers that fix the direction to ``"column"`` and
|
|
23
|
+
``"row"`` respectively.
|
|
18
24
|
"""
|
|
19
25
|
|
|
20
26
|
from typing import Any, Callable, Dict, List, Optional
|
|
@@ -204,15 +210,48 @@ def Slider(
|
|
|
204
210
|
# ======================================================================
|
|
205
211
|
|
|
206
212
|
|
|
213
|
+
def View(
|
|
214
|
+
*children: Element,
|
|
215
|
+
style: StyleValue = None,
|
|
216
|
+
key: Optional[str] = None,
|
|
217
|
+
) -> Element:
|
|
218
|
+
"""Universal flex container (like React Native's ``View``).
|
|
219
|
+
|
|
220
|
+
Defaults to ``flex_direction: "column"``. Override via ``style``::
|
|
221
|
+
|
|
222
|
+
pn.View(child_a, child_b, style={"flex_direction": "row"})
|
|
223
|
+
|
|
224
|
+
Flex container properties (inside ``style``):
|
|
225
|
+
|
|
226
|
+
- ``flex_direction`` — ``"column"`` (default), ``"row"``,
|
|
227
|
+
``"column_reverse"``, ``"row_reverse"``
|
|
228
|
+
- ``justify_content`` — main-axis distribution:
|
|
229
|
+
``"flex_start"`` (default), ``"center"``, ``"flex_end"``,
|
|
230
|
+
``"space_between"``, ``"space_around"``, ``"space_evenly"``
|
|
231
|
+
- ``align_items`` — cross-axis alignment:
|
|
232
|
+
``"stretch"`` (default), ``"flex_start"``, ``"center"``,
|
|
233
|
+
``"flex_end"``
|
|
234
|
+
- ``overflow`` — ``"visible"`` (default) or ``"hidden"``
|
|
235
|
+
- ``spacing``, ``padding``, ``background_color``
|
|
236
|
+
"""
|
|
237
|
+
props: Dict[str, Any] = {"flex_direction": "column"}
|
|
238
|
+
props.update(resolve_style(style))
|
|
239
|
+
return Element("View", props, list(children), key=key)
|
|
240
|
+
|
|
241
|
+
|
|
207
242
|
def Column(
|
|
208
243
|
*children: Element,
|
|
209
244
|
style: StyleValue = None,
|
|
210
245
|
key: Optional[str] = None,
|
|
211
246
|
) -> Element:
|
|
212
|
-
"""Arrange children vertically.
|
|
247
|
+
"""Arrange children vertically (``flex_direction: "column"``).
|
|
248
|
+
|
|
249
|
+
Convenience wrapper around :func:`View`. The direction is fixed;
|
|
250
|
+
use :func:`View` directly if you need ``flex_direction: "row"``.
|
|
213
251
|
|
|
214
252
|
Style properties: ``spacing``, ``padding``, ``align_items``,
|
|
215
|
-
``justify_content``, ``background_color``,
|
|
253
|
+
``justify_content``, ``background_color``, ``overflow``,
|
|
254
|
+
plus common layout props.
|
|
216
255
|
|
|
217
256
|
``align_items`` controls cross-axis (horizontal) alignment:
|
|
218
257
|
``"stretch"`` (default), ``"flex_start"``/``"leading"``,
|
|
@@ -222,8 +261,9 @@ def Column(
|
|
|
222
261
|
``"flex_start"`` (default), ``"center"``, ``"flex_end"``,
|
|
223
262
|
``"space_between"``, ``"space_around"``, ``"space_evenly"``.
|
|
224
263
|
"""
|
|
225
|
-
props: Dict[str, Any] = {}
|
|
264
|
+
props: Dict[str, Any] = {"flex_direction": "column"}
|
|
226
265
|
props.update(resolve_style(style))
|
|
266
|
+
props["flex_direction"] = "column"
|
|
227
267
|
return Element("Column", props, list(children), key=key)
|
|
228
268
|
|
|
229
269
|
|
|
@@ -232,10 +272,14 @@ def Row(
|
|
|
232
272
|
style: StyleValue = None,
|
|
233
273
|
key: Optional[str] = None,
|
|
234
274
|
) -> Element:
|
|
235
|
-
"""Arrange children horizontally.
|
|
275
|
+
"""Arrange children horizontally (``flex_direction: "row"``).
|
|
276
|
+
|
|
277
|
+
Convenience wrapper around :func:`View`. The direction is fixed;
|
|
278
|
+
use :func:`View` directly if you need ``flex_direction: "column"``.
|
|
236
279
|
|
|
237
280
|
Style properties: ``spacing``, ``padding``, ``align_items``,
|
|
238
|
-
``justify_content``, ``background_color``,
|
|
281
|
+
``justify_content``, ``background_color``, ``overflow``,
|
|
282
|
+
plus common layout props.
|
|
239
283
|
|
|
240
284
|
``align_items`` controls cross-axis (vertical) alignment:
|
|
241
285
|
``"stretch"`` (default), ``"flex_start"``/``"top"``,
|
|
@@ -245,8 +289,9 @@ def Row(
|
|
|
245
289
|
``"flex_start"`` (default), ``"center"``, ``"flex_end"``,
|
|
246
290
|
``"space_between"``, ``"space_around"``, ``"space_evenly"``.
|
|
247
291
|
"""
|
|
248
|
-
props: Dict[str, Any] = {}
|
|
292
|
+
props: Dict[str, Any] = {"flex_direction": "row"}
|
|
249
293
|
props.update(resolve_style(style))
|
|
294
|
+
props["flex_direction"] = "row"
|
|
250
295
|
return Element("Row", props, list(children), key=key)
|
|
251
296
|
|
|
252
297
|
|
|
@@ -263,17 +308,6 @@ def ScrollView(
|
|
|
263
308
|
return Element("ScrollView", props, children, key=key)
|
|
264
309
|
|
|
265
310
|
|
|
266
|
-
def View(
|
|
267
|
-
*children: Element,
|
|
268
|
-
style: StyleValue = None,
|
|
269
|
-
key: Optional[str] = None,
|
|
270
|
-
) -> Element:
|
|
271
|
-
"""Generic container view (``UIView`` / ``android.view.View``)."""
|
|
272
|
-
props: Dict[str, Any] = {}
|
|
273
|
-
props.update(resolve_style(style))
|
|
274
|
-
return Element("View", props, list(children), key=key)
|
|
275
|
-
|
|
276
|
-
|
|
277
311
|
def SafeAreaView(
|
|
278
312
|
*children: Element,
|
|
279
313
|
style: StyleValue = None,
|
|
@@ -323,6 +357,29 @@ def Pressable(
|
|
|
323
357
|
return Element("Pressable", props, children, key=key)
|
|
324
358
|
|
|
325
359
|
|
|
360
|
+
def ErrorBoundary(
|
|
361
|
+
child: Optional[Element] = None,
|
|
362
|
+
*,
|
|
363
|
+
fallback: Optional[Any] = None,
|
|
364
|
+
key: Optional[str] = None,
|
|
365
|
+
) -> Element:
|
|
366
|
+
"""Catch render errors in *child* and display *fallback* instead.
|
|
367
|
+
|
|
368
|
+
*fallback* may be an ``Element`` or a callable that receives the
|
|
369
|
+
exception and returns an ``Element``::
|
|
370
|
+
|
|
371
|
+
pn.ErrorBoundary(
|
|
372
|
+
MyRiskyComponent(),
|
|
373
|
+
fallback=lambda err: pn.Text(f"Error: {err}"),
|
|
374
|
+
)
|
|
375
|
+
"""
|
|
376
|
+
props: Dict[str, Any] = {}
|
|
377
|
+
if fallback is not None:
|
|
378
|
+
props["__fallback__"] = fallback
|
|
379
|
+
children = [child] if child is not None else []
|
|
380
|
+
return Element("__ErrorBoundary__", props, children, key=key)
|
|
381
|
+
|
|
382
|
+
|
|
326
383
|
def FlatList(
|
|
327
384
|
*,
|
|
328
385
|
data: Optional[List[Any]] = None,
|
pythonnative/hooks.py
CHANGED
|
@@ -19,7 +19,8 @@ Usage::
|
|
|
19
19
|
|
|
20
20
|
import inspect
|
|
21
21
|
import threading
|
|
22
|
-
from
|
|
22
|
+
from contextlib import contextmanager
|
|
23
|
+
from typing import Any, Callable, Dict, Generator, List, Optional, Tuple, TypeVar
|
|
23
24
|
|
|
24
25
|
from .element import Element
|
|
25
26
|
|
|
@@ -29,6 +30,7 @@ _SENTINEL = object()
|
|
|
29
30
|
|
|
30
31
|
_hook_context: threading.local = threading.local()
|
|
31
32
|
|
|
33
|
+
_batch_context: threading.local = threading.local()
|
|
32
34
|
|
|
33
35
|
# ======================================================================
|
|
34
36
|
# Hook state container
|
|
@@ -36,9 +38,22 @@ _hook_context: threading.local = threading.local()
|
|
|
36
38
|
|
|
37
39
|
|
|
38
40
|
class HookState:
|
|
39
|
-
"""Stores all hook data for a single function component instance.
|
|
41
|
+
"""Stores all hook data for a single function component instance.
|
|
40
42
|
|
|
41
|
-
|
|
43
|
+
Effects are **queued** during the render phase and **flushed** after
|
|
44
|
+
the reconciler commits native-view mutations. This guarantees that
|
|
45
|
+
effect callbacks can safely interact with the committed native tree.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
__slots__ = (
|
|
49
|
+
"states",
|
|
50
|
+
"effects",
|
|
51
|
+
"memos",
|
|
52
|
+
"refs",
|
|
53
|
+
"hook_index",
|
|
54
|
+
"_trigger_render",
|
|
55
|
+
"_pending_effects",
|
|
56
|
+
)
|
|
42
57
|
|
|
43
58
|
def __init__(self) -> None:
|
|
44
59
|
self.states: List[Any] = []
|
|
@@ -47,15 +62,24 @@ class HookState:
|
|
|
47
62
|
self.refs: List[dict] = []
|
|
48
63
|
self.hook_index: int = 0
|
|
49
64
|
self._trigger_render: Optional[Callable[[], None]] = None
|
|
65
|
+
self._pending_effects: List[Tuple[int, Callable, Any]] = []
|
|
50
66
|
|
|
51
67
|
def reset_index(self) -> None:
|
|
52
68
|
self.hook_index = 0
|
|
53
69
|
|
|
54
|
-
def
|
|
55
|
-
"""Execute effects
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
70
|
+
def flush_pending_effects(self) -> None:
|
|
71
|
+
"""Execute effects queued during the render pass (called after commit)."""
|
|
72
|
+
pending = self._pending_effects
|
|
73
|
+
self._pending_effects = []
|
|
74
|
+
for idx, effect_fn, deps in pending:
|
|
75
|
+
_, prev_cleanup = self.effects[idx]
|
|
76
|
+
if callable(prev_cleanup):
|
|
77
|
+
try:
|
|
78
|
+
prev_cleanup()
|
|
79
|
+
except Exception:
|
|
80
|
+
pass
|
|
81
|
+
cleanup = effect_fn()
|
|
82
|
+
self.effects[idx] = (list(deps) if deps is not None else None, cleanup)
|
|
59
83
|
|
|
60
84
|
def cleanup_all_effects(self) -> None:
|
|
61
85
|
"""Run all outstanding cleanup functions (called on unmount)."""
|
|
@@ -66,6 +90,7 @@ class HookState:
|
|
|
66
90
|
except Exception:
|
|
67
91
|
pass
|
|
68
92
|
self.effects[i] = (_SENTINEL, None)
|
|
93
|
+
self._pending_effects = []
|
|
69
94
|
|
|
70
95
|
|
|
71
96
|
# ======================================================================
|
|
@@ -91,6 +116,45 @@ def _deps_changed(prev: Any, current: Any) -> bool:
|
|
|
91
116
|
return any(p is not c and p != c for p, c in zip(prev, current))
|
|
92
117
|
|
|
93
118
|
|
|
119
|
+
# ======================================================================
|
|
120
|
+
# Batching helpers
|
|
121
|
+
# ======================================================================
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _schedule_trigger(trigger: Callable[[], None]) -> None:
|
|
125
|
+
"""Call *trigger* now, or defer it if inside :func:`batch_updates`."""
|
|
126
|
+
if getattr(_batch_context, "depth", 0) > 0:
|
|
127
|
+
_batch_context.pending_trigger = trigger
|
|
128
|
+
else:
|
|
129
|
+
trigger()
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
@contextmanager
|
|
133
|
+
def batch_updates() -> Generator[None, None, None]:
|
|
134
|
+
"""Batch multiple state updates so only one re-render occurs.
|
|
135
|
+
|
|
136
|
+
Usage::
|
|
137
|
+
|
|
138
|
+
with pn.batch_updates():
|
|
139
|
+
set_count(1)
|
|
140
|
+
set_name("hello")
|
|
141
|
+
# single re-render happens here
|
|
142
|
+
"""
|
|
143
|
+
depth = getattr(_batch_context, "depth", 0)
|
|
144
|
+
_batch_context.depth = depth + 1
|
|
145
|
+
if depth == 0:
|
|
146
|
+
_batch_context.pending_trigger = None
|
|
147
|
+
try:
|
|
148
|
+
yield
|
|
149
|
+
finally:
|
|
150
|
+
_batch_context.depth -= 1
|
|
151
|
+
if _batch_context.depth == 0:
|
|
152
|
+
trigger = _batch_context.pending_trigger
|
|
153
|
+
_batch_context.pending_trigger = None
|
|
154
|
+
if trigger is not None:
|
|
155
|
+
trigger()
|
|
156
|
+
|
|
157
|
+
|
|
94
158
|
# ======================================================================
|
|
95
159
|
# Public hooks
|
|
96
160
|
# ======================================================================
|
|
@@ -121,13 +185,60 @@ def use_state(initial: Any = None) -> Tuple[Any, Callable]:
|
|
|
121
185
|
if ctx.states[idx] is not new_value and ctx.states[idx] != new_value:
|
|
122
186
|
ctx.states[idx] = new_value
|
|
123
187
|
if ctx._trigger_render:
|
|
124
|
-
ctx._trigger_render
|
|
188
|
+
_schedule_trigger(ctx._trigger_render)
|
|
125
189
|
|
|
126
190
|
return current, setter
|
|
127
191
|
|
|
128
192
|
|
|
193
|
+
def use_reducer(reducer: Callable[[Any, Any], Any], initial_state: Any) -> Tuple[Any, Callable]:
|
|
194
|
+
"""Return ``(state, dispatch)`` for reducer-based state management.
|
|
195
|
+
|
|
196
|
+
The *reducer* is called as ``reducer(current_state, action)`` and
|
|
197
|
+
must return the new state. If *initial_state* is callable it is
|
|
198
|
+
invoked once (lazy initialisation).
|
|
199
|
+
|
|
200
|
+
Usage::
|
|
201
|
+
|
|
202
|
+
def reducer(state, action):
|
|
203
|
+
if action == "increment":
|
|
204
|
+
return state + 1
|
|
205
|
+
if action == "reset":
|
|
206
|
+
return 0
|
|
207
|
+
return state
|
|
208
|
+
|
|
209
|
+
count, dispatch = pn.use_reducer(reducer, 0)
|
|
210
|
+
# dispatch("increment") -> re-render with count = 1
|
|
211
|
+
"""
|
|
212
|
+
ctx = _get_hook_state()
|
|
213
|
+
if ctx is None:
|
|
214
|
+
raise RuntimeError("use_reducer must be called inside a @component function")
|
|
215
|
+
|
|
216
|
+
idx = ctx.hook_index
|
|
217
|
+
ctx.hook_index += 1
|
|
218
|
+
|
|
219
|
+
if idx >= len(ctx.states):
|
|
220
|
+
val = initial_state() if callable(initial_state) else initial_state
|
|
221
|
+
ctx.states.append(val)
|
|
222
|
+
|
|
223
|
+
current = ctx.states[idx]
|
|
224
|
+
|
|
225
|
+
def dispatch(action: Any) -> None:
|
|
226
|
+
new_state = reducer(ctx.states[idx], action)
|
|
227
|
+
if ctx.states[idx] is not new_state and ctx.states[idx] != new_state:
|
|
228
|
+
ctx.states[idx] = new_state
|
|
229
|
+
if ctx._trigger_render:
|
|
230
|
+
_schedule_trigger(ctx._trigger_render)
|
|
231
|
+
|
|
232
|
+
return current, dispatch
|
|
233
|
+
|
|
234
|
+
|
|
129
235
|
def use_effect(effect: Callable, deps: Optional[list] = None) -> None:
|
|
130
|
-
"""Schedule *effect* to run after
|
|
236
|
+
"""Schedule *effect* to run **after** the native tree is committed.
|
|
237
|
+
|
|
238
|
+
Effects are queued during the render pass and flushed once the
|
|
239
|
+
reconciler has finished applying all native-view mutations. This
|
|
240
|
+
means effects can safely measure layout or interact with committed
|
|
241
|
+
native views.
|
|
131
242
|
|
|
132
243
|
*deps* controls when the effect re-runs:
|
|
133
244
|
|
|
@@ -146,18 +257,12 @@ def use_effect(effect: Callable, deps: Optional[list] = None) -> None:
|
|
|
146
257
|
|
|
147
258
|
if idx >= len(ctx.effects):
|
|
148
259
|
ctx.effects.append((_SENTINEL, None))
|
|
260
|
+
ctx._pending_effects.append((idx, effect, deps))
|
|
261
|
+
return
|
|
149
262
|
|
|
150
|
-
prev_deps,
|
|
263
|
+
prev_deps, _prev_cleanup = ctx.effects[idx]
|
|
151
264
|
if _deps_changed(prev_deps, deps):
|
|
152
|
-
|
|
153
|
-
try:
|
|
154
|
-
prev_cleanup()
|
|
155
|
-
except Exception:
|
|
156
|
-
pass
|
|
157
|
-
cleanup = effect()
|
|
158
|
-
ctx.effects[idx] = (list(deps) if deps is not None else None, cleanup)
|
|
159
|
-
else:
|
|
160
|
-
ctx.effects[idx] = (prev_deps, prev_cleanup)
|
|
265
|
+
ctx._pending_effects.append((idx, effect, deps))
|
|
161
266
|
|
|
162
267
|
|
|
163
268
|
def use_memo(factory: Callable[[], T], deps: list) -> T:
|
|
@@ -255,27 +360,28 @@ _NavigationContext: Context = create_context(None)
|
|
|
255
360
|
|
|
256
361
|
|
|
257
362
|
class NavigationHandle:
|
|
258
|
-
"""Object returned by :func:`use_navigation` providing
|
|
363
|
+
"""Object returned by :func:`use_navigation` providing navigation methods.
|
|
259
364
|
|
|
260
|
-
|
|
365
|
+
::
|
|
261
366
|
|
|
262
367
|
nav = pn.use_navigation()
|
|
263
|
-
nav.
|
|
368
|
+
nav.navigate(DetailScreen, params={"id": 42})
|
|
369
|
+
nav.go_back()
|
|
264
370
|
"""
|
|
265
371
|
|
|
266
372
|
def __init__(self, host: Any) -> None:
|
|
267
373
|
self._host = host
|
|
268
374
|
|
|
269
|
-
def
|
|
270
|
-
"""Navigate forward to *page*
|
|
271
|
-
self._host._push(page,
|
|
375
|
+
def navigate(self, page: Any, params: Optional[Dict[str, Any]] = None) -> None:
|
|
376
|
+
"""Navigate forward to *page* with optional *params*."""
|
|
377
|
+
self._host._push(page, params)
|
|
272
378
|
|
|
273
|
-
def
|
|
379
|
+
def go_back(self) -> None:
|
|
274
380
|
"""Navigate back to the previous screen."""
|
|
275
381
|
self._host._pop()
|
|
276
382
|
|
|
277
|
-
def
|
|
278
|
-
"""Return
|
|
383
|
+
def get_params(self) -> Dict[str, Any]:
|
|
384
|
+
"""Return parameters passed from the previous screen."""
|
|
279
385
|
return self._host._get_nav_args()
|
|
280
386
|
|
|
281
387
|
|
pythonnative/hot_reload.py
CHANGED
|
@@ -137,7 +137,7 @@ class ModuleReloader:
|
|
|
137
137
|
@staticmethod
|
|
138
138
|
def reload_page(page_instance: Any) -> None:
|
|
139
139
|
"""Force a page re-render after module reload."""
|
|
140
|
-
from .page import
|
|
140
|
+
from .page import _request_render
|
|
141
141
|
|
|
142
142
|
if hasattr(page_instance, "_reconciler") and page_instance._reconciler is not None:
|
|
143
|
-
|
|
143
|
+
_request_render(page_instance)
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""Platform-specific native view creation and update logic.
|
|
2
|
+
|
|
3
|
+
This package provides the :class:`NativeViewRegistry` that maps element type
|
|
4
|
+
names to platform-specific :class:`~.base.ViewHandler` implementations.
|
|
5
|
+
|
|
6
|
+
Platform handlers live in dedicated submodules:
|
|
7
|
+
|
|
8
|
+
- :mod:`~.base` — shared :class:`~.base.ViewHandler` protocol and utilities
|
|
9
|
+
- :mod:`~.android` — Android handlers (Chaquopy / Java bridge)
|
|
10
|
+
- :mod:`~.ios` — iOS handlers (rubicon-objc)
|
|
11
|
+
|
|
12
|
+
All platform-branching is handled at registration time via lazy imports,
|
|
13
|
+
so the package can be imported on any platform for testing.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from typing import Any, Dict, Optional
|
|
17
|
+
|
|
18
|
+
from .base import ViewHandler
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class NativeViewRegistry:
|
|
22
|
+
"""Maps element type names to platform-specific :class:`ViewHandler` instances."""
|
|
23
|
+
|
|
24
|
+
def __init__(self) -> None:
|
|
25
|
+
self._handlers: Dict[str, ViewHandler] = {}
|
|
26
|
+
|
|
27
|
+
def register(self, type_name: str, handler: ViewHandler) -> None:
|
|
28
|
+
self._handlers[type_name] = handler
|
|
29
|
+
|
|
30
|
+
def create_view(self, type_name: str, props: Dict[str, Any]) -> Any:
|
|
31
|
+
handler = self._handlers.get(type_name)
|
|
32
|
+
if handler is None:
|
|
33
|
+
raise ValueError(f"Unknown element type: {type_name!r}")
|
|
34
|
+
return handler.create(props)
|
|
35
|
+
|
|
36
|
+
def update_view(self, native_view: Any, type_name: str, changed_props: Dict[str, Any]) -> None:
|
|
37
|
+
handler = self._handlers.get(type_name)
|
|
38
|
+
if handler is not None:
|
|
39
|
+
handler.update(native_view, changed_props)
|
|
40
|
+
|
|
41
|
+
def add_child(self, parent: Any, child: Any, parent_type: str) -> None:
|
|
42
|
+
handler = self._handlers.get(parent_type)
|
|
43
|
+
if handler is not None:
|
|
44
|
+
handler.add_child(parent, child)
|
|
45
|
+
|
|
46
|
+
def remove_child(self, parent: Any, child: Any, parent_type: str) -> None:
|
|
47
|
+
handler = self._handlers.get(parent_type)
|
|
48
|
+
if handler is not None:
|
|
49
|
+
handler.remove_child(parent, child)
|
|
50
|
+
|
|
51
|
+
def insert_child(self, parent: Any, child: Any, parent_type: str, index: int) -> None:
|
|
52
|
+
handler = self._handlers.get(parent_type)
|
|
53
|
+
if handler is not None:
|
|
54
|
+
handler.insert_child(parent, child, index)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
# ======================================================================
|
|
58
|
+
# Singleton registry
|
|
59
|
+
# ======================================================================
|
|
60
|
+
|
|
61
|
+
_registry: Optional[NativeViewRegistry] = None
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def get_registry() -> NativeViewRegistry:
|
|
65
|
+
"""Return the singleton registry, lazily creating platform handlers."""
|
|
66
|
+
global _registry
|
|
67
|
+
if _registry is not None:
|
|
68
|
+
return _registry
|
|
69
|
+
_registry = NativeViewRegistry()
|
|
70
|
+
|
|
71
|
+
from ..utils import IS_ANDROID
|
|
72
|
+
|
|
73
|
+
if IS_ANDROID:
|
|
74
|
+
from .android import register_handlers
|
|
75
|
+
|
|
76
|
+
register_handlers(_registry)
|
|
77
|
+
else:
|
|
78
|
+
from .ios import register_handlers
|
|
79
|
+
|
|
80
|
+
register_handlers(_registry)
|
|
81
|
+
return _registry
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def set_registry(registry: NativeViewRegistry) -> None:
|
|
85
|
+
"""Inject a custom or mock registry (primarily for testing)."""
|
|
86
|
+
global _registry
|
|
87
|
+
_registry = registry
|