pythonnative 0.23.0__py3-none-any.whl → 0.25.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 +44 -53
- pythonnative/animated.py +3 -3
- pythonnative/cli/pn.py +3 -3
- pythonnative/components.py +309 -513
- pythonnative/diagnostics.py +214 -0
- pythonnative/element.py +5 -2
- pythonnative/events.py +13 -8
- pythonnative/hooks.py +761 -114
- pythonnative/hot_reload.py +9 -1
- pythonnative/native_modules/notifications.py +36 -4
- pythonnative/native_modules/permissions.py +24 -1
- pythonnative/native_views/android.py +85 -3
- pythonnative/native_views/desktop.py +38 -1
- pythonnative/native_views/ios.py +252 -7
- pythonnative/navigation.py +41 -17
- pythonnative/preview.py +43 -7
- pythonnative/reconciler.py +1360 -457
- pythonnative/runtime.py +407 -215
- pythonnative/screen.py +377 -34
- pythonnative/storage.py +5 -5
- pythonnative/style.py +83 -0
- pythonnative/suspense.py +417 -0
- pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/ScreenFragment.kt +22 -0
- {pythonnative-0.23.0.dist-info → pythonnative-0.25.0.dist-info}/METADATA +6 -4
- {pythonnative-0.23.0.dist-info → pythonnative-0.25.0.dist-info}/RECORD +29 -27
- {pythonnative-0.23.0.dist-info → pythonnative-0.25.0.dist-info}/WHEEL +1 -1
- {pythonnative-0.23.0.dist-info → pythonnative-0.25.0.dist-info}/entry_points.txt +0 -0
- {pythonnative-0.23.0.dist-info → pythonnative-0.25.0.dist-info}/licenses/LICENSE +0 -0
- {pythonnative-0.23.0.dist-info → pythonnative-0.25.0.dist-info}/top_level.txt +0 -0
pythonnative/navigation.py
CHANGED
|
@@ -45,11 +45,12 @@ Example:
|
|
|
45
45
|
```
|
|
46
46
|
"""
|
|
47
47
|
|
|
48
|
-
from typing import Any, Callable, Dict, List, Optional
|
|
48
|
+
from typing import Any, Callable, Dict, List, Optional, TypedDict, Union
|
|
49
49
|
|
|
50
50
|
from .element import Element
|
|
51
51
|
from .hooks import (
|
|
52
52
|
Provider,
|
|
53
|
+
Ref,
|
|
53
54
|
_NavigationContext,
|
|
54
55
|
component,
|
|
55
56
|
create_context,
|
|
@@ -60,6 +61,29 @@ from .hooks import (
|
|
|
60
61
|
use_state,
|
|
61
62
|
)
|
|
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
|
+
|
|
63
87
|
# ======================================================================
|
|
64
88
|
# Focus context
|
|
65
89
|
# ======================================================================
|
|
@@ -85,14 +109,14 @@ class _ScreenDef:
|
|
|
85
109
|
name: Route name used by `navigate(name, ...)`.
|
|
86
110
|
component: A `@component` function rendered when this screen is
|
|
87
111
|
active.
|
|
88
|
-
options:
|
|
89
|
-
|
|
90
|
-
specific navigator implementation.
|
|
112
|
+
options: Per-screen [`ScreenOptions`][pythonnative.ScreenOptions]
|
|
113
|
+
(e.g., `title`, `tab_bar_icon`). Interpretation is up to
|
|
114
|
+
the specific navigator implementation.
|
|
91
115
|
"""
|
|
92
116
|
|
|
93
117
|
__slots__ = ("name", "component", "options")
|
|
94
118
|
|
|
95
|
-
def __init__(self, name: str, component_fn: Any, options: Optional[
|
|
119
|
+
def __init__(self, name: str, component_fn: Any, options: Optional[ScreenOptions] = None) -> None:
|
|
96
120
|
self.name = name
|
|
97
121
|
self.component = component_fn
|
|
98
122
|
self.options = options or {}
|
|
@@ -450,10 +474,10 @@ def _stack_navigator_impl(screens: Any = None, initial_route: Optional[str] = No
|
|
|
450
474
|
stack, set_stack = use_state(lambda: [_RouteEntry(first_route, first_params)])
|
|
451
475
|
|
|
452
476
|
stack_ref = use_ref(None)
|
|
453
|
-
stack_ref
|
|
477
|
+
stack_ref.current = stack
|
|
454
478
|
|
|
455
479
|
handle = use_memo(
|
|
456
|
-
lambda: _DeclarativeNavHandle(screen_map, lambda: stack_ref
|
|
480
|
+
lambda: _DeclarativeNavHandle(screen_map, lambda: stack_ref.current, set_stack, parent=parent_nav), []
|
|
457
481
|
)
|
|
458
482
|
handle._screen_map = screen_map
|
|
459
483
|
handle._parent = parent_nav
|
|
@@ -505,7 +529,7 @@ def create_stack_navigator() -> Any:
|
|
|
505
529
|
|
|
506
530
|
class _StackNavigator:
|
|
507
531
|
@staticmethod
|
|
508
|
-
def Screen(name: str, *, component: Any, options: Optional[
|
|
532
|
+
def Screen(name: str, *, component: Any, options: Optional[ScreenOptions] = None) -> "_ScreenDef":
|
|
509
533
|
"""Define a screen within this stack navigator.
|
|
510
534
|
|
|
511
535
|
Args:
|
|
@@ -556,8 +580,8 @@ def _tab_navigator_impl(screens: Any = None, initial_route: Optional[str] = None
|
|
|
556
580
|
active_tab, set_active_tab = use_state(first_route)
|
|
557
581
|
tab_params, set_tab_params = use_state(lambda: {first_route: {}})
|
|
558
582
|
|
|
559
|
-
params_ref = use_ref(None)
|
|
560
|
-
params_ref
|
|
583
|
+
params_ref: Ref[Dict[str, Dict[str, Any]]] = use_ref(None)
|
|
584
|
+
params_ref.current = tab_params
|
|
561
585
|
|
|
562
586
|
def switch_tab(name: str, params: Optional[Dict[str, Any]] = None) -> None:
|
|
563
587
|
set_active_tab(name)
|
|
@@ -565,7 +589,7 @@ def _tab_navigator_impl(screens: Any = None, initial_route: Optional[str] = None
|
|
|
565
589
|
set_tab_params(lambda p: {**p, name: params})
|
|
566
590
|
|
|
567
591
|
def get_stack() -> List[_RouteEntry]:
|
|
568
|
-
p = params_ref
|
|
592
|
+
p = params_ref.current or {}
|
|
569
593
|
return [_RouteEntry(active_tab, p.get(active_tab, {}))]
|
|
570
594
|
|
|
571
595
|
handle = use_memo(lambda: _TabNavHandle(screen_map, get_stack, lambda _: None, switch_tab, parent=parent_nav), [])
|
|
@@ -643,7 +667,7 @@ def create_tab_navigator() -> Any:
|
|
|
643
667
|
|
|
644
668
|
class _TabNavigator:
|
|
645
669
|
@staticmethod
|
|
646
|
-
def Screen(name: str, *, component: Any, options: Optional[
|
|
670
|
+
def Screen(name: str, *, component: Any, options: Optional[ScreenOptions] = None) -> "_ScreenDef":
|
|
647
671
|
"""Define a screen within this tab navigator.
|
|
648
672
|
|
|
649
673
|
Args:
|
|
@@ -704,8 +728,8 @@ def _drawer_navigator_impl(screens: Any = None, initial_route: Optional[str] = N
|
|
|
704
728
|
drawer_open, set_drawer_open = use_state(False)
|
|
705
729
|
screen_params, set_screen_params = use_state(lambda: {first_route: {}})
|
|
706
730
|
|
|
707
|
-
params_ref = use_ref(None)
|
|
708
|
-
params_ref
|
|
731
|
+
params_ref: Ref[Dict[str, Dict[str, Any]]] = use_ref(None)
|
|
732
|
+
params_ref.current = screen_params
|
|
709
733
|
|
|
710
734
|
def switch_screen(name: str, params: Optional[Dict[str, Any]] = None) -> None:
|
|
711
735
|
set_active_screen(name)
|
|
@@ -713,7 +737,7 @@ def _drawer_navigator_impl(screens: Any = None, initial_route: Optional[str] = N
|
|
|
713
737
|
set_screen_params(lambda p: {**p, name: params})
|
|
714
738
|
|
|
715
739
|
def get_stack() -> List[_RouteEntry]:
|
|
716
|
-
p = params_ref
|
|
740
|
+
p = params_ref.current or {}
|
|
717
741
|
return [_RouteEntry(active_screen, p.get(active_screen, {}))]
|
|
718
742
|
|
|
719
743
|
handle = use_memo(
|
|
@@ -763,7 +787,7 @@ def _drawer_navigator_impl(screens: Any = None, initial_route: Optional[str] = N
|
|
|
763
787
|
return _select
|
|
764
788
|
|
|
765
789
|
menu_items.append(
|
|
766
|
-
Element("Button", {"title": label, "
|
|
790
|
+
Element("Button", {"title": label, "on_press": make_select(item_name)}, [], key=f"__drawer_{item_name}")
|
|
767
791
|
)
|
|
768
792
|
|
|
769
793
|
drawer_panel = Element(
|
|
@@ -813,7 +837,7 @@ def create_drawer_navigator() -> Any:
|
|
|
813
837
|
|
|
814
838
|
class _DrawerNavigator:
|
|
815
839
|
@staticmethod
|
|
816
|
-
def Screen(name: str, *, component: Any, options: Optional[
|
|
840
|
+
def Screen(name: str, *, component: Any, options: Optional[ScreenOptions] = None) -> "_ScreenDef":
|
|
817
841
|
"""Define a screen within this drawer navigator.
|
|
818
842
|
|
|
819
843
|
Args:
|
pythonnative/preview.py
CHANGED
|
@@ -231,17 +231,13 @@ class DesktopApp:
|
|
|
231
231
|
host.on_pause()
|
|
232
232
|
except Exception:
|
|
233
233
|
pass
|
|
234
|
+
# ``on_destroy`` unmounts the host's reconciler: effect cleanups
|
|
235
|
+
# run, native widgets are destroyed, and event registrations are
|
|
236
|
+
# released (see ``screen._destroy_host``).
|
|
234
237
|
try:
|
|
235
238
|
host.on_destroy()
|
|
236
239
|
except Exception:
|
|
237
240
|
pass
|
|
238
|
-
reconciler = getattr(host, "_reconciler", None)
|
|
239
|
-
tree = getattr(reconciler, "_tree", None) if reconciler is not None else None
|
|
240
|
-
if reconciler is not None and tree is not None:
|
|
241
|
-
try:
|
|
242
|
-
reconciler._destroy_tree(tree)
|
|
243
|
-
except Exception:
|
|
244
|
-
pass
|
|
245
241
|
container = getattr(host, "_pn_container", None)
|
|
246
242
|
if container is not None:
|
|
247
243
|
try:
|
|
@@ -249,6 +245,29 @@ class DesktopApp:
|
|
|
249
245
|
except Exception:
|
|
250
246
|
pass
|
|
251
247
|
|
|
248
|
+
def teardown_all(self) -> None:
|
|
249
|
+
"""Destroy every screen on the stack (preview window closing)."""
|
|
250
|
+
while self._stack:
|
|
251
|
+
self._teardown(self._stack.pop())
|
|
252
|
+
|
|
253
|
+
def back_pressed(self) -> None:
|
|
254
|
+
"""Route a desktop back gesture (Escape) like a hardware back press.
|
|
255
|
+
|
|
256
|
+
``use_back_handler`` subscribers on the active screen get the
|
|
257
|
+
first chance to consume the event; otherwise the stack pops
|
|
258
|
+
(matching Android's default back behavior). At the root the
|
|
259
|
+
event is ignored.
|
|
260
|
+
"""
|
|
261
|
+
host = self.active_host()
|
|
262
|
+
if host is None:
|
|
263
|
+
return
|
|
264
|
+
try:
|
|
265
|
+
if host.on_back_pressed():
|
|
266
|
+
return
|
|
267
|
+
except Exception:
|
|
268
|
+
pass
|
|
269
|
+
self.pop_screen()
|
|
270
|
+
|
|
252
271
|
# -- viewport / resize --------------------------------------------
|
|
253
272
|
|
|
254
273
|
def resize(self, width: float, height: float) -> None:
|
|
@@ -391,6 +410,12 @@ def run_preview(
|
|
|
391
410
|
root_dir, watched = _resolve_paths(component_path, project_root, watch_dir)
|
|
392
411
|
_publish_desktop_color_scheme()
|
|
393
412
|
|
|
413
|
+
# The preview is inherently a development surface: turn on dev
|
|
414
|
+
# diagnostics (validation warnings, hook-order checks, RedBox).
|
|
415
|
+
from . import diagnostics
|
|
416
|
+
|
|
417
|
+
diagnostics.set_dev_mode(True)
|
|
418
|
+
|
|
394
419
|
root = tk.Tk()
|
|
395
420
|
root.title(title)
|
|
396
421
|
root.geometry(f"{int(width)}x{int(height)}")
|
|
@@ -414,6 +439,11 @@ def run_preview(
|
|
|
414
439
|
|
|
415
440
|
stage.bind("<Configure>", _on_configure)
|
|
416
441
|
|
|
442
|
+
# Escape acts as the desktop stand-in for the hardware back button:
|
|
443
|
+
# ``use_back_handler`` subscribers can intercept it, and otherwise
|
|
444
|
+
# the navigation stack pops.
|
|
445
|
+
root.bind("<Escape>", lambda _event: app.back_pressed())
|
|
446
|
+
|
|
417
447
|
watcher = _build_watcher(watched, root_dir, app, main_queue) if hot_reload else None
|
|
418
448
|
if watcher is not None:
|
|
419
449
|
watcher.start()
|
|
@@ -446,6 +476,12 @@ def run_preview(
|
|
|
446
476
|
watcher.stop()
|
|
447
477
|
except Exception:
|
|
448
478
|
pass
|
|
479
|
+
# Unmount every screen first so effect cleanups (timers, tasks,
|
|
480
|
+
# subscriptions) run before the Tk interpreter goes away.
|
|
481
|
+
try:
|
|
482
|
+
app.teardown_all()
|
|
483
|
+
except Exception:
|
|
484
|
+
pass
|
|
449
485
|
runtime_module.set_desktop_main_dispatch(None)
|
|
450
486
|
desktop_backend.clear_root_container()
|
|
451
487
|
try:
|