pythonnative 0.7.0__py3-none-any.whl → 0.9.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/_ios_log.py +94 -0
- pythonnative/cli/pn.py +131 -11
- 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 +77 -17
- pythonnative/reconciler.py +89 -1
- pythonnative/templates/ios_template/ios_template/ViewController.swift +19 -25
- pythonnative/utils.py +40 -1
- {pythonnative-0.7.0.dist-info → pythonnative-0.9.0.dist-info}/METADATA +1 -1
- {pythonnative-0.7.0.dist-info → pythonnative-0.9.0.dist-info}/RECORD +21 -16
- pythonnative/native_views.py +0 -1404
- {pythonnative-0.7.0.dist-info → pythonnative-0.9.0.dist-info}/WHEEL +0 -0
- {pythonnative-0.7.0.dist-info → pythonnative-0.9.0.dist-info}/entry_points.txt +0 -0
- {pythonnative-0.7.0.dist-info → pythonnative-0.9.0.dist-info}/licenses/LICENSE +0 -0
- {pythonnative-0.7.0.dist-info → pythonnative-0.9.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,571 @@
|
|
|
1
|
+
"""Declarative navigation for PythonNative.
|
|
2
|
+
|
|
3
|
+
Provides a component-based navigation system inspired by React Navigation.
|
|
4
|
+
Navigators manage screen state in Python; they render the active screen's
|
|
5
|
+
component using the standard reconciler pipeline.
|
|
6
|
+
|
|
7
|
+
Usage::
|
|
8
|
+
|
|
9
|
+
from pythonnative.navigation import (
|
|
10
|
+
NavigationContainer,
|
|
11
|
+
create_stack_navigator,
|
|
12
|
+
create_tab_navigator,
|
|
13
|
+
create_drawer_navigator,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
Stack = create_stack_navigator()
|
|
17
|
+
|
|
18
|
+
@pn.component
|
|
19
|
+
def App():
|
|
20
|
+
return NavigationContainer(
|
|
21
|
+
Stack.Navigator(
|
|
22
|
+
Stack.Screen("Home", component=HomeScreen),
|
|
23
|
+
Stack.Screen("Detail", component=DetailScreen),
|
|
24
|
+
)
|
|
25
|
+
)
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from typing import Any, Callable, Dict, List, Optional
|
|
29
|
+
|
|
30
|
+
from .element import Element
|
|
31
|
+
from .hooks import (
|
|
32
|
+
Provider,
|
|
33
|
+
_NavigationContext,
|
|
34
|
+
component,
|
|
35
|
+
create_context,
|
|
36
|
+
use_context,
|
|
37
|
+
use_effect,
|
|
38
|
+
use_memo,
|
|
39
|
+
use_ref,
|
|
40
|
+
use_state,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
# ======================================================================
|
|
44
|
+
# Focus context
|
|
45
|
+
# ======================================================================
|
|
46
|
+
|
|
47
|
+
_FocusContext = create_context(False)
|
|
48
|
+
|
|
49
|
+
# ======================================================================
|
|
50
|
+
# Data structures
|
|
51
|
+
# ======================================================================
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class _ScreenDef:
|
|
55
|
+
"""Configuration for a single screen within a navigator."""
|
|
56
|
+
|
|
57
|
+
__slots__ = ("name", "component", "options")
|
|
58
|
+
|
|
59
|
+
def __init__(self, name: str, component_fn: Any, options: Optional[Dict[str, Any]] = None) -> None:
|
|
60
|
+
self.name = name
|
|
61
|
+
self.component = component_fn
|
|
62
|
+
self.options = options or {}
|
|
63
|
+
|
|
64
|
+
def __repr__(self) -> str:
|
|
65
|
+
return f"Screen({self.name!r})"
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class _RouteEntry:
|
|
69
|
+
"""An entry in the navigation stack."""
|
|
70
|
+
|
|
71
|
+
__slots__ = ("name", "params")
|
|
72
|
+
|
|
73
|
+
def __init__(self, name: str, params: Optional[Dict[str, Any]] = None) -> None:
|
|
74
|
+
self.name = name
|
|
75
|
+
self.params = params or {}
|
|
76
|
+
|
|
77
|
+
def __repr__(self) -> str:
|
|
78
|
+
return f"Route({self.name!r})"
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
# ======================================================================
|
|
82
|
+
# Navigation handle for declarative navigators
|
|
83
|
+
# ======================================================================
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class _DeclarativeNavHandle:
|
|
87
|
+
"""Navigation handle provided by declarative navigators.
|
|
88
|
+
|
|
89
|
+
Implements the same interface as :class:`~pythonnative.hooks.NavigationHandle`
|
|
90
|
+
so that ``use_navigation()`` returns a compatible object regardless of
|
|
91
|
+
whether the app uses the legacy page-based navigation or declarative
|
|
92
|
+
navigators.
|
|
93
|
+
|
|
94
|
+
When *parent* is provided, unknown routes and root-level ``go_back``
|
|
95
|
+
calls are forwarded to the parent handle. This enables nested
|
|
96
|
+
navigators (e.g. a stack inside a tab) to delegate navigation actions
|
|
97
|
+
that they cannot handle locally.
|
|
98
|
+
"""
|
|
99
|
+
|
|
100
|
+
def __init__(
|
|
101
|
+
self,
|
|
102
|
+
screen_map: Dict[str, "_ScreenDef"],
|
|
103
|
+
get_stack: Callable[[], List["_RouteEntry"]],
|
|
104
|
+
set_stack: Callable,
|
|
105
|
+
parent: Any = None,
|
|
106
|
+
) -> None:
|
|
107
|
+
self._screen_map = screen_map
|
|
108
|
+
self._get_stack = get_stack
|
|
109
|
+
self._set_stack = set_stack
|
|
110
|
+
self._parent = parent
|
|
111
|
+
|
|
112
|
+
def navigate(self, route_name: str, params: Optional[Dict[str, Any]] = None) -> None:
|
|
113
|
+
"""Navigate to a named route, pushing it onto the stack.
|
|
114
|
+
|
|
115
|
+
If *route_name* is not known locally and a parent handle exists,
|
|
116
|
+
the call is forwarded to the parent navigator.
|
|
117
|
+
"""
|
|
118
|
+
if route_name in self._screen_map:
|
|
119
|
+
entry = _RouteEntry(route_name, params)
|
|
120
|
+
self._set_stack(lambda s: list(s) + [entry])
|
|
121
|
+
elif self._parent is not None:
|
|
122
|
+
self._parent.navigate(route_name, params=params)
|
|
123
|
+
else:
|
|
124
|
+
raise ValueError(f"Unknown route: {route_name!r}. Known routes: {list(self._screen_map)}")
|
|
125
|
+
|
|
126
|
+
def go_back(self) -> None:
|
|
127
|
+
"""Pop the current screen from the stack.
|
|
128
|
+
|
|
129
|
+
If the stack is at its root and a parent handle exists, the call
|
|
130
|
+
is forwarded to the parent navigator.
|
|
131
|
+
"""
|
|
132
|
+
stack = self._get_stack()
|
|
133
|
+
if len(stack) > 1:
|
|
134
|
+
self._set_stack(lambda s: list(s[:-1]))
|
|
135
|
+
elif self._parent is not None:
|
|
136
|
+
self._parent.go_back()
|
|
137
|
+
|
|
138
|
+
def get_params(self) -> Dict[str, Any]:
|
|
139
|
+
"""Return the parameters for the current route."""
|
|
140
|
+
stack = self._get_stack()
|
|
141
|
+
return stack[-1].params if stack else {}
|
|
142
|
+
|
|
143
|
+
def reset(self, route_name: str, params: Optional[Dict[str, Any]] = None) -> None:
|
|
144
|
+
"""Reset the stack to a single route."""
|
|
145
|
+
if route_name not in self._screen_map:
|
|
146
|
+
raise ValueError(f"Unknown route: {route_name!r}. Known routes: {list(self._screen_map)}")
|
|
147
|
+
self._set_stack([_RouteEntry(route_name, params)])
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class _TabNavHandle(_DeclarativeNavHandle):
|
|
151
|
+
"""Navigation handle for tab navigators with tab switching."""
|
|
152
|
+
|
|
153
|
+
def __init__(
|
|
154
|
+
self,
|
|
155
|
+
screen_map: Dict[str, "_ScreenDef"],
|
|
156
|
+
get_stack: Callable[[], List["_RouteEntry"]],
|
|
157
|
+
set_stack: Callable,
|
|
158
|
+
switch_tab: Callable[[str, Optional[Dict[str, Any]]], None],
|
|
159
|
+
parent: Any = None,
|
|
160
|
+
) -> None:
|
|
161
|
+
super().__init__(screen_map, get_stack, set_stack, parent=parent)
|
|
162
|
+
self._switch_tab = switch_tab
|
|
163
|
+
|
|
164
|
+
def navigate(self, route_name: str, params: Optional[Dict[str, Any]] = None) -> None:
|
|
165
|
+
"""Switch to a tab by name, or forward to parent for unknown routes."""
|
|
166
|
+
if route_name in self._screen_map:
|
|
167
|
+
self._switch_tab(route_name, params)
|
|
168
|
+
elif self._parent is not None:
|
|
169
|
+
self._parent.navigate(route_name, params=params)
|
|
170
|
+
else:
|
|
171
|
+
raise ValueError(f"Unknown route: {route_name!r}. Known routes: {list(self._screen_map)}")
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
class _DrawerNavHandle(_DeclarativeNavHandle):
|
|
175
|
+
"""Navigation handle for drawer navigators with open/close control."""
|
|
176
|
+
|
|
177
|
+
def __init__(
|
|
178
|
+
self,
|
|
179
|
+
screen_map: Dict[str, "_ScreenDef"],
|
|
180
|
+
get_stack: Callable[[], List["_RouteEntry"]],
|
|
181
|
+
set_stack: Callable,
|
|
182
|
+
switch_screen: Callable[[str, Optional[Dict[str, Any]]], None],
|
|
183
|
+
set_drawer_open: Callable[[bool], None],
|
|
184
|
+
get_drawer_open: Callable[[], bool],
|
|
185
|
+
parent: Any = None,
|
|
186
|
+
) -> None:
|
|
187
|
+
super().__init__(screen_map, get_stack, set_stack, parent=parent)
|
|
188
|
+
self._switch_screen = switch_screen
|
|
189
|
+
self._set_drawer_open = set_drawer_open
|
|
190
|
+
self._get_drawer_open = get_drawer_open
|
|
191
|
+
|
|
192
|
+
def navigate(self, route_name: str, params: Optional[Dict[str, Any]] = None) -> None:
|
|
193
|
+
"""Switch to a screen and close the drawer, or forward to parent."""
|
|
194
|
+
if route_name in self._screen_map:
|
|
195
|
+
self._switch_screen(route_name, params)
|
|
196
|
+
self._set_drawer_open(False)
|
|
197
|
+
elif self._parent is not None:
|
|
198
|
+
self._parent.navigate(route_name, params=params)
|
|
199
|
+
else:
|
|
200
|
+
raise ValueError(f"Unknown route: {route_name!r}. Known routes: {list(self._screen_map)}")
|
|
201
|
+
|
|
202
|
+
def open_drawer(self) -> None:
|
|
203
|
+
"""Open the drawer."""
|
|
204
|
+
self._set_drawer_open(True)
|
|
205
|
+
|
|
206
|
+
def close_drawer(self) -> None:
|
|
207
|
+
"""Close the drawer."""
|
|
208
|
+
self._set_drawer_open(False)
|
|
209
|
+
|
|
210
|
+
def toggle_drawer(self) -> None:
|
|
211
|
+
"""Toggle the drawer open/closed."""
|
|
212
|
+
self._set_drawer_open(not self._get_drawer_open())
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
# ======================================================================
|
|
216
|
+
# Stack navigator
|
|
217
|
+
# ======================================================================
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _build_screen_map(screens: Any) -> Dict[str, "_ScreenDef"]:
|
|
221
|
+
"""Build an ordered dict of name -> _ScreenDef from a list."""
|
|
222
|
+
result: Dict[str, _ScreenDef] = {}
|
|
223
|
+
for s in screens or []:
|
|
224
|
+
if isinstance(s, _ScreenDef):
|
|
225
|
+
result[s.name] = s
|
|
226
|
+
return result
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
@component
|
|
230
|
+
def _stack_navigator_impl(screens: Any = None, initial_route: Optional[str] = None) -> Element:
|
|
231
|
+
screen_map = _build_screen_map(screens)
|
|
232
|
+
if not screen_map:
|
|
233
|
+
return Element("View", {}, [])
|
|
234
|
+
|
|
235
|
+
parent_nav = use_context(_NavigationContext)
|
|
236
|
+
|
|
237
|
+
first_route = initial_route or next(iter(screen_map))
|
|
238
|
+
stack, set_stack = use_state(lambda: [_RouteEntry(first_route)])
|
|
239
|
+
|
|
240
|
+
stack_ref = use_ref(None)
|
|
241
|
+
stack_ref["current"] = stack
|
|
242
|
+
|
|
243
|
+
handle = use_memo(
|
|
244
|
+
lambda: _DeclarativeNavHandle(screen_map, lambda: stack_ref["current"], set_stack, parent=parent_nav), []
|
|
245
|
+
)
|
|
246
|
+
handle._screen_map = screen_map
|
|
247
|
+
handle._parent = parent_nav
|
|
248
|
+
|
|
249
|
+
current = stack[-1]
|
|
250
|
+
screen_def = screen_map.get(current.name)
|
|
251
|
+
if screen_def is None:
|
|
252
|
+
return Element("Text", {"text": f"Unknown route: {current.name}"}, [])
|
|
253
|
+
|
|
254
|
+
screen_el = screen_def.component()
|
|
255
|
+
return Provider(_NavigationContext, handle, Provider(_FocusContext, True, screen_el))
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def create_stack_navigator() -> Any:
|
|
259
|
+
"""Create a stack-based navigator.
|
|
260
|
+
|
|
261
|
+
Returns an object with ``Navigator`` and ``Screen`` members::
|
|
262
|
+
|
|
263
|
+
Stack = create_stack_navigator()
|
|
264
|
+
|
|
265
|
+
Stack.Screen("Home", component=HomeScreen)
|
|
266
|
+
|
|
267
|
+
Stack.Navigator(
|
|
268
|
+
Stack.Screen("Home", component=HomeScreen),
|
|
269
|
+
Stack.Screen("Detail", component=DetailScreen),
|
|
270
|
+
initial_route="Home",
|
|
271
|
+
)
|
|
272
|
+
"""
|
|
273
|
+
|
|
274
|
+
class _StackNavigator:
|
|
275
|
+
@staticmethod
|
|
276
|
+
def Screen(name: str, *, component: Any, options: Optional[Dict[str, Any]] = None) -> "_ScreenDef":
|
|
277
|
+
"""Define a screen within this stack navigator."""
|
|
278
|
+
return _ScreenDef(name, component, options)
|
|
279
|
+
|
|
280
|
+
@staticmethod
|
|
281
|
+
def Navigator(*screens: Any, initial_route: Optional[str] = None, key: Optional[str] = None) -> Element:
|
|
282
|
+
"""Render the stack navigator with the given screens."""
|
|
283
|
+
return _stack_navigator_impl(screens=list(screens), initial_route=initial_route, key=key)
|
|
284
|
+
|
|
285
|
+
return _StackNavigator()
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
# ======================================================================
|
|
289
|
+
# Tab navigator
|
|
290
|
+
# ======================================================================
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
@component
|
|
294
|
+
def _tab_navigator_impl(screens: Any = None, initial_route: Optional[str] = None) -> Element:
|
|
295
|
+
screen_list = list(screens or [])
|
|
296
|
+
screen_map = _build_screen_map(screen_list)
|
|
297
|
+
if not screen_map:
|
|
298
|
+
return Element("View", {}, [])
|
|
299
|
+
|
|
300
|
+
parent_nav = use_context(_NavigationContext)
|
|
301
|
+
|
|
302
|
+
first_route = initial_route or screen_list[0].name
|
|
303
|
+
active_tab, set_active_tab = use_state(first_route)
|
|
304
|
+
tab_params, set_tab_params = use_state(lambda: {first_route: {}})
|
|
305
|
+
|
|
306
|
+
params_ref = use_ref(None)
|
|
307
|
+
params_ref["current"] = tab_params
|
|
308
|
+
|
|
309
|
+
def switch_tab(name: str, params: Optional[Dict[str, Any]] = None) -> None:
|
|
310
|
+
set_active_tab(name)
|
|
311
|
+
if params:
|
|
312
|
+
set_tab_params(lambda p: {**p, name: params})
|
|
313
|
+
|
|
314
|
+
def get_stack() -> List[_RouteEntry]:
|
|
315
|
+
p = params_ref["current"] or {}
|
|
316
|
+
return [_RouteEntry(active_tab, p.get(active_tab, {}))]
|
|
317
|
+
|
|
318
|
+
handle = use_memo(lambda: _TabNavHandle(screen_map, get_stack, lambda _: None, switch_tab, parent=parent_nav), [])
|
|
319
|
+
handle._screen_map = screen_map
|
|
320
|
+
handle._switch_tab = switch_tab
|
|
321
|
+
handle._parent = parent_nav
|
|
322
|
+
|
|
323
|
+
screen_def = screen_map.get(active_tab)
|
|
324
|
+
if screen_def is None:
|
|
325
|
+
screen_def = screen_map[screen_list[0].name]
|
|
326
|
+
|
|
327
|
+
tab_items: List[Dict[str, str]] = []
|
|
328
|
+
for s in screen_list:
|
|
329
|
+
if isinstance(s, _ScreenDef):
|
|
330
|
+
tab_items.append({"name": s.name, "title": s.options.get("title", s.name)})
|
|
331
|
+
|
|
332
|
+
def on_tab_select(name: str) -> None:
|
|
333
|
+
switch_tab(name)
|
|
334
|
+
|
|
335
|
+
tab_bar = Element(
|
|
336
|
+
"TabBar",
|
|
337
|
+
{"items": tab_items, "active_tab": active_tab, "on_tab_select": on_tab_select},
|
|
338
|
+
[],
|
|
339
|
+
key="__tab_bar__",
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
screen_el = screen_def.component()
|
|
343
|
+
content = Provider(
|
|
344
|
+
_NavigationContext,
|
|
345
|
+
handle,
|
|
346
|
+
Provider(_FocusContext, True, screen_el),
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
return Element(
|
|
350
|
+
"View",
|
|
351
|
+
{"flex_direction": "column", "flex": 1},
|
|
352
|
+
[Element("View", {"flex": 1}, [content]), tab_bar],
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def create_tab_navigator() -> Any:
|
|
357
|
+
"""Create a tab-based navigator.
|
|
358
|
+
|
|
359
|
+
Returns an object with ``Navigator`` and ``Screen`` members::
|
|
360
|
+
|
|
361
|
+
Tab = create_tab_navigator()
|
|
362
|
+
|
|
363
|
+
Tab.Navigator(
|
|
364
|
+
Tab.Screen("Home", component=HomeScreen, options={"title": "Home"}),
|
|
365
|
+
Tab.Screen("Settings", component=SettingsScreen),
|
|
366
|
+
)
|
|
367
|
+
"""
|
|
368
|
+
|
|
369
|
+
class _TabNavigator:
|
|
370
|
+
@staticmethod
|
|
371
|
+
def Screen(name: str, *, component: Any, options: Optional[Dict[str, Any]] = None) -> "_ScreenDef":
|
|
372
|
+
"""Define a screen within this tab navigator."""
|
|
373
|
+
return _ScreenDef(name, component, options)
|
|
374
|
+
|
|
375
|
+
@staticmethod
|
|
376
|
+
def Navigator(*screens: Any, initial_route: Optional[str] = None, key: Optional[str] = None) -> Element:
|
|
377
|
+
"""Render the tab navigator with the given screens."""
|
|
378
|
+
return _tab_navigator_impl(screens=list(screens), initial_route=initial_route, key=key)
|
|
379
|
+
|
|
380
|
+
return _TabNavigator()
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
# ======================================================================
|
|
384
|
+
# Drawer navigator
|
|
385
|
+
# ======================================================================
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
@component
|
|
389
|
+
def _drawer_navigator_impl(screens: Any = None, initial_route: Optional[str] = None) -> Element:
|
|
390
|
+
screen_list = list(screens or [])
|
|
391
|
+
screen_map = _build_screen_map(screen_list)
|
|
392
|
+
if not screen_map:
|
|
393
|
+
return Element("View", {}, [])
|
|
394
|
+
|
|
395
|
+
parent_nav = use_context(_NavigationContext)
|
|
396
|
+
|
|
397
|
+
first_route = initial_route or screen_list[0].name
|
|
398
|
+
active_screen, set_active_screen = use_state(first_route)
|
|
399
|
+
drawer_open, set_drawer_open = use_state(False)
|
|
400
|
+
screen_params, set_screen_params = use_state(lambda: {first_route: {}})
|
|
401
|
+
|
|
402
|
+
params_ref = use_ref(None)
|
|
403
|
+
params_ref["current"] = screen_params
|
|
404
|
+
|
|
405
|
+
def switch_screen(name: str, params: Optional[Dict[str, Any]] = None) -> None:
|
|
406
|
+
set_active_screen(name)
|
|
407
|
+
if params:
|
|
408
|
+
set_screen_params(lambda p: {**p, name: params})
|
|
409
|
+
|
|
410
|
+
def get_stack() -> List[_RouteEntry]:
|
|
411
|
+
p = params_ref["current"] or {}
|
|
412
|
+
return [_RouteEntry(active_screen, p.get(active_screen, {}))]
|
|
413
|
+
|
|
414
|
+
handle = use_memo(
|
|
415
|
+
lambda: _DrawerNavHandle(
|
|
416
|
+
screen_map,
|
|
417
|
+
get_stack,
|
|
418
|
+
lambda _: None,
|
|
419
|
+
switch_screen,
|
|
420
|
+
set_drawer_open,
|
|
421
|
+
lambda: drawer_open,
|
|
422
|
+
parent=parent_nav,
|
|
423
|
+
),
|
|
424
|
+
[],
|
|
425
|
+
)
|
|
426
|
+
handle._screen_map = screen_map
|
|
427
|
+
handle._switch_screen = switch_screen
|
|
428
|
+
handle._set_drawer_open = set_drawer_open
|
|
429
|
+
handle._get_drawer_open = lambda: drawer_open
|
|
430
|
+
handle._parent = parent_nav
|
|
431
|
+
|
|
432
|
+
screen_def = screen_map.get(active_screen)
|
|
433
|
+
if screen_def is None:
|
|
434
|
+
screen_def = screen_map[screen_list[0].name]
|
|
435
|
+
|
|
436
|
+
screen_el = screen_def.component()
|
|
437
|
+
content = Provider(
|
|
438
|
+
_NavigationContext,
|
|
439
|
+
handle,
|
|
440
|
+
Provider(_FocusContext, True, screen_el),
|
|
441
|
+
)
|
|
442
|
+
|
|
443
|
+
children: List[Element] = [Element("View", {"flex": 1}, [content])]
|
|
444
|
+
|
|
445
|
+
if drawer_open:
|
|
446
|
+
menu_items: List[Element] = []
|
|
447
|
+
for s in screen_list:
|
|
448
|
+
if not isinstance(s, _ScreenDef):
|
|
449
|
+
continue
|
|
450
|
+
label = s.options.get("title", s.name)
|
|
451
|
+
item_name = s.name
|
|
452
|
+
|
|
453
|
+
def make_select(n: str) -> Callable[[], None]:
|
|
454
|
+
def _select() -> None:
|
|
455
|
+
switch_screen(n)
|
|
456
|
+
set_drawer_open(False)
|
|
457
|
+
|
|
458
|
+
return _select
|
|
459
|
+
|
|
460
|
+
menu_items.append(
|
|
461
|
+
Element("Button", {"title": label, "on_click": make_select(item_name)}, [], key=f"__drawer_{item_name}")
|
|
462
|
+
)
|
|
463
|
+
|
|
464
|
+
drawer_panel = Element(
|
|
465
|
+
"View",
|
|
466
|
+
{"background_color": "#FFFFFF", "width": 250},
|
|
467
|
+
menu_items,
|
|
468
|
+
)
|
|
469
|
+
children.insert(0, drawer_panel)
|
|
470
|
+
|
|
471
|
+
return Element("View", {"flex_direction": "row", "flex": 1}, children)
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
def create_drawer_navigator() -> Any:
|
|
475
|
+
"""Create a drawer-based navigator.
|
|
476
|
+
|
|
477
|
+
Returns an object with ``Navigator`` and ``Screen`` members::
|
|
478
|
+
|
|
479
|
+
Drawer = create_drawer_navigator()
|
|
480
|
+
|
|
481
|
+
Drawer.Navigator(
|
|
482
|
+
Drawer.Screen("Home", component=HomeScreen, options={"title": "Home"}),
|
|
483
|
+
Drawer.Screen("Settings", component=SettingsScreen),
|
|
484
|
+
)
|
|
485
|
+
|
|
486
|
+
The navigation handle returned by ``use_navigation()`` inside a drawer
|
|
487
|
+
navigator includes ``open_drawer()``, ``close_drawer()``, and
|
|
488
|
+
``toggle_drawer()`` methods.
|
|
489
|
+
"""
|
|
490
|
+
|
|
491
|
+
class _DrawerNavigator:
|
|
492
|
+
@staticmethod
|
|
493
|
+
def Screen(name: str, *, component: Any, options: Optional[Dict[str, Any]] = None) -> "_ScreenDef":
|
|
494
|
+
"""Define a screen within this drawer navigator."""
|
|
495
|
+
return _ScreenDef(name, component, options)
|
|
496
|
+
|
|
497
|
+
@staticmethod
|
|
498
|
+
def Navigator(*screens: Any, initial_route: Optional[str] = None, key: Optional[str] = None) -> Element:
|
|
499
|
+
"""Render the drawer navigator with the given screens."""
|
|
500
|
+
return _drawer_navigator_impl(screens=list(screens), initial_route=initial_route, key=key)
|
|
501
|
+
|
|
502
|
+
return _DrawerNavigator()
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
# ======================================================================
|
|
506
|
+
# NavigationContainer
|
|
507
|
+
# ======================================================================
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
def NavigationContainer(child: Element, *, key: Optional[str] = None) -> Element:
|
|
511
|
+
"""Root container for the navigation tree.
|
|
512
|
+
|
|
513
|
+
Wraps the child navigator in a full-size view. All declarative
|
|
514
|
+
navigators (stack, tab, drawer) should be nested inside a
|
|
515
|
+
``NavigationContainer``::
|
|
516
|
+
|
|
517
|
+
@pn.component
|
|
518
|
+
def App():
|
|
519
|
+
return NavigationContainer(
|
|
520
|
+
Stack.Navigator(
|
|
521
|
+
Stack.Screen("Home", component=HomeScreen),
|
|
522
|
+
)
|
|
523
|
+
)
|
|
524
|
+
"""
|
|
525
|
+
return Element("View", {"flex": 1}, [child], key=key)
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
# ======================================================================
|
|
529
|
+
# Hooks
|
|
530
|
+
# ======================================================================
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
def use_route() -> Dict[str, Any]:
|
|
534
|
+
"""Return the current route's parameters.
|
|
535
|
+
|
|
536
|
+
Convenience hook that reads from the navigation context::
|
|
537
|
+
|
|
538
|
+
@pn.component
|
|
539
|
+
def DetailScreen():
|
|
540
|
+
params = pn.use_route()
|
|
541
|
+
item_id = params.get("id")
|
|
542
|
+
...
|
|
543
|
+
"""
|
|
544
|
+
nav = use_context(_NavigationContext)
|
|
545
|
+
if nav is None:
|
|
546
|
+
return {}
|
|
547
|
+
get_params = getattr(nav, "get_params", None)
|
|
548
|
+
if get_params:
|
|
549
|
+
return get_params()
|
|
550
|
+
return {}
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
def use_focus_effect(effect: Callable, deps: Optional[list] = None) -> None:
|
|
554
|
+
"""Run *effect* only when the screen is focused.
|
|
555
|
+
|
|
556
|
+
Like ``use_effect`` but skips execution when the screen is not the
|
|
557
|
+
active/focused screen in a navigator::
|
|
558
|
+
|
|
559
|
+
@pn.component
|
|
560
|
+
def HomeScreen():
|
|
561
|
+
pn.use_focus_effect(lambda: print("screen focused"), [])
|
|
562
|
+
"""
|
|
563
|
+
is_focused = use_context(_FocusContext)
|
|
564
|
+
all_deps = [is_focused] + (list(deps) if deps is not None else [])
|
|
565
|
+
|
|
566
|
+
def wrapped_effect() -> Any:
|
|
567
|
+
if is_focused:
|
|
568
|
+
return effect()
|
|
569
|
+
return None
|
|
570
|
+
|
|
571
|
+
use_effect(wrapped_effect, all_deps)
|
pythonnative/page.py
CHANGED
|
@@ -27,7 +27,9 @@ import importlib
|
|
|
27
27
|
import json
|
|
28
28
|
from typing import Any, Dict, Optional
|
|
29
29
|
|
|
30
|
-
from .utils import IS_ANDROID, set_android_context
|
|
30
|
+
from .utils import IS_ANDROID, IS_IOS, set_android_context
|
|
31
|
+
|
|
32
|
+
_MAX_RENDER_PASSES = 25
|
|
31
33
|
|
|
32
34
|
# ======================================================================
|
|
33
35
|
# Component path resolution
|
|
@@ -62,6 +64,9 @@ def _init_host_common(host: Any) -> None:
|
|
|
62
64
|
host._args = {}
|
|
63
65
|
host._reconciler = None
|
|
64
66
|
host._root_native_view = None
|
|
67
|
+
host._nav_handle = None
|
|
68
|
+
host._is_rendering = False
|
|
69
|
+
host._render_queued = False
|
|
65
70
|
|
|
66
71
|
|
|
67
72
|
def _on_create(host: Any) -> None:
|
|
@@ -70,28 +75,68 @@ def _on_create(host: Any) -> None:
|
|
|
70
75
|
from .reconciler import Reconciler
|
|
71
76
|
|
|
72
77
|
host._reconciler = Reconciler(get_registry())
|
|
73
|
-
host._reconciler._page_re_render = lambda:
|
|
78
|
+
host._reconciler._page_re_render = lambda: _request_render(host)
|
|
79
|
+
host._nav_handle = NavigationHandle(host)
|
|
74
80
|
|
|
75
|
-
nav_handle = NavigationHandle(host)
|
|
76
81
|
app_element = host._component()
|
|
77
|
-
provider_element = Provider(_NavigationContext,
|
|
82
|
+
provider_element = Provider(_NavigationContext, host._nav_handle, app_element)
|
|
83
|
+
|
|
84
|
+
host._is_rendering = True
|
|
85
|
+
try:
|
|
86
|
+
host._root_native_view = host._reconciler.mount(provider_element)
|
|
87
|
+
host._attach_root(host._root_native_view)
|
|
88
|
+
_drain_renders(host)
|
|
89
|
+
finally:
|
|
90
|
+
host._is_rendering = False
|
|
78
91
|
|
|
79
|
-
|
|
80
|
-
|
|
92
|
+
|
|
93
|
+
def _request_render(host: Any) -> None:
|
|
94
|
+
"""State-change trigger. Defers if a render is already in progress."""
|
|
95
|
+
if host._is_rendering:
|
|
96
|
+
host._render_queued = True
|
|
97
|
+
return
|
|
98
|
+
_re_render(host)
|
|
81
99
|
|
|
82
100
|
|
|
83
101
|
def _re_render(host: Any) -> None:
|
|
84
|
-
|
|
102
|
+
"""Perform a full render pass, draining any state set during effects."""
|
|
103
|
+
from .hooks import Provider, _NavigationContext
|
|
85
104
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
105
|
+
host._is_rendering = True
|
|
106
|
+
try:
|
|
107
|
+
host._render_queued = False
|
|
108
|
+
|
|
109
|
+
app_element = host._component()
|
|
110
|
+
provider_element = Provider(_NavigationContext, host._nav_handle, app_element)
|
|
111
|
+
|
|
112
|
+
new_root = host._reconciler.reconcile(provider_element)
|
|
113
|
+
if new_root is not host._root_native_view:
|
|
114
|
+
host._detach_root(host._root_native_view)
|
|
115
|
+
host._root_native_view = new_root
|
|
116
|
+
host._attach_root(new_root)
|
|
89
117
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
host.
|
|
93
|
-
|
|
94
|
-
|
|
118
|
+
_drain_renders(host)
|
|
119
|
+
finally:
|
|
120
|
+
host._is_rendering = False
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _drain_renders(host: Any) -> None:
|
|
124
|
+
"""Flush additional renders queued by effects that set state."""
|
|
125
|
+
from .hooks import Provider, _NavigationContext
|
|
126
|
+
|
|
127
|
+
for _ in range(_MAX_RENDER_PASSES):
|
|
128
|
+
if not host._render_queued:
|
|
129
|
+
break
|
|
130
|
+
host._render_queued = False
|
|
131
|
+
|
|
132
|
+
app_element = host._component()
|
|
133
|
+
provider_element = Provider(_NavigationContext, host._nav_handle, app_element)
|
|
134
|
+
|
|
135
|
+
new_root = host._reconciler.reconcile(provider_element)
|
|
136
|
+
if new_root is not host._root_native_view:
|
|
137
|
+
host._detach_root(host._root_native_view)
|
|
138
|
+
host._root_native_view = new_root
|
|
139
|
+
host._attach_root(new_root)
|
|
95
140
|
|
|
96
141
|
|
|
97
142
|
def _set_args(host: Any, args: Any) -> None:
|
|
@@ -205,6 +250,21 @@ else:
|
|
|
205
250
|
except ImportError:
|
|
206
251
|
pass
|
|
207
252
|
|
|
253
|
+
# Redirect Python's stdout/stderr through fd 2 so ``print()`` output is
|
|
254
|
+
# visible via ``xcrun simctl launch --console-pty``. This runs at
|
|
255
|
+
# ``pythonnative.page`` import time, i.e. before any user page module
|
|
256
|
+
# (e.g. ``app.main_page``) is imported, so their top-level ``print()``
|
|
257
|
+
# calls are captured too. Gated on ``IS_IOS`` rather than rubicon-objc
|
|
258
|
+
# being importable, so installing the ``[ios]`` extra on macOS does
|
|
259
|
+
# not silently swap ``sys.stdout`` on a dev machine.
|
|
260
|
+
if IS_IOS:
|
|
261
|
+
try:
|
|
262
|
+
from . import _ios_log
|
|
263
|
+
|
|
264
|
+
_ios_log.install()
|
|
265
|
+
except Exception:
|
|
266
|
+
pass
|
|
267
|
+
|
|
208
268
|
_IOS_PAGE_REGISTRY: _Dict[int, Any] = {}
|
|
209
269
|
|
|
210
270
|
def _ios_register_page(vc_instance: Any, host_obj: Any) -> None:
|
|
@@ -392,10 +452,10 @@ else:
|
|
|
392
452
|
return self._args
|
|
393
453
|
|
|
394
454
|
def _push(self, page: Any, args: Optional[Dict[str, Any]] = None) -> None:
|
|
395
|
-
raise RuntimeError("
|
|
455
|
+
raise RuntimeError("navigate() requires a native runtime (iOS or Android)")
|
|
396
456
|
|
|
397
457
|
def _pop(self) -> None:
|
|
398
|
-
raise RuntimeError("
|
|
458
|
+
raise RuntimeError("go_back() requires a native runtime (iOS or Android)")
|
|
399
459
|
|
|
400
460
|
def _attach_root(self, native_view: Any) -> None:
|
|
401
461
|
pass
|