pythonnative 0.13.1__py3-none-any.whl → 0.15.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 CHANGED
@@ -23,8 +23,18 @@ Key building blocks:
23
23
  factories.
24
24
  - **Styling** uses a single ``style`` dict per element (or a list of
25
25
  dicts), composable via [`StyleSheet`][pythonnative.StyleSheet].
26
+ PythonNative ships a fully-typed [`Style`][pythonnative.style.Style]
27
+ TypedDict so editors and ``mypy`` validate every key as you type.
26
28
  - **Animations** use the ``Animated`` namespace, modeled on React
27
29
  Native's animation API.
30
+ - **Custom native components** can be authored with the
31
+ ``pythonnative.sdk`` package: define a typed
32
+ [`Props`][pythonnative.sdk.Props] dataclass, implement a
33
+ [`ViewHandler`][pythonnative.native_views.base.ViewHandler] for each
34
+ platform, and register it via
35
+ [`@native_component`][pythonnative.sdk.native_component] (or expose
36
+ it from a PyPI package via the ``pythonnative.handlers`` entry-point
37
+ group).
28
38
 
29
39
  Example:
30
40
  ```python
@@ -34,15 +44,16 @@ Example:
34
44
  def App():
35
45
  count, set_count = pn.use_state(0)
36
46
  return pn.Column(
37
- pn.Text(f"Count: {count}", style={"font_size": 24}),
47
+ pn.Text(f"Count: {count}", style=pn.style(font_size=24)),
38
48
  pn.Button("+", on_click=lambda: set_count(count + 1)),
39
- style={"spacing": 12},
49
+ style=pn.style(spacing=12),
40
50
  )
41
51
  ```
42
52
  """
43
53
 
44
- __version__ = "0.13.1"
54
+ __version__ = "0.15.0"
45
55
 
56
+ from . import sdk
46
57
  from .alerts import Alert
47
58
  from .animated import Animated, AnimatedValue
48
59
  from .components import (
@@ -100,7 +111,39 @@ from .navigation import (
100
111
  )
101
112
  from .platform import Platform
102
113
  from .screen import create_screen
103
- from .style import StyleSheet, ThemeContext
114
+ from .sdk import (
115
+ Props,
116
+ ViewHandler,
117
+ element_factory,
118
+ native_component,
119
+ register_component,
120
+ )
121
+ from .style import (
122
+ AlignItems,
123
+ AlignSelf,
124
+ AutoCapitalize,
125
+ Color,
126
+ Dimension,
127
+ EdgeInsets,
128
+ FlexDirection,
129
+ FontWeight,
130
+ JustifyContent,
131
+ KeyboardType,
132
+ Overflow,
133
+ Position,
134
+ ReturnKeyType,
135
+ ScaleType,
136
+ ShadowOffset,
137
+ Style,
138
+ StyleProp,
139
+ StyleSheet,
140
+ TextAlign,
141
+ TextDecoration,
142
+ ThemeContext,
143
+ TransformSpec,
144
+ resolve_style,
145
+ style,
146
+ )
104
147
 
105
148
  __all__ = [
106
149
  # Components
@@ -154,9 +197,31 @@ __all__ = [
154
197
  "create_drawer_navigator",
155
198
  "create_stack_navigator",
156
199
  "create_tab_navigator",
157
- # Styling
200
+ # Styling - typed primitives
201
+ "AlignItems",
202
+ "AlignSelf",
203
+ "AutoCapitalize",
204
+ "Color",
205
+ "Dimension",
206
+ "EdgeInsets",
207
+ "FlexDirection",
208
+ "FontWeight",
209
+ "JustifyContent",
210
+ "KeyboardType",
211
+ "Overflow",
212
+ "Position",
213
+ "ReturnKeyType",
214
+ "ScaleType",
215
+ "ShadowOffset",
216
+ "Style",
217
+ "StyleProp",
158
218
  "StyleSheet",
219
+ "TextAlign",
220
+ "TextDecoration",
159
221
  "ThemeContext",
222
+ "TransformSpec",
223
+ "resolve_style",
224
+ "style",
160
225
  # Animation
161
226
  "Animated",
162
227
  "AnimatedValue",
@@ -169,4 +234,11 @@ __all__ = [
169
234
  "Notifications",
170
235
  # Platform
171
236
  "Platform",
237
+ # Custom-component SDK
238
+ "Props",
239
+ "ViewHandler",
240
+ "element_factory",
241
+ "native_component",
242
+ "register_component",
243
+ "sdk",
172
244
  ]
pythonnative/animated.py CHANGED
@@ -58,7 +58,7 @@ from typing import Any, Callable, Dict, List, Optional, Tuple
58
58
 
59
59
  from .element import Element
60
60
  from .hooks import use_effect, use_ref
61
- from .style import StyleValue, resolve_style
61
+ from .style import StyleProp, resolve_style
62
62
 
63
63
  # Maximum frame rate at which the Python ticker drives animations.
64
64
  # We aim for 60 Hz but back off when no animation is active.
@@ -433,7 +433,7 @@ class _AnimationHandle:
433
433
  # ======================================================================
434
434
 
435
435
 
436
- def _resolve_style_with_values(style: StyleValue) -> Tuple[Dict[str, Any], Dict[str, AnimatedValue]]:
436
+ def _resolve_style_with_values(style: StyleProp) -> Tuple[Dict[str, Any], Dict[str, AnimatedValue]]:
437
437
  """Return ``(plain_style, animated_bindings)``.
438
438
 
439
439
  AnimatedValue entries in the style are replaced with their
@@ -35,10 +35,18 @@ Example:
35
35
  ```
36
36
  """
37
37
 
38
- from typing import Any, Callable, Dict, List, Optional
38
+ from typing import Any, Callable, Dict, List, Literal, Optional
39
39
 
40
40
  from .element import Element
41
- from .style import StyleValue, resolve_style
41
+ from .style import (
42
+ AutoCapitalize,
43
+ Color,
44
+ KeyboardType,
45
+ ReturnKeyType,
46
+ ScaleType,
47
+ StyleProp,
48
+ resolve_style,
49
+ )
42
50
 
43
51
  # ======================================================================
44
52
  # Leaf components
@@ -73,7 +81,7 @@ def _accessibility_props(
73
81
  def Text(
74
82
  text: str = "",
75
83
  *,
76
- style: StyleValue = None,
84
+ style: StyleProp = None,
77
85
  accessibility_label: Optional[str] = None,
78
86
  accessibility_hint: Optional[str] = None,
79
87
  accessibility_role: Optional[str] = None,
@@ -118,7 +126,7 @@ def Button(
118
126
  *,
119
127
  on_click: Optional[Callable[[], None]] = None,
120
128
  enabled: bool = True,
121
- style: StyleValue = None,
129
+ style: StyleProp = None,
122
130
  accessibility_label: Optional[str] = None,
123
131
  accessibility_hint: Optional[str] = None,
124
132
  accessible: Optional[bool] = None,
@@ -174,14 +182,14 @@ def TextInput(
174
182
  on_submit: Optional[Callable[[str], None]] = None,
175
183
  secure: bool = False,
176
184
  multiline: bool = False,
177
- keyboard_type: Optional[str] = None,
178
- auto_capitalize: Optional[str] = None,
185
+ keyboard_type: Optional[KeyboardType] = None,
186
+ auto_capitalize: Optional[AutoCapitalize] = None,
179
187
  auto_correct: Optional[bool] = None,
180
188
  auto_focus: bool = False,
181
- return_key_type: Optional[str] = None,
189
+ return_key_type: Optional[ReturnKeyType] = None,
182
190
  max_length: Optional[int] = None,
183
- placeholder_color: Optional[str] = None,
184
- style: StyleValue = None,
191
+ placeholder_color: Optional[Color] = None,
192
+ style: StyleProp = None,
185
193
  accessibility_label: Optional[str] = None,
186
194
  accessibility_hint: Optional[str] = None,
187
195
  accessible: Optional[bool] = None,
@@ -257,9 +265,9 @@ def TextInput(
257
265
  def Image(
258
266
  source: str = "",
259
267
  *,
260
- scale_type: Optional[str] = None,
261
- tint_color: Optional[str] = None,
262
- style: StyleValue = None,
268
+ scale_type: Optional[ScaleType] = None,
269
+ tint_color: Optional[Color] = None,
270
+ style: StyleProp = None,
263
271
  accessibility_label: Optional[str] = None,
264
272
  accessible: Optional[bool] = None,
265
273
  ref: Optional[Dict[str, Any]] = None,
@@ -307,7 +315,7 @@ def Switch(
307
315
  *,
308
316
  value: bool = False,
309
317
  on_change: Optional[Callable[[bool], None]] = None,
310
- style: StyleValue = None,
318
+ style: StyleProp = None,
311
319
  key: Optional[str] = None,
312
320
  ) -> Element:
313
321
  """Display a toggle switch.
@@ -331,7 +339,7 @@ def Switch(
331
339
  def ProgressBar(
332
340
  *,
333
341
  value: float = 0.0,
334
- style: StyleValue = None,
342
+ style: StyleProp = None,
335
343
  key: Optional[str] = None,
336
344
  ) -> Element:
337
345
  """Show determinate progress as a value between 0.0 and 1.0.
@@ -356,7 +364,7 @@ def ProgressBar(
356
364
  def ActivityIndicator(
357
365
  *,
358
366
  animating: bool = True,
359
- style: StyleValue = None,
367
+ style: StyleProp = None,
360
368
  key: Optional[str] = None,
361
369
  ) -> Element:
362
370
  """Show an indeterminate loading spinner.
@@ -378,7 +386,7 @@ def ActivityIndicator(
378
386
  def WebView(
379
387
  *,
380
388
  url: str = "",
381
- style: StyleValue = None,
389
+ style: StyleProp = None,
382
390
  key: Optional[str] = None,
383
391
  ) -> Element:
384
392
  """Embed web content from a URL.
@@ -440,7 +448,7 @@ def Slider(
440
448
  min_value: float = 0.0,
441
449
  max_value: float = 1.0,
442
450
  on_change: Optional[Callable[[float], None]] = None,
443
- style: StyleValue = None,
451
+ style: StyleProp = None,
444
452
  key: Optional[str] = None,
445
453
  ) -> Element:
446
454
  """Continuous-value slider between `min_value` and `max_value`.
@@ -475,7 +483,7 @@ def Slider(
475
483
 
476
484
  def View(
477
485
  *children: Element,
478
- style: StyleValue = None,
486
+ style: StyleProp = None,
479
487
  accessibility_label: Optional[str] = None,
480
488
  accessibility_hint: Optional[str] = None,
481
489
  accessibility_role: Optional[str] = None,
@@ -530,7 +538,7 @@ def View(
530
538
 
531
539
  def Column(
532
540
  *children: Element,
533
- style: StyleValue = None,
541
+ style: StyleProp = None,
534
542
  ref: Optional[Dict[str, Any]] = None,
535
543
  key: Optional[str] = None,
536
544
  ) -> Element:
@@ -571,7 +579,7 @@ def Column(
571
579
 
572
580
  def Row(
573
581
  *children: Element,
574
- style: StyleValue = None,
582
+ style: StyleProp = None,
575
583
  ref: Optional[Dict[str, Any]] = None,
576
584
  key: Optional[str] = None,
577
585
  ) -> Element:
@@ -614,7 +622,7 @@ def ScrollView(
614
622
  child: Optional[Element] = None,
615
623
  *,
616
624
  refresh_control: Optional[Dict[str, Any]] = None,
617
- style: StyleValue = None,
625
+ style: StyleProp = None,
618
626
  ref: Optional[Dict[str, Any]] = None,
619
627
  key: Optional[str] = None,
620
628
  ) -> Element:
@@ -647,7 +655,7 @@ def ScrollView(
647
655
 
648
656
  def SafeAreaView(
649
657
  *children: Element,
650
- style: StyleValue = None,
658
+ style: StyleProp = None,
651
659
  key: Optional[str] = None,
652
660
  ) -> Element:
653
661
  """Container that respects safe-area insets (notch, status bar, home indicator).
@@ -670,9 +678,9 @@ def Modal(
670
678
  visible: bool = False,
671
679
  on_dismiss: Optional[Callable[[], None]] = None,
672
680
  title: Optional[str] = None,
673
- animation_type: str = "slide",
681
+ animation_type: Literal["slide", "fade", "none"] = "slide",
674
682
  transparent: bool = False,
675
- style: StyleValue = None,
683
+ style: StyleProp = None,
676
684
  key: Optional[str] = None,
677
685
  ) -> Element:
678
686
  """Overlay modal dialog backed by a real native presentation.
@@ -720,7 +728,7 @@ def Pressable(
720
728
  on_press: Optional[Callable[[], None]] = None,
721
729
  on_long_press: Optional[Callable[[], None]] = None,
722
730
  pressed_opacity: float = 0.6,
723
- style: StyleValue = None,
731
+ style: StyleProp = None,
724
732
  accessibility_label: Optional[str] = None,
725
733
  accessibility_hint: Optional[str] = None,
726
734
  accessible: Optional[bool] = None,
@@ -811,7 +819,7 @@ def FlatList(
811
819
  separator_height: float = 0,
812
820
  refresh_control: Optional[Dict[str, Any]] = None,
813
821
  on_item_press: Optional[Callable[[int], None]] = None,
814
- style: StyleValue = None,
822
+ style: StyleProp = None,
815
823
  key: Optional[str] = None,
816
824
  ) -> Element:
817
825
  """Virtualized scrollable list that renders items from `data` lazily.
@@ -941,7 +949,7 @@ def SectionList(
941
949
  item_height: Optional[float] = None,
942
950
  section_header_height: float = 32.0,
943
951
  separator_height: float = 0,
944
- style: StyleValue = None,
952
+ style: StyleProp = None,
945
953
  key: Optional[str] = None,
946
954
  ) -> Element:
947
955
  """Virtualized list that supports section headers.
@@ -1040,8 +1048,8 @@ def SectionList(
1040
1048
 
1041
1049
  def StatusBar(
1042
1050
  *,
1043
- style: Optional[str] = None,
1044
- background_color: Optional[str] = None,
1051
+ style: Optional[Literal["light", "dark", "default"]] = None,
1052
+ background_color: Optional[Color] = None,
1045
1053
  hidden: Optional[bool] = None,
1046
1054
  key: Optional[str] = None,
1047
1055
  ) -> Element:
@@ -1075,8 +1083,8 @@ def StatusBar(
1075
1083
 
1076
1084
  def KeyboardAvoidingView(
1077
1085
  *children: Element,
1078
- behavior: str = "padding",
1079
- style: StyleValue = None,
1086
+ behavior: Literal["padding", "position"] = "padding",
1087
+ style: StyleProp = None,
1080
1088
  key: Optional[str] = None,
1081
1089
  ) -> Element:
1082
1090
  """Wrap content that should shift up when the keyboard is shown.
@@ -1106,7 +1114,7 @@ def RefreshControl(
1106
1114
  *,
1107
1115
  refreshing: bool = False,
1108
1116
  on_refresh: Optional[Callable[[], None]] = None,
1109
- tint_color: Optional[str] = None,
1117
+ tint_color: Optional[Color] = None,
1110
1118
  ) -> Dict[str, Any]:
1111
1119
  """Pull-to-refresh spec for [`ScrollView`][pythonnative.ScrollView] / [`FlatList`][pythonnative.FlatList].
1112
1120
 
@@ -1162,7 +1170,7 @@ def Picker(
1162
1170
  items: Optional[List[Dict[str, Any]]] = None,
1163
1171
  on_change: Optional[Callable[[Any], None]] = None,
1164
1172
  placeholder: str = "Select…",
1165
- style: StyleValue = None,
1173
+ style: StyleProp = None,
1166
1174
  key: Optional[str] = None,
1167
1175
  ) -> Element:
1168
1176
  """A select / dropdown widget.
@@ -237,12 +237,50 @@ class NativeViewRegistry:
237
237
  _registry: Optional[NativeViewRegistry] = None
238
238
 
239
239
 
240
+ def _active_platform_name() -> str:
241
+ """Return ``"android"`` or ``"ios"`` for the active runtime."""
242
+ from ..utils import IS_ANDROID
243
+
244
+ return "android" if IS_ANDROID else "ios"
245
+
246
+
247
+ def _register_builtin_handlers(registry: NativeViewRegistry) -> None:
248
+ """Register every built-in handler for the active platform."""
249
+ from ..utils import IS_ANDROID
250
+
251
+ if IS_ANDROID:
252
+ from .android import register_handlers
253
+ else:
254
+ from .ios import register_handlers
255
+ register_handlers(registry)
256
+
257
+
258
+ def _install_sdk_handlers(registry: NativeViewRegistry) -> None:
259
+ """Copy decorator-registered SDK handlers + entry-point plugins.
260
+
261
+ Imported lazily so unit tests that never touch the SDK don't pay the
262
+ entry-point discovery cost.
263
+ """
264
+ try:
265
+ from ..sdk._components import install_into_registry as _sdk_install
266
+ except Exception:
267
+ return
268
+ try:
269
+ _sdk_install(registry, _active_platform_name())
270
+ except Exception:
271
+ # A misbehaving plugin must not break PythonNative's startup.
272
+ pass
273
+
274
+
240
275
  def get_registry() -> NativeViewRegistry:
241
276
  """Return the process-wide registry, lazily registering handlers.
242
277
 
243
- The first call instantiates the registry and registers either the
244
- Android or iOS handlers based on `IS_ANDROID`. Subsequent calls
245
- return the same instance.
278
+ The first call instantiates the registry, registers either the
279
+ Android or iOS handlers based on `IS_ANDROID`, then layers on every
280
+ decorator-registered SDK handler (and any handlers exposed by
281
+ third-party packages via the
282
+ [`pythonnative.handlers`][pythonnative.sdk.ENTRY_POINT_GROUP] entry
283
+ point group). Subsequent calls return the same instance.
246
284
 
247
285
  Returns:
248
286
  The active `NativeViewRegistry`.
@@ -251,30 +289,40 @@ def get_registry() -> NativeViewRegistry:
251
289
  if _registry is not None:
252
290
  return _registry
253
291
  _registry = NativeViewRegistry()
292
+ _register_builtin_handlers(_registry)
293
+ _install_sdk_handlers(_registry)
294
+ return _registry
254
295
 
255
- from ..utils import IS_ANDROID
256
296
 
257
- if IS_ANDROID:
258
- from .android import register_handlers
297
+ def refresh_registry() -> NativeViewRegistry:
298
+ """Re-run SDK handler installation against the existing registry.
259
299
 
260
- register_handlers(_registry)
261
- else:
262
- from .ios import register_handlers
300
+ Call this after registering a new component at runtime if the
301
+ registry has already been instantiated. This is mostly useful in
302
+ REPL sessions and tests; the normal flow is "register, then call
303
+ [`get_registry`][pythonnative.native_views.get_registry]" and the
304
+ handlers come along automatically.
263
305
 
264
- register_handlers(_registry)
265
- return _registry
306
+ Returns:
307
+ The active `NativeViewRegistry`.
308
+ """
309
+ registry = get_registry()
310
+ _install_sdk_handlers(registry)
311
+ return registry
266
312
 
267
313
 
268
- def set_registry(registry: NativeViewRegistry) -> None:
314
+ def set_registry(registry: Optional[NativeViewRegistry]) -> None:
269
315
  """Install a custom registry (primarily for testing).
270
316
 
271
317
  Replaces the lazy singleton so subsequent
272
318
  [`get_registry`][pythonnative.native_views.get_registry] calls
273
319
  return `registry`. Pass a mock to drive the reconciler from
274
- unit tests without touching real native APIs.
320
+ unit tests without touching real native APIs. Pass ``None`` to
321
+ reset the singleton; the next ``get_registry`` call will then
322
+ rebuild it from scratch.
275
323
 
276
324
  Args:
277
- registry: The replacement registry.
325
+ registry: The replacement registry, or ``None`` to clear.
278
326
  """
279
327
  global _registry
280
328
  _registry = registry
@@ -1116,10 +1116,41 @@ class TabBarHandler(AndroidViewHandler):
1116
1116
  menu.clear()
1117
1117
  for i, item in enumerate(items):
1118
1118
  title = item.get("title", item.get("name", ""))
1119
- menu.add(0, i, i, str(title))
1119
+ menu_item = menu.add(0, i, i, str(title))
1120
+ res_id = self._resolve_icon(item.get("icon"))
1121
+ if res_id:
1122
+ try:
1123
+ menu_item.setIcon(res_id)
1124
+ except Exception:
1125
+ pass
1120
1126
  except Exception:
1121
1127
  pass
1122
1128
 
1129
+ def _resolve_icon(self, icon: Any) -> int:
1130
+ """Resolve a tab icon spec to an `android.R.drawable.*` res id.
1131
+
1132
+ Accepts a bare string (treated as the drawable's field name on
1133
+ ``android.R.drawable``) or a dict of the form
1134
+ ``{"ios": "...", "android": "ic_menu_home"}``. Returns ``0``
1135
+ when the icon can't be resolved, which the caller treats as
1136
+ "no icon".
1137
+ """
1138
+ if icon is None:
1139
+ return 0
1140
+ name: Any = None
1141
+ if isinstance(icon, str):
1142
+ name = icon
1143
+ elif isinstance(icon, dict):
1144
+ name = icon.get("android")
1145
+ if not name:
1146
+ return 0
1147
+ try:
1148
+ RDrawable = jclass("android.R$drawable")
1149
+ res_id = getattr(RDrawable, str(name), 0)
1150
+ return int(res_id) if res_id else 0
1151
+ except Exception:
1152
+ return 0
1153
+
1123
1154
  def _set_active(self, bnv: Any, active: Any, items: list) -> None:
1124
1155
  if active and items:
1125
1156
  for i, item in enumerate(items):
@@ -2345,13 +2345,38 @@ class TabBarHandler(IOSViewHandler):
2345
2345
 
2346
2346
  def _set_bar_items(self, tab_bar: Any, items: list) -> None:
2347
2347
  UITabBarItem = ObjCClass("UITabBarItem")
2348
+ UIImage = ObjCClass("UIImage")
2348
2349
  bar_items = []
2349
2350
  for i, item in enumerate(items):
2350
2351
  title = item.get("title", item.get("name", ""))
2351
- bar_item = UITabBarItem.alloc().initWithTitle_image_tag_(str(title), None, i)
2352
+ image = self._resolve_icon(UIImage, item.get("icon"))
2353
+ bar_item = UITabBarItem.alloc().initWithTitle_image_tag_(str(title), image, i)
2352
2354
  bar_items.append(bar_item)
2353
2355
  tab_bar.setItems_animated_(bar_items, False)
2354
2356
 
2357
+ def _resolve_icon(self, UIImage: Any, icon: Any) -> Any:
2358
+ """Resolve a tab icon spec to a UIImage, or return None.
2359
+
2360
+ Accepts a bare string (treated as an SF Symbol name) or a dict
2361
+ of the form ``{"ios": "house.fill", "android": "..."}``. SF
2362
+ Symbols are looked up via ``UIImage.systemImageNamed:``; names
2363
+ that don't resolve produce a text-only tab.
2364
+ """
2365
+ if icon is None:
2366
+ return None
2367
+ name: Any = None
2368
+ if isinstance(icon, str):
2369
+ name = icon
2370
+ elif isinstance(icon, dict):
2371
+ name = icon.get("ios")
2372
+ if not name:
2373
+ return None
2374
+ try:
2375
+ image = UIImage.systemImageNamed_(str(name))
2376
+ return image if image else None
2377
+ except Exception:
2378
+ return None
2379
+
2355
2380
  def _set_active(self, tab_bar: Any, active: Any, items: list) -> None:
2356
2381
  if not active or not items:
2357
2382
  return
@@ -570,10 +570,14 @@ def _tab_navigator_impl(screens: Any = None, initial_route: Optional[str] = None
570
570
  if screen_def is None:
571
571
  screen_def = screen_map[screen_list[0].name]
572
572
 
573
- tab_items: List[Dict[str, str]] = []
573
+ tab_items: List[Dict[str, Any]] = []
574
574
  for s in screen_list:
575
575
  if isinstance(s, _ScreenDef):
576
- tab_items.append({"name": s.name, "title": s.options.get("title", s.name)})
576
+ item: Dict[str, Any] = {"name": s.name, "title": s.options.get("title", s.name)}
577
+ icon = s.options.get("tab_bar_icon")
578
+ if icon is not None:
579
+ item["icon"] = icon
580
+ tab_items.append(item)
577
581
 
578
582
  def on_tab_select(name: str) -> None:
579
583
  switch_tab(name)
@@ -639,8 +643,17 @@ def create_tab_navigator() -> Any:
639
643
  name: Route name and default tab title.
640
644
  component: A `@component` function rendered when this
641
645
  tab is active.
642
- options: Optional per-screen settings (e.g.,
643
- `{"title": "..."}`).
646
+ options: Optional per-screen settings. Recognized keys:
647
+
648
+ - `title` (str): Tab label.
649
+ - `tab_bar_icon` (str | dict): Native system icon
650
+ identifier. A string is used on every platform; a
651
+ dict like `{"ios": "house.fill", "android":
652
+ "ic_menu_home"}` selects per platform. iOS values
653
+ are resolved via SF Symbols
654
+ (`UIImage.systemImageNamed_`); Android values are
655
+ resolved against `android.R.drawable.<name>`.
656
+ Names that don't resolve fall back to text-only.
644
657
 
645
658
  Returns:
646
659
  A `_ScreenDef` consumed by `Navigator(...)`.