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,2856 +0,0 @@
1
- """Built-in element factories.
2
-
3
- Each factory function (``Text``, ``Button``, …) is a fully-typed thin
4
- wrapper that builds an [`Element`][pythonnative.Element] through the
5
- shared :func:`_make_element` helper, so style resolution, ``ref``
6
- attachment, ``None``-default dropping, and forced overrides (e.g.
7
- ``Column``'s fixed ``flex_direction``) live in exactly one place. The
8
- factory signatures themselves are the canonical prop schemas: editors
9
- and type checkers validate calls directly against them.
10
-
11
- Example:
12
- ```python
13
- import pythonnative as pn
14
-
15
- pn.Column(
16
- pn.Text("Hello", style=pn.style(font_size=18)),
17
- pn.Button("Tap", on_press=lambda: print("tapped")),
18
- style=pn.style(spacing=12, padding=16),
19
- )
20
- ```
21
- """
22
-
23
- import bisect
24
- from typing import Any, Callable, Dict, List, Literal, Optional, Tuple, Union
25
-
26
- from .element import Element
27
- from .hooks import (
28
- Ref,
29
- component,
30
- use_imperative_handle,
31
- use_keyboard_height,
32
- use_ref,
33
- use_safe_area_insets,
34
- use_state,
35
- )
36
- from .style import (
37
- AccessibilityState,
38
- AutoCapitalize,
39
- Color,
40
- KeyboardType,
41
- ReturnKeyType,
42
- ScaleType,
43
- StyleProp,
44
- resolve_style,
45
- validate_style_keys,
46
- )
47
-
48
- # ======================================================================
49
- # Canonical element builder
50
- # ======================================================================
51
-
52
-
53
- def _make_element(
54
- name: str,
55
- *children: Element,
56
- style: StyleProp = None,
57
- ref: Optional[Ref] = None,
58
- key: Optional[str] = None,
59
- _defaults: Optional[Dict[str, Any]] = None,
60
- _forced: Optional[Dict[str, Any]] = None,
61
- **props: Any,
62
- ) -> Element:
63
- """Build an [`Element`][pythonnative.Element] of type ``name``.
64
-
65
- This is the single helper every built-in factory routes through, so
66
- the cross-cutting concerns that used to be duplicated per component
67
- live in one place:
68
-
69
- 1. ``style`` is flattened via
70
- [`resolve_style`][pythonnative.style.resolve_style] (list-of-dicts
71
- and ``None`` both handled).
72
- 2. ``_defaults`` are filled in for keys not already present (used for
73
- things like ``View``'s default ``flex_direction: "column"`` that
74
- a user style may legitimately override).
75
- 3. ``**props`` are merged on top, with ``None`` values *dropped* so
76
- optional kwargs don't pollute the prop dict.
77
- 4. ``ref`` is attached under the reserved ``"ref"`` key.
78
- 5. ``_forced`` overrides everything (used by ``Column`` / ``Row`` to
79
- lock their flex direction regardless of user style).
80
-
81
- Args:
82
- name: Element type name (e.g. ``"Text"``).
83
- *children: Child elements.
84
- style: Style dict, list of dicts, or ``None``.
85
- ref: Optional [`Ref`][pythonnative.Ref] from ``use_ref()``; the
86
- reconciler populates ``ref.current`` with the underlying
87
- native view.
88
- key: Stable identity for keyed reconciliation.
89
- _defaults: Internal: fill-only-if-missing prop defaults.
90
- _forced: Internal: prop overrides applied last.
91
- **props: Per-component props. ``None`` values are dropped.
92
-
93
- Returns:
94
- A fresh [`Element`][pythonnative.Element].
95
- """
96
- out: Dict[str, Any] = dict(resolve_style(style))
97
- if out:
98
- validate_style_keys(out, owner=name)
99
- if _defaults:
100
- for k, v in _defaults.items():
101
- out.setdefault(k, v)
102
- for k, v in props.items():
103
- if v is not None:
104
- out[k] = v
105
- if ref is not None:
106
- out["ref"] = ref
107
- if _forced:
108
- out.update(_forced)
109
- return Element(name, out, list(children), key=key)
110
-
111
-
112
- # ======================================================================
113
- # Leaf factories
114
- # ======================================================================
115
-
116
-
117
- # Style keys that can vary per span inside a rich-text run. Layout
118
- # keys are excluded: spans are inline fragments of one paragraph and
119
- # have no boxes of their own.
120
- _SPAN_STYLE_KEYS = (
121
- "color",
122
- "background_color",
123
- "font_size",
124
- "font_family",
125
- "font_weight",
126
- "bold",
127
- "italic",
128
- "text_decoration",
129
- "letter_spacing",
130
- )
131
-
132
-
133
- def _flatten_text_spans(
134
- parts: Tuple[Any, ...],
135
- inherited: Dict[str, Any],
136
- ) -> List[Dict[str, Any]]:
137
- """Flatten nested ``Text`` parts into a flat list of styled spans.
138
-
139
- Strings become spans carrying only the inherited style overrides;
140
- nested ``Text`` elements contribute their own span-style keys
141
- (merged over what they inherit) and recurse into their parts.
142
- """
143
- spans: List[Dict[str, Any]] = []
144
- for part in parts:
145
- if part is None:
146
- continue
147
- if isinstance(part, Element):
148
- props = part.props or {}
149
- child_style = dict(inherited)
150
- for k in _SPAN_STYLE_KEYS:
151
- if props.get(k) is not None:
152
- child_style[k] = props[k]
153
- nested = props.get("spans")
154
- if nested:
155
- for span in nested:
156
- merged = dict(child_style)
157
- for k in _SPAN_STYLE_KEYS:
158
- if span.get(k) is not None:
159
- merged[k] = span[k]
160
- spans.append({"text": span.get("text", ""), **merged})
161
- else:
162
- spans.append({"text": str(props.get("text", "")), **child_style})
163
- else:
164
- spans.append({"text": str(part), **inherited})
165
- return spans
166
-
167
-
168
- def Text(
169
- *parts: Any,
170
- style: StyleProp = None,
171
- accessibility_label: Optional[str] = None,
172
- accessibility_hint: Optional[str] = None,
173
- accessibility_role: Optional[str] = None,
174
- accessible: Optional[bool] = None,
175
- accessibility_state: Optional[AccessibilityState] = None,
176
- accessibility_live_region: Optional[Literal["none", "polite", "assertive"]] = None,
177
- test_id: Optional[str] = None,
178
- ref: Optional[Ref] = None,
179
- key: Optional[str] = None,
180
- ) -> Element:
181
- """Display a string of text, optionally with styled nested spans.
182
-
183
- Style properties: ``font_size``, ``color``, ``bold``,
184
- ``font_weight``, ``font_family``, ``italic``, ``text_align``,
185
- ``background_color``, ``max_lines``, ``letter_spacing``,
186
- ``line_height``, ``text_decoration`` (``"underline"`` /
187
- ``"line_through"``), ``border_radius``, ``border_width``,
188
- ``border_color``, ``shadow_*``, ``opacity``, ``transform``, plus
189
- the common layout props.
190
-
191
- **Rich text**: pass multiple parts, mixing plain strings and
192
- nested ``Text`` elements, to render one paragraph with per-span
193
- styling (a single ``TextView`` / ``UILabel`` natively, so line
194
- wrapping flows across spans):
195
-
196
- ```python
197
- pn.Text(
198
- "Hello, ",
199
- pn.Text("world", style=pn.style(bold=True, color="#0A84FF")),
200
- "!",
201
- style=pn.style(font_size=18),
202
- )
203
- ```
204
-
205
- Nested spans inherit the outer element's text styling and may
206
- override ``color``, ``background_color``, ``font_size``,
207
- ``font_family``, ``font_weight``, ``bold``, ``italic``,
208
- ``text_decoration``, and ``letter_spacing``.
209
-
210
- Args:
211
- *parts: Text content: a single string, or any mix of strings
212
- and nested ``Text`` elements for rich text.
213
- style: Style dict (or list of dicts).
214
- accessibility_label: Spoken description for screen readers.
215
- accessibility_hint: Spoken extra detail (iOS only).
216
- accessibility_role: Semantic role for assistive tech.
217
- accessible: Override whether the element is exposed to AT.
218
- accessibility_state: Current widget state for assistive tech,
219
- e.g. ``{"disabled": True, "selected": False}``. Recognized
220
- keys: ``disabled``, ``selected``, ``checked``, ``busy``,
221
- ``expanded``.
222
- accessibility_live_region: How AT announces dynamic changes to
223
- this view: ``"none"``, ``"polite"``, or ``"assertive"``
224
- (Android only).
225
- test_id: Stable identifier for UI tests; exposed as
226
- ``resource-id`` on Android and ``accessibilityIdentifier``
227
- on iOS.
228
- ref: Optional [`Ref`][pythonnative.Ref] from ``use_ref()``.
229
- key: Stable identity for keyed reconciliation.
230
-
231
- Returns:
232
- An [`Element`][pythonnative.Element] of type ``"Text"``.
233
- """
234
- rich = any(isinstance(p, Element) for p in parts) or len(parts) > 1
235
- if rich:
236
- spans = _flatten_text_spans(parts, {})
237
- text = "".join(s["text"] for s in spans)
238
- else:
239
- spans = None
240
- text = str(parts[0]) if parts else ""
241
- return _make_element(
242
- "Text",
243
- style=style,
244
- ref=ref,
245
- key=key,
246
- text=text,
247
- spans=spans,
248
- accessibility_label=accessibility_label,
249
- accessibility_hint=accessibility_hint,
250
- accessibility_role=accessibility_role,
251
- accessible=accessible,
252
- accessibility_state=accessibility_state,
253
- accessibility_live_region=accessibility_live_region,
254
- test_id=test_id,
255
- )
256
-
257
-
258
- def Button(
259
- title: str = "",
260
- *,
261
- on_press: Optional[Callable[[], None]] = None,
262
- enabled: bool = True,
263
- style: StyleProp = None,
264
- accessibility_label: Optional[str] = None,
265
- accessibility_hint: Optional[str] = None,
266
- accessibility_role: Optional[str] = None,
267
- accessible: Optional[bool] = None,
268
- accessibility_state: Optional[AccessibilityState] = None,
269
- accessibility_live_region: Optional[Literal["none", "polite", "assertive"]] = None,
270
- test_id: Optional[str] = None,
271
- ref: Optional[Ref] = None,
272
- key: Optional[str] = None,
273
- ) -> Element:
274
- """Display a tappable button.
275
-
276
- Style properties: ``color``, ``background_color``, ``font_size``,
277
- ``border_radius``, ``border_width``, ``border_color``, ``shadow_*``,
278
- ``opacity``, ``transform``, plus the common layout props.
279
-
280
- Buttons get ``accessibility_role="button"`` by default.
281
-
282
- Args:
283
- title: Button label.
284
- on_press: Callback invoked when the user taps the button.
285
- enabled: When ``False``, the button is disabled and cannot be
286
- tapped.
287
- style: Style dict (or list of dicts).
288
- accessibility_label: Spoken description for screen readers.
289
- accessibility_hint: Spoken extra detail (iOS only).
290
- accessibility_role: Override the default ``"button"`` role.
291
- accessible: Override whether the element is exposed to AT.
292
- accessibility_state: Current widget state for assistive tech,
293
- e.g. ``{"disabled": True, "selected": False}``. Recognized
294
- keys: ``disabled``, ``selected``, ``checked``, ``busy``,
295
- ``expanded``.
296
- accessibility_live_region: How AT announces dynamic changes to
297
- this view: ``"none"``, ``"polite"``, or ``"assertive"``
298
- (Android only).
299
- test_id: Stable identifier for UI tests; exposed as
300
- ``resource-id`` on Android and ``accessibilityIdentifier``
301
- on iOS.
302
- ref: Optional [`Ref`][pythonnative.Ref] from ``use_ref()``.
303
- key: Stable identity for keyed reconciliation.
304
-
305
- Returns:
306
- An [`Element`][pythonnative.Element] of type ``"Button"``.
307
- """
308
- return _make_element(
309
- "Button",
310
- style=style,
311
- ref=ref,
312
- key=key,
313
- title=title,
314
- on_press=on_press,
315
- enabled=enabled,
316
- accessibility_label=accessibility_label,
317
- accessibility_hint=accessibility_hint,
318
- accessibility_role=accessibility_role,
319
- accessible=accessible,
320
- accessibility_state=accessibility_state,
321
- accessibility_live_region=accessibility_live_region,
322
- test_id=test_id,
323
- _defaults={"accessibility_role": "button"},
324
- )
325
-
326
-
327
- def TextInput(
328
- *,
329
- value: str = "",
330
- placeholder: Optional[str] = None,
331
- on_change: Optional[Callable[[str], None]] = None,
332
- on_submit: Optional[Callable[[str], None]] = None,
333
- secure: bool = False,
334
- multiline: bool = False,
335
- keyboard_type: Optional[KeyboardType] = None,
336
- auto_capitalize: Optional[AutoCapitalize] = None,
337
- auto_correct: Optional[bool] = None,
338
- auto_focus: bool = False,
339
- return_key_type: Optional[ReturnKeyType] = None,
340
- max_length: Optional[int] = None,
341
- placeholder_color: Optional[Color] = None,
342
- editable: bool = True,
343
- clear_button: bool = False,
344
- on_focus: Optional[Callable[[], None]] = None,
345
- on_blur: Optional[Callable[[], None]] = None,
346
- selection_color: Optional[Color] = None,
347
- text_content_type: Optional[str] = None,
348
- style: StyleProp = None,
349
- accessibility_label: Optional[str] = None,
350
- accessibility_hint: Optional[str] = None,
351
- accessible: Optional[bool] = None,
352
- accessibility_state: Optional[AccessibilityState] = None,
353
- accessibility_live_region: Optional[Literal["none", "polite", "assertive"]] = None,
354
- test_id: Optional[str] = None,
355
- ref: Optional[Ref] = None,
356
- key: Optional[str] = None,
357
- ) -> Element:
358
- """Display a text-entry field (single-line by default, or ``multiline``).
359
-
360
- Style properties: ``font_size``, ``color``, ``background_color``,
361
- ``border_*``, plus the common layout props.
362
-
363
- Args:
364
- value: Current text content (controlled-input pattern).
365
- placeholder: Hint shown when ``value`` is empty.
366
- on_change: Callback invoked with the new string each keystroke.
367
- on_submit: Callback invoked when the user submits (Return /
368
- Done / etc.). Receives the final text.
369
- secure: When ``True``, characters are masked (use for passwords).
370
- multiline: When ``True``, allows multiple lines of input.
371
- keyboard_type: One of ``"default"``, ``"email_address"``,
372
- ``"number_pad"``, ``"decimal_pad"``, ``"phone_pad"``, ``"url"``.
373
- auto_capitalize: One of ``"none"``, ``"sentences"``, ``"words"``,
374
- ``"characters"``.
375
- auto_correct: Enable/disable autocorrection.
376
- auto_focus: Request focus on mount.
377
- return_key_type: One of ``"default"``, ``"done"``, ``"go"``,
378
- ``"next"``, ``"send"``, ``"search"``.
379
- max_length: Maximum number of characters allowed.
380
- placeholder_color: Color used for the placeholder string.
381
- editable: When ``False``, the field is read-only (still
382
- selectable).
383
- clear_button: When ``True``, shows a clear ("x") button while
384
- editing (iOS ``clearButtonMode``; an inline button on
385
- Android).
386
- on_focus: Callback invoked when the field gains focus.
387
- on_blur: Callback invoked when the field loses focus.
388
- selection_color: Cursor / selection highlight color.
389
- text_content_type: Semantic content hint for autofill (e.g.
390
- ``"username"``, ``"password"``, ``"one_time_code"``).
391
- style: Style dict (or list of dicts).
392
- accessibility_label: Spoken description for screen readers.
393
- accessibility_hint: Spoken extra detail (iOS only).
394
- accessible: Override whether the element is exposed to AT.
395
- accessibility_state: Current widget state for assistive tech,
396
- e.g. ``{"disabled": True, "selected": False}``. Recognized
397
- keys: ``disabled``, ``selected``, ``checked``, ``busy``,
398
- ``expanded``.
399
- accessibility_live_region: How AT announces dynamic changes to
400
- this view: ``"none"``, ``"polite"``, or ``"assertive"``
401
- (Android only).
402
- test_id: Stable identifier for UI tests; exposed as
403
- ``resource-id`` on Android and ``accessibilityIdentifier``
404
- on iOS.
405
- ref: Optional [`Ref`][pythonnative.Ref] from ``use_ref()``.
406
- key: Stable identity for keyed reconciliation.
407
-
408
- Returns:
409
- An [`Element`][pythonnative.Element] of type ``"TextInput"``.
410
- """
411
- return _make_element(
412
- "TextInput",
413
- style=style,
414
- ref=ref,
415
- key=key,
416
- value=value,
417
- placeholder=placeholder,
418
- on_change=on_change,
419
- on_submit=on_submit,
420
- secure=secure or None,
421
- multiline=multiline or None,
422
- keyboard_type=keyboard_type,
423
- auto_capitalize=auto_capitalize,
424
- auto_correct=auto_correct,
425
- auto_focus=auto_focus or None,
426
- return_key_type=return_key_type,
427
- max_length=max_length,
428
- placeholder_color=placeholder_color,
429
- editable=False if editable is False else None,
430
- clear_button=clear_button or None,
431
- on_focus=on_focus,
432
- on_blur=on_blur,
433
- selection_color=selection_color,
434
- text_content_type=text_content_type,
435
- accessibility_label=accessibility_label,
436
- accessibility_hint=accessibility_hint,
437
- accessible=accessible,
438
- accessibility_state=accessibility_state,
439
- accessibility_live_region=accessibility_live_region,
440
- test_id=test_id,
441
- )
442
-
443
-
444
- def Image(
445
- source: str = "",
446
- *,
447
- scale_type: Optional[ScaleType] = None,
448
- tint_color: Optional[Color] = None,
449
- placeholder_color: Optional[Color] = None,
450
- on_load: Optional[Callable[[], None]] = None,
451
- on_error: Optional[Callable[[str], None]] = None,
452
- style: StyleProp = None,
453
- accessibility_label: Optional[str] = None,
454
- accessibility_role: Optional[str] = None,
455
- accessible: Optional[bool] = None,
456
- accessibility_state: Optional[AccessibilityState] = None,
457
- accessibility_live_region: Optional[Literal["none", "polite", "assertive"]] = None,
458
- test_id: Optional[str] = None,
459
- ref: Optional[Ref] = None,
460
- key: Optional[str] = None,
461
- ) -> Element:
462
- """Display an image from a resource path or URL.
463
-
464
- Style properties: ``background_color``, ``border_*``, ``opacity``,
465
- ``transform``, plus the common layout props.
466
-
467
- Network images (``http://`` / ``https://``) go through the shared
468
- image pipeline (`pythonnative.images`): downloads happen on a
469
- background thread, bytes are cached in memory and on disk keyed by
470
- URL, concurrent requests for the same URL share one download, and
471
- large bitmaps are downsampled to the view size when decoded.
472
-
473
- Args:
474
- source: Image resource name or URL.
475
- scale_type: Fit mode: ``"cover"``, ``"contain"``, ``"stretch"``,
476
- ``"center"``.
477
- tint_color: Color overlay applied to template images
478
- (monochrome icons).
479
- placeholder_color: Background color shown while a remote image
480
- is loading (and left in place if it fails).
481
- on_load: Callback invoked once the image has been decoded and
482
- displayed.
483
- on_error: Callback invoked with an error message when a remote
484
- image fails to download or decode.
485
- style: Style dict (or list of dicts).
486
- accessibility_label: Spoken description for screen readers.
487
- accessibility_role: Override the default ``"image"`` role.
488
- accessible: Override whether the element is exposed to AT.
489
- accessibility_state: Current widget state for assistive tech,
490
- e.g. ``{"disabled": True, "selected": False}``. Recognized
491
- keys: ``disabled``, ``selected``, ``checked``, ``busy``,
492
- ``expanded``.
493
- accessibility_live_region: How AT announces dynamic changes to
494
- this view: ``"none"``, ``"polite"``, or ``"assertive"``
495
- (Android only).
496
- test_id: Stable identifier for UI tests; exposed as
497
- ``resource-id`` on Android and ``accessibilityIdentifier``
498
- on iOS.
499
- ref: Optional [`Ref`][pythonnative.Ref] from ``use_ref()``.
500
- key: Stable identity for keyed reconciliation.
501
-
502
- Returns:
503
- An [`Element`][pythonnative.Element] of type ``"Image"``.
504
- """
505
- return _make_element(
506
- "Image",
507
- style=style,
508
- ref=ref,
509
- key=key,
510
- source=source or None,
511
- scale_type=scale_type,
512
- tint_color=tint_color,
513
- placeholder_color=placeholder_color,
514
- on_load=on_load,
515
- on_error=on_error,
516
- accessibility_label=accessibility_label,
517
- accessibility_role=accessibility_role,
518
- accessible=accessible,
519
- accessibility_state=accessibility_state,
520
- accessibility_live_region=accessibility_live_region,
521
- test_id=test_id,
522
- _defaults={"accessibility_role": "image"},
523
- )
524
-
525
-
526
- def Switch(
527
- *,
528
- value: bool = False,
529
- on_change: Optional[Callable[[bool], None]] = None,
530
- accessibility_label: Optional[str] = None,
531
- style: StyleProp = None,
532
- key: Optional[str] = None,
533
- ) -> Element:
534
- """Display a toggle switch.
535
-
536
- Args:
537
- value: Current on/off state.
538
- on_change: Callback invoked with the new boolean state.
539
- accessibility_label: Label exposed to assistive technology (and
540
- UI test drivers) for the switch.
541
- style: Style dict (or list of dicts).
542
- key: Stable identity for keyed reconciliation.
543
-
544
- Returns:
545
- An [`Element`][pythonnative.Element] of type ``"Switch"``.
546
- """
547
- return _make_element(
548
- "Switch",
549
- style=style,
550
- key=key,
551
- value=value,
552
- on_change=on_change,
553
- accessibility_label=accessibility_label,
554
- )
555
-
556
-
557
- def ProgressBar(
558
- *,
559
- value: float = 0.0,
560
- color: Optional[Color] = None,
561
- track_color: Optional[Color] = None,
562
- indeterminate: bool = False,
563
- style: StyleProp = None,
564
- key: Optional[str] = None,
565
- ) -> Element:
566
- """Show determinate progress as a value between ``0.0`` and ``1.0``.
567
-
568
- For a spinner instead of a bar, use
569
- [`ActivityIndicator`][pythonnative.ActivityIndicator]; for an
570
- indeterminate *bar* pass ``indeterminate=True``.
571
-
572
- Args:
573
- value: Fraction complete (clamped to ``[0.0, 1.0]`` by the
574
- platform handler).
575
- color: Color of the filled portion of the bar.
576
- track_color: Color of the unfilled track behind the fill.
577
- indeterminate: When ``True``, the bar animates continuously and
578
- ``value`` is ignored.
579
- style: Style dict (or list of dicts).
580
- key: Stable identity for keyed reconciliation.
581
-
582
- Returns:
583
- An [`Element`][pythonnative.Element] of type ``"ProgressBar"``.
584
- """
585
- return _make_element(
586
- "ProgressBar",
587
- style=style,
588
- key=key,
589
- value=value,
590
- color=color,
591
- track_color=track_color,
592
- indeterminate=indeterminate or None,
593
- )
594
-
595
-
596
- def ActivityIndicator(
597
- *,
598
- animating: bool = True,
599
- color: Optional[Color] = None,
600
- size: Literal["small", "large"] = "small",
601
- style: StyleProp = None,
602
- key: Optional[str] = None,
603
- ) -> Element:
604
- """Show an indeterminate loading spinner.
605
-
606
- Args:
607
- animating: When ``False``, the spinner is hidden.
608
- color: Spinner color.
609
- size: ``"small"`` (default) or ``"large"``.
610
- style: Style dict (or list of dicts).
611
- key: Stable identity for keyed reconciliation.
612
-
613
- Returns:
614
- An [`Element`][pythonnative.Element] of type
615
- ``"ActivityIndicator"``.
616
- """
617
- return _make_element(
618
- "ActivityIndicator",
619
- style=style,
620
- key=key,
621
- animating=animating,
622
- color=color,
623
- size=size,
624
- )
625
-
626
-
627
- def WebView(
628
- *,
629
- url: str = "",
630
- html: Optional[str] = None,
631
- on_load: Optional[Callable[[str], None]] = None,
632
- on_message: Optional[Callable[[str], None]] = None,
633
- on_navigation_state_change: Optional[Callable[[str], None]] = None,
634
- inject_javascript: Optional[str] = None,
635
- scroll_enabled: bool = True,
636
- style: StyleProp = None,
637
- key: Optional[str] = None,
638
- ) -> Element:
639
- """Embed web content from a URL or an inline HTML string.
640
-
641
- Args:
642
- url: HTTP(S) URL to load. Ignored when ``html`` is given.
643
- html: Inline HTML markup to render instead of loading a URL.
644
- on_load: Callback invoked with the final URL once a page
645
- finishes loading.
646
- on_message: Callback invoked with the string payload whenever
647
- page JavaScript calls
648
- ``window.pythonnative.postMessage(...)``.
649
- on_navigation_state_change: Callback invoked with the URL each
650
- time the top-level document begins navigating.
651
- inject_javascript: JavaScript evaluated after each page load
652
- (useful for installing the ``postMessage`` bridge or
653
- tweaking the DOM).
654
- scroll_enabled: When ``False``, disables scrolling inside the
655
- web content.
656
- style: Style dict (or list of dicts).
657
- key: Stable identity for keyed reconciliation.
658
-
659
- Returns:
660
- An [`Element`][pythonnative.Element] of type ``"WebView"``.
661
- """
662
- return _make_element(
663
- "WebView",
664
- style=style,
665
- key=key,
666
- url=url or None,
667
- html=html,
668
- on_load=on_load,
669
- on_message=on_message,
670
- on_navigation_state_change=on_navigation_state_change,
671
- inject_javascript=inject_javascript,
672
- scroll_enabled=False if scroll_enabled is False else None,
673
- )
674
-
675
-
676
- def Spacer(
677
- *,
678
- size: Optional[float] = None,
679
- flex: Optional[float] = None,
680
- key: Optional[str] = None,
681
- ) -> Element:
682
- """Insert empty space inside a flex container.
683
-
684
- Pass ``size`` for a fixed gap, or ``flex`` to expand and absorb
685
- remaining space.
686
-
687
- Args:
688
- size: Fixed gap in dp/pt along the parent's main axis. Mirrored
689
- on both axes: whichever axis the parent's
690
- ``flex_direction`` chooses as main becomes the actual gap.
691
- flex: Flex-grow weight; useful for pushing siblings to the
692
- opposite end of a [`Row`][pythonnative.Row] or
693
- [`Column`][pythonnative.Column].
694
- key: Stable identity for keyed reconciliation.
695
-
696
- Returns:
697
- An [`Element`][pythonnative.Element] of type ``"Spacer"``.
698
- """
699
- width = size if size is not None else None
700
- height = size if size is not None else None
701
- return _make_element(
702
- "Spacer",
703
- key=key,
704
- size=size,
705
- width=width,
706
- height=height,
707
- flex=flex,
708
- )
709
-
710
-
711
- def Slider(
712
- *,
713
- value: float = 0.0,
714
- min_value: float = 0.0,
715
- max_value: float = 1.0,
716
- on_change: Optional[Callable[[float], None]] = None,
717
- accessibility_label: Optional[str] = None,
718
- style: StyleProp = None,
719
- key: Optional[str] = None,
720
- ) -> Element:
721
- """Continuous-value slider between ``min_value`` and ``max_value``.
722
-
723
- Args:
724
- value: Current slider value.
725
- min_value: Lower bound.
726
- max_value: Upper bound.
727
- on_change: Callback invoked with the new value as the user
728
- drags.
729
- accessibility_label: Label exposed to assistive technology (and
730
- UI test drivers) for the slider.
731
- style: Style dict (or list of dicts).
732
- key: Stable identity for keyed reconciliation.
733
-
734
- Returns:
735
- An [`Element`][pythonnative.Element] of type ``"Slider"``.
736
- """
737
- return _make_element(
738
- "Slider",
739
- style=style,
740
- key=key,
741
- value=value,
742
- min_value=min_value,
743
- max_value=max_value,
744
- on_change=on_change,
745
- accessibility_label=accessibility_label,
746
- )
747
-
748
-
749
- # ======================================================================
750
- # Container factories
751
- # ======================================================================
752
-
753
-
754
- def View(
755
- *children: Element,
756
- style: StyleProp = None,
757
- gestures: Optional[List[Any]] = None,
758
- hit_slop: Optional[Union[float, Dict[str, float]]] = None,
759
- on_layout: Optional[Callable[[Dict[str, float]], None]] = None,
760
- accessibility_label: Optional[str] = None,
761
- accessibility_hint: Optional[str] = None,
762
- accessibility_role: Optional[str] = None,
763
- accessible: Optional[bool] = None,
764
- accessibility_state: Optional[AccessibilityState] = None,
765
- accessibility_live_region: Optional[Literal["none", "polite", "assertive"]] = None,
766
- test_id: Optional[str] = None,
767
- ref: Optional[Ref] = None,
768
- key: Optional[str] = None,
769
- ) -> Element:
770
- """Universal flex container (like React Native's ``View``).
771
-
772
- Defaults to ``flex_direction: "column"`` (override via ``style``).
773
-
774
- Flex container properties (passed via ``style``):
775
-
776
- - ``flex_direction``: ``"column"`` (default), ``"row"``,
777
- ``"column_reverse"``, ``"row_reverse"``.
778
- - ``flex_wrap``: ``"nowrap"`` (default), ``"wrap"``,
779
- ``"wrap_reverse"``, with ``align_content`` controlling how
780
- wrapped lines share leftover cross-axis space.
781
- - ``justify_content``: main-axis distribution. Accepts
782
- ``"flex_start"`` (default), ``"center"``, ``"flex_end"``,
783
- ``"space_between"``, ``"space_around"``, ``"space_evenly"``.
784
- - ``align_items``: cross-axis alignment. Accepts ``"stretch"``
785
- (default), ``"flex_start"``, ``"center"``, ``"flex_end"``.
786
- - ``direction``: ``"ltr"`` (default) or ``"rtl"``. Flips rows and
787
- resolves ``margin_start`` / ``padding_end`` / absolute ``start``
788
- / ``end`` insets.
789
- - ``overflow``: ``"visible"`` (default) or ``"hidden"``.
790
- - ``spacing`` (alias ``gap``; per-axis ``row_gap`` /
791
- ``column_gap``), ``padding``, ``background_color``,
792
- ``border_radius``, ``border_width``, ``border_color``,
793
- ``shadow_color``, ``shadow_offset``, ``shadow_opacity``,
794
- ``shadow_radius``, ``elevation``, ``opacity``, ``transform``.
795
-
796
- Args:
797
- *children: Child elements rendered inside the container.
798
- style: Style dict (or list of dicts).
799
- gestures: Optional list of gesture descriptors from
800
- `pythonnative.gestures` (e.g. ``[gestures.Pan(on_change=…)]``)
801
- recognized natively on this view.
802
- hit_slop: Extend the touch target beyond the view's bounds
803
- without changing layout: a uniform number of points, or a
804
- dict with any of ``top`` / ``left`` / ``bottom`` /
805
- ``right``.
806
- on_layout: Callback invoked with
807
- ``{"x", "y", "width", "height"}`` after this view is laid
808
- out, and again whenever its frame changes.
809
- accessibility_label: Spoken description for screen readers.
810
- accessibility_hint: Spoken extra detail (iOS only).
811
- accessibility_role: Semantic role for assistive tech.
812
- accessible: Override whether the element is exposed to AT.
813
- accessibility_state: Current widget state for assistive tech,
814
- e.g. ``{"disabled": True, "selected": False}``. Recognized
815
- keys: ``disabled``, ``selected``, ``checked``, ``busy``,
816
- ``expanded``.
817
- accessibility_live_region: How AT announces dynamic changes to
818
- this view: ``"none"``, ``"polite"``, or ``"assertive"``
819
- (Android only).
820
- test_id: Stable identifier for UI tests; exposed as
821
- ``resource-id`` on Android and ``accessibilityIdentifier``
822
- on iOS.
823
- ref: Optional [`Ref`][pythonnative.Ref] from ``use_ref()``.
824
- key: Stable identity for keyed reconciliation.
825
-
826
- Returns:
827
- An [`Element`][pythonnative.Element] of type ``"View"``.
828
- """
829
- return _make_element(
830
- "View",
831
- *children,
832
- style=style,
833
- ref=ref,
834
- key=key,
835
- gestures=gestures,
836
- hit_slop=hit_slop,
837
- on_layout=on_layout,
838
- accessibility_label=accessibility_label,
839
- accessibility_hint=accessibility_hint,
840
- accessibility_role=accessibility_role,
841
- accessible=accessible,
842
- accessibility_state=accessibility_state,
843
- accessibility_live_region=accessibility_live_region,
844
- test_id=test_id,
845
- _defaults={"flex_direction": "column"},
846
- )
847
-
848
-
849
- def Column(
850
- *children: Element,
851
- style: StyleProp = None,
852
- on_layout: Optional[Callable[[Dict[str, float]], None]] = None,
853
- ref: Optional[Ref] = None,
854
- key: Optional[str] = None,
855
- ) -> Element:
856
- """Arrange children vertically.
857
-
858
- Convenience wrapper around [`View`][pythonnative.View] with
859
- ``flex_direction`` locked to ``"column"``. Use ``View`` directly if
860
- you need to switch between row and column at runtime.
861
-
862
- Args:
863
- *children: Child elements stacked top to bottom.
864
- style: Style dict (or list of dicts).
865
- on_layout: Callback invoked with
866
- ``{"x", "y", "width", "height"}`` after layout and on
867
- frame changes.
868
- ref: Optional [`Ref`][pythonnative.Ref] for native-view access.
869
- key: Stable identity for keyed reconciliation.
870
-
871
- Returns:
872
- An [`Element`][pythonnative.Element] of type ``"Column"``.
873
- """
874
- return _make_element(
875
- "Column",
876
- *children,
877
- style=style,
878
- on_layout=on_layout,
879
- ref=ref,
880
- key=key,
881
- _forced={"flex_direction": "column"},
882
- )
883
-
884
-
885
- def Row(
886
- *children: Element,
887
- style: StyleProp = None,
888
- on_layout: Optional[Callable[[Dict[str, float]], None]] = None,
889
- ref: Optional[Ref] = None,
890
- key: Optional[str] = None,
891
- ) -> Element:
892
- """Arrange children horizontally.
893
-
894
- Convenience wrapper around [`View`][pythonnative.View] with
895
- ``flex_direction`` locked to ``"row"``. Use ``View`` directly if you
896
- need to switch between row and column at runtime.
897
-
898
- Args:
899
- *children: Child elements arranged left to right.
900
- style: Style dict (or list of dicts).
901
- on_layout: Callback invoked with
902
- ``{"x", "y", "width", "height"}`` after layout and on
903
- frame changes.
904
- ref: Optional [`Ref`][pythonnative.Ref] for native-view access.
905
- key: Stable identity for keyed reconciliation.
906
-
907
- Returns:
908
- An [`Element`][pythonnative.Element] of type ``"Row"``.
909
- """
910
- return _make_element(
911
- "Row",
912
- *children,
913
- style=style,
914
- on_layout=on_layout,
915
- ref=ref,
916
- key=key,
917
- _forced={"flex_direction": "row"},
918
- )
919
-
920
-
921
- def ScrollView(
922
- *children: Element,
923
- refresh_control: Optional[Dict[str, Any]] = None,
924
- scroll_axis: Optional[Literal["vertical", "horizontal"]] = None,
925
- on_scroll: Optional[Callable[[Dict[str, float]], None]] = None,
926
- shows_scroll_indicator: bool = True,
927
- paging_enabled: bool = False,
928
- bounces: bool = True,
929
- content_container_style: StyleProp = None,
930
- keyboard_dismiss_mode: Optional[Literal["none", "on_drag", "interactive"]] = None,
931
- style: StyleProp = None,
932
- ref: Optional[Ref] = None,
933
- key: Optional[str] = None,
934
- ) -> Element:
935
- """Wrap children in a scrollable container.
936
-
937
- ``ScrollView`` typically takes a single child (a ``Column`` or
938
- ``Row`` aggregating the scrollable content). It accepts ``*children``
939
- for ergonomic call sites; the underlying native scroll view stacks
940
- them on its content axis.
941
-
942
- Args:
943
- *children: Child elements to scroll.
944
- refresh_control: Optional pull-to-refresh spec, typically
945
- constructed via
946
- [`RefreshControl`][pythonnative.RefreshControl]. The dict
947
- must have ``refreshing`` (bool) and ``on_refresh``
948
- (callable).
949
- scroll_axis: ``"vertical"`` (default) or ``"horizontal"``.
950
- on_scroll: Callback invoked with ``{"x": …, "y": …}`` content
951
- offsets as the user scrolls.
952
- shows_scroll_indicator: When ``False``, hides the scroll bar.
953
- paging_enabled: When ``True``, the scroll view snaps to
954
- multiples of its own size (carousel behavior).
955
- bounces: When ``False``, disables the iOS rubber-band overscroll.
956
- content_container_style: Style applied to the inner content
957
- wrapper (padding, alignment, spacing of the scrollable
958
- content), distinct from ``style`` (the scroll view frame).
959
- keyboard_dismiss_mode: ``"none"`` (default), ``"on_drag"``, or
960
- ``"interactive"``. Controls whether scrolling dismisses
961
- the keyboard.
962
- style: Style dict (or list of dicts).
963
- ref: Optional [`Ref`][pythonnative.Ref] from ``use_ref()``.
964
- key: Stable identity for keyed reconciliation.
965
-
966
- Returns:
967
- An [`Element`][pythonnative.Element] of type ``"ScrollView"``.
968
- """
969
- return _make_element(
970
- "ScrollView",
971
- *children,
972
- style=style,
973
- ref=ref,
974
- key=key,
975
- refresh_control=refresh_control,
976
- scroll_axis=scroll_axis,
977
- on_scroll=on_scroll,
978
- shows_scroll_indicator=False if shows_scroll_indicator is False else None,
979
- paging_enabled=paging_enabled or None,
980
- bounces=False if bounces is False else None,
981
- content_container_style=resolve_style(content_container_style) or None,
982
- keyboard_dismiss_mode=keyboard_dismiss_mode,
983
- )
984
-
985
-
986
- _SAFE_AREA_EDGES: Tuple[str, ...] = ("top", "left", "bottom", "right")
987
-
988
-
989
- def _numeric_edge_padding(style: Dict[str, Any], edge: str) -> float:
990
- """Return the numeric padding already declared for ``edge`` in ``style``.
991
-
992
- Only numeric values participate; percentage strings and dict
993
- shorthands are left alone (the inset simply overrides them for
994
- that edge). Resolution order matches the layout engine:
995
- ``padding_{edge}`` beats the axis shorthand, which beats
996
- ``padding``.
997
- """
998
- axis_key = "padding_vertical" if edge in ("top", "bottom") else "padding_horizontal"
999
- for key in (f"padding_{edge}", axis_key, "padding"):
1000
- value = style.get(key)
1001
- if isinstance(value, (int, float)) and not isinstance(value, bool):
1002
- return float(value)
1003
- return 0.0
1004
-
1005
-
1006
- @component
1007
- def _SafeAreaContainer(**p: Any) -> Element:
1008
- """Hook-driven body of `SafeAreaView`.
1009
-
1010
- Reads the live insets via
1011
- [`use_safe_area_insets`][pythonnative.use_safe_area_insets] (so the
1012
- subtree re-renders when the platform publishes new values, e.g. on
1013
- rotation) and adds each selected edge's inset on top of any padding
1014
- the user declared for that edge.
1015
- """
1016
- insets = use_safe_area_insets()
1017
- style: Dict[str, Any] = dict(p.get("style") or {})
1018
- edges = p.get("edges") or _SAFE_AREA_EDGES
1019
- for edge in _SAFE_AREA_EDGES:
1020
- if edge not in edges:
1021
- continue
1022
- inset = float(insets.get(edge, 0.0) or 0.0)
1023
- if inset > 0:
1024
- style[f"padding_{edge}"] = _numeric_edge_padding(style, edge) + inset
1025
- children = p.get("children") or []
1026
- return Element("SafeAreaView", style, list(children))
1027
-
1028
-
1029
- def SafeAreaView(
1030
- *children: Element,
1031
- edges: Optional[Tuple[Literal["top", "left", "bottom", "right"], ...]] = None,
1032
- style: StyleProp = None,
1033
- key: Optional[str] = None,
1034
- ) -> Element:
1035
- """Container that respects safe-area insets (notch, status bar, home indicator).
1036
-
1037
- Applies the platform-reported insets as extra padding on the
1038
- selected edges and re-renders automatically when the insets change
1039
- (rotation, split view). User padding on an inset edge is added to
1040
- the inset, matching ``react-native-safe-area-context``.
1041
-
1042
- Args:
1043
- *children: Child elements that should avoid system UI overlays.
1044
- edges: Which edges to pad; defaults to all four.
1045
- style: Style dict (or list of dicts).
1046
- key: Stable identity for keyed reconciliation.
1047
-
1048
- Returns:
1049
- An [`Element`][pythonnative.Element] that renders a
1050
- ``"SafeAreaView"`` container.
1051
- """
1052
- return _SafeAreaContainer(
1053
- children=list(children),
1054
- edges=tuple(edges) if edges else None,
1055
- style=resolve_style(style),
1056
- key=key,
1057
- )
1058
-
1059
-
1060
- def Modal(
1061
- *children: Element,
1062
- visible: bool = False,
1063
- on_dismiss: Optional[Callable[[], None]] = None,
1064
- on_show: Optional[Callable[[], None]] = None,
1065
- title: Optional[str] = None,
1066
- animation_type: Literal["slide", "fade", "none"] = "slide",
1067
- transparent: bool = False,
1068
- presentation_style: Literal["page_sheet", "form_sheet", "full_screen", "overlay"] = "page_sheet",
1069
- dismiss_on_backdrop: bool = True,
1070
- style: StyleProp = None,
1071
- key: Optional[str] = None,
1072
- ) -> Element:
1073
- """Overlay modal dialog backed by a real native presentation.
1074
-
1075
- The modal is shown when ``visible=True`` and hidden when ``False``.
1076
- Drive ``visible`` from a hook so the parent component can dismiss
1077
- the modal in response to user actions. On iOS this presents a
1078
- ``UIViewController``; on Android it shows an ``android.app.Dialog``.
1079
-
1080
- Children are mounted as the modal's content view, not into the
1081
- on-tree placeholder, so they appear above all other native content
1082
- and don't influence the underlying layout.
1083
-
1084
- Args:
1085
- *children: Modal content.
1086
- visible: Controls whether the modal is presented.
1087
- on_dismiss: Callback invoked when the user dismisses the modal
1088
- via system gesture.
1089
- on_show: Callback invoked once the modal has finished
1090
- presenting.
1091
- title: Optional title-bar text.
1092
- animation_type: ``"slide"`` (default), ``"fade"``, or ``"none"``.
1093
- transparent: When ``True``, the underlying view is dimmed
1094
- instead of fully covered.
1095
- presentation_style: iOS presentation style,
1096
- ``"page_sheet"`` (default), ``"form_sheet"``,
1097
- ``"full_screen"``, or ``"overlay"`` (custom dimmed
1098
- overlay). On Android, ``"overlay"`` keeps the dialog
1099
- non-fullscreen.
1100
- dismiss_on_backdrop: When ``True`` (default) and
1101
- ``transparent`` / ``"overlay"``, tapping the dimmed
1102
- backdrop dismisses the modal.
1103
- style: Style dict (or list of dicts).
1104
- key: Stable identity for keyed reconciliation.
1105
-
1106
- Returns:
1107
- An [`Element`][pythonnative.Element] of type ``"Modal"``.
1108
- """
1109
- return _make_element(
1110
- "Modal",
1111
- *children,
1112
- style=style,
1113
- key=key,
1114
- visible=visible,
1115
- animation_type=animation_type,
1116
- transparent=transparent,
1117
- presentation_style=presentation_style,
1118
- dismiss_on_backdrop=False if dismiss_on_backdrop is False else None,
1119
- on_dismiss=on_dismiss,
1120
- on_show=on_show,
1121
- title=title,
1122
- )
1123
-
1124
-
1125
- @component
1126
- def _StatefulPressable(**p: Any) -> Element:
1127
- """Hook-driven Pressable used when ``style`` is a callable.
1128
-
1129
- Tracks the pressed state with ``use_state`` and re-invokes the
1130
- user's style function with ``{"pressed": bool}`` on every press
1131
- transition, mirroring React Native's function-style ``style`` prop.
1132
- """
1133
- pressed, set_pressed = use_state(False)
1134
- style_fn = p["style_fn"]
1135
- user_press_in = p.get("on_press_in")
1136
- user_press_out = p.get("on_press_out")
1137
-
1138
- def _press_in() -> None:
1139
- set_pressed(True)
1140
- if user_press_in is not None:
1141
- user_press_in()
1142
-
1143
- def _press_out() -> None:
1144
- set_pressed(False)
1145
- if user_press_out is not None:
1146
- user_press_out()
1147
-
1148
- forwarded = {k: v for k, v in p.items() if k not in ("children", "style_fn", "on_press_in", "on_press_out")}
1149
- return _make_element(
1150
- "Pressable",
1151
- *(p.get("children") or []),
1152
- style=resolve_style(style_fn({"pressed": pressed})),
1153
- on_press_in=_press_in,
1154
- on_press_out=_press_out,
1155
- _defaults={"accessibility_role": "button"},
1156
- **forwarded,
1157
- )
1158
-
1159
-
1160
- def Pressable(
1161
- *children: Element,
1162
- on_press: Optional[Callable[[], None]] = None,
1163
- on_long_press: Optional[Callable[[], None]] = None,
1164
- on_press_in: Optional[Callable[[], None]] = None,
1165
- on_press_out: Optional[Callable[[], None]] = None,
1166
- pressed_opacity: float = 0.6,
1167
- gestures: Optional[List[Any]] = None,
1168
- hit_slop: Optional[Union[float, Dict[str, float]]] = None,
1169
- on_layout: Optional[Callable[[Dict[str, float]], None]] = None,
1170
- style: Union[StyleProp, Callable[[Dict[str, bool]], StyleProp]] = None,
1171
- accessibility_label: Optional[str] = None,
1172
- accessibility_hint: Optional[str] = None,
1173
- accessibility_role: Optional[str] = None,
1174
- accessible: Optional[bool] = None,
1175
- accessibility_state: Optional[AccessibilityState] = None,
1176
- accessibility_live_region: Optional[Literal["none", "polite", "assertive"]] = None,
1177
- test_id: Optional[str] = None,
1178
- ref: Optional[Ref] = None,
1179
- key: Optional[str] = None,
1180
- ) -> Element:
1181
- """Wrap children with tap / long-press / gesture handlers.
1182
-
1183
- Useful for making non-button elements (text, images, custom views)
1184
- respond to user taps. The wrapper view fades to ``pressed_opacity``
1185
- on touch-down and back to full opacity on touch-up.
1186
-
1187
- Pressable gets ``accessibility_role="button"`` by default.
1188
-
1189
- Args:
1190
- *children: Elements to make pressable.
1191
- on_press: Callback invoked on a normal tap.
1192
- on_long_press: Callback invoked on a sustained press.
1193
- on_press_in: Callback invoked the moment the press starts.
1194
- on_press_out: Callback invoked when the press lifts or cancels.
1195
- pressed_opacity: Opacity (0–1) applied while the user's finger
1196
- is down. Set to ``1.0`` for no visual feedback.
1197
- gestures: Optional list of gesture descriptors from
1198
- `pythonnative.gestures` recognized natively on this view
1199
- (pan / swipe / pinch / rotation / multi-tap).
1200
- hit_slop: Extend the touch target beyond the view's bounds
1201
- without changing layout: a uniform number of points, or a
1202
- dict with any of ``top`` / ``left`` / ``bottom`` /
1203
- ``right``. Essential for small touch targets (icons,
1204
- chips) that should honor the 44-point guideline.
1205
- on_layout: Callback invoked with
1206
- ``{"x", "y", "width", "height"}`` after layout and on
1207
- frame changes.
1208
- style: Style dict applied to the wrapper, or a callable
1209
- receiving the interaction state (``{"pressed": bool}``)
1210
- and returning a style, re-evaluated on every press
1211
- transition:
1212
-
1213
- ```python
1214
- pn.Pressable(
1215
- pn.Text("Tap"),
1216
- style=lambda s: pn.style(
1217
- background_color="#0051A8" if s["pressed"] else "#007AFF",
1218
- ),
1219
- )
1220
- ```
1221
- accessibility_label: Spoken description for screen readers.
1222
- accessibility_hint: Spoken extra detail (iOS only).
1223
- accessibility_role: Override the default ``"button"`` role.
1224
- accessible: Override whether the element is exposed to AT.
1225
- accessibility_state: Current widget state for assistive tech,
1226
- e.g. ``{"disabled": True, "selected": False}``. Recognized
1227
- keys: ``disabled``, ``selected``, ``checked``, ``busy``,
1228
- ``expanded``.
1229
- accessibility_live_region: How AT announces dynamic changes to
1230
- this view: ``"none"``, ``"polite"``, or ``"assertive"``
1231
- (Android only).
1232
- test_id: Stable identifier for UI tests; exposed as
1233
- ``resource-id`` on Android and ``accessibilityIdentifier``
1234
- on iOS.
1235
- ref: Optional [`Ref`][pythonnative.Ref] from ``use_ref()``.
1236
- key: Stable identity for keyed reconciliation.
1237
-
1238
- Returns:
1239
- An [`Element`][pythonnative.Element] of type ``"Pressable"``
1240
- (wrapped in a stateful composite when ``style`` is callable).
1241
- """
1242
- if callable(style):
1243
- return _StatefulPressable(
1244
- children=list(children),
1245
- style_fn=style,
1246
- ref=ref,
1247
- key=key,
1248
- on_press=on_press,
1249
- on_long_press=on_long_press,
1250
- on_press_in=on_press_in,
1251
- on_press_out=on_press_out,
1252
- pressed_opacity=pressed_opacity,
1253
- gestures=gestures,
1254
- hit_slop=hit_slop,
1255
- on_layout=on_layout,
1256
- accessibility_label=accessibility_label,
1257
- accessibility_hint=accessibility_hint,
1258
- accessibility_role=accessibility_role,
1259
- accessible=accessible,
1260
- accessibility_state=accessibility_state,
1261
- accessibility_live_region=accessibility_live_region,
1262
- test_id=test_id,
1263
- )
1264
- return _make_element(
1265
- "Pressable",
1266
- *children,
1267
- style=style,
1268
- ref=ref,
1269
- key=key,
1270
- on_press=on_press,
1271
- on_long_press=on_long_press,
1272
- on_press_in=on_press_in,
1273
- on_press_out=on_press_out,
1274
- pressed_opacity=pressed_opacity,
1275
- gestures=gestures,
1276
- hit_slop=hit_slop,
1277
- on_layout=on_layout,
1278
- accessibility_label=accessibility_label,
1279
- accessibility_hint=accessibility_hint,
1280
- accessibility_role=accessibility_role,
1281
- accessible=accessible,
1282
- accessibility_state=accessibility_state,
1283
- accessibility_live_region=accessibility_live_region,
1284
- test_id=test_id,
1285
- _defaults={"accessibility_role": "button"},
1286
- )
1287
-
1288
-
1289
- # ======================================================================
1290
- # Touchables & controls
1291
- # ======================================================================
1292
-
1293
-
1294
- def TouchableOpacity(
1295
- *children: Element,
1296
- on_press: Optional[Callable[[], None]] = None,
1297
- on_long_press: Optional[Callable[[], None]] = None,
1298
- active_opacity: float = 0.2,
1299
- disabled: bool = False,
1300
- style: StyleProp = None,
1301
- accessibility_label: Optional[str] = None,
1302
- accessibility_hint: Optional[str] = None,
1303
- accessibility_role: Optional[str] = None,
1304
- accessible: Optional[bool] = None,
1305
- accessibility_state: Optional[AccessibilityState] = None,
1306
- accessibility_live_region: Optional[Literal["none", "polite", "assertive"]] = None,
1307
- test_id: Optional[str] = None,
1308
- key: Optional[str] = None,
1309
- ) -> Element:
1310
- """Wrap children so they fade to ``active_opacity`` while pressed.
1311
-
1312
- A thin ergonomic alias over [`Pressable`][pythonnative.Pressable]
1313
- that mirrors React Native's ``TouchableOpacity``: the only visual
1314
- feedback is an opacity dip on touch-down. When ``disabled`` is set,
1315
- the press callbacks are dropped so the wrapper is inert.
1316
-
1317
- Args:
1318
- *children: Elements to make tappable.
1319
- on_press: Callback invoked on a normal tap.
1320
- on_long_press: Callback invoked on a sustained press.
1321
- active_opacity: Opacity (0–1) applied while the finger is down.
1322
- disabled: When ``True``, ignores presses and renders at reduced
1323
- opacity.
1324
- style: Style dict applied to the wrapper.
1325
- accessibility_label: Spoken description for screen readers.
1326
- accessibility_hint: Spoken extra detail (iOS only).
1327
- accessibility_role: Override the default ``"button"`` role.
1328
- accessible: Override whether the element is exposed to AT.
1329
- accessibility_state: Current widget state for assistive tech,
1330
- e.g. ``{"disabled": True, "selected": False}``. Recognized
1331
- keys: ``disabled``, ``selected``, ``checked``, ``busy``,
1332
- ``expanded``.
1333
- accessibility_live_region: How AT announces dynamic changes to
1334
- this view: ``"none"``, ``"polite"``, or ``"assertive"``
1335
- (Android only).
1336
- test_id: Stable identifier for UI tests; exposed as
1337
- ``resource-id`` on Android and ``accessibilityIdentifier``
1338
- on iOS.
1339
- key: Stable identity for keyed reconciliation.
1340
-
1341
- Returns:
1342
- An [`Element`][pythonnative.Element] of type ``"Pressable"``.
1343
- """
1344
- merged_style: StyleProp
1345
- if disabled:
1346
- base = resolve_style(style)
1347
- base.setdefault("opacity", 0.4)
1348
- merged_style = base
1349
- else:
1350
- merged_style = style
1351
- return Pressable(
1352
- *children,
1353
- on_press=None if disabled else on_press,
1354
- on_long_press=None if disabled else on_long_press,
1355
- pressed_opacity=active_opacity,
1356
- style=merged_style,
1357
- accessibility_label=accessibility_label,
1358
- accessibility_hint=accessibility_hint,
1359
- accessibility_role=accessibility_role,
1360
- accessible=accessible,
1361
- accessibility_state=accessibility_state,
1362
- accessibility_live_region=accessibility_live_region,
1363
- test_id=test_id,
1364
- key=key,
1365
- )
1366
-
1367
-
1368
- def ImageBackground(
1369
- *children: Element,
1370
- source: str = "",
1371
- scale_type: Optional[ScaleType] = None,
1372
- style: StyleProp = None,
1373
- accessibility_label: Optional[str] = None,
1374
- accessible: Optional[bool] = None,
1375
- accessibility_state: Optional[AccessibilityState] = None,
1376
- accessibility_live_region: Optional[Literal["none", "polite", "assertive"]] = None,
1377
- test_id: Optional[str] = None,
1378
- key: Optional[str] = None,
1379
- ) -> Element:
1380
- """Render ``children`` layered on top of a background image.
1381
-
1382
- Composed entirely from existing primitives: an absolutely-filled
1383
- [`Image`][pythonnative.Image] sits behind a content
1384
- [`View`][pythonnative.View] holding ``children``. The container's
1385
- ``style`` controls sizing/padding; the image stretches to fill it
1386
- via ``position: "absolute"`` and zeroed insets.
1387
-
1388
- Args:
1389
- *children: Foreground content drawn over the image.
1390
- source: Image resource name or URL.
1391
- scale_type: Background fit mode (``"cover"`` is the most common
1392
- for backgrounds).
1393
- style: Style dict for the container (size, padding, alignment).
1394
- accessibility_label: Spoken description of the background image.
1395
- accessible: Override whether the image is exposed to AT.
1396
- accessibility_state: Current widget state for assistive tech,
1397
- e.g. ``{"disabled": True, "selected": False}``. Recognized
1398
- keys: ``disabled``, ``selected``, ``checked``, ``busy``,
1399
- ``expanded``.
1400
- accessibility_live_region: How AT announces dynamic changes to
1401
- this view: ``"none"``, ``"polite"``, or ``"assertive"``
1402
- (Android only).
1403
- test_id: Stable identifier for UI tests; exposed as
1404
- ``resource-id`` on Android and ``accessibilityIdentifier``
1405
- on iOS.
1406
- key: Stable identity for keyed reconciliation.
1407
-
1408
- Returns:
1409
- An [`Element`][pythonnative.Element] of type ``"View"`` wrapping
1410
- the background image and foreground content.
1411
- """
1412
- fill = {"position": "absolute", "top": 0, "left": 0, "right": 0, "bottom": 0}
1413
- background = Image(
1414
- source,
1415
- scale_type=scale_type or "cover",
1416
- style=fill,
1417
- accessibility_label=accessibility_label,
1418
- accessible=accessible,
1419
- accessibility_state=accessibility_state,
1420
- accessibility_live_region=accessibility_live_region,
1421
- test_id=test_id,
1422
- )
1423
- content = View(*children, style={"flex": 1})
1424
- return View(
1425
- background,
1426
- content,
1427
- style=[{"overflow": "hidden"}, resolve_style(style)],
1428
- key=key,
1429
- )
1430
-
1431
-
1432
- def Checkbox(
1433
- *,
1434
- value: bool = False,
1435
- on_change: Optional[Callable[[bool], None]] = None,
1436
- label: Optional[str] = None,
1437
- disabled: bool = False,
1438
- color: Optional[Color] = None,
1439
- style: StyleProp = None,
1440
- accessibility_label: Optional[str] = None,
1441
- accessibility_hint: Optional[str] = None,
1442
- accessible: Optional[bool] = None,
1443
- accessibility_state: Optional[AccessibilityState] = None,
1444
- accessibility_live_region: Optional[Literal["none", "polite", "assertive"]] = None,
1445
- test_id: Optional[str] = None,
1446
- key: Optional[str] = None,
1447
- ) -> Element:
1448
- """A boolean checkbox with an optional inline label.
1449
-
1450
- Backed by ``android.widget.CheckBox`` on Android and a checkmark
1451
- ``UIButton`` on iOS. Tapping the control (or its label) toggles the
1452
- value and fires ``on_change(new_value)``.
1453
-
1454
- Args:
1455
- value: Current checked state.
1456
- on_change: Callback invoked with the new boolean state.
1457
- label: Optional text shown beside the box (also tappable).
1458
- disabled: When ``True``, the control is greyed out and inert.
1459
- color: Tint applied to the checked box.
1460
- style: Style dict (or list of dicts).
1461
- accessibility_label: Spoken description for screen readers.
1462
- accessibility_hint: Spoken extra detail (iOS only).
1463
- accessible: Override whether the element is exposed to AT.
1464
- accessibility_state: Current widget state for assistive tech,
1465
- e.g. ``{"disabled": True, "selected": False}``. Recognized
1466
- keys: ``disabled``, ``selected``, ``checked``, ``busy``,
1467
- ``expanded``.
1468
- accessibility_live_region: How AT announces dynamic changes to
1469
- this view: ``"none"``, ``"polite"``, or ``"assertive"``
1470
- (Android only).
1471
- test_id: Stable identifier for UI tests; exposed as
1472
- ``resource-id`` on Android and ``accessibilityIdentifier``
1473
- on iOS.
1474
- key: Stable identity for keyed reconciliation.
1475
-
1476
- Returns:
1477
- An [`Element`][pythonnative.Element] of type ``"Checkbox"``.
1478
- """
1479
- return _make_element(
1480
- "Checkbox",
1481
- style=style,
1482
- key=key,
1483
- value=value,
1484
- on_change=on_change,
1485
- label=label,
1486
- disabled=disabled or None,
1487
- color=color,
1488
- accessibility_label=accessibility_label,
1489
- accessibility_hint=accessibility_hint,
1490
- accessible=accessible,
1491
- accessibility_state=accessibility_state,
1492
- accessibility_live_region=accessibility_live_region,
1493
- test_id=test_id,
1494
- _defaults={"accessibility_role": "checkbox"},
1495
- )
1496
-
1497
-
1498
- def SegmentedControl(
1499
- *,
1500
- segments: Optional[List[str]] = None,
1501
- selected_index: int = 0,
1502
- on_change: Optional[Callable[[int], None]] = None,
1503
- enabled: bool = True,
1504
- tint_color: Optional[Color] = None,
1505
- style: StyleProp = None,
1506
- accessibility_label: Optional[str] = None,
1507
- accessible: Optional[bool] = None,
1508
- accessibility_state: Optional[AccessibilityState] = None,
1509
- accessibility_live_region: Optional[Literal["none", "polite", "assertive"]] = None,
1510
- test_id: Optional[str] = None,
1511
- key: Optional[str] = None,
1512
- ) -> Element:
1513
- """A horizontal multi-choice control (one selected segment at a time).
1514
-
1515
- Backed by ``UISegmentedControl`` on iOS and a styled toggle row on
1516
- Android. Selecting a segment fires ``on_change(index)``.
1517
-
1518
- Args:
1519
- segments: Ordered list of segment labels.
1520
- selected_index: Index of the currently selected segment.
1521
- on_change: Callback invoked with the newly selected index.
1522
- enabled: When ``False``, the control is disabled.
1523
- tint_color: Accent color for the selected segment.
1524
- style: Style dict (or list of dicts).
1525
- accessibility_label: Spoken description for screen readers.
1526
- accessible: Override whether the element is exposed to AT.
1527
- accessibility_state: Current widget state for assistive tech,
1528
- e.g. ``{"disabled": True, "selected": False}``. Recognized
1529
- keys: ``disabled``, ``selected``, ``checked``, ``busy``,
1530
- ``expanded``.
1531
- accessibility_live_region: How AT announces dynamic changes to
1532
- this view: ``"none"``, ``"polite"``, or ``"assertive"``
1533
- (Android only).
1534
- test_id: Stable identifier for UI tests; exposed as
1535
- ``resource-id`` on Android and ``accessibilityIdentifier``
1536
- on iOS.
1537
- key: Stable identity for keyed reconciliation.
1538
-
1539
- Returns:
1540
- An [`Element`][pythonnative.Element] of type
1541
- ``"SegmentedControl"``.
1542
- """
1543
- return _make_element(
1544
- "SegmentedControl",
1545
- style=style,
1546
- key=key,
1547
- segments=list(segments) if segments is not None else [],
1548
- selected_index=selected_index,
1549
- on_change=on_change,
1550
- enabled=False if enabled is False else None,
1551
- tint_color=tint_color,
1552
- accessibility_label=accessibility_label,
1553
- accessible=accessible,
1554
- accessibility_state=accessibility_state,
1555
- accessibility_live_region=accessibility_live_region,
1556
- test_id=test_id,
1557
- )
1558
-
1559
-
1560
- def DatePicker(
1561
- *,
1562
- value: Optional[str] = None,
1563
- mode: Literal["date", "time", "datetime"] = "date",
1564
- on_change: Optional[Callable[[str], None]] = None,
1565
- minimum: Optional[str] = None,
1566
- maximum: Optional[str] = None,
1567
- enabled: bool = True,
1568
- style: StyleProp = None,
1569
- accessibility_label: Optional[str] = None,
1570
- accessible: Optional[bool] = None,
1571
- accessibility_state: Optional[AccessibilityState] = None,
1572
- accessibility_live_region: Optional[Literal["none", "polite", "assertive"]] = None,
1573
- test_id: Optional[str] = None,
1574
- key: Optional[str] = None,
1575
- ) -> Element:
1576
- """A native date / time picker.
1577
-
1578
- Backed by ``UIDatePicker`` on iOS and a trigger button that opens
1579
- the platform ``DatePickerDialog`` / ``TimePickerDialog`` on
1580
- Android. ``value`` and the value reported to ``on_change`` are
1581
- ISO-8601 strings (``"2026-05-31"`` for ``mode="date"``, ``"14:30"``
1582
- for ``mode="time"``, ``"2026-05-31T14:30"`` for
1583
- ``mode="datetime"``), so values stay JSON-serializable and
1584
- platform-agnostic.
1585
-
1586
- Args:
1587
- value: Currently selected value as an ISO-8601 string.
1588
- mode: ``"date"`` (default), ``"time"``, or ``"datetime"``.
1589
- on_change: Callback invoked with the new ISO-8601 string.
1590
- minimum: Earliest selectable value (ISO-8601), if any.
1591
- maximum: Latest selectable value (ISO-8601), if any.
1592
- enabled: When ``False``, the picker is disabled.
1593
- style: Style dict (or list of dicts).
1594
- accessibility_label: Spoken description for screen readers.
1595
- accessible: Override whether the element is exposed to AT.
1596
- accessibility_state: Current widget state for assistive tech,
1597
- e.g. ``{"disabled": True, "selected": False}``. Recognized
1598
- keys: ``disabled``, ``selected``, ``checked``, ``busy``,
1599
- ``expanded``.
1600
- accessibility_live_region: How AT announces dynamic changes to
1601
- this view: ``"none"``, ``"polite"``, or ``"assertive"``
1602
- (Android only).
1603
- test_id: Stable identifier for UI tests; exposed as
1604
- ``resource-id`` on Android and ``accessibilityIdentifier``
1605
- on iOS.
1606
- key: Stable identity for keyed reconciliation.
1607
-
1608
- Returns:
1609
- An [`Element`][pythonnative.Element] of type ``"DatePicker"``.
1610
- """
1611
- return _make_element(
1612
- "DatePicker",
1613
- style=style,
1614
- key=key,
1615
- value=value,
1616
- mode=mode,
1617
- on_change=on_change,
1618
- minimum=minimum,
1619
- maximum=maximum,
1620
- enabled=False if enabled is False else None,
1621
- accessibility_label=accessibility_label,
1622
- accessible=accessible,
1623
- accessibility_state=accessibility_state,
1624
- accessibility_live_region=accessibility_live_region,
1625
- test_id=test_id,
1626
- _defaults={"accessibility_role": "button"},
1627
- )
1628
-
1629
-
1630
- # ======================================================================
1631
- # Fragment
1632
- # ======================================================================
1633
-
1634
-
1635
- def Fragment(*children: Optional[Element], key: Optional[str] = None) -> Element:
1636
- """Group children without adding a wrapping native view.
1637
-
1638
- Like React's ``<></>``: groups elements without introducing an
1639
- extra container. Each child mounts as a direct sibling of the
1640
- Fragment's position in the parent's child list. Components may
1641
- also simply return a plain ``list`` of elements; ``Fragment``
1642
- exists for when the group needs a ``key`` (e.g. rendering a list
1643
- of pairs) or when a single expression reads better.
1644
-
1645
- ```python
1646
- pn.Column(
1647
- pn.Text("Top"),
1648
- pn.Fragment(
1649
- pn.Text("Middle A"),
1650
- pn.Text("Middle B"),
1651
- ),
1652
- pn.Text("Bottom"),
1653
- )
1654
- ```
1655
-
1656
- Args:
1657
- *children: Child elements to expose at the parent level.
1658
- ``None`` and ``False`` children are dropped, which makes
1659
- conditional rendering with ``cond and pn.Text(...)``
1660
- ergonomic.
1661
- key: Stable identity for keyed reconciliation. A keyed
1662
- Fragment moves all of its children as one unit and
1663
- preserves their state across reorders.
1664
-
1665
- Returns:
1666
- An [`Element`][pythonnative.Element] of type ``"__Fragment__"``.
1667
- """
1668
- kept = [c for c in children if c is not None and c is not False]
1669
- return Element("__Fragment__", {}, kept, key=key)
1670
-
1671
-
1672
- def Portal(*children: Element, key: Optional[str] = None) -> Element:
1673
- """Render ``children`` into a full-screen overlay above everything else.
1674
-
1675
- Like React DOM's ``createPortal``: the children stay part of this
1676
- component's tree for state, context, and events, but their native
1677
- views mount in a transparent overlay attached to the window (above
1678
- the screen's content) instead of inside the surrounding parent.
1679
- Use it for toasts, dropdowns, tooltips, and lightweight custom
1680
- overlays that must escape ``overflow: "hidden"`` ancestors. For a
1681
- system-styled dialog with its own presentation and dismissal
1682
- gestures, use [`Modal`][pythonnative.Modal] instead.
1683
-
1684
- The overlay itself does not intercept touches; only the children
1685
- themselves are hit-testable. Children are laid out against the
1686
- full viewport, so position them with absolute insets:
1687
-
1688
- ```python
1689
- pn.Portal(
1690
- pn.View(
1691
- pn.Text("Saved!"),
1692
- style=pn.style(position="absolute", bottom=40, left=40, right=40),
1693
- ),
1694
- )
1695
- ```
1696
-
1697
- Args:
1698
- *children: Overlay content.
1699
- key: Stable identity for keyed reconciliation.
1700
-
1701
- Returns:
1702
- An [`Element`][pythonnative.Element] of type ``"Portal"``.
1703
- """
1704
- return Element("Portal", {}, list(children), key=key)
1705
-
1706
-
1707
- # ======================================================================
1708
- # Error boundary
1709
- # ======================================================================
1710
-
1711
-
1712
- def ErrorBoundary(
1713
- *children: Element,
1714
- fallback: Optional[Any] = None,
1715
- on_error: Optional[Callable[[BaseException], None]] = None,
1716
- key: Optional[str] = None,
1717
- ) -> Element:
1718
- """Catch render errors in the wrapped subtree and display ``fallback`` instead.
1719
-
1720
- When any descendant raises during render (initial mount, a parent
1721
- re-render, or a local state-driven update), the failed subtree is
1722
- torn down and ``fallback`` is mounted in its place. Without a
1723
- boundary the error propagates to the screen host (which shows the
1724
- dev error overlay in dev mode).
1725
-
1726
- ``fallback`` may be:
1727
-
1728
- - An [`Element`][pythonnative.Element], shown as-is.
1729
- - ``fallback(error) -> Element``.
1730
- - ``fallback(error, reset) -> Element``, where ``reset`` is a
1731
- zero-arg callable that clears the error and remounts the
1732
- original children (fresh state), for retry buttons.
1733
-
1734
- Args:
1735
- *children: Subtree to wrap.
1736
- fallback: Fallback content (see above). Required for the
1737
- boundary to actually catch; without it errors propagate
1738
- to the next boundary up.
1739
- on_error: Callback invoked with the exception when the
1740
- boundary catches, before the fallback mounts. Use it for
1741
- error reporting.
1742
- key: Stable identity for keyed reconciliation.
1743
-
1744
- Returns:
1745
- An [`Element`][pythonnative.Element] of type
1746
- ``"__ErrorBoundary__"``.
1747
-
1748
- Example:
1749
- ```python
1750
- import pythonnative as pn
1751
-
1752
- pn.ErrorBoundary(
1753
- MyRiskyComponent(),
1754
- fallback=lambda err, reset: pn.Column(
1755
- pn.Text(f"Error: {err}"),
1756
- pn.Button("Retry", on_press=reset),
1757
- ),
1758
- on_error=lambda err: log.exception(err),
1759
- )
1760
- ```
1761
- """
1762
- props: Dict[str, Any] = {}
1763
- if fallback is not None:
1764
- props["__fallback__"] = fallback
1765
- if on_error is not None:
1766
- props["__on_error__"] = on_error
1767
- return Element("__ErrorBoundary__", props, list(children), key=key)
1768
-
1769
-
1770
- def Suspense(
1771
- *children: Element,
1772
- fallback: Optional[Any] = None,
1773
- key: Optional[str] = None,
1774
- ) -> Element:
1775
- """Show ``fallback`` while descendants wait on async work, then swap in the content.
1776
-
1777
- A Suspense boundary catches **suspensions** from the subtree it
1778
- wraps: an ``async def`` component body blocking on a pending
1779
- await, or a regular component calling
1780
- [`Resource.read`][pythonnative.suspense.Resource.read] on data that hasn't
1781
- arrived (see [`use_resource`][pythonnative.use_resource] and
1782
- [`lazy`][pythonnative.lazy]). While anything is pending the
1783
- boundary renders ``fallback``; when the awaited work completes it
1784
- retries the content and swaps it in. Suspended components keep
1785
- their hook state across retries, so cached resources aren't
1786
- refetched.
1787
-
1788
- Two timing behaviors, matching React:
1789
-
1790
- - **Initial mount**: the fallback shows until the content is ready.
1791
- - **Updates**: a component that's already on screen and suspends
1792
- again (its dependencies changed) keeps its previous content
1793
- visible and re-renders when ready; there's no fallback flash.
1794
-
1795
- Args:
1796
- *children: Subtree to wrap (the async content).
1797
- fallback: Content shown while suspended: an
1798
- [`Element`][pythonnative.Element] or a zero-arg callable
1799
- returning one. Without it, suspensions propagate to the
1800
- next Suspense boundary up.
1801
- key: Stable identity for keyed reconciliation.
1802
-
1803
- Returns:
1804
- An [`Element`][pythonnative.Element] of type ``"__Suspense__"``.
1805
-
1806
- Example:
1807
- ```python
1808
- import pythonnative as pn
1809
-
1810
- @pn.component
1811
- async def Profile(user_id: str):
1812
- user = await api.fetch_user(user_id)
1813
- return pn.Text(user.name)
1814
-
1815
- @pn.component
1816
- def Screen():
1817
- return pn.Suspense(
1818
- Profile(user_id="42"),
1819
- fallback=pn.ActivityIndicator(),
1820
- )
1821
- ```
1822
- """
1823
- props: Dict[str, Any] = {}
1824
- if fallback is not None:
1825
- props["__fallback__"] = fallback
1826
- return Element("__Suspense__", props, list(children), key=key)
1827
-
1828
-
1829
- # ======================================================================
1830
- # Lists (native virtualization with a Python-windowed fallback)
1831
- # ======================================================================
1832
- #
1833
- # FlatList and SectionList pick between two engines:
1834
- #
1835
- # 1. **Native virtualization** (`_NativeList` -> the ``VirtualList``
1836
- # element): on Android and iOS, when every row extent is known up
1837
- # front and no windowed-only feature is requested, the list is
1838
- # backed by a real ``RecyclerView`` / ``UITableView``. The platform
1839
- # owns row recycling; each visible row hosts a nested-reconciler
1840
- # subtree (see ``pythonnative.virtual_rows``).
1841
- # 2. **Python windowing** (`_VirtualizedList`): a windowed slice of
1842
- # rows rendered into a ScrollView (leading spacer, visible rows,
1843
- # trailing spacer), the window shifting from scroll events (the
1844
- # same architecture as React Native's VirtualizedList). Because
1845
- # every windowed row lives in the *main* layout tree, rows may be
1846
- # any height: estimates steer the spacer sizes and measured extents
1847
- # correct them over time. This is the desktop path and the fallback
1848
- # for variable-height rows, grids, horizontal lists, ornaments, and
1849
- # pull-to-refresh.
1850
-
1851
- _DEFAULT_ROW_EXTENT = 44.0
1852
-
1853
-
1854
- class _RowSpec:
1855
- """One virtualized row: a stable key, a lazy renderer, and an extent hint."""
1856
-
1857
- __slots__ = ("key", "make", "extent", "item", "index")
1858
-
1859
- def __init__(
1860
- self,
1861
- key: str,
1862
- make: Callable[[], Element],
1863
- extent: Optional[float],
1864
- item: Any = None,
1865
- index: int = 0,
1866
- ) -> None:
1867
- self.key = key
1868
- self.make = make
1869
- self.extent = extent
1870
- self.item = item
1871
- self.index = index
1872
-
1873
-
1874
- def _dispatch_scroll_command(scroll_ref: Any, name: str, args: Dict[str, Any]) -> Any:
1875
- """Send an imperative command to the ScrollView under ``scroll_ref``."""
1876
- tag = getattr(scroll_ref, "_pn_tag", None)
1877
- if tag is None:
1878
- return None
1879
- from .native_views import get_registry
1880
-
1881
- try:
1882
- return get_registry().command(tag, name, args)
1883
- except Exception:
1884
- return None
1885
-
1886
-
1887
- class ListController:
1888
- """Imperative scroll handle published on a list's ``ref``.
1889
-
1890
- [`FlatList`][pythonnative.FlatList] and
1891
- [`SectionList`][pythonnative.SectionList] install a
1892
- ``ListController`` on ``ref.current`` (via
1893
- [`use_imperative_handle`][pythonnative.use_imperative_handle])
1894
- after mount and clear it back to ``None`` on unmount.
1895
-
1896
- Example:
1897
- ```python
1898
- import pythonnative as pn
1899
-
1900
- @pn.component
1901
- def Chat(messages):
1902
- list_ref = pn.use_ref()
1903
- pn.use_layout_effect(
1904
- lambda: list_ref.current and list_ref.current.scroll_to_end(animated=False),
1905
- [len(messages)],
1906
- )
1907
- return pn.FlatList(data=messages, render_item=Bubble, ref=list_ref)
1908
- ```
1909
- """
1910
-
1911
- __slots__ = ("_scroll_to_offset", "_scroll_to_index", "_scroll_to_end")
1912
-
1913
- def __init__(
1914
- self,
1915
- scroll_to_offset: Callable[[float, bool], None],
1916
- scroll_to_index: Callable[[int, bool], None],
1917
- scroll_to_end: Callable[[bool], None],
1918
- ) -> None:
1919
- self._scroll_to_offset = scroll_to_offset
1920
- self._scroll_to_index = scroll_to_index
1921
- self._scroll_to_end = scroll_to_end
1922
-
1923
- def scroll_to_offset(self, offset: float, animated: bool = True) -> None:
1924
- """Scroll to an absolute content offset in points."""
1925
- self._scroll_to_offset(offset, animated)
1926
-
1927
- def scroll_to_index(self, index: int, animated: bool = True) -> None:
1928
- """Scroll so the row at ``index`` sits at the top of the viewport."""
1929
- self._scroll_to_index(index, animated)
1930
-
1931
- def scroll_to_end(self, animated: bool = True) -> None:
1932
- """Scroll to the end of the content."""
1933
- self._scroll_to_end(animated)
1934
-
1935
-
1936
- @component
1937
- def _VirtualizedList(**p: Any) -> Element:
1938
- """Shared windowing engine behind FlatList and SectionList."""
1939
- rows: List[_RowSpec] = p.get("rows") or []
1940
- n = len(rows)
1941
- horizontal: bool = bool(p.get("horizontal"))
1942
- estimated: float = float(p.get("estimated_row_extent") or _DEFAULT_ROW_EXTENT)
1943
- overscan: float = float(p.get("overscan_extent") or 0.0)
1944
- initial_extent: float = float(p.get("initial_window_extent") or 800.0)
1945
-
1946
- window, set_window = use_state((0, -1))
1947
- measured: Ref[Dict[str, float]] = use_ref({}) # row key -> measured extent (points)
1948
- row_refs: Ref[Dict[str, Ref]] = use_ref({}) # row key -> Ref for live rows
1949
- end_latch = use_ref({"fired_for": -1})
1950
- viewable_ref: Ref[Dict[str, Tuple[str, ...]]] = use_ref({"keys": ()})
1951
- scroll_pos = use_ref({"offset": 0.0})
1952
- sv_ref: Ref = use_ref(None)
1953
-
1954
- # ------------------------------------------------------------------
1955
- # Extent model: measured > per-row hint > estimate. ``starts`` are
1956
- # prefix sums; ``starts[n]`` is the total content extent.
1957
- # ------------------------------------------------------------------
1958
- measured_map: Dict[str, float] = measured.current
1959
- starts: List[float] = [0.0] * (n + 1)
1960
- acc = 0.0
1961
- for i, spec in enumerate(rows):
1962
- starts[i] = acc
1963
- extent = measured_map.get(spec.key)
1964
- if extent is None:
1965
- extent = spec.extent if spec.extent is not None else estimated
1966
- acc += max(0.0, float(extent))
1967
- starts[n] = acc
1968
- total_extent = acc
1969
-
1970
- def _viewport_extent() -> float:
1971
- frame = sv_ref._pn_frame
1972
- if frame:
1973
- extent = frame[2] if horizontal else frame[3]
1974
- if extent and extent > 0:
1975
- return float(extent)
1976
- return initial_extent
1977
-
1978
- def _window_for(offset: float, viewport: float) -> Tuple[int, int]:
1979
- if n == 0:
1980
- return (0, -1)
1981
- pad = overscan if overscan > 0 else viewport
1982
- lo = max(0.0, offset - pad)
1983
- hi = offset + viewport + pad
1984
- first = max(0, bisect.bisect_right(starts, lo, 0, n) - 1)
1985
- last = min(n - 1, bisect.bisect_left(starts, hi, 0, n))
1986
- return (first, last)
1987
-
1988
- first, last = window
1989
- if last < 0 or first >= n:
1990
- first, last = _window_for(scroll_pos.current["offset"], _viewport_extent())
1991
- last = min(last, n - 1)
1992
- first = max(0, min(first, max(0, n - 1)))
1993
-
1994
- # ------------------------------------------------------------------
1995
- # Scroll handling: sweep measured extents, shift the window, fire
1996
- # end-reached / viewability callbacks. State only changes when the
1997
- # window actually moves, so steady scrolling inside the overscan
1998
- # region costs no re-render.
1999
- # ------------------------------------------------------------------
2000
- on_end_reached = p.get("on_end_reached")
2001
- end_threshold = float(p.get("on_end_reached_threshold") or 0.5)
2002
- on_viewable = p.get("on_viewable_items_changed")
2003
- user_on_scroll = p.get("on_scroll")
2004
-
2005
- def _sweep_measured() -> None:
2006
- for row_key, row_ref in row_refs.current.items():
2007
- frame = getattr(row_ref, "_pn_frame", None)
2008
- if frame:
2009
- extent = frame[2] if horizontal else frame[3]
2010
- if extent and extent > 0:
2011
- measured_map[row_key] = float(extent)
2012
-
2013
- def _handle_scroll(payload: Any) -> None:
2014
- if isinstance(payload, dict):
2015
- offset = float(payload.get("x" if horizontal else "y", 0.0) or 0.0)
2016
- else:
2017
- offset = float(payload or 0.0)
2018
- scroll_pos.current["offset"] = offset
2019
- _sweep_measured()
2020
- viewport = _viewport_extent()
2021
-
2022
- new_window = _window_for(offset, viewport)
2023
- if new_window != (first, last):
2024
- set_window(new_window)
2025
-
2026
- if on_end_reached is not None and total_extent > 0:
2027
- remaining = total_extent - (offset + viewport)
2028
- if remaining <= end_threshold * viewport:
2029
- if end_latch.current["fired_for"] != n:
2030
- end_latch.current["fired_for"] = n
2031
- on_end_reached()
2032
- elif remaining > end_threshold * viewport + viewport:
2033
- end_latch.current["fired_for"] = -1
2034
-
2035
- if on_viewable is not None and n > 0:
2036
- v_first = max(0, bisect.bisect_right(starts, offset, 0, n) - 1)
2037
- v_last = min(n - 1, bisect.bisect_left(starts, offset + viewport, 0, n))
2038
- keys = tuple(rows[i].key for i in range(v_first, v_last + 1))
2039
- if keys != viewable_ref.current["keys"]:
2040
- viewable_ref.current["keys"] = keys
2041
- on_viewable(
2042
- [
2043
- {"index": rows[i].index, "key": rows[i].key, "item": rows[i].item}
2044
- for i in range(v_first, v_last + 1)
2045
- ]
2046
- )
2047
-
2048
- if user_on_scroll is not None:
2049
- user_on_scroll(payload)
2050
-
2051
- # ------------------------------------------------------------------
2052
- # Imperative controller (scroll_to_index / offset / end) published
2053
- # on the user's ref. Rebuilt every render (deps=None) so the
2054
- # closures see fresh extents.
2055
- # ------------------------------------------------------------------
2056
- def _scroll_to_offset(offset: float, animated: bool = True) -> None:
2057
- axis = "x" if horizontal else "y"
2058
- _dispatch_scroll_command(sv_ref, "scroll_to_offset", {axis: float(offset), "animated": animated})
2059
-
2060
- def _scroll_to_index(index: int, animated: bool = True) -> None:
2061
- idx = max(0, min(int(index), n - 1)) if n else 0
2062
- _scroll_to_offset(starts[idx], animated)
2063
-
2064
- def _scroll_to_end(animated: bool = True) -> None:
2065
- _scroll_to_offset(max(0.0, total_extent - _viewport_extent()), animated)
2066
-
2067
- use_imperative_handle(
2068
- p.get("controller_ref"),
2069
- lambda: ListController(_scroll_to_offset, _scroll_to_index, _scroll_to_end),
2070
- None,
2071
- )
2072
-
2073
- # ------------------------------------------------------------------
2074
- # Children: header, leading spacer, windowed rows, trailing spacer,
2075
- # footer. Rows keep per-key refs so their measured extents survive
2076
- # recycling.
2077
- # ------------------------------------------------------------------
2078
- spacer_key = "width" if horizontal else "height"
2079
- children: List[Element] = []
2080
- header = p.get("header")
2081
- footer = p.get("footer")
2082
- if header is not None:
2083
- children.append(View(header, key="__pn_header__"))
2084
-
2085
- if n == 0:
2086
- empty = p.get("empty")
2087
- if empty is not None:
2088
- children.append(View(empty, key="__pn_empty__"))
2089
- else:
2090
- live_refs: Dict[str, Ref] = {}
2091
- lead = starts[first]
2092
- if lead > 0:
2093
- lead_style: Dict[str, Any] = {spacer_key: lead}
2094
- children.append(View(style=lead_style, key="__pn_lead__"))
2095
- for i in range(first, last + 1):
2096
- spec = rows[i]
2097
- row_ref = row_refs.current.get(spec.key) or Ref()
2098
- live_refs[spec.key] = row_ref
2099
- children.append(View(spec.make(), ref=row_ref, key=spec.key))
2100
- row_refs.current = live_refs
2101
- trail = total_extent - starts[last + 1]
2102
- if trail > 0:
2103
- trail_style: Dict[str, Any] = {spacer_key: trail}
2104
- children.append(View(style=trail_style, key="__pn_trail__"))
2105
-
2106
- if footer is not None:
2107
- children.append(View(footer, key="__pn_footer__"))
2108
-
2109
- wrapper = Row if horizontal else Column
2110
- inner = wrapper(*children, style=p.get("content_container_style"))
2111
- return ScrollView(
2112
- inner,
2113
- scroll_axis="horizontal" if horizontal else "vertical",
2114
- on_scroll=_handle_scroll,
2115
- refresh_control=p.get("refresh_control"),
2116
- shows_scroll_indicator=p.get("shows_scroll_indicator", True),
2117
- style=p.get("list_style"),
2118
- ref=sv_ref,
2119
- )
2120
-
2121
-
2122
- def _native_lists_supported() -> bool:
2123
- """Whether the natively virtualized list path is available.
2124
-
2125
- Android (RecyclerView) and iOS (UITableView) have native handlers;
2126
- the desktop preview and off-device tests use the Python-windowed
2127
- engine. Patchable in tests to exercise the native routing.
2128
- """
2129
- from .utils import IS_ANDROID, IS_IOS
2130
-
2131
- return IS_ANDROID or IS_IOS
2132
-
2133
-
2134
- @component
2135
- def _NativeList(**p: Any) -> Element:
2136
- """Platform-virtualized list: emits a ``VirtualList`` native element.
2137
-
2138
- The native side (RecyclerView / UITableView) owns row windowing and
2139
- recycling; each visible row hosts a nested-reconciler subtree (see
2140
- ``pythonnative.virtual_rows``). This composite adapts the FlatList /
2141
- SectionList surface onto that element: it forwards ``render_row``,
2142
- derives ``on_end_reached`` and ``on_viewable_items_changed`` from
2143
- native scroll reports, and wires the imperative scroll controller.
2144
-
2145
- Requires every row's extent to be known up front (the native
2146
- virtualizers need exact heights before rows are rendered); callers
2147
- fall back to the Python-windowed engine otherwise.
2148
- """
2149
- rows: List[_RowSpec] = p.get("rows") or []
2150
- n = len(rows)
2151
- heights: List[float] = [float(spec.extent or 0.0) for spec in rows]
2152
- uniform = len(set(heights)) <= 1
2153
-
2154
- internal_ref: Ref = use_ref(None)
2155
- end_latch = use_ref({"fired_for": -1})
2156
- viewable_ref: Ref[Dict[str, Tuple[str, ...]]] = use_ref({"keys": ()})
2157
-
2158
- starts: List[float] = [0.0] * (n + 1)
2159
- acc = 0.0
2160
- for i, extent in enumerate(heights):
2161
- starts[i] = acc
2162
- acc += max(0.0, extent)
2163
- starts[n] = acc
2164
- total_extent = acc
2165
-
2166
- def _render_row(index: int) -> Element:
2167
- if 0 <= index < n:
2168
- return rows[index].make()
2169
- return View()
2170
-
2171
- on_end_reached = p.get("on_end_reached")
2172
- end_threshold = float(p.get("on_end_reached_threshold") or 0.5)
2173
- on_viewable = p.get("on_viewable_items_changed")
2174
- user_on_scroll = p.get("on_scroll")
2175
-
2176
- def _handle_scroll(payload: Any) -> None:
2177
- offset = float(payload.get("y", 0.0) or 0.0) if isinstance(payload, dict) else float(payload or 0.0)
2178
- viewport = float(payload.get("extent", 0.0) or 0.0) if isinstance(payload, dict) else 0.0
2179
- if viewport <= 0:
2180
- viewport = 800.0
2181
-
2182
- if on_end_reached is not None and total_extent > 0:
2183
- remaining = total_extent - (offset + viewport)
2184
- if remaining <= end_threshold * viewport:
2185
- if end_latch.current["fired_for"] != n:
2186
- end_latch.current["fired_for"] = n
2187
- on_end_reached()
2188
- elif remaining > end_threshold * viewport + viewport:
2189
- end_latch.current["fired_for"] = -1
2190
-
2191
- if on_viewable is not None and n > 0:
2192
- v_first = max(0, bisect.bisect_right(starts, offset, 0, n) - 1)
2193
- v_last = min(n - 1, bisect.bisect_left(starts, offset + viewport, 0, n))
2194
- keys = tuple(rows[i].key for i in range(v_first, v_last + 1))
2195
- if keys != viewable_ref.current["keys"]:
2196
- viewable_ref.current["keys"] = keys
2197
- on_viewable(
2198
- [
2199
- {"index": rows[i].index, "key": rows[i].key, "item": rows[i].item}
2200
- for i in range(v_first, v_last + 1)
2201
- ]
2202
- )
2203
-
2204
- if user_on_scroll is not None:
2205
- user_on_scroll({"x": 0.0, "y": offset})
2206
-
2207
- def _scroll_to_offset(offset: float, animated: bool = True) -> None:
2208
- _dispatch_scroll_command(internal_ref, "scroll_to_offset", {"y": float(offset), "animated": animated})
2209
-
2210
- def _scroll_to_index(index: int, animated: bool = True) -> None:
2211
- _dispatch_scroll_command(internal_ref, "scroll_to_index", {"index": int(index), "animated": animated})
2212
-
2213
- def _scroll_to_end(animated: bool = True) -> None:
2214
- _dispatch_scroll_command(internal_ref, "scroll_to_end", {"animated": animated})
2215
-
2216
- use_imperative_handle(
2217
- p.get("controller_ref"),
2218
- lambda: ListController(_scroll_to_offset, _scroll_to_index, _scroll_to_end),
2219
- None,
2220
- )
2221
-
2222
- props: Dict[str, Any] = dict(p.get("list_style") or {})
2223
- props["count"] = n
2224
- if uniform:
2225
- props["row_height"] = heights[0] if heights else _DEFAULT_ROW_EXTENT
2226
- else:
2227
- props["row_heights"] = heights
2228
- props["render_row"] = _render_row
2229
- props["ref"] = internal_ref
2230
- wants_scroll = on_end_reached is not None or on_viewable is not None or user_on_scroll is not None
2231
- if wants_scroll:
2232
- props["on_scroll"] = _handle_scroll
2233
- if p.get("shows_scroll_indicator") is False:
2234
- props["shows_scroll_indicator"] = False
2235
- return Element("VirtualList", props, [])
2236
-
2237
-
2238
- def _all_extents_known(rows: List[_RowSpec]) -> bool:
2239
- return all(spec.extent is not None for spec in rows)
2240
-
2241
-
2242
- def FlatList(
2243
- *,
2244
- data: Optional[List[Any]] = None,
2245
- render_item: Optional[Callable[[Any, int], Element]] = None,
2246
- key_extractor: Optional[Callable[[Any, int], str]] = None,
2247
- item_height: Optional[float] = None,
2248
- get_item_height: Optional[Callable[[Any, int], float]] = None,
2249
- estimated_item_height: Optional[float] = None,
2250
- separator_height: float = 0,
2251
- refresh_control: Optional[Dict[str, Any]] = None,
2252
- horizontal: bool = False,
2253
- num_columns: int = 1,
2254
- list_header: Optional[Element] = None,
2255
- list_footer: Optional[Element] = None,
2256
- list_empty: Optional[Element] = None,
2257
- on_end_reached: Optional[Callable[[], None]] = None,
2258
- on_end_reached_threshold: float = 0.5,
2259
- on_viewable_items_changed: Optional[Callable[[List[Dict[str, Any]]], None]] = None,
2260
- on_scroll: Optional[Callable[[Dict[str, float]], None]] = None,
2261
- shows_scroll_indicator: bool = True,
2262
- content_container_style: StyleProp = None,
2263
- style: StyleProp = None,
2264
- ref: Optional[Ref] = None,
2265
- key: Optional[str] = None,
2266
- ) -> Element:
2267
- """Virtualized scrollable list that renders items from ``data`` lazily.
2268
-
2269
- Only the rows inside (and just beyond) the viewport are mounted;
2270
- leading and trailing spacers stand in for everything else, and the
2271
- window shifts as the user scrolls. Rows may have **variable
2272
- heights**: pass ``item_height`` when rows are uniform,
2273
- ``get_item_height`` for exact per-item extents, or nothing at all;
2274
- unknown rows start at ``estimated_item_height`` and are corrected
2275
- with their measured extent once they've been on screen.
2276
-
2277
- Pass a [`Ref`][pythonnative.Ref] (from
2278
- [`use_ref`][pythonnative.use_ref]) to receive a
2279
- [`ListController`][pythonnative.ListController] on ``ref.current``:
2280
- ``ref.current.scroll_to_index(i)``,
2281
- ``ref.current.scroll_to_offset(pts)``, and
2282
- ``ref.current.scroll_to_end()``.
2283
-
2284
- Args:
2285
- data: List of arbitrary item values.
2286
- render_item: ``render_item(item, index) -> Element``. Defaults
2287
- to wrapping each item in a [`Text`][pythonnative.Text].
2288
- key_extractor: Function returning a stable key per item
2289
- (recommended whenever ``data`` can reorder).
2290
- item_height: Uniform row extent in points, when known.
2291
- get_item_height: ``get_item_height(item, index) -> float`` for
2292
- exact variable extents without measurement.
2293
- estimated_item_height: Starting extent estimate for rows whose
2294
- true size isn't known yet (default 44).
2295
- separator_height: Gap below each row, in points.
2296
- refresh_control: Optional pull-to-refresh spec from
2297
- [`RefreshControl`][pythonnative.RefreshControl].
2298
- horizontal: Scroll horizontally (extents become widths).
2299
- num_columns: Render items in a grid of this many columns.
2300
- list_header: Element rendered once before all rows.
2301
- list_footer: Element rendered once after all rows.
2302
- list_empty: Element rendered when ``data`` is empty.
2303
- on_end_reached: Called when the user scrolls within
2304
- ``on_end_reached_threshold`` viewports of the end (fires
2305
- once per data length).
2306
- on_end_reached_threshold: Distance from the end, in viewport
2307
- multiples, at which ``on_end_reached`` fires.
2308
- on_viewable_items_changed: Called with a list of
2309
- ``{"index", "key", "item"}`` dicts whenever the set of
2310
- visible rows changes.
2311
- on_scroll: Called with the raw scroll payload
2312
- (``{"x": …, "y": …}``).
2313
- shows_scroll_indicator: When ``False``, hides the scroll bar.
2314
- content_container_style: Style applied to the inner content
2315
- wrapper.
2316
- style: Style for the outer scroll container.
2317
- ref: Optional [`Ref`][pythonnative.Ref]; receives a
2318
- [`ListController`][pythonnative.ListController] on
2319
- ``ref.current`` after mount.
2320
- key: Stable identity for keyed reconciliation of the list.
2321
-
2322
- Returns:
2323
- A virtualized list element (a function component instance).
2324
-
2325
- Example:
2326
- ```python
2327
- import pythonnative as pn
2328
-
2329
- items = [{"id": i, "name": f"Item {i}"} for i in range(10000)]
2330
-
2331
- pn.FlatList(
2332
- data=items,
2333
- item_height=44,
2334
- render_item=lambda item, _: pn.Text(item["name"]),
2335
- key_extractor=lambda item, _: str(item["id"]),
2336
- )
2337
- ```
2338
- """
2339
- items_list = list(data or [])
2340
- sep = float(separator_height or 0.0)
2341
-
2342
- def _row_key(item: Any, index: int) -> str:
2343
- if key_extractor is not None:
2344
- try:
2345
- return str(key_extractor(item, index))
2346
- except Exception:
2347
- pass
2348
- return f"__pn_row_{index}__"
2349
-
2350
- def _row_extent(item: Any, index: int) -> Optional[float]:
2351
- if get_item_height is not None:
2352
- try:
2353
- return float(get_item_height(item, index)) + sep
2354
- except Exception:
2355
- return None
2356
- if item_height is not None:
2357
- return float(item_height) + sep
2358
- return None
2359
-
2360
- def _make_row(item: Any, index: int) -> Callable[[], Element]:
2361
- def _make() -> Element:
2362
- el = render_item(item, index) if render_item else Text(str(item))
2363
- if sep > 0:
2364
- pad_style: Dict[str, Any] = {"padding_end" if horizontal else "padding_bottom": sep}
2365
- return View(el, style=pad_style)
2366
- return el
2367
-
2368
- return _make
2369
-
2370
- rows: List[_RowSpec] = []
2371
- if num_columns > 1 and not horizontal:
2372
- for start in range(0, len(items_list), num_columns):
2373
- chunk = items_list[start : start + num_columns]
2374
-
2375
- def _make_group(group: List[Any] = chunk, base: int = start) -> Element:
2376
- cells = [
2377
- View(
2378
- render_item(it, base + j) if render_item else Text(str(it)),
2379
- style={"flex": 1},
2380
- key=_row_key(it, base + j),
2381
- )
2382
- for j, it in enumerate(group)
2383
- ]
2384
- row = Row(*cells)
2385
- if sep > 0:
2386
- return View(row, style={"padding_bottom": sep})
2387
- return row
2388
-
2389
- group_key = "__pn_grp_" + "|".join(_row_key(it, start + j) for j, it in enumerate(chunk))
2390
- extent = (float(item_height) + sep) if item_height is not None else None
2391
- rows.append(_RowSpec(group_key, _make_group, extent, item=chunk, index=start))
2392
- else:
2393
- for i, item in enumerate(items_list):
2394
- rows.append(_RowSpec(_row_key(item, i), _make_row(item, i), _row_extent(item, i), item=item, index=i))
2395
-
2396
- estimated = estimated_item_height if estimated_item_height is not None else (item_height or _DEFAULT_ROW_EXTENT)
2397
-
2398
- # Route to the platform virtualizer (RecyclerView / UITableView)
2399
- # when it can represent this list exactly: vertical, single-column,
2400
- # every row extent known up front, and no features that only the
2401
- # Python-windowed engine implements (ornaments, pull-to-refresh).
2402
- if (
2403
- _native_lists_supported()
2404
- and not horizontal
2405
- and num_columns == 1
2406
- and list_header is None
2407
- and list_footer is None
2408
- and list_empty is None
2409
- and refresh_control is None
2410
- and _all_extents_known(rows)
2411
- ):
2412
- return _NativeList(
2413
- rows=rows,
2414
- on_end_reached=on_end_reached,
2415
- on_end_reached_threshold=on_end_reached_threshold,
2416
- on_viewable_items_changed=on_viewable_items_changed,
2417
- on_scroll=on_scroll,
2418
- shows_scroll_indicator=shows_scroll_indicator,
2419
- list_style=resolve_style(style) or None,
2420
- controller_ref=ref,
2421
- key=key,
2422
- )
2423
-
2424
- return _VirtualizedList(
2425
- rows=rows,
2426
- horizontal=horizontal,
2427
- estimated_row_extent=float(estimated) + sep,
2428
- header=list_header,
2429
- footer=list_footer,
2430
- empty=list_empty,
2431
- refresh_control=refresh_control,
2432
- on_end_reached=on_end_reached,
2433
- on_end_reached_threshold=on_end_reached_threshold,
2434
- on_viewable_items_changed=on_viewable_items_changed,
2435
- on_scroll=on_scroll,
2436
- shows_scroll_indicator=shows_scroll_indicator,
2437
- content_container_style=resolve_style(content_container_style) or None,
2438
- list_style=resolve_style(style) or None,
2439
- controller_ref=ref,
2440
- key=key,
2441
- )
2442
-
2443
-
2444
- def SectionList(
2445
- *,
2446
- sections: Optional[List[Dict[str, Any]]] = None,
2447
- render_item: Optional[Callable[[Any, int, int], Element]] = None,
2448
- render_section_header: Optional[Callable[[Dict[str, Any], int], Element]] = None,
2449
- key_extractor: Optional[Callable[[Any, int], str]] = None,
2450
- item_height: Optional[float] = None,
2451
- get_item_height: Optional[Callable[[Any, int, int], float]] = None,
2452
- estimated_item_height: Optional[float] = None,
2453
- section_header_height: Optional[float] = None,
2454
- separator_height: float = 0,
2455
- refresh_control: Optional[Dict[str, Any]] = None,
2456
- list_header: Optional[Element] = None,
2457
- list_footer: Optional[Element] = None,
2458
- list_empty: Optional[Element] = None,
2459
- on_end_reached: Optional[Callable[[], None]] = None,
2460
- on_end_reached_threshold: float = 0.5,
2461
- on_scroll: Optional[Callable[[Dict[str, float]], None]] = None,
2462
- style: StyleProp = None,
2463
- ref: Optional[Ref] = None,
2464
- key: Optional[str] = None,
2465
- ) -> Element:
2466
- """Virtualized list with section headers interleaved between row groups.
2467
-
2468
- Flattens ``sections`` into a single virtualized sequence where each
2469
- entry is either a header or an item, then reuses the same windowing
2470
- engine as [`FlatList`][pythonnative.FlatList]; headers and items
2471
- may have different (and variable) heights.
2472
-
2473
- Args:
2474
- sections: Each section is ``{"title": ..., "data": [...]}``.
2475
- render_item: ``render_item(item, item_index, section_index) ->
2476
- Element``.
2477
- render_section_header: ``render_section_header(section,
2478
- section_index) -> Element``. Defaults to a bold
2479
- [`Text`][pythonnative.Text] of the section title.
2480
- key_extractor: Stable key per item: ``key_extractor(item,
2481
- item_index) -> str``.
2482
- item_height: Uniform item extent in points, when known.
2483
- get_item_height: ``get_item_height(item, item_index,
2484
- section_index) -> float`` for exact variable extents.
2485
- estimated_item_height: Starting estimate for unmeasured rows.
2486
- section_header_height: Header extent in points, when known.
2487
- separator_height: Gap below each item, in points.
2488
- refresh_control: Optional pull-to-refresh spec.
2489
- list_header: Element rendered once before everything.
2490
- list_footer: Element rendered once after everything.
2491
- list_empty: Element rendered when there are no sections.
2492
- on_end_reached: Called near the end of the content.
2493
- on_end_reached_threshold: Distance from the end, in viewport
2494
- multiples, at which ``on_end_reached`` fires.
2495
- on_scroll: Called with the raw scroll payload.
2496
- style: Style for the outer scroll container.
2497
- ref: Optional [`Ref`][pythonnative.Ref]; receives a
2498
- [`ListController`][pythonnative.ListController] on
2499
- ``ref.current`` after mount.
2500
- key: Stable identity for keyed reconciliation of the list.
2501
-
2502
- Returns:
2503
- A virtualized list element (a function component instance).
2504
- """
2505
- sections_list = list(sections or [])
2506
- sep = float(separator_height or 0.0)
2507
-
2508
- def _header_el(section: Dict[str, Any], s_idx: int) -> Element:
2509
- if render_section_header is not None:
2510
- return render_section_header(section, s_idx)
2511
- return Text(str(section.get("title", "")), style={"bold": True, "padding": 8})
2512
-
2513
- def _item_el(item: Any, i_idx: int, s_idx: int) -> Element:
2514
- if render_item is not None:
2515
- return render_item(item, i_idx, s_idx)
2516
- return Text(str(item))
2517
-
2518
- rows: List[_RowSpec] = []
2519
- flat_index = 0
2520
- for s_idx, section in enumerate(sections_list):
2521
-
2522
- def _make_header(sec: Dict[str, Any] = section, si: int = s_idx) -> Element:
2523
- return _header_el(sec, si)
2524
-
2525
- rows.append(
2526
- _RowSpec(
2527
- f"__pn_sec_{s_idx}__",
2528
- _make_header,
2529
- float(section_header_height) if section_header_height is not None else None,
2530
- item=section,
2531
- index=flat_index,
2532
- )
2533
- )
2534
- flat_index += 1
2535
- for i_idx, item in enumerate(section.get("data", []) or []):
2536
- if key_extractor is not None:
2537
- try:
2538
- row_key = f"s{s_idx}:" + str(key_extractor(item, i_idx))
2539
- except Exception:
2540
- row_key = f"__pn_row_{s_idx}_{i_idx}__"
2541
- else:
2542
- row_key = f"__pn_row_{s_idx}_{i_idx}__"
2543
-
2544
- def _make_item(it: Any = item, ii: int = i_idx, si: int = s_idx) -> Element:
2545
- el = _item_el(it, ii, si)
2546
- if sep > 0:
2547
- return View(el, style={"padding_bottom": sep})
2548
- return el
2549
-
2550
- extent: Optional[float] = None
2551
- if get_item_height is not None:
2552
- try:
2553
- extent = float(get_item_height(item, i_idx, s_idx)) + sep
2554
- except Exception:
2555
- extent = None
2556
- elif item_height is not None:
2557
- extent = float(item_height) + sep
2558
- rows.append(_RowSpec(row_key, _make_item, extent, item=item, index=flat_index))
2559
- flat_index += 1
2560
-
2561
- estimated = estimated_item_height if estimated_item_height is not None else (item_height or _DEFAULT_ROW_EXTENT)
2562
-
2563
- # Same native routing as FlatList: headers and items become one
2564
- # flattened row sequence with per-row heights.
2565
- if (
2566
- _native_lists_supported()
2567
- and list_header is None
2568
- and list_footer is None
2569
- and list_empty is None
2570
- and refresh_control is None
2571
- and _all_extents_known(rows)
2572
- ):
2573
- return _NativeList(
2574
- rows=rows,
2575
- on_end_reached=on_end_reached,
2576
- on_end_reached_threshold=on_end_reached_threshold,
2577
- on_scroll=on_scroll,
2578
- list_style=resolve_style(style) or None,
2579
- controller_ref=ref,
2580
- key=key,
2581
- )
2582
-
2583
- return _VirtualizedList(
2584
- rows=rows,
2585
- horizontal=False,
2586
- estimated_row_extent=float(estimated) + sep,
2587
- header=list_header,
2588
- footer=list_footer,
2589
- empty=list_empty,
2590
- refresh_control=refresh_control,
2591
- on_end_reached=on_end_reached,
2592
- on_end_reached_threshold=on_end_reached_threshold,
2593
- on_scroll=on_scroll,
2594
- list_style=resolve_style(style) or None,
2595
- controller_ref=ref,
2596
- key=key,
2597
- )
2598
-
2599
-
2600
- # ======================================================================
2601
- # StatusBar / KeyboardAvoidingView / RefreshControl / Picker
2602
- # ======================================================================
2603
-
2604
-
2605
- def StatusBar(
2606
- *,
2607
- bar_style: Optional[Literal["light", "dark", "default"]] = None,
2608
- background_color: Optional[Color] = None,
2609
- hidden: Optional[bool] = None,
2610
- key: Optional[str] = None,
2611
- ) -> Element:
2612
- """Configure the device's status bar appearance.
2613
-
2614
- StatusBar is a side-effect element: it doesn't render any visible
2615
- content but applies its props to the host platform's status bar.
2616
- Mount one near the top of your tree.
2617
-
2618
- The ``bar_style`` parameter is named separately from the universal
2619
- ``style`` kwarg (which is unused here) to avoid the conflict that
2620
- ``style="light"`` would create with the visual-style dict used
2621
- elsewhere.
2622
-
2623
- Args:
2624
- bar_style: ``"light"`` (light icons over dark backgrounds),
2625
- ``"dark"`` (dark icons over light backgrounds), or
2626
- ``"default"`` (system default).
2627
- background_color: Color of the status-bar background (Android
2628
- only; iOS draws the bar transparent over your content).
2629
- hidden: When ``True``, the status bar is hidden.
2630
- key: Stable identity for keyed reconciliation.
2631
-
2632
- Returns:
2633
- An [`Element`][pythonnative.Element] of type ``"StatusBar"``.
2634
- """
2635
- props: Dict[str, Any] = {}
2636
- if bar_style is not None:
2637
- props["bar_style"] = bar_style
2638
- if background_color is not None:
2639
- props["background_color"] = background_color
2640
- if hidden is not None:
2641
- props["hidden"] = hidden
2642
- return Element("StatusBar", props, [], key=key)
2643
-
2644
-
2645
- @component
2646
- def _KeyboardAvoidingContainer(**p: Any) -> Element:
2647
- """Hook-driven body of `KeyboardAvoidingView`.
2648
-
2649
- Subscribes to the platform-reported keyboard height via
2650
- [`use_keyboard_height`][pythonnative.use_keyboard_height] and
2651
- applies the shift according to ``behavior``:
2652
-
2653
- - ``"padding"``: adds the shift as bottom padding, resizing the
2654
- content area (the default, and the right choice for forms
2655
- inside a full-height container).
2656
- - ``"position"``: translates the whole container upward without
2657
- resizing it (useful for pinned footers/toolbars).
2658
- - ``"height"``: shrinks the container's own height by the shift.
2659
- The resting height is captured via ``on_layout`` while the
2660
- keyboard is hidden, so flex-sized containers work too.
2661
- """
2662
- keyboard = use_keyboard_height()
2663
- base_height, set_base_height = use_state(0.0)
2664
- behavior = p.get("behavior") or "padding"
2665
- offset = float(p.get("keyboard_vertical_offset") or 0.0)
2666
- shift = max(0.0, keyboard - offset) if keyboard > 0 else 0.0
2667
- style: Dict[str, Any] = dict(p.get("style") or {})
2668
- props: Dict[str, Any] = style
2669
- if behavior == "height":
2670
-
2671
- def _record_layout(frame: Dict[str, float]) -> None:
2672
- if keyboard <= 0:
2673
- measured = float(frame.get("height", 0.0))
2674
- if measured > 0 and abs(measured - base_height) > 0.5:
2675
- set_base_height(measured)
2676
-
2677
- props = dict(style)
2678
- props["on_layout"] = _record_layout
2679
- if shift > 0 and base_height > 0:
2680
- props["height"] = max(0.0, base_height - shift)
2681
- elif shift > 0:
2682
- if behavior == "position":
2683
- transform = list(style.get("transform") or [])
2684
- transform.append({"translate_y": -shift})
2685
- style["transform"] = transform
2686
- else:
2687
- style["padding_bottom"] = _numeric_edge_padding(style, "bottom") + shift
2688
- children = p.get("children") or []
2689
- return Element("KeyboardAvoidingView", props, list(children))
2690
-
2691
-
2692
- def KeyboardAvoidingView(
2693
- *children: Element,
2694
- behavior: Literal["padding", "position", "height"] = "padding",
2695
- keyboard_vertical_offset: float = 0.0,
2696
- style: StyleProp = None,
2697
- key: Optional[str] = None,
2698
- ) -> Element:
2699
- """Wrap content that should shift up when the keyboard is shown.
2700
-
2701
- Subscribes to the platform-reported keyboard height (via
2702
- [`use_keyboard_height`][pythonnative.use_keyboard_height]
2703
- internally) and shifts its content so the focused text input stays
2704
- visible. On iOS the height comes from
2705
- ``UIKeyboardWillShowNotification``; on Android from the window's
2706
- IME insets.
2707
-
2708
- Args:
2709
- *children: Children rendered inside the avoiding container.
2710
- behavior: ``"padding"`` (adds bottom padding, resizing the
2711
- content), ``"position"`` (translates the container upward
2712
- without resizing), or ``"height"`` (shrinks the
2713
- container's height by the keyboard overlap, matching
2714
- React Native's ``"height"`` behavior).
2715
- keyboard_vertical_offset: Distance in layout units already
2716
- covered by other UI (e.g. a nav bar); subtracted from the
2717
- keyboard height before applying the shift.
2718
- style: Style dict (or list of dicts).
2719
- key: Stable identity for keyed reconciliation.
2720
-
2721
- Returns:
2722
- An [`Element`][pythonnative.Element] that renders a
2723
- ``"KeyboardAvoidingView"`` container.
2724
- """
2725
- return _KeyboardAvoidingContainer(
2726
- children=list(children),
2727
- behavior=behavior,
2728
- keyboard_vertical_offset=keyboard_vertical_offset,
2729
- style=resolve_style(style),
2730
- key=key,
2731
- )
2732
-
2733
-
2734
- def RefreshControl(
2735
- *,
2736
- refreshing: bool = False,
2737
- on_refresh: Optional[Callable[[], None]] = None,
2738
- tint_color: Optional[Color] = None,
2739
- ) -> Dict[str, Any]:
2740
- """Pull-to-refresh spec for [`ScrollView`][pythonnative.ScrollView] / [`FlatList`][pythonnative.FlatList].
2741
-
2742
- Returns a plain dict that should be passed as the
2743
- ``refresh_control=`` prop. Modeled as a dict (not an
2744
- [`Element`][pythonnative.Element]) so the host scroll container can
2745
- hold one without it appearing as a child node.
2746
-
2747
- Args:
2748
- refreshing: Drive the spinner's visibility from a use_state
2749
- value.
2750
- on_refresh: Callback invoked when the user pulls down past the
2751
- threshold. Set ``refreshing`` to ``True`` for the duration
2752
- of the work, then back to ``False`` on completion.
2753
- tint_color: Color of the spinner.
2754
-
2755
- Returns:
2756
- Dict suitable for the ``refresh_control`` prop on a scroll
2757
- container.
2758
-
2759
- Example:
2760
- ```python
2761
- import pythonnative as pn
2762
-
2763
- @pn.component
2764
- def MyList():
2765
- refreshing, set_refreshing = pn.use_state(False)
2766
-
2767
- def reload():
2768
- set_refreshing(True)
2769
- # ... fetch data ...
2770
- set_refreshing(False)
2771
-
2772
- return pn.ScrollView(
2773
- pn.Text("Pull me!"),
2774
- refresh_control=pn.RefreshControl(
2775
- refreshing=refreshing, on_refresh=reload
2776
- ),
2777
- )
2778
- ```
2779
- """
2780
- spec: Dict[str, Any] = {"refreshing": bool(refreshing)}
2781
- if on_refresh is not None:
2782
- spec["on_refresh"] = on_refresh
2783
- if tint_color is not None:
2784
- spec["tint_color"] = tint_color
2785
- return spec
2786
-
2787
-
2788
- def Picker(
2789
- *,
2790
- value: Any = None,
2791
- items: Optional[List[Dict[str, Any]]] = None,
2792
- on_change: Optional[Callable[[Any], None]] = None,
2793
- placeholder: str = "Select…",
2794
- style: StyleProp = None,
2795
- accessibility_label: Optional[str] = None,
2796
- accessibility_hint: Optional[str] = None,
2797
- accessible: Optional[bool] = None,
2798
- accessibility_state: Optional[AccessibilityState] = None,
2799
- accessibility_live_region: Optional[Literal["none", "polite", "assertive"]] = None,
2800
- test_id: Optional[str] = None,
2801
- ref: Optional[Ref] = None,
2802
- key: Optional[str] = None,
2803
- ) -> Element:
2804
- """A real native dropdown / select widget.
2805
-
2806
- Renders a tappable trigger labelled with the selected item; the
2807
- iOS handler attaches a ``UIMenu`` (system dropdown) and the Android
2808
- handler uses a native ``Spinner``. Selecting an item fires
2809
- ``on_change(value)``.
2810
-
2811
- ``items`` is an ordered list of ``{"value": Any, "label": str}``
2812
- entries (``label`` defaults to ``str(value)`` when omitted).
2813
-
2814
- Args:
2815
- value: Currently selected value (matched against
2816
- ``items[i]["value"]``).
2817
- items: Selectable options.
2818
- on_change: Callback invoked with the new value.
2819
- placeholder: Label shown when no item matches ``value``.
2820
- style: Style dict applied to the trigger.
2821
- accessibility_label: Spoken description for screen readers.
2822
- accessibility_hint: Spoken extra detail (iOS only).
2823
- accessible: Override whether the element is exposed to AT.
2824
- accessibility_state: Current widget state for assistive tech,
2825
- e.g. ``{"disabled": True, "selected": False}``. Recognized
2826
- keys: ``disabled``, ``selected``, ``checked``, ``busy``,
2827
- ``expanded``.
2828
- accessibility_live_region: How AT announces dynamic changes to
2829
- this view: ``"none"``, ``"polite"``, or ``"assertive"``
2830
- (Android only).
2831
- test_id: Stable identifier for UI tests; exposed as
2832
- ``resource-id`` on Android and ``accessibilityIdentifier``
2833
- on iOS.
2834
- ref: Optional [`Ref`][pythonnative.Ref] from ``use_ref()``.
2835
- key: Stable identity for keyed reconciliation.
2836
-
2837
- Returns:
2838
- An [`Element`][pythonnative.Element] of type ``"Picker"``.
2839
- """
2840
- return _make_element(
2841
- "Picker",
2842
- style=style,
2843
- ref=ref,
2844
- key=key,
2845
- value=value,
2846
- items=list(items) if items is not None else [],
2847
- on_change=on_change,
2848
- placeholder=placeholder,
2849
- accessibility_label=accessibility_label,
2850
- accessibility_hint=accessibility_hint,
2851
- accessible=accessible,
2852
- accessibility_state=accessibility_state,
2853
- accessibility_live_region=accessibility_live_region,
2854
- test_id=test_id,
2855
- _defaults={"accessibility_role": "button"},
2856
- )