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.
@@ -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: Free-form per-screen options (e.g., `title` for the
89
- tab bar, `headerShown`, etc.). Interpretation is up to the
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[Dict[str, Any]] = None) -> None:
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["current"] = stack
477
+ stack_ref.current = stack
454
478
 
455
479
  handle = use_memo(
456
- lambda: _DeclarativeNavHandle(screen_map, lambda: stack_ref["current"], set_stack, parent=parent_nav), []
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[Dict[str, Any]] = None) -> "_ScreenDef":
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["current"] = tab_params
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["current"] or {}
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[Dict[str, Any]] = None) -> "_ScreenDef":
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["current"] = screen_params
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["current"] or {}
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, "on_click": make_select(item_name)}, [], key=f"__drawer_{item_name}")
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[Dict[str, Any]] = None) -> "_ScreenDef":
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: