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
@@ -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)