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.
- pythonnative/__init__.py +27 -15
- pythonnative/animated.py +7 -13
- pythonnative/cli/pn.py +1 -2
- pythonnative/component.py +255 -0
- pythonnative/components/__init__.py +106 -0
- pythonnative/components/_base.py +132 -0
- pythonnative/components/controls.py +515 -0
- pythonnative/components/layout.py +459 -0
- pythonnative/components/lists.py +809 -0
- pythonnative/components/media.py +204 -0
- pythonnative/components/overlays.py +107 -0
- pythonnative/components/pressable.py +252 -0
- pythonnative/components/structural.py +167 -0
- pythonnative/components/text.py +291 -0
- pythonnative/diagnostics.py +1 -1
- pythonnative/element.py +108 -29
- pythonnative/gestures.py +1 -1
- pythonnative/hooks.py +366 -671
- pythonnative/hosts/__init__.py +85 -0
- pythonnative/hosts/android.py +269 -0
- pythonnative/hosts/base.py +665 -0
- pythonnative/hosts/desktop.py +107 -0
- pythonnative/hosts/ios.py +404 -0
- pythonnative/hot_reload.py +16 -24
- pythonnative/layout.py +372 -66
- pythonnative/native_modules/__init__.py +23 -0
- pythonnative/native_modules/net_info.py +5 -1
- pythonnative/native_views/__init__.py +1 -1
- pythonnative/native_views/android.py +50 -6
- pythonnative/native_views/base.py +64 -1
- pythonnative/native_views/desktop.py +11 -3
- pythonnative/native_views/ios.py +72 -12
- pythonnative/navigation/__init__.py +102 -0
- pythonnative/navigation/container.py +154 -0
- pythonnative/navigation/handle.py +573 -0
- pythonnative/navigation/hooks.py +94 -0
- pythonnative/navigation/host.py +58 -0
- pythonnative/navigation/linking.py +200 -0
- pythonnative/navigation/navigators.py +637 -0
- pythonnative/navigation/screen.py +149 -0
- pythonnative/navigation/state.py +248 -0
- pythonnative/net.py +4 -0
- pythonnative/platform_metrics.py +1 -1
- pythonnative/preview.py +21 -19
- pythonnative/project/android.py +7 -1
- pythonnative/project/doctor.py +1 -1
- pythonnative/project/ios.py +1 -0
- pythonnative/project/runtime_assets.py +1 -1
- pythonnative/reconciler/__init__.py +29 -0
- pythonnative/reconciler/boundaries.py +365 -0
- pythonnative/reconciler/children.py +88 -0
- pythonnative/reconciler/core.py +1153 -0
- pythonnative/reconciler/layout_pass.py +359 -0
- pythonnative/reconciler/vnode.py +266 -0
- pythonnative/scheduler.py +159 -0
- pythonnative/sdk/_components.py +2 -4
- pythonnative/style.py +95 -13
- pythonnative/suspense.py +8 -12
- pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/MainActivity.kt +85 -1
- pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/ScreenFragment.kt +6 -7
- pythonnative/templates/android_template/app/src/main/res/navigation/nav_graph.xml +1 -1
- pythonnative/templates/android_template/app/src/main/res/values/strings.xml +3 -1
- pythonnative/templates/ios_template/ios_template/AppDelegate.swift +33 -0
- pythonnative/templates/ios_template/ios_template/PythonRuntime.swift +1 -1
- pythonnative/templates/ios_template/ios_template/ViewController.swift +12 -10
- pythonnative/testing/__init__.py +53 -0
- pythonnative/testing/backend.py +276 -0
- pythonnative/testing/harness.py +391 -0
- {pythonnative-0.32.0.dist-info → pythonnative-0.34.0.dist-info}/METADATA +2 -21
- {pythonnative-0.32.0.dist-info → pythonnative-0.34.0.dist-info}/RECORD +74 -43
- pythonnative/components.py +0 -2856
- pythonnative/navigation.py +0 -1031
- pythonnative/reconciler.py +0 -2298
- pythonnative/screen.py +0 -2068
- {pythonnative-0.32.0.dist-info → pythonnative-0.34.0.dist-info}/WHEEL +0 -0
- {pythonnative-0.32.0.dist-info → pythonnative-0.34.0.dist-info}/entry_points.txt +0 -0
- {pythonnative-0.32.0.dist-info → pythonnative-0.34.0.dist-info}/licenses/LICENSE +0 -0
- {pythonnative-0.32.0.dist-info → pythonnative-0.34.0.dist-info}/top_level.txt +0 -0
pythonnative/components.py
DELETED
|
@@ -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
|
-
)
|