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
pythonnative/navigation.py
DELETED
|
@@ -1,1031 +0,0 @@
|
|
|
1
|
-
"""Declarative navigation for PythonNative.
|
|
2
|
-
|
|
3
|
-
Provides a component-based navigation system inspired by React
|
|
4
|
-
Navigation. Navigators manage screen state in Python and the
|
|
5
|
-
[`Stack`][pythonnative.create_stack_navigator] navigator pushes real
|
|
6
|
-
native screen containers (``UIViewController`` on iOS,
|
|
7
|
-
``Fragment`` on Android) so users get system-grade transitions and
|
|
8
|
-
swipe-back gestures for free.
|
|
9
|
-
|
|
10
|
-
Three navigator factories are provided out of the box:
|
|
11
|
-
|
|
12
|
-
- [`create_stack_navigator`][pythonnative.create_stack_navigator]: push
|
|
13
|
-
and pop screens with native transitions when the navigator is mounted
|
|
14
|
-
at the root of an app host.
|
|
15
|
-
- [`create_tab_navigator`][pythonnative.create_tab_navigator]: switch
|
|
16
|
-
between sibling tabs (with a tab bar).
|
|
17
|
-
- [`create_drawer_navigator`][pythonnative.create_drawer_navigator]:
|
|
18
|
-
switch between sibling screens via a side drawer menu.
|
|
19
|
-
|
|
20
|
-
Navigators may be nested arbitrarily; nested handles forward unknown
|
|
21
|
-
routes and root-level ``go_back`` calls to their parent.
|
|
22
|
-
|
|
23
|
-
Stack navigators rendered as the root of an app host (i.e. the parent
|
|
24
|
-
[`use_navigation`][pythonnative.use_navigation] handle is the host's
|
|
25
|
-
own handle) talk to the platform via that host's ``_push`` / ``_pop``
|
|
26
|
-
methods, so the back stack matches what UIKit / AndroidX maintain.
|
|
27
|
-
Nested stacks (e.g. a stack inside a tab) fall back to in-Python
|
|
28
|
-
state; there's no second native navigation controller to push
|
|
29
|
-
onto in that case.
|
|
30
|
-
|
|
31
|
-
Example:
|
|
32
|
-
```python
|
|
33
|
-
import pythonnative as pn
|
|
34
|
-
|
|
35
|
-
Stack = pn.create_stack_navigator()
|
|
36
|
-
|
|
37
|
-
@pn.component
|
|
38
|
-
def App():
|
|
39
|
-
return pn.NavigationContainer(
|
|
40
|
-
Stack.Navigator(
|
|
41
|
-
Stack.Screen("Home", component=HomeScreen, options={"title": "Home"}),
|
|
42
|
-
Stack.Screen("Detail", component=DetailScreen, options={"title": "Detail"}),
|
|
43
|
-
)
|
|
44
|
-
)
|
|
45
|
-
```
|
|
46
|
-
"""
|
|
47
|
-
|
|
48
|
-
from typing import Any, Callable, Dict, List, Optional, TypedDict, Union
|
|
49
|
-
|
|
50
|
-
from .element import Element
|
|
51
|
-
from .hooks import (
|
|
52
|
-
Provider,
|
|
53
|
-
Ref,
|
|
54
|
-
_NavigationContext,
|
|
55
|
-
component,
|
|
56
|
-
create_context,
|
|
57
|
-
use_context,
|
|
58
|
-
use_effect,
|
|
59
|
-
use_memo,
|
|
60
|
-
use_ref,
|
|
61
|
-
use_state,
|
|
62
|
-
)
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
class ScreenOptions(TypedDict, total=False):
|
|
66
|
-
"""Typed per-screen options accepted by ``Screen(..., options=...)``.
|
|
67
|
-
|
|
68
|
-
All keys are optional. Navigators ignore keys they don't use.
|
|
69
|
-
|
|
70
|
-
Attributes:
|
|
71
|
-
title: Human-readable screen title. Stack navigators forward it
|
|
72
|
-
to the host's native navigation bar; tab and drawer
|
|
73
|
-
navigators use it as the item label.
|
|
74
|
-
tab_bar_icon: Native system icon identifier for tab items. A
|
|
75
|
-
string is used on every platform; a dict like
|
|
76
|
-
``{"ios": "house.fill", "android": "ic_menu_home"}``
|
|
77
|
-
selects per platform. iOS values are resolved via
|
|
78
|
-
SF Symbols (``UIImage.systemImageNamed_``); Android values
|
|
79
|
-
are resolved against ``android.R.drawable.<name>``. Names
|
|
80
|
-
that don't resolve fall back to text-only.
|
|
81
|
-
"""
|
|
82
|
-
|
|
83
|
-
title: str
|
|
84
|
-
tab_bar_icon: Union[str, Dict[str, str]]
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
# ======================================================================
|
|
88
|
-
# Focus context
|
|
89
|
-
# ======================================================================
|
|
90
|
-
|
|
91
|
-
# Defaults to True: components rendered outside any declarative
|
|
92
|
-
# navigator (e.g. the root component of a screen pushed via the host's
|
|
93
|
-
# native nav stack) are by definition focused; the host's own
|
|
94
|
-
# ``on_resume`` / ``on_pause`` lifecycle drives the focus state for
|
|
95
|
-
# those. Declarative navigators override this provider on the active
|
|
96
|
-
# subtree (always True today; reserved for future inactive-screen
|
|
97
|
-
# rendering).
|
|
98
|
-
_FocusContext = create_context(True)
|
|
99
|
-
|
|
100
|
-
# ======================================================================
|
|
101
|
-
# Data structures
|
|
102
|
-
# ======================================================================
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
class _ScreenDef:
|
|
106
|
-
"""Configuration for a single screen within a navigator.
|
|
107
|
-
|
|
108
|
-
Attributes:
|
|
109
|
-
name: Route name used by `navigate(name, ...)`.
|
|
110
|
-
component: A `@component` function rendered when this screen is
|
|
111
|
-
active.
|
|
112
|
-
options: Per-screen [`ScreenOptions`][pythonnative.ScreenOptions]
|
|
113
|
-
(e.g., `title`, `tab_bar_icon`). Interpretation is up to
|
|
114
|
-
the specific navigator implementation.
|
|
115
|
-
"""
|
|
116
|
-
|
|
117
|
-
__slots__ = ("name", "component", "options")
|
|
118
|
-
|
|
119
|
-
def __init__(self, name: str, component_fn: Any, options: Optional[ScreenOptions] = None) -> None:
|
|
120
|
-
self.name = name
|
|
121
|
-
self.component = component_fn
|
|
122
|
-
self.options = options or {}
|
|
123
|
-
|
|
124
|
-
def __repr__(self) -> str:
|
|
125
|
-
return f"Screen({self.name!r})"
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
class _RouteEntry:
|
|
129
|
-
"""One entry on a stack-style navigation stack.
|
|
130
|
-
|
|
131
|
-
Attributes:
|
|
132
|
-
name: Route name (matches a `_ScreenDef.name`).
|
|
133
|
-
params: Params dict passed via `navigate(..., params=...)`.
|
|
134
|
-
"""
|
|
135
|
-
|
|
136
|
-
__slots__ = ("name", "params")
|
|
137
|
-
|
|
138
|
-
def __init__(self, name: str, params: Optional[Dict[str, Any]] = None) -> None:
|
|
139
|
-
self.name = name
|
|
140
|
-
self.params = params or {}
|
|
141
|
-
|
|
142
|
-
def __repr__(self) -> str:
|
|
143
|
-
return f"Route({self.name!r})"
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
# ======================================================================
|
|
147
|
-
# Navigation handle for declarative navigators
|
|
148
|
-
# ======================================================================
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
_INITIAL_ROUTE_KEY = "__pn_initial_route__"
|
|
152
|
-
_INITIAL_PARAMS_KEY = "__pn_initial_params__"
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
def _parent_is_app_host(parent: Any) -> bool:
|
|
156
|
-
"""Return True when ``parent`` is the host's own NavigationHandle.
|
|
157
|
-
|
|
158
|
-
The host's
|
|
159
|
-
[`NavigationHandle`][pythonnative.hooks.NavigationHandle] sets a
|
|
160
|
-
``_host`` attribute pointing at the owning app host. Declarative
|
|
161
|
-
navigators check for that marker to decide whether they should
|
|
162
|
-
push real native screens (root navigator) or fall back to
|
|
163
|
-
in-Python state (nested navigator).
|
|
164
|
-
"""
|
|
165
|
-
return parent is not None and hasattr(parent, "_host") and getattr(parent, "_host", None) is not None
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
class _DeclarativeNavHandle:
|
|
169
|
-
"""Navigation handle provided by declarative navigators.
|
|
170
|
-
|
|
171
|
-
Implements the same interface as
|
|
172
|
-
[`NavigationHandle`][pythonnative.hooks.NavigationHandle] so
|
|
173
|
-
[`use_navigation`][pythonnative.use_navigation] returns a
|
|
174
|
-
compatible object regardless of whether the app drives navigation
|
|
175
|
-
imperatively or through declarative navigators.
|
|
176
|
-
|
|
177
|
-
When ``parent`` is the host's own ``NavigationHandle`` (root
|
|
178
|
-
Stack), ``navigate`` / ``go_back`` / ``reset`` drive the native
|
|
179
|
-
navigation controller and the in-Python stack is bypassed; the
|
|
180
|
-
OS owns the back-stack source of truth.
|
|
181
|
-
|
|
182
|
-
When ``parent`` is another declarative handle (nested navigator),
|
|
183
|
-
unknown routes and root-level ``go_back`` calls are forwarded to
|
|
184
|
-
the parent so a stack inside a tab still pops correctly.
|
|
185
|
-
"""
|
|
186
|
-
|
|
187
|
-
def __init__(
|
|
188
|
-
self,
|
|
189
|
-
screen_map: Dict[str, "_ScreenDef"],
|
|
190
|
-
get_stack: Callable[[], List["_RouteEntry"]],
|
|
191
|
-
set_stack: Callable,
|
|
192
|
-
parent: Any = None,
|
|
193
|
-
) -> None:
|
|
194
|
-
self._screen_map = screen_map
|
|
195
|
-
self._get_stack = get_stack
|
|
196
|
-
self._set_stack = set_stack
|
|
197
|
-
self._parent = parent
|
|
198
|
-
self._host_component_path: Optional[str] = None
|
|
199
|
-
|
|
200
|
-
def _push_via_host(self, route_name: str, params: Optional[Dict[str, Any]]) -> bool:
|
|
201
|
-
"""Try to push via the underlying app host. Returns True if it ran."""
|
|
202
|
-
if not _parent_is_app_host(self._parent):
|
|
203
|
-
return False
|
|
204
|
-
host = self._parent._host
|
|
205
|
-
component_path = self._host_component_path or getattr(host, "_component_path", None)
|
|
206
|
-
if not component_path:
|
|
207
|
-
return False
|
|
208
|
-
push_args = {
|
|
209
|
-
_INITIAL_ROUTE_KEY: route_name,
|
|
210
|
-
_INITIAL_PARAMS_KEY: params or {},
|
|
211
|
-
}
|
|
212
|
-
try:
|
|
213
|
-
host._push(component_path, push_args)
|
|
214
|
-
return True
|
|
215
|
-
except Exception:
|
|
216
|
-
return False
|
|
217
|
-
|
|
218
|
-
def _pop_via_host(self) -> bool:
|
|
219
|
-
"""Try to pop via the underlying app host. Returns True if it ran."""
|
|
220
|
-
if not _parent_is_app_host(self._parent):
|
|
221
|
-
return False
|
|
222
|
-
try:
|
|
223
|
-
self._parent._host._pop()
|
|
224
|
-
return True
|
|
225
|
-
except Exception:
|
|
226
|
-
return False
|
|
227
|
-
|
|
228
|
-
def navigate(self, route_name: str, params: Optional[Dict[str, Any]] = None) -> None:
|
|
229
|
-
"""Navigate to a named route.
|
|
230
|
-
|
|
231
|
-
Behaviour depends on the kind of navigator:
|
|
232
|
-
|
|
233
|
-
- **Root stack** (parent is the host's NavigationHandle): the
|
|
234
|
-
host's native ``_push`` is invoked so the user sees a real
|
|
235
|
-
UIKit / AndroidX transition; the in-Python stack is left
|
|
236
|
-
untouched because the native controller is the source of
|
|
237
|
-
truth.
|
|
238
|
-
- **Nested stack / tab / drawer**: ``set_stack`` is called with
|
|
239
|
-
the new route; the parent reconciler re-renders the active
|
|
240
|
-
screen subtree in place.
|
|
241
|
-
- **Unknown route**: forwarded to ``parent`` if one exists,
|
|
242
|
-
otherwise raises ``ValueError``.
|
|
243
|
-
|
|
244
|
-
Args:
|
|
245
|
-
route_name: A route registered on this navigator.
|
|
246
|
-
params: Optional dict made available to the destination
|
|
247
|
-
screen via [`use_route`][pythonnative.use_route] or
|
|
248
|
-
``nav.get_params()``.
|
|
249
|
-
|
|
250
|
-
Raises:
|
|
251
|
-
ValueError: If ``route_name`` is unknown and no parent
|
|
252
|
-
handle exists.
|
|
253
|
-
"""
|
|
254
|
-
if route_name in self._screen_map:
|
|
255
|
-
if self._push_via_host(route_name, params):
|
|
256
|
-
return
|
|
257
|
-
entry = _RouteEntry(route_name, params)
|
|
258
|
-
self._set_stack(lambda s: list(s) + [entry])
|
|
259
|
-
elif self._parent is not None:
|
|
260
|
-
self._parent.navigate(route_name, params=params)
|
|
261
|
-
else:
|
|
262
|
-
raise ValueError(f"Unknown route: {route_name!r}. Known routes: {list(self._screen_map)}")
|
|
263
|
-
|
|
264
|
-
def go_back(self) -> None:
|
|
265
|
-
"""Pop the current screen from the stack.
|
|
266
|
-
|
|
267
|
-
Resolution order:
|
|
268
|
-
|
|
269
|
-
1. If the current stack has more than one entry (a real
|
|
270
|
-
in-Python push), pop the top.
|
|
271
|
-
2. If the parent is the host's NavigationHandle, pop the
|
|
272
|
-
native navigation controller.
|
|
273
|
-
3. If the parent is another declarative handle, forward.
|
|
274
|
-
4. Otherwise no-op.
|
|
275
|
-
"""
|
|
276
|
-
stack = self._get_stack()
|
|
277
|
-
if len(stack) > 1:
|
|
278
|
-
self._set_stack(lambda s: list(s[:-1]))
|
|
279
|
-
return
|
|
280
|
-
if self._pop_via_host():
|
|
281
|
-
return
|
|
282
|
-
if self._parent is not None:
|
|
283
|
-
self._parent.go_back()
|
|
284
|
-
|
|
285
|
-
def get_params(self) -> Dict[str, Any]:
|
|
286
|
-
"""Return the params dict for the current route.
|
|
287
|
-
|
|
288
|
-
Returns:
|
|
289
|
-
The dict supplied to the most recent matching ``navigate(...)``
|
|
290
|
-
call, or an empty dict if none was supplied.
|
|
291
|
-
"""
|
|
292
|
-
stack = self._get_stack()
|
|
293
|
-
return stack[-1].params if stack else {}
|
|
294
|
-
|
|
295
|
-
def reset(self, route_name: str, params: Optional[Dict[str, Any]] = None) -> None:
|
|
296
|
-
"""Reset the stack to a single route.
|
|
297
|
-
|
|
298
|
-
For a root stack this pops every screen above the original
|
|
299
|
-
root (the screen the user landed on when the app launched)
|
|
300
|
-
and then pushes ``route_name`` on top. For nested navigators
|
|
301
|
-
the in-Python stack is replaced wholesale.
|
|
302
|
-
|
|
303
|
-
Args:
|
|
304
|
-
route_name: Route to install as the new root.
|
|
305
|
-
params: Optional params for that route.
|
|
306
|
-
|
|
307
|
-
Raises:
|
|
308
|
-
ValueError: If ``route_name`` is unknown.
|
|
309
|
-
"""
|
|
310
|
-
if route_name not in self._screen_map:
|
|
311
|
-
raise ValueError(f"Unknown route: {route_name!r}. Known routes: {list(self._screen_map)}")
|
|
312
|
-
if _parent_is_app_host(self._parent):
|
|
313
|
-
host = self._parent._host
|
|
314
|
-
reset_fn = getattr(host, "_reset_to_root", None)
|
|
315
|
-
if callable(reset_fn):
|
|
316
|
-
try:
|
|
317
|
-
reset_fn()
|
|
318
|
-
except Exception:
|
|
319
|
-
pass
|
|
320
|
-
if self._push_via_host(route_name, params):
|
|
321
|
-
return
|
|
322
|
-
self._set_stack([_RouteEntry(route_name, params)])
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
class _TabNavHandle(_DeclarativeNavHandle):
|
|
326
|
-
"""Navigation handle for tab navigators.
|
|
327
|
-
|
|
328
|
-
`navigate(name)` switches the active tab instead of pushing a new
|
|
329
|
-
entry onto a stack. Unknown routes are forwarded to the parent
|
|
330
|
-
handle if one exists.
|
|
331
|
-
"""
|
|
332
|
-
|
|
333
|
-
def __init__(
|
|
334
|
-
self,
|
|
335
|
-
screen_map: Dict[str, "_ScreenDef"],
|
|
336
|
-
get_stack: Callable[[], List["_RouteEntry"]],
|
|
337
|
-
set_stack: Callable,
|
|
338
|
-
switch_tab: Callable[[str, Optional[Dict[str, Any]]], None],
|
|
339
|
-
parent: Any = None,
|
|
340
|
-
) -> None:
|
|
341
|
-
super().__init__(screen_map, get_stack, set_stack, parent=parent)
|
|
342
|
-
self._switch_tab = switch_tab
|
|
343
|
-
|
|
344
|
-
def navigate(self, route_name: str, params: Optional[Dict[str, Any]] = None) -> None:
|
|
345
|
-
"""Switch to a tab by name, or forward to parent for unknown routes."""
|
|
346
|
-
if route_name in self._screen_map:
|
|
347
|
-
self._switch_tab(route_name, params)
|
|
348
|
-
elif self._parent is not None:
|
|
349
|
-
self._parent.navigate(route_name, params=params)
|
|
350
|
-
else:
|
|
351
|
-
raise ValueError(f"Unknown route: {route_name!r}. Known routes: {list(self._screen_map)}")
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
class _DrawerNavHandle(_DeclarativeNavHandle):
|
|
355
|
-
"""Navigation handle for drawer navigators.
|
|
356
|
-
|
|
357
|
-
Adds drawer-specific methods
|
|
358
|
-
([`open_drawer`][pythonnative.navigation._DrawerNavHandle.open_drawer],
|
|
359
|
-
[`close_drawer`][pythonnative.navigation._DrawerNavHandle.close_drawer],
|
|
360
|
-
[`toggle_drawer`][pythonnative.navigation._DrawerNavHandle.toggle_drawer])
|
|
361
|
-
on top of the standard `_DeclarativeNavHandle` interface.
|
|
362
|
-
"""
|
|
363
|
-
|
|
364
|
-
def __init__(
|
|
365
|
-
self,
|
|
366
|
-
screen_map: Dict[str, "_ScreenDef"],
|
|
367
|
-
get_stack: Callable[[], List["_RouteEntry"]],
|
|
368
|
-
set_stack: Callable,
|
|
369
|
-
switch_screen: Callable[[str, Optional[Dict[str, Any]]], None],
|
|
370
|
-
set_drawer_open: Callable[[bool], None],
|
|
371
|
-
get_drawer_open: Callable[[], bool],
|
|
372
|
-
parent: Any = None,
|
|
373
|
-
) -> None:
|
|
374
|
-
super().__init__(screen_map, get_stack, set_stack, parent=parent)
|
|
375
|
-
self._switch_screen = switch_screen
|
|
376
|
-
self._set_drawer_open = set_drawer_open
|
|
377
|
-
self._get_drawer_open = get_drawer_open
|
|
378
|
-
|
|
379
|
-
def navigate(self, route_name: str, params: Optional[Dict[str, Any]] = None) -> None:
|
|
380
|
-
"""Switch to a screen and close the drawer, or forward to parent."""
|
|
381
|
-
if route_name in self._screen_map:
|
|
382
|
-
self._switch_screen(route_name, params)
|
|
383
|
-
self._set_drawer_open(False)
|
|
384
|
-
elif self._parent is not None:
|
|
385
|
-
self._parent.navigate(route_name, params=params)
|
|
386
|
-
else:
|
|
387
|
-
raise ValueError(f"Unknown route: {route_name!r}. Known routes: {list(self._screen_map)}")
|
|
388
|
-
|
|
389
|
-
def open_drawer(self) -> None:
|
|
390
|
-
"""Open the drawer."""
|
|
391
|
-
self._set_drawer_open(True)
|
|
392
|
-
|
|
393
|
-
def close_drawer(self) -> None:
|
|
394
|
-
"""Close the drawer."""
|
|
395
|
-
self._set_drawer_open(False)
|
|
396
|
-
|
|
397
|
-
def toggle_drawer(self) -> None:
|
|
398
|
-
"""Toggle the drawer open/closed."""
|
|
399
|
-
self._set_drawer_open(not self._get_drawer_open())
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
# ======================================================================
|
|
403
|
-
# Stack navigator
|
|
404
|
-
# ======================================================================
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
def _build_screen_map(screens: Any) -> Dict[str, "_ScreenDef"]:
|
|
408
|
-
"""Build an ordered dict of name -> _ScreenDef from a list."""
|
|
409
|
-
result: Dict[str, _ScreenDef] = {}
|
|
410
|
-
for s in screens or []:
|
|
411
|
-
if isinstance(s, _ScreenDef):
|
|
412
|
-
result[s.name] = s
|
|
413
|
-
return result
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
def _read_host_initial_route(parent_nav: Any) -> "tuple[Optional[str], Dict[str, Any]]":
|
|
417
|
-
"""Extract a host-provided initial route from a parent NavigationHandle.
|
|
418
|
-
|
|
419
|
-
When a pushed VC/Fragment boots, the host's ``_args`` carry the
|
|
420
|
-
requested route name and params (set by
|
|
421
|
-
[`_DeclarativeNavHandle._push_via_host`][pythonnative.navigation._DeclarativeNavHandle._push_via_host]).
|
|
422
|
-
The root Stack navigator reads them so the new screen renders on
|
|
423
|
-
the right entry rather than the navigator's first registered
|
|
424
|
-
screen.
|
|
425
|
-
|
|
426
|
-
Returns:
|
|
427
|
-
``(route_name, params)``. Both are ``None`` / ``{}`` when no
|
|
428
|
-
host args are present.
|
|
429
|
-
"""
|
|
430
|
-
if not _parent_is_app_host(parent_nav):
|
|
431
|
-
return None, {}
|
|
432
|
-
args = parent_nav.get_params() if parent_nav is not None else {}
|
|
433
|
-
if not isinstance(args, dict):
|
|
434
|
-
return None, {}
|
|
435
|
-
route = args.get(_INITIAL_ROUTE_KEY)
|
|
436
|
-
params = args.get(_INITIAL_PARAMS_KEY) or {}
|
|
437
|
-
if not isinstance(route, str) or not route:
|
|
438
|
-
return None, {}
|
|
439
|
-
if not isinstance(params, dict):
|
|
440
|
-
params = {}
|
|
441
|
-
return route, params
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
def _apply_screen_options(parent_nav: Any, screen_def: "_ScreenDef") -> None:
|
|
445
|
-
"""Push the active screen's options into the host (title, etc.)."""
|
|
446
|
-
if not _parent_is_app_host(parent_nav):
|
|
447
|
-
return
|
|
448
|
-
host = parent_nav._host
|
|
449
|
-
title = screen_def.options.get("title") if screen_def is not None else None
|
|
450
|
-
setter = getattr(host, "_set_screen_options", None)
|
|
451
|
-
if callable(setter):
|
|
452
|
-
try:
|
|
453
|
-
setter({"title": title} if title is not None else {})
|
|
454
|
-
except Exception:
|
|
455
|
-
pass
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
@component
|
|
459
|
-
def _stack_navigator_impl(screens: Any = None, initial_route: Optional[str] = None) -> Element:
|
|
460
|
-
screen_map = _build_screen_map(screens)
|
|
461
|
-
if not screen_map:
|
|
462
|
-
return Element("View", {}, [])
|
|
463
|
-
|
|
464
|
-
parent_nav = use_context(_NavigationContext)
|
|
465
|
-
|
|
466
|
-
requested_route, requested_params = _read_host_initial_route(parent_nav)
|
|
467
|
-
if requested_route is not None and requested_route in screen_map:
|
|
468
|
-
first_route = requested_route
|
|
469
|
-
first_params: Dict[str, Any] = dict(requested_params)
|
|
470
|
-
else:
|
|
471
|
-
first_route = initial_route or next(iter(screen_map))
|
|
472
|
-
first_params = {}
|
|
473
|
-
|
|
474
|
-
stack, set_stack = use_state(lambda: [_RouteEntry(first_route, first_params)])
|
|
475
|
-
|
|
476
|
-
stack_ref = use_ref(None)
|
|
477
|
-
stack_ref.current = stack
|
|
478
|
-
|
|
479
|
-
handle = use_memo(
|
|
480
|
-
lambda: _DeclarativeNavHandle(screen_map, lambda: stack_ref.current, set_stack, parent=parent_nav), []
|
|
481
|
-
)
|
|
482
|
-
handle._screen_map = screen_map
|
|
483
|
-
handle._parent = parent_nav
|
|
484
|
-
|
|
485
|
-
current = stack[-1]
|
|
486
|
-
screen_def = screen_map.get(current.name)
|
|
487
|
-
if screen_def is None:
|
|
488
|
-
return Element("Text", {"text": f"Unknown route: {current.name}"}, [])
|
|
489
|
-
|
|
490
|
-
_apply_screen_options(parent_nav, screen_def)
|
|
491
|
-
|
|
492
|
-
screen_el = screen_def.component()
|
|
493
|
-
return Provider(_NavigationContext, handle, Provider(_FocusContext, True, screen_el))
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
def create_stack_navigator() -> Any:
|
|
497
|
-
"""Create a stack-based navigator.
|
|
498
|
-
|
|
499
|
-
Stacks support push (`navigate(name, params=...)`) and pop
|
|
500
|
-
(`go_back()`). The active screen is the top of the stack.
|
|
501
|
-
|
|
502
|
-
Returns:
|
|
503
|
-
An object exposing two static factories:
|
|
504
|
-
|
|
505
|
-
- `Screen(name, *, component, options=None)`: define a screen.
|
|
506
|
-
- `Navigator(*screens, initial_route=None, key=None)`: render
|
|
507
|
-
the navigator with the given screens.
|
|
508
|
-
|
|
509
|
-
Example:
|
|
510
|
-
```python
|
|
511
|
-
import pythonnative as pn
|
|
512
|
-
from pythonnative.navigation import (
|
|
513
|
-
NavigationContainer, create_stack_navigator
|
|
514
|
-
)
|
|
515
|
-
|
|
516
|
-
Stack = create_stack_navigator()
|
|
517
|
-
|
|
518
|
-
@pn.component
|
|
519
|
-
def App():
|
|
520
|
-
return NavigationContainer(
|
|
521
|
-
Stack.Navigator(
|
|
522
|
-
Stack.Screen("Home", component=HomeScreen),
|
|
523
|
-
Stack.Screen("Detail", component=DetailScreen),
|
|
524
|
-
initial_route="Home",
|
|
525
|
-
)
|
|
526
|
-
)
|
|
527
|
-
```
|
|
528
|
-
"""
|
|
529
|
-
|
|
530
|
-
class _StackNavigator:
|
|
531
|
-
@staticmethod
|
|
532
|
-
def Screen(name: str, *, component: Any, options: Optional[ScreenOptions] = None) -> "_ScreenDef":
|
|
533
|
-
"""Define a screen within this stack navigator.
|
|
534
|
-
|
|
535
|
-
Args:
|
|
536
|
-
name: Route name used by `navigate(name)`.
|
|
537
|
-
component: A `@component` function rendered when this
|
|
538
|
-
screen is active.
|
|
539
|
-
options: Optional per-screen settings.
|
|
540
|
-
|
|
541
|
-
Returns:
|
|
542
|
-
A `_ScreenDef` consumed by `Navigator(...)`.
|
|
543
|
-
"""
|
|
544
|
-
return _ScreenDef(name, component, options)
|
|
545
|
-
|
|
546
|
-
@staticmethod
|
|
547
|
-
def Navigator(*screens: Any, initial_route: Optional[str] = None, key: Optional[str] = None) -> Element:
|
|
548
|
-
"""Render the stack navigator with the given screens.
|
|
549
|
-
|
|
550
|
-
Args:
|
|
551
|
-
*screens: One or more `Screen(...)` definitions.
|
|
552
|
-
initial_route: Optional name of the route to mount
|
|
553
|
-
first. Defaults to the first screen passed.
|
|
554
|
-
key: Stable identity for keyed reconciliation.
|
|
555
|
-
|
|
556
|
-
Returns:
|
|
557
|
-
An [`Element`][pythonnative.Element] that mounts the
|
|
558
|
-
stack navigator.
|
|
559
|
-
"""
|
|
560
|
-
return _stack_navigator_impl(screens=list(screens), initial_route=initial_route, key=key)
|
|
561
|
-
|
|
562
|
-
return _StackNavigator()
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
# ======================================================================
|
|
566
|
-
# Tab navigator
|
|
567
|
-
# ======================================================================
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
@component
|
|
571
|
-
def _tab_navigator_impl(screens: Any = None, initial_route: Optional[str] = None) -> Element:
|
|
572
|
-
screen_list = list(screens or [])
|
|
573
|
-
screen_map = _build_screen_map(screen_list)
|
|
574
|
-
if not screen_map:
|
|
575
|
-
return Element("View", {}, [])
|
|
576
|
-
|
|
577
|
-
parent_nav = use_context(_NavigationContext)
|
|
578
|
-
|
|
579
|
-
first_route = initial_route or screen_list[0].name
|
|
580
|
-
active_tab, set_active_tab = use_state(first_route)
|
|
581
|
-
tab_params, set_tab_params = use_state(lambda: {first_route: {}})
|
|
582
|
-
|
|
583
|
-
params_ref: Ref[Dict[str, Dict[str, Any]]] = use_ref(None)
|
|
584
|
-
params_ref.current = tab_params
|
|
585
|
-
|
|
586
|
-
def switch_tab(name: str, params: Optional[Dict[str, Any]] = None) -> None:
|
|
587
|
-
set_active_tab(name)
|
|
588
|
-
if params:
|
|
589
|
-
set_tab_params(lambda p: {**p, name: params})
|
|
590
|
-
|
|
591
|
-
def get_stack() -> List[_RouteEntry]:
|
|
592
|
-
p = params_ref.current or {}
|
|
593
|
-
return [_RouteEntry(active_tab, p.get(active_tab, {}))]
|
|
594
|
-
|
|
595
|
-
handle = use_memo(lambda: _TabNavHandle(screen_map, get_stack, lambda _: None, switch_tab, parent=parent_nav), [])
|
|
596
|
-
handle._screen_map = screen_map
|
|
597
|
-
handle._switch_tab = switch_tab
|
|
598
|
-
handle._parent = parent_nav
|
|
599
|
-
|
|
600
|
-
screen_def = screen_map.get(active_tab)
|
|
601
|
-
if screen_def is None:
|
|
602
|
-
screen_def = screen_map[screen_list[0].name]
|
|
603
|
-
|
|
604
|
-
tab_items: List[Dict[str, Any]] = []
|
|
605
|
-
for s in screen_list:
|
|
606
|
-
if isinstance(s, _ScreenDef):
|
|
607
|
-
item: Dict[str, Any] = {"name": s.name, "title": s.options.get("title", s.name)}
|
|
608
|
-
icon = s.options.get("tab_bar_icon")
|
|
609
|
-
if icon is not None:
|
|
610
|
-
item["icon"] = icon
|
|
611
|
-
tab_items.append(item)
|
|
612
|
-
|
|
613
|
-
def on_tab_select(name: str) -> None:
|
|
614
|
-
switch_tab(name)
|
|
615
|
-
|
|
616
|
-
tab_bar = Element(
|
|
617
|
-
"TabBar",
|
|
618
|
-
{"items": tab_items, "active_tab": active_tab, "on_tab_select": on_tab_select},
|
|
619
|
-
[],
|
|
620
|
-
key="__tab_bar__",
|
|
621
|
-
)
|
|
622
|
-
|
|
623
|
-
screen_el = screen_def.component()
|
|
624
|
-
content = Provider(
|
|
625
|
-
_NavigationContext,
|
|
626
|
-
handle,
|
|
627
|
-
Provider(_FocusContext, True, screen_el),
|
|
628
|
-
)
|
|
629
|
-
|
|
630
|
-
return Element(
|
|
631
|
-
"View",
|
|
632
|
-
{"flex_direction": "column", "flex": 1},
|
|
633
|
-
[Element("View", {"flex": 1}, [content]), tab_bar],
|
|
634
|
-
)
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
def create_tab_navigator() -> Any:
|
|
638
|
-
"""Create a tab-based navigator.
|
|
639
|
-
|
|
640
|
-
Tab navigators render a tab bar and switch between sibling screens.
|
|
641
|
-
Use `options={"title": "..."}` on a `Screen` to label the tab.
|
|
642
|
-
|
|
643
|
-
Returns:
|
|
644
|
-
An object exposing static `Screen(...)` and `Navigator(...)`
|
|
645
|
-
factories analogous to
|
|
646
|
-
[`create_stack_navigator`][pythonnative.create_stack_navigator].
|
|
647
|
-
|
|
648
|
-
Example:
|
|
649
|
-
```python
|
|
650
|
-
import pythonnative as pn
|
|
651
|
-
from pythonnative.navigation import (
|
|
652
|
-
NavigationContainer, create_tab_navigator
|
|
653
|
-
)
|
|
654
|
-
|
|
655
|
-
Tab = create_tab_navigator()
|
|
656
|
-
|
|
657
|
-
@pn.component
|
|
658
|
-
def App():
|
|
659
|
-
return NavigationContainer(
|
|
660
|
-
Tab.Navigator(
|
|
661
|
-
Tab.Screen("Home", component=HomeScreen, options={"title": "Home"}),
|
|
662
|
-
Tab.Screen("Settings", component=SettingsScreen),
|
|
663
|
-
)
|
|
664
|
-
)
|
|
665
|
-
```
|
|
666
|
-
"""
|
|
667
|
-
|
|
668
|
-
class _TabNavigator:
|
|
669
|
-
@staticmethod
|
|
670
|
-
def Screen(name: str, *, component: Any, options: Optional[ScreenOptions] = None) -> "_ScreenDef":
|
|
671
|
-
"""Define a screen within this tab navigator.
|
|
672
|
-
|
|
673
|
-
Args:
|
|
674
|
-
name: Route name and default tab title.
|
|
675
|
-
component: A `@component` function rendered when this
|
|
676
|
-
tab is active.
|
|
677
|
-
options: Optional per-screen settings. Recognized keys:
|
|
678
|
-
|
|
679
|
-
- `title` (str): Tab label.
|
|
680
|
-
- `tab_bar_icon` (str | dict): Native system icon
|
|
681
|
-
identifier. A string is used on every platform; a
|
|
682
|
-
dict like `{"ios": "house.fill", "android":
|
|
683
|
-
"ic_menu_home"}` selects per platform. iOS values
|
|
684
|
-
are resolved via SF Symbols
|
|
685
|
-
(`UIImage.systemImageNamed_`); Android values are
|
|
686
|
-
resolved against `android.R.drawable.<name>`.
|
|
687
|
-
Names that don't resolve fall back to text-only.
|
|
688
|
-
|
|
689
|
-
Returns:
|
|
690
|
-
A `_ScreenDef` consumed by `Navigator(...)`.
|
|
691
|
-
"""
|
|
692
|
-
return _ScreenDef(name, component, options)
|
|
693
|
-
|
|
694
|
-
@staticmethod
|
|
695
|
-
def Navigator(*screens: Any, initial_route: Optional[str] = None, key: Optional[str] = None) -> Element:
|
|
696
|
-
"""Render the tab navigator with the given screens.
|
|
697
|
-
|
|
698
|
-
Args:
|
|
699
|
-
*screens: One or more `Screen(...)` definitions.
|
|
700
|
-
initial_route: Optional name of the tab to start on.
|
|
701
|
-
key: Stable identity for keyed reconciliation.
|
|
702
|
-
|
|
703
|
-
Returns:
|
|
704
|
-
An [`Element`][pythonnative.Element] that mounts the
|
|
705
|
-
tab navigator.
|
|
706
|
-
"""
|
|
707
|
-
return _tab_navigator_impl(screens=list(screens), initial_route=initial_route, key=key)
|
|
708
|
-
|
|
709
|
-
return _TabNavigator()
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
# ======================================================================
|
|
713
|
-
# Drawer navigator
|
|
714
|
-
# ======================================================================
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
@component
|
|
718
|
-
def _drawer_navigator_impl(screens: Any = None, initial_route: Optional[str] = None) -> Element:
|
|
719
|
-
screen_list = list(screens or [])
|
|
720
|
-
screen_map = _build_screen_map(screen_list)
|
|
721
|
-
if not screen_map:
|
|
722
|
-
return Element("View", {}, [])
|
|
723
|
-
|
|
724
|
-
parent_nav = use_context(_NavigationContext)
|
|
725
|
-
|
|
726
|
-
first_route = initial_route or screen_list[0].name
|
|
727
|
-
active_screen, set_active_screen = use_state(first_route)
|
|
728
|
-
drawer_open, set_drawer_open = use_state(False)
|
|
729
|
-
screen_params, set_screen_params = use_state(lambda: {first_route: {}})
|
|
730
|
-
|
|
731
|
-
params_ref: Ref[Dict[str, Dict[str, Any]]] = use_ref(None)
|
|
732
|
-
params_ref.current = screen_params
|
|
733
|
-
|
|
734
|
-
def switch_screen(name: str, params: Optional[Dict[str, Any]] = None) -> None:
|
|
735
|
-
set_active_screen(name)
|
|
736
|
-
if params:
|
|
737
|
-
set_screen_params(lambda p: {**p, name: params})
|
|
738
|
-
|
|
739
|
-
def get_stack() -> List[_RouteEntry]:
|
|
740
|
-
p = params_ref.current or {}
|
|
741
|
-
return [_RouteEntry(active_screen, p.get(active_screen, {}))]
|
|
742
|
-
|
|
743
|
-
handle = use_memo(
|
|
744
|
-
lambda: _DrawerNavHandle(
|
|
745
|
-
screen_map,
|
|
746
|
-
get_stack,
|
|
747
|
-
lambda _: None,
|
|
748
|
-
switch_screen,
|
|
749
|
-
set_drawer_open,
|
|
750
|
-
lambda: drawer_open,
|
|
751
|
-
parent=parent_nav,
|
|
752
|
-
),
|
|
753
|
-
[],
|
|
754
|
-
)
|
|
755
|
-
handle._screen_map = screen_map
|
|
756
|
-
handle._switch_screen = switch_screen
|
|
757
|
-
handle._set_drawer_open = set_drawer_open
|
|
758
|
-
handle._get_drawer_open = lambda: drawer_open
|
|
759
|
-
handle._parent = parent_nav
|
|
760
|
-
|
|
761
|
-
screen_def = screen_map.get(active_screen)
|
|
762
|
-
if screen_def is None:
|
|
763
|
-
screen_def = screen_map[screen_list[0].name]
|
|
764
|
-
|
|
765
|
-
screen_el = screen_def.component()
|
|
766
|
-
content = Provider(
|
|
767
|
-
_NavigationContext,
|
|
768
|
-
handle,
|
|
769
|
-
Provider(_FocusContext, True, screen_el),
|
|
770
|
-
)
|
|
771
|
-
|
|
772
|
-
children: List[Element] = [Element("View", {"flex": 1}, [content])]
|
|
773
|
-
|
|
774
|
-
if drawer_open:
|
|
775
|
-
menu_items: List[Element] = []
|
|
776
|
-
for s in screen_list:
|
|
777
|
-
if not isinstance(s, _ScreenDef):
|
|
778
|
-
continue
|
|
779
|
-
label = s.options.get("title", s.name)
|
|
780
|
-
item_name = s.name
|
|
781
|
-
|
|
782
|
-
def make_select(n: str) -> Callable[[], None]:
|
|
783
|
-
def _select() -> None:
|
|
784
|
-
switch_screen(n)
|
|
785
|
-
set_drawer_open(False)
|
|
786
|
-
|
|
787
|
-
return _select
|
|
788
|
-
|
|
789
|
-
menu_items.append(
|
|
790
|
-
Element("Button", {"title": label, "on_press": make_select(item_name)}, [], key=f"__drawer_{item_name}")
|
|
791
|
-
)
|
|
792
|
-
|
|
793
|
-
drawer_panel = Element(
|
|
794
|
-
"View",
|
|
795
|
-
{"background_color": "#FFFFFF", "width": 250},
|
|
796
|
-
menu_items,
|
|
797
|
-
)
|
|
798
|
-
children.insert(0, drawer_panel)
|
|
799
|
-
|
|
800
|
-
return Element("View", {"flex_direction": "row", "flex": 1}, children)
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
def create_drawer_navigator() -> Any:
|
|
804
|
-
"""Create a drawer-based navigator.
|
|
805
|
-
|
|
806
|
-
Drawer navigators render a side panel for switching between sibling
|
|
807
|
-
screens. The navigation handle returned by
|
|
808
|
-
[`use_navigation`][pythonnative.use_navigation] inside a drawer
|
|
809
|
-
navigator includes `open_drawer()`, `close_drawer()`, and
|
|
810
|
-
`toggle_drawer()` methods in addition to the standard
|
|
811
|
-
`navigate`/`go_back`/`reset` interface.
|
|
812
|
-
|
|
813
|
-
Returns:
|
|
814
|
-
An object exposing static `Screen(...)` and `Navigator(...)`
|
|
815
|
-
factories analogous to
|
|
816
|
-
[`create_stack_navigator`][pythonnative.create_stack_navigator].
|
|
817
|
-
|
|
818
|
-
Example:
|
|
819
|
-
```python
|
|
820
|
-
import pythonnative as pn
|
|
821
|
-
from pythonnative.navigation import (
|
|
822
|
-
NavigationContainer, create_drawer_navigator
|
|
823
|
-
)
|
|
824
|
-
|
|
825
|
-
Drawer = create_drawer_navigator()
|
|
826
|
-
|
|
827
|
-
@pn.component
|
|
828
|
-
def App():
|
|
829
|
-
return NavigationContainer(
|
|
830
|
-
Drawer.Navigator(
|
|
831
|
-
Drawer.Screen("Home", component=HomeScreen, options={"title": "Home"}),
|
|
832
|
-
Drawer.Screen("Settings", component=SettingsScreen),
|
|
833
|
-
)
|
|
834
|
-
)
|
|
835
|
-
```
|
|
836
|
-
"""
|
|
837
|
-
|
|
838
|
-
class _DrawerNavigator:
|
|
839
|
-
@staticmethod
|
|
840
|
-
def Screen(name: str, *, component: Any, options: Optional[ScreenOptions] = None) -> "_ScreenDef":
|
|
841
|
-
"""Define a screen within this drawer navigator.
|
|
842
|
-
|
|
843
|
-
Args:
|
|
844
|
-
name: Route name and default drawer-menu label.
|
|
845
|
-
component: A `@component` function rendered when this
|
|
846
|
-
screen is active.
|
|
847
|
-
options: Optional per-screen settings (e.g.,
|
|
848
|
-
`{"title": "..."}`).
|
|
849
|
-
|
|
850
|
-
Returns:
|
|
851
|
-
A `_ScreenDef` consumed by `Navigator(...)`.
|
|
852
|
-
"""
|
|
853
|
-
return _ScreenDef(name, component, options)
|
|
854
|
-
|
|
855
|
-
@staticmethod
|
|
856
|
-
def Navigator(*screens: Any, initial_route: Optional[str] = None, key: Optional[str] = None) -> Element:
|
|
857
|
-
"""Render the drawer navigator with the given screens.
|
|
858
|
-
|
|
859
|
-
Args:
|
|
860
|
-
*screens: One or more `Screen(...)` definitions.
|
|
861
|
-
initial_route: Optional name of the screen to mount
|
|
862
|
-
first.
|
|
863
|
-
key: Stable identity for keyed reconciliation.
|
|
864
|
-
|
|
865
|
-
Returns:
|
|
866
|
-
An [`Element`][pythonnative.Element] that mounts the
|
|
867
|
-
drawer navigator.
|
|
868
|
-
"""
|
|
869
|
-
return _drawer_navigator_impl(screens=list(screens), initial_route=initial_route, key=key)
|
|
870
|
-
|
|
871
|
-
return _DrawerNavigator()
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
# ======================================================================
|
|
875
|
-
# NavigationContainer
|
|
876
|
-
# ======================================================================
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
def NavigationContainer(child: Element, *, key: Optional[str] = None) -> Element:
|
|
880
|
-
"""Root container for the navigation tree.
|
|
881
|
-
|
|
882
|
-
Wraps the child navigator in a full-size view. All declarative
|
|
883
|
-
navigators (stack, tab, drawer) should be nested inside a
|
|
884
|
-
`NavigationContainer`.
|
|
885
|
-
|
|
886
|
-
Args:
|
|
887
|
-
child: The root navigator element (typically a `Stack.Navigator`,
|
|
888
|
-
`Tab.Navigator`, or `Drawer.Navigator`).
|
|
889
|
-
key: Stable identity for keyed reconciliation.
|
|
890
|
-
|
|
891
|
-
Returns:
|
|
892
|
-
An [`Element`][pythonnative.Element] of type `"View"`.
|
|
893
|
-
|
|
894
|
-
Example:
|
|
895
|
-
```python
|
|
896
|
-
import pythonnative as pn
|
|
897
|
-
from pythonnative.navigation import (
|
|
898
|
-
NavigationContainer, create_stack_navigator
|
|
899
|
-
)
|
|
900
|
-
|
|
901
|
-
Stack = create_stack_navigator()
|
|
902
|
-
|
|
903
|
-
@pn.component
|
|
904
|
-
def App():
|
|
905
|
-
return NavigationContainer(
|
|
906
|
-
Stack.Navigator(
|
|
907
|
-
Stack.Screen("Home", component=HomeScreen),
|
|
908
|
-
)
|
|
909
|
-
)
|
|
910
|
-
```
|
|
911
|
-
"""
|
|
912
|
-
return Element("View", {"flex": 1}, [child], key=key)
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
# ======================================================================
|
|
916
|
-
# Hooks
|
|
917
|
-
# ======================================================================
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
def use_route() -> Dict[str, Any]:
|
|
921
|
-
"""Return the current route's params dict.
|
|
922
|
-
|
|
923
|
-
Convenience hook that reads from the navigation context. Equivalent
|
|
924
|
-
to `use_navigation().get_params()` but tolerates the no-navigation
|
|
925
|
-
case (returns `{}` instead of raising).
|
|
926
|
-
|
|
927
|
-
Returns:
|
|
928
|
-
The current route's `params` dict, or `{}` if no navigator is
|
|
929
|
-
in scope.
|
|
930
|
-
|
|
931
|
-
Example:
|
|
932
|
-
```python
|
|
933
|
-
import pythonnative as pn
|
|
934
|
-
|
|
935
|
-
@pn.component
|
|
936
|
-
def DetailScreen():
|
|
937
|
-
params = pn.use_route()
|
|
938
|
-
item_id = params.get("id")
|
|
939
|
-
return pn.Text(f"Item {item_id}")
|
|
940
|
-
```
|
|
941
|
-
"""
|
|
942
|
-
nav = use_context(_NavigationContext)
|
|
943
|
-
if nav is None:
|
|
944
|
-
return {}
|
|
945
|
-
get_params = getattr(nav, "get_params", None)
|
|
946
|
-
if get_params:
|
|
947
|
-
return get_params()
|
|
948
|
-
return {}
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
def use_focus_effect(effect: Callable, deps: Optional[list] = None) -> None:
|
|
952
|
-
"""Run `effect` only while the screen is focused.
|
|
953
|
-
|
|
954
|
-
Like [`use_effect`][pythonnative.use_effect], but the callback runs
|
|
955
|
-
only when the enclosing navigator marks this screen as the active
|
|
956
|
-
one. Useful for starting subscriptions, refreshing data, or
|
|
957
|
-
pausing animations on the inactive screen.
|
|
958
|
-
|
|
959
|
-
The focus state combines two sources of truth:
|
|
960
|
-
|
|
961
|
-
- The screen host's lifecycle (``on_resume`` / ``on_pause``), so
|
|
962
|
-
pushing a sibling onto the navigation stack blurs this screen and
|
|
963
|
-
popping back to it refocuses it.
|
|
964
|
-
- The in-tree ``_FocusContext`` value, which lets declarative
|
|
965
|
-
navigators (e.g. tabs, drawers) mark only the active subtree as
|
|
966
|
-
focused even when both screens are part of the same host.
|
|
967
|
-
|
|
968
|
-
Args:
|
|
969
|
-
effect: A zero-arg callable invoked when focused. Optionally
|
|
970
|
-
returns a cleanup callable.
|
|
971
|
-
deps: Dependency list, or `None` to run on every render while
|
|
972
|
-
focused.
|
|
973
|
-
|
|
974
|
-
Example:
|
|
975
|
-
```python
|
|
976
|
-
import pythonnative as pn
|
|
977
|
-
|
|
978
|
-
@pn.component
|
|
979
|
-
def HomeScreen():
|
|
980
|
-
pn.use_focus_effect(lambda: print("screen focused"), [])
|
|
981
|
-
return pn.Text("Home")
|
|
982
|
-
```
|
|
983
|
-
"""
|
|
984
|
-
context_focused = use_context(_FocusContext)
|
|
985
|
-
|
|
986
|
-
nav = use_context(_NavigationContext)
|
|
987
|
-
# Walk the navigator parent chain to find the screen host. Declarative
|
|
988
|
-
# navigators (Stack/Tab/Drawer) wrap the host's ``NavigationHandle``
|
|
989
|
-
# as ``_parent``; only the host-level handle has ``_host``.
|
|
990
|
-
host = None
|
|
991
|
-
cursor = nav
|
|
992
|
-
while cursor is not None:
|
|
993
|
-
candidate = getattr(cursor, "_host", None)
|
|
994
|
-
if candidate is not None:
|
|
995
|
-
host = candidate
|
|
996
|
-
break
|
|
997
|
-
cursor = getattr(cursor, "_parent", None)
|
|
998
|
-
initial_host_focus = bool(getattr(host, "_is_focused", True)) if host is not None else True
|
|
999
|
-
host_focused, set_host_focused = use_state(initial_host_focus)
|
|
1000
|
-
|
|
1001
|
-
def subscribe_to_host_focus() -> Any:
|
|
1002
|
-
if host is None:
|
|
1003
|
-
return None
|
|
1004
|
-
subscribers = getattr(host, "_focus_subscribers", None)
|
|
1005
|
-
if subscribers is None:
|
|
1006
|
-
return None
|
|
1007
|
-
subscribers.append(set_host_focused)
|
|
1008
|
-
# The host may have changed focus state between the initial
|
|
1009
|
-
# ``use_state`` call and this effect running (e.g. mid-render
|
|
1010
|
-
# lifecycle event); resync once to avoid stale state.
|
|
1011
|
-
set_host_focused(bool(host._is_focused))
|
|
1012
|
-
|
|
1013
|
-
def cleanup() -> None:
|
|
1014
|
-
try:
|
|
1015
|
-
subscribers.remove(set_host_focused)
|
|
1016
|
-
except ValueError:
|
|
1017
|
-
pass
|
|
1018
|
-
|
|
1019
|
-
return cleanup
|
|
1020
|
-
|
|
1021
|
-
use_effect(subscribe_to_host_focus, [])
|
|
1022
|
-
|
|
1023
|
-
is_focused = context_focused and host_focused
|
|
1024
|
-
all_deps = [is_focused] + (list(deps) if deps is not None else [])
|
|
1025
|
-
|
|
1026
|
-
def wrapped_effect() -> Any:
|
|
1027
|
-
if is_focused:
|
|
1028
|
-
return effect()
|
|
1029
|
-
return None
|
|
1030
|
-
|
|
1031
|
-
use_effect(wrapped_effect, all_deps)
|