pythonnative 0.22.1__py3-none-any.whl → 0.24.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 +43 -50
- pythonnative/animated.py +1 -1
- pythonnative/appearance.py +124 -0
- pythonnative/cli/pn.py +3 -3
- pythonnative/components.py +873 -466
- pythonnative/diagnostics.py +214 -0
- pythonnative/element.py +5 -2
- pythonnative/events.py +13 -8
- pythonnative/hooks.py +454 -49
- pythonnative/hot_reload.py +9 -1
- pythonnative/images.py +196 -0
- pythonnative/mutations.py +3 -12
- pythonnative/native_views/__init__.py +1 -7
- pythonnative/native_views/android.py +651 -46
- pythonnative/native_views/desktop.py +116 -20
- pythonnative/native_views/ios.py +974 -52
- pythonnative/navigation.py +41 -17
- pythonnative/preview.py +74 -7
- pythonnative/project/config.py +1 -9
- pythonnative/reconciler.py +863 -441
- pythonnative/screen.py +409 -27
- pythonnative/storage.py +3 -3
- pythonnative/style.py +148 -6
- pythonnative/templates/android_template/app/src/main/AndroidManifest.xml +2 -1
- pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/PNAccessibilityDelegate.kt +47 -0
- pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/PNBorderDrawable.kt +67 -0
- pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/PNVirtualListView.java +172 -0
- pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/ScreenFragment.kt +22 -0
- pythonnative/virtual_rows.py +137 -0
- {pythonnative-0.22.1.dist-info → pythonnative-0.24.0.dist-info}/METADATA +5 -4
- {pythonnative-0.22.1.dist-info → pythonnative-0.24.0.dist-info}/RECORD +35 -28
- {pythonnative-0.22.1.dist-info → pythonnative-0.24.0.dist-info}/WHEEL +1 -1
- {pythonnative-0.22.1.dist-info → pythonnative-0.24.0.dist-info}/entry_points.txt +0 -0
- {pythonnative-0.22.1.dist-info → pythonnative-0.24.0.dist-info}/licenses/LICENSE +0 -0
- {pythonnative-0.22.1.dist-info → pythonnative-0.24.0.dist-info}/top_level.txt +0 -0
pythonnative/components.py
CHANGED
|
@@ -1,16 +1,12 @@
|
|
|
1
|
-
"""Built-in element factories
|
|
1
|
+
"""Built-in element factories.
|
|
2
2
|
|
|
3
|
-
Each
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
The same Props dataclasses are used by the `pythonnative.sdk` surface
|
|
12
|
-
for third-party components, so the built-in API and the extension API
|
|
13
|
-
speak the same shape.
|
|
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.
|
|
14
10
|
|
|
15
11
|
Example:
|
|
16
12
|
```python
|
|
@@ -18,20 +14,27 @@ Example:
|
|
|
18
14
|
|
|
19
15
|
pn.Column(
|
|
20
16
|
pn.Text("Hello", style=pn.style(font_size=18)),
|
|
21
|
-
pn.Button("Tap",
|
|
17
|
+
pn.Button("Tap", on_press=lambda: print("tapped")),
|
|
22
18
|
style=pn.style(spacing=12, padding=16),
|
|
23
19
|
)
|
|
24
20
|
```
|
|
25
21
|
"""
|
|
26
22
|
|
|
27
23
|
import bisect
|
|
28
|
-
from
|
|
29
|
-
from typing import Any, Callable, Dict, List, Literal, Optional, Tuple
|
|
24
|
+
from typing import Any, Callable, Dict, List, Literal, Optional, Tuple, Union
|
|
30
25
|
|
|
31
26
|
from .element import Element
|
|
32
|
-
from .hooks import
|
|
33
|
-
|
|
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
|
+
)
|
|
34
36
|
from .style import (
|
|
37
|
+
AccessibilityState,
|
|
35
38
|
AutoCapitalize,
|
|
36
39
|
Color,
|
|
37
40
|
KeyboardType,
|
|
@@ -39,6 +42,7 @@ from .style import (
|
|
|
39
42
|
ScaleType,
|
|
40
43
|
StyleProp,
|
|
41
44
|
resolve_style,
|
|
45
|
+
validate_style_keys,
|
|
42
46
|
)
|
|
43
47
|
|
|
44
48
|
# ======================================================================
|
|
@@ -50,7 +54,7 @@ def _make_element(
|
|
|
50
54
|
name: str,
|
|
51
55
|
*children: Element,
|
|
52
56
|
style: StyleProp = None,
|
|
53
|
-
ref: Optional[
|
|
57
|
+
ref: Optional[Ref] = None,
|
|
54
58
|
key: Optional[str] = None,
|
|
55
59
|
_defaults: Optional[Dict[str, Any]] = None,
|
|
56
60
|
_forced: Optional[Dict[str, Any]] = None,
|
|
@@ -78,8 +82,9 @@ def _make_element(
|
|
|
78
82
|
name: Element type name (e.g. ``"Text"``).
|
|
79
83
|
*children: Child elements.
|
|
80
84
|
style: Style dict, list of dicts, or ``None``.
|
|
81
|
-
ref: Optional ``use_ref()
|
|
82
|
-
``ref
|
|
85
|
+
ref: Optional [`Ref`][pythonnative.Ref] from ``use_ref()``; the
|
|
86
|
+
reconciler populates ``ref.current`` with the underlying
|
|
87
|
+
native view.
|
|
83
88
|
key: Stable identity for keyed reconciliation.
|
|
84
89
|
_defaults: Internal: fill-only-if-missing prop defaults.
|
|
85
90
|
_forced: Internal: prop overrides applied last.
|
|
@@ -89,6 +94,8 @@ def _make_element(
|
|
|
89
94
|
A fresh [`Element`][pythonnative.Element].
|
|
90
95
|
"""
|
|
91
96
|
out: Dict[str, Any] = dict(resolve_style(style))
|
|
97
|
+
if out:
|
|
98
|
+
validate_style_keys(out, owner=name)
|
|
92
99
|
if _defaults:
|
|
93
100
|
for k, v in _defaults.items():
|
|
94
101
|
out.setdefault(k, v)
|
|
@@ -103,325 +110,75 @@ def _make_element(
|
|
|
103
110
|
|
|
104
111
|
|
|
105
112
|
# ======================================================================
|
|
106
|
-
#
|
|
113
|
+
# Leaf factories
|
|
107
114
|
# ======================================================================
|
|
108
|
-
#
|
|
109
|
-
# These are the canonical schemas for every built-in component. They
|
|
110
|
-
# subclass the SDK's ``Props`` base, so the same shape works for both
|
|
111
|
-
# the built-in factory functions and the third-party
|
|
112
|
-
# [`element_factory`][pythonnative.element_factory] API.
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
@dataclass(frozen=True)
|
|
116
|
-
class TextProps(Props):
|
|
117
|
-
"""Props for [`Text`][pythonnative.Text]."""
|
|
118
|
-
|
|
119
|
-
text: str = ""
|
|
120
|
-
accessibility_label: Optional[str] = None
|
|
121
|
-
accessibility_hint: Optional[str] = None
|
|
122
|
-
accessibility_role: Optional[str] = None
|
|
123
|
-
accessible: Optional[bool] = None
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
@dataclass(frozen=True)
|
|
127
|
-
class ButtonProps(Props):
|
|
128
|
-
"""Props for [`Button`][pythonnative.Button]."""
|
|
129
|
-
|
|
130
|
-
title: str = ""
|
|
131
|
-
on_click: Optional[Callable[[], None]] = None
|
|
132
|
-
enabled: bool = True
|
|
133
|
-
accessibility_label: Optional[str] = None
|
|
134
|
-
accessibility_hint: Optional[str] = None
|
|
135
|
-
accessibility_role: Optional[str] = None
|
|
136
|
-
accessible: Optional[bool] = None
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
@dataclass(frozen=True)
|
|
140
|
-
class TextInputProps(Props):
|
|
141
|
-
"""Props for [`TextInput`][pythonnative.TextInput]."""
|
|
142
|
-
|
|
143
|
-
value: str = ""
|
|
144
|
-
placeholder: Optional[str] = None
|
|
145
|
-
on_change: Optional[Callable[[str], None]] = None
|
|
146
|
-
on_submit: Optional[Callable[[str], None]] = None
|
|
147
|
-
secure: bool = False
|
|
148
|
-
multiline: bool = False
|
|
149
|
-
keyboard_type: Optional[KeyboardType] = None
|
|
150
|
-
auto_capitalize: Optional[AutoCapitalize] = None
|
|
151
|
-
auto_correct: Optional[bool] = None
|
|
152
|
-
auto_focus: bool = False
|
|
153
|
-
return_key_type: Optional[ReturnKeyType] = None
|
|
154
|
-
max_length: Optional[int] = None
|
|
155
|
-
placeholder_color: Optional[Color] = None
|
|
156
|
-
editable: bool = True
|
|
157
|
-
clear_button: bool = False
|
|
158
|
-
on_focus: Optional[Callable[[], None]] = None
|
|
159
|
-
on_blur: Optional[Callable[[], None]] = None
|
|
160
|
-
selection_color: Optional[Color] = None
|
|
161
|
-
text_content_type: Optional[str] = None
|
|
162
|
-
accessibility_label: Optional[str] = None
|
|
163
|
-
accessibility_hint: Optional[str] = None
|
|
164
|
-
accessible: Optional[bool] = None
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
@dataclass(frozen=True)
|
|
168
|
-
class ImageProps(Props):
|
|
169
|
-
"""Props for [`Image`][pythonnative.Image]."""
|
|
170
|
-
|
|
171
|
-
source: Optional[str] = None
|
|
172
|
-
scale_type: Optional[ScaleType] = None
|
|
173
|
-
tint_color: Optional[Color] = None
|
|
174
|
-
accessibility_label: Optional[str] = None
|
|
175
|
-
accessibility_role: Optional[str] = None
|
|
176
|
-
accessible: Optional[bool] = None
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
@dataclass(frozen=True)
|
|
180
|
-
class SwitchProps(Props):
|
|
181
|
-
"""Props for [`Switch`][pythonnative.Switch]."""
|
|
182
|
-
|
|
183
|
-
value: bool = False
|
|
184
|
-
on_change: Optional[Callable[[bool], None]] = None
|
|
185
|
-
accessibility_label: Optional[str] = None
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
@dataclass(frozen=True)
|
|
189
|
-
class ProgressBarProps(Props):
|
|
190
|
-
"""Props for [`ProgressBar`][pythonnative.ProgressBar]."""
|
|
191
|
-
|
|
192
|
-
value: float = 0.0
|
|
193
|
-
color: Optional[Color] = None
|
|
194
|
-
track_color: Optional[Color] = None
|
|
195
|
-
indeterminate: bool = False
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
@dataclass(frozen=True)
|
|
199
|
-
class ActivityIndicatorProps(Props):
|
|
200
|
-
"""Props for [`ActivityIndicator`][pythonnative.ActivityIndicator]."""
|
|
201
|
-
|
|
202
|
-
animating: bool = True
|
|
203
|
-
color: Optional[Color] = None
|
|
204
|
-
size: Literal["small", "large"] = "small"
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
@dataclass(frozen=True)
|
|
208
|
-
class WebViewProps(Props):
|
|
209
|
-
"""Props for [`WebView`][pythonnative.WebView]."""
|
|
210
|
-
|
|
211
|
-
url: Optional[str] = None
|
|
212
|
-
html: Optional[str] = None
|
|
213
|
-
on_load: Optional[Callable[[str], None]] = None
|
|
214
|
-
on_message: Optional[Callable[[str], None]] = None
|
|
215
|
-
on_navigation_state_change: Optional[Callable[[str], None]] = None
|
|
216
|
-
inject_javascript: Optional[str] = None
|
|
217
|
-
scroll_enabled: bool = True
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
@dataclass(frozen=True)
|
|
221
|
-
class SpacerProps(Props):
|
|
222
|
-
"""Props for [`Spacer`][pythonnative.Spacer]."""
|
|
223
|
-
|
|
224
|
-
size: Optional[float] = None
|
|
225
|
-
flex: Optional[float] = None
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
@dataclass(frozen=True)
|
|
229
|
-
class SliderProps(Props):
|
|
230
|
-
"""Props for [`Slider`][pythonnative.Slider]."""
|
|
231
|
-
|
|
232
|
-
value: float = 0.0
|
|
233
|
-
min_value: float = 0.0
|
|
234
|
-
max_value: float = 1.0
|
|
235
|
-
on_change: Optional[Callable[[float], None]] = None
|
|
236
|
-
accessibility_label: Optional[str] = None
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
@dataclass(frozen=True)
|
|
240
|
-
class ViewProps(Props):
|
|
241
|
-
"""Props for [`View`][pythonnative.View], [`Column`][pythonnative.Column], and [`Row`][pythonnative.Row]."""
|
|
242
|
-
|
|
243
|
-
gestures: Optional[List[Any]] = None
|
|
244
|
-
accessibility_label: Optional[str] = None
|
|
245
|
-
accessibility_hint: Optional[str] = None
|
|
246
|
-
accessibility_role: Optional[str] = None
|
|
247
|
-
accessible: Optional[bool] = None
|
|
248
|
-
|
|
249
115
|
|
|
250
|
-
@dataclass(frozen=True)
|
|
251
|
-
class ScrollViewProps(Props):
|
|
252
|
-
"""Props for [`ScrollView`][pythonnative.ScrollView].
|
|
253
|
-
|
|
254
|
-
``on_scroll`` receives a single payload dict with ``"x"`` and
|
|
255
|
-
``"y"`` content offsets in points.
|
|
256
|
-
"""
|
|
257
|
-
|
|
258
|
-
refresh_control: Optional[Dict[str, Any]] = None
|
|
259
|
-
scroll_axis: Optional[Literal["vertical", "horizontal"]] = None
|
|
260
|
-
on_scroll: Optional[Callable[[Dict[str, float]], None]] = None
|
|
261
|
-
shows_scroll_indicator: bool = True
|
|
262
|
-
paging_enabled: bool = False
|
|
263
|
-
bounces: bool = True
|
|
264
|
-
content_container_style: StyleProp = None
|
|
265
|
-
keyboard_dismiss_mode: Optional[Literal["none", "on_drag", "interactive"]] = None
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
@dataclass(frozen=True)
|
|
269
|
-
class SafeAreaViewProps(Props):
|
|
270
|
-
"""Props for [`SafeAreaView`][pythonnative.SafeAreaView]."""
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
@dataclass(frozen=True)
|
|
274
|
-
class ModalProps(Props):
|
|
275
|
-
"""Props for [`Modal`][pythonnative.Modal]."""
|
|
276
|
-
|
|
277
|
-
visible: bool = False
|
|
278
|
-
on_dismiss: Optional[Callable[[], None]] = None
|
|
279
|
-
on_show: Optional[Callable[[], None]] = None
|
|
280
|
-
title: Optional[str] = None
|
|
281
|
-
animation_type: Literal["slide", "fade", "none"] = "slide"
|
|
282
|
-
transparent: bool = False
|
|
283
|
-
presentation_style: Literal["page_sheet", "form_sheet", "full_screen", "overlay"] = "page_sheet"
|
|
284
|
-
dismiss_on_backdrop: bool = True
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
@dataclass(frozen=True)
|
|
288
|
-
class PressableProps(Props):
|
|
289
|
-
"""Props for [`Pressable`][pythonnative.Pressable]."""
|
|
290
|
-
|
|
291
|
-
on_press: Optional[Callable[[], None]] = None
|
|
292
|
-
on_long_press: Optional[Callable[[], None]] = None
|
|
293
|
-
on_press_in: Optional[Callable[[], None]] = None
|
|
294
|
-
on_press_out: Optional[Callable[[], None]] = None
|
|
295
|
-
pressed_opacity: float = 0.6
|
|
296
|
-
gestures: Optional[List[Any]] = None
|
|
297
|
-
accessibility_label: Optional[str] = None
|
|
298
|
-
accessibility_hint: Optional[str] = None
|
|
299
|
-
accessibility_role: Optional[str] = None
|
|
300
|
-
accessible: Optional[bool] = None
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
@dataclass(frozen=True)
|
|
304
|
-
class StatusBarProps(Props):
|
|
305
|
-
"""Props for [`StatusBar`][pythonnative.StatusBar]."""
|
|
306
|
-
|
|
307
|
-
bar_style: Optional[Literal["light", "dark", "default"]] = None
|
|
308
|
-
background_color: Optional[Color] = None
|
|
309
|
-
hidden: Optional[bool] = None
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
@dataclass(frozen=True)
|
|
313
|
-
class KeyboardAvoidingViewProps(Props):
|
|
314
|
-
"""Props for [`KeyboardAvoidingView`][pythonnative.KeyboardAvoidingView]."""
|
|
315
|
-
|
|
316
|
-
behavior: Literal["padding", "position"] = "padding"
|
|
317
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
|
+
)
|
|
318
131
|
|
|
319
|
-
@dataclass(frozen=True)
|
|
320
|
-
class PickerProps(Props):
|
|
321
|
-
"""Props for [`Picker`][pythonnative.Picker].
|
|
322
132
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
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.
|
|
327
138
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
placeholder: str = "Select…"
|
|
332
|
-
accessibility_label: Optional[str] = None
|
|
333
|
-
accessibility_hint: Optional[str] = None
|
|
334
|
-
accessible: Optional[bool] = None
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
@dataclass(frozen=True)
|
|
338
|
-
class TouchableOpacityProps(Props):
|
|
339
|
-
"""Props for [`TouchableOpacity`][pythonnative.TouchableOpacity]."""
|
|
340
|
-
|
|
341
|
-
on_press: Optional[Callable[[], None]] = None
|
|
342
|
-
on_long_press: Optional[Callable[[], None]] = None
|
|
343
|
-
active_opacity: float = 0.2
|
|
344
|
-
disabled: bool = False
|
|
345
|
-
accessibility_label: Optional[str] = None
|
|
346
|
-
accessibility_hint: Optional[str] = None
|
|
347
|
-
accessibility_role: Optional[str] = None
|
|
348
|
-
accessible: Optional[bool] = None
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
@dataclass(frozen=True)
|
|
352
|
-
class ImageBackgroundProps(Props):
|
|
353
|
-
"""Props for [`ImageBackground`][pythonnative.ImageBackground]."""
|
|
354
|
-
|
|
355
|
-
source: Optional[str] = None
|
|
356
|
-
scale_type: Optional[ScaleType] = None
|
|
357
|
-
accessibility_label: Optional[str] = None
|
|
358
|
-
accessible: Optional[bool] = None
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
@dataclass(frozen=True)
|
|
362
|
-
class CheckboxProps(Props):
|
|
363
|
-
"""Props for [`Checkbox`][pythonnative.Checkbox]."""
|
|
364
|
-
|
|
365
|
-
value: bool = False
|
|
366
|
-
on_change: Optional[Callable[[bool], None]] = None
|
|
367
|
-
label: Optional[str] = None
|
|
368
|
-
disabled: bool = False
|
|
369
|
-
color: Optional[Color] = None
|
|
370
|
-
accessibility_label: Optional[str] = None
|
|
371
|
-
accessibility_hint: Optional[str] = None
|
|
372
|
-
accessible: Optional[bool] = None
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
@dataclass(frozen=True)
|
|
376
|
-
class SegmentedControlProps(Props):
|
|
377
|
-
"""Props for [`SegmentedControl`][pythonnative.SegmentedControl]."""
|
|
378
|
-
|
|
379
|
-
segments: List[str] = field(default_factory=list)
|
|
380
|
-
selected_index: int = 0
|
|
381
|
-
on_change: Optional[Callable[[int], None]] = None
|
|
382
|
-
enabled: bool = True
|
|
383
|
-
tint_color: Optional[Color] = None
|
|
384
|
-
accessibility_label: Optional[str] = None
|
|
385
|
-
accessible: Optional[bool] = None
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
@dataclass(frozen=True)
|
|
389
|
-
class DatePickerProps(Props):
|
|
390
|
-
"""Props for [`DatePicker`][pythonnative.DatePicker].
|
|
391
|
-
|
|
392
|
-
``value`` and the value passed to ``on_change`` are ISO-8601
|
|
393
|
-
strings (``"2026-05-31"`` for ``mode="date"``, ``"14:30"`` for
|
|
394
|
-
``mode="time"``, ``"2026-05-31T14:30"`` for ``mode="datetime"``),
|
|
395
|
-
so the schema stays JSON-serializable and platform-agnostic.
|
|
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.
|
|
396
142
|
"""
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
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
|
|
411
166
|
|
|
412
167
|
|
|
413
168
|
def Text(
|
|
414
|
-
|
|
415
|
-
*,
|
|
169
|
+
*parts: Any,
|
|
416
170
|
style: StyleProp = None,
|
|
417
171
|
accessibility_label: Optional[str] = None,
|
|
418
172
|
accessibility_hint: Optional[str] = None,
|
|
419
173
|
accessibility_role: Optional[str] = None,
|
|
420
174
|
accessible: Optional[bool] = None,
|
|
421
|
-
|
|
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,
|
|
422
179
|
key: Optional[str] = None,
|
|
423
180
|
) -> Element:
|
|
424
|
-
"""Display a string of text.
|
|
181
|
+
"""Display a string of text, optionally with styled nested spans.
|
|
425
182
|
|
|
426
183
|
Style properties: ``font_size``, ``color``, ``bold``,
|
|
427
184
|
``font_weight``, ``font_family``, ``italic``, ``text_align``,
|
|
@@ -431,43 +188,87 @@ def Text(
|
|
|
431
188
|
``border_color``, ``shadow_*``, ``opacity``, ``transform``, plus
|
|
432
189
|
the common layout props.
|
|
433
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
|
+
|
|
434
210
|
Args:
|
|
435
|
-
|
|
211
|
+
*parts: Text content: a single string, or any mix of strings
|
|
212
|
+
and nested ``Text`` elements for rich text.
|
|
436
213
|
style: Style dict (or list of dicts).
|
|
437
214
|
accessibility_label: Spoken description for screen readers.
|
|
438
215
|
accessibility_hint: Spoken extra detail (iOS only).
|
|
439
216
|
accessibility_role: Semantic role for assistive tech.
|
|
440
217
|
accessible: Override whether the element is exposed to AT.
|
|
441
|
-
|
|
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()``.
|
|
442
229
|
key: Stable identity for keyed reconciliation.
|
|
443
230
|
|
|
444
231
|
Returns:
|
|
445
232
|
An [`Element`][pythonnative.Element] of type ``"Text"``.
|
|
446
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 ""
|
|
447
241
|
return _make_element(
|
|
448
242
|
"Text",
|
|
449
243
|
style=style,
|
|
450
244
|
ref=ref,
|
|
451
245
|
key=key,
|
|
452
246
|
text=text,
|
|
247
|
+
spans=spans,
|
|
453
248
|
accessibility_label=accessibility_label,
|
|
454
249
|
accessibility_hint=accessibility_hint,
|
|
455
250
|
accessibility_role=accessibility_role,
|
|
456
251
|
accessible=accessible,
|
|
252
|
+
accessibility_state=accessibility_state,
|
|
253
|
+
accessibility_live_region=accessibility_live_region,
|
|
254
|
+
test_id=test_id,
|
|
457
255
|
)
|
|
458
256
|
|
|
459
257
|
|
|
460
258
|
def Button(
|
|
461
259
|
title: str = "",
|
|
462
260
|
*,
|
|
463
|
-
|
|
261
|
+
on_press: Optional[Callable[[], None]] = None,
|
|
464
262
|
enabled: bool = True,
|
|
465
263
|
style: StyleProp = None,
|
|
466
264
|
accessibility_label: Optional[str] = None,
|
|
467
265
|
accessibility_hint: Optional[str] = None,
|
|
468
266
|
accessibility_role: Optional[str] = None,
|
|
469
267
|
accessible: Optional[bool] = None,
|
|
470
|
-
|
|
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,
|
|
471
272
|
key: Optional[str] = None,
|
|
472
273
|
) -> Element:
|
|
473
274
|
"""Display a tappable button.
|
|
@@ -480,7 +281,7 @@ def Button(
|
|
|
480
281
|
|
|
481
282
|
Args:
|
|
482
283
|
title: Button label.
|
|
483
|
-
|
|
284
|
+
on_press: Callback invoked when the user taps the button.
|
|
484
285
|
enabled: When ``False``, the button is disabled and cannot be
|
|
485
286
|
tapped.
|
|
486
287
|
style: Style dict (or list of dicts).
|
|
@@ -488,7 +289,17 @@ def Button(
|
|
|
488
289
|
accessibility_hint: Spoken extra detail (iOS only).
|
|
489
290
|
accessibility_role: Override the default ``"button"`` role.
|
|
490
291
|
accessible: Override whether the element is exposed to AT.
|
|
491
|
-
|
|
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()``.
|
|
492
303
|
key: Stable identity for keyed reconciliation.
|
|
493
304
|
|
|
494
305
|
Returns:
|
|
@@ -500,12 +311,15 @@ def Button(
|
|
|
500
311
|
ref=ref,
|
|
501
312
|
key=key,
|
|
502
313
|
title=title,
|
|
503
|
-
|
|
314
|
+
on_press=on_press,
|
|
504
315
|
enabled=enabled,
|
|
505
316
|
accessibility_label=accessibility_label,
|
|
506
317
|
accessibility_hint=accessibility_hint,
|
|
507
318
|
accessibility_role=accessibility_role,
|
|
508
319
|
accessible=accessible,
|
|
320
|
+
accessibility_state=accessibility_state,
|
|
321
|
+
accessibility_live_region=accessibility_live_region,
|
|
322
|
+
test_id=test_id,
|
|
509
323
|
_defaults={"accessibility_role": "button"},
|
|
510
324
|
)
|
|
511
325
|
|
|
@@ -535,7 +349,10 @@ def TextInput(
|
|
|
535
349
|
accessibility_label: Optional[str] = None,
|
|
536
350
|
accessibility_hint: Optional[str] = None,
|
|
537
351
|
accessible: Optional[bool] = None,
|
|
538
|
-
|
|
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,
|
|
539
356
|
key: Optional[str] = None,
|
|
540
357
|
) -> Element:
|
|
541
358
|
"""Display a text-entry field (single-line by default, or ``multiline``).
|
|
@@ -575,7 +392,17 @@ def TextInput(
|
|
|
575
392
|
accessibility_label: Spoken description for screen readers.
|
|
576
393
|
accessibility_hint: Spoken extra detail (iOS only).
|
|
577
394
|
accessible: Override whether the element is exposed to AT.
|
|
578
|
-
|
|
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()``.
|
|
579
406
|
key: Stable identity for keyed reconciliation.
|
|
580
407
|
|
|
581
408
|
Returns:
|
|
@@ -608,6 +435,9 @@ def TextInput(
|
|
|
608
435
|
accessibility_label=accessibility_label,
|
|
609
436
|
accessibility_hint=accessibility_hint,
|
|
610
437
|
accessible=accessible,
|
|
438
|
+
accessibility_state=accessibility_state,
|
|
439
|
+
accessibility_live_region=accessibility_live_region,
|
|
440
|
+
test_id=test_id,
|
|
611
441
|
)
|
|
612
442
|
|
|
613
443
|
|
|
@@ -616,11 +446,17 @@ def Image(
|
|
|
616
446
|
*,
|
|
617
447
|
scale_type: Optional[ScaleType] = None,
|
|
618
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,
|
|
619
452
|
style: StyleProp = None,
|
|
620
453
|
accessibility_label: Optional[str] = None,
|
|
621
454
|
accessibility_role: Optional[str] = None,
|
|
622
455
|
accessible: Optional[bool] = None,
|
|
623
|
-
|
|
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,
|
|
624
460
|
key: Optional[str] = None,
|
|
625
461
|
) -> Element:
|
|
626
462
|
"""Display an image from a resource path or URL.
|
|
@@ -628,10 +464,11 @@ def Image(
|
|
|
628
464
|
Style properties: ``background_color``, ``border_*``, ``opacity``,
|
|
629
465
|
``transform``, plus the common layout props.
|
|
630
466
|
|
|
631
|
-
Network images (``http://`` / ``https://``)
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
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.
|
|
635
472
|
|
|
636
473
|
Args:
|
|
637
474
|
source: Image resource name or URL.
|
|
@@ -639,11 +476,27 @@ def Image(
|
|
|
639
476
|
``"center"``.
|
|
640
477
|
tint_color: Color overlay applied to template images
|
|
641
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.
|
|
642
485
|
style: Style dict (or list of dicts).
|
|
643
486
|
accessibility_label: Spoken description for screen readers.
|
|
644
487
|
accessibility_role: Override the default ``"image"`` role.
|
|
645
488
|
accessible: Override whether the element is exposed to AT.
|
|
646
|
-
|
|
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()``.
|
|
647
500
|
key: Stable identity for keyed reconciliation.
|
|
648
501
|
|
|
649
502
|
Returns:
|
|
@@ -657,9 +510,15 @@ def Image(
|
|
|
657
510
|
source=source or None,
|
|
658
511
|
scale_type=scale_type,
|
|
659
512
|
tint_color=tint_color,
|
|
513
|
+
placeholder_color=placeholder_color,
|
|
514
|
+
on_load=on_load,
|
|
515
|
+
on_error=on_error,
|
|
660
516
|
accessibility_label=accessibility_label,
|
|
661
517
|
accessibility_role=accessibility_role,
|
|
662
518
|
accessible=accessible,
|
|
519
|
+
accessibility_state=accessibility_state,
|
|
520
|
+
accessibility_live_region=accessibility_live_region,
|
|
521
|
+
test_id=test_id,
|
|
663
522
|
_defaults={"accessibility_role": "image"},
|
|
664
523
|
)
|
|
665
524
|
|
|
@@ -900,7 +759,10 @@ def View(
|
|
|
900
759
|
accessibility_hint: Optional[str] = None,
|
|
901
760
|
accessibility_role: Optional[str] = None,
|
|
902
761
|
accessible: Optional[bool] = None,
|
|
903
|
-
|
|
762
|
+
accessibility_state: Optional[AccessibilityState] = None,
|
|
763
|
+
accessibility_live_region: Optional[Literal["none", "polite", "assertive"]] = None,
|
|
764
|
+
test_id: Optional[str] = None,
|
|
765
|
+
ref: Optional[Ref] = None,
|
|
904
766
|
key: Optional[str] = None,
|
|
905
767
|
) -> Element:
|
|
906
768
|
"""Universal flex container (like React Native's ``View``).
|
|
@@ -939,7 +801,17 @@ def View(
|
|
|
939
801
|
accessibility_hint: Spoken extra detail (iOS only).
|
|
940
802
|
accessibility_role: Semantic role for assistive tech.
|
|
941
803
|
accessible: Override whether the element is exposed to AT.
|
|
942
|
-
|
|
804
|
+
accessibility_state: Current widget state for assistive tech,
|
|
805
|
+
e.g. ``{"disabled": True, "selected": False}``. Recognized
|
|
806
|
+
keys: ``disabled``, ``selected``, ``checked``, ``busy``,
|
|
807
|
+
``expanded``.
|
|
808
|
+
accessibility_live_region: How AT announces dynamic changes to
|
|
809
|
+
this view: ``"none"``, ``"polite"``, or ``"assertive"``
|
|
810
|
+
(Android only).
|
|
811
|
+
test_id: Stable identifier for UI tests; exposed as
|
|
812
|
+
``resource-id`` on Android and ``accessibilityIdentifier``
|
|
813
|
+
on iOS.
|
|
814
|
+
ref: Optional [`Ref`][pythonnative.Ref] from ``use_ref()``.
|
|
943
815
|
key: Stable identity for keyed reconciliation.
|
|
944
816
|
|
|
945
817
|
Returns:
|
|
@@ -956,6 +828,9 @@ def View(
|
|
|
956
828
|
accessibility_hint=accessibility_hint,
|
|
957
829
|
accessibility_role=accessibility_role,
|
|
958
830
|
accessible=accessible,
|
|
831
|
+
accessibility_state=accessibility_state,
|
|
832
|
+
accessibility_live_region=accessibility_live_region,
|
|
833
|
+
test_id=test_id,
|
|
959
834
|
_defaults={"flex_direction": "column"},
|
|
960
835
|
)
|
|
961
836
|
|
|
@@ -963,7 +838,7 @@ def View(
|
|
|
963
838
|
def Column(
|
|
964
839
|
*children: Element,
|
|
965
840
|
style: StyleProp = None,
|
|
966
|
-
ref: Optional[
|
|
841
|
+
ref: Optional[Ref] = None,
|
|
967
842
|
key: Optional[str] = None,
|
|
968
843
|
) -> Element:
|
|
969
844
|
"""Arrange children vertically.
|
|
@@ -975,7 +850,7 @@ def Column(
|
|
|
975
850
|
Args:
|
|
976
851
|
*children: Child elements stacked top to bottom.
|
|
977
852
|
style: Style dict (or list of dicts).
|
|
978
|
-
ref: Optional
|
|
853
|
+
ref: Optional [`Ref`][pythonnative.Ref] for native-view access.
|
|
979
854
|
key: Stable identity for keyed reconciliation.
|
|
980
855
|
|
|
981
856
|
Returns:
|
|
@@ -994,7 +869,7 @@ def Column(
|
|
|
994
869
|
def Row(
|
|
995
870
|
*children: Element,
|
|
996
871
|
style: StyleProp = None,
|
|
997
|
-
ref: Optional[
|
|
872
|
+
ref: Optional[Ref] = None,
|
|
998
873
|
key: Optional[str] = None,
|
|
999
874
|
) -> Element:
|
|
1000
875
|
"""Arrange children horizontally.
|
|
@@ -1006,7 +881,7 @@ def Row(
|
|
|
1006
881
|
Args:
|
|
1007
882
|
*children: Child elements arranged left to right.
|
|
1008
883
|
style: Style dict (or list of dicts).
|
|
1009
|
-
ref: Optional
|
|
884
|
+
ref: Optional [`Ref`][pythonnative.Ref] for native-view access.
|
|
1010
885
|
key: Stable identity for keyed reconciliation.
|
|
1011
886
|
|
|
1012
887
|
Returns:
|
|
@@ -1033,7 +908,7 @@ def ScrollView(
|
|
|
1033
908
|
content_container_style: StyleProp = None,
|
|
1034
909
|
keyboard_dismiss_mode: Optional[Literal["none", "on_drag", "interactive"]] = None,
|
|
1035
910
|
style: StyleProp = None,
|
|
1036
|
-
ref: Optional[
|
|
911
|
+
ref: Optional[Ref] = None,
|
|
1037
912
|
key: Optional[str] = None,
|
|
1038
913
|
) -> Element:
|
|
1039
914
|
"""Wrap children in a scrollable container.
|
|
@@ -1064,7 +939,7 @@ def ScrollView(
|
|
|
1064
939
|
``"interactive"``. Controls whether scrolling dismisses
|
|
1065
940
|
the keyboard.
|
|
1066
941
|
style: Style dict (or list of dicts).
|
|
1067
|
-
ref: Optional ``use_ref()
|
|
942
|
+
ref: Optional [`Ref`][pythonnative.Ref] from ``use_ref()``.
|
|
1068
943
|
key: Stable identity for keyed reconciliation.
|
|
1069
944
|
|
|
1070
945
|
Returns:
|
|
@@ -1087,25 +962,76 @@ def ScrollView(
|
|
|
1087
962
|
)
|
|
1088
963
|
|
|
1089
964
|
|
|
965
|
+
_SAFE_AREA_EDGES: Tuple[str, ...] = ("top", "left", "bottom", "right")
|
|
966
|
+
|
|
967
|
+
|
|
968
|
+
def _numeric_edge_padding(style: Dict[str, Any], edge: str) -> float:
|
|
969
|
+
"""Return the numeric padding already declared for ``edge`` in ``style``.
|
|
970
|
+
|
|
971
|
+
Only numeric values participate; percentage strings and dict
|
|
972
|
+
shorthands are left alone (the inset simply overrides them for
|
|
973
|
+
that edge). Resolution order matches the layout engine:
|
|
974
|
+
``padding_{edge}`` beats the axis shorthand, which beats
|
|
975
|
+
``padding``.
|
|
976
|
+
"""
|
|
977
|
+
axis_key = "padding_vertical" if edge in ("top", "bottom") else "padding_horizontal"
|
|
978
|
+
for key in (f"padding_{edge}", axis_key, "padding"):
|
|
979
|
+
value = style.get(key)
|
|
980
|
+
if isinstance(value, (int, float)) and not isinstance(value, bool):
|
|
981
|
+
return float(value)
|
|
982
|
+
return 0.0
|
|
983
|
+
|
|
984
|
+
|
|
985
|
+
@component
|
|
986
|
+
def _SafeAreaContainer(**p: Any) -> Element:
|
|
987
|
+
"""Hook-driven body of `SafeAreaView`.
|
|
988
|
+
|
|
989
|
+
Reads the live insets via
|
|
990
|
+
[`use_safe_area_insets`][pythonnative.use_safe_area_insets] (so the
|
|
991
|
+
subtree re-renders when the platform publishes new values, e.g. on
|
|
992
|
+
rotation) and adds each selected edge's inset on top of any padding
|
|
993
|
+
the user declared for that edge.
|
|
994
|
+
"""
|
|
995
|
+
insets = use_safe_area_insets()
|
|
996
|
+
style: Dict[str, Any] = dict(p.get("style") or {})
|
|
997
|
+
edges = p.get("edges") or _SAFE_AREA_EDGES
|
|
998
|
+
for edge in _SAFE_AREA_EDGES:
|
|
999
|
+
if edge not in edges:
|
|
1000
|
+
continue
|
|
1001
|
+
inset = float(insets.get(edge, 0.0) or 0.0)
|
|
1002
|
+
if inset > 0:
|
|
1003
|
+
style[f"padding_{edge}"] = _numeric_edge_padding(style, edge) + inset
|
|
1004
|
+
children = p.get("children") or []
|
|
1005
|
+
return Element("SafeAreaView", style, list(children))
|
|
1006
|
+
|
|
1007
|
+
|
|
1090
1008
|
def SafeAreaView(
|
|
1091
1009
|
*children: Element,
|
|
1010
|
+
edges: Optional[Tuple[Literal["top", "left", "bottom", "right"], ...]] = None,
|
|
1092
1011
|
style: StyleProp = None,
|
|
1093
1012
|
key: Optional[str] = None,
|
|
1094
1013
|
) -> Element:
|
|
1095
1014
|
"""Container that respects safe-area insets (notch, status bar, home indicator).
|
|
1096
1015
|
|
|
1016
|
+
Applies the platform-reported insets as extra padding on the
|
|
1017
|
+
selected edges and re-renders automatically when the insets change
|
|
1018
|
+
(rotation, split view). User padding on an inset edge is added to
|
|
1019
|
+
the inset, matching ``react-native-safe-area-context``.
|
|
1020
|
+
|
|
1097
1021
|
Args:
|
|
1098
1022
|
*children: Child elements that should avoid system UI overlays.
|
|
1023
|
+
edges: Which edges to pad; defaults to all four.
|
|
1099
1024
|
style: Style dict (or list of dicts).
|
|
1100
1025
|
key: Stable identity for keyed reconciliation.
|
|
1101
1026
|
|
|
1102
1027
|
Returns:
|
|
1103
|
-
An [`Element`][pythonnative.Element]
|
|
1028
|
+
An [`Element`][pythonnative.Element] that renders a
|
|
1029
|
+
``"SafeAreaView"`` container.
|
|
1104
1030
|
"""
|
|
1105
|
-
return
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
style=style,
|
|
1031
|
+
return _SafeAreaContainer(
|
|
1032
|
+
children=list(children),
|
|
1033
|
+
edges=tuple(edges) if edges else None,
|
|
1034
|
+
style=resolve_style(style),
|
|
1109
1035
|
key=key,
|
|
1110
1036
|
)
|
|
1111
1037
|
|
|
@@ -1175,6 +1101,41 @@ def Modal(
|
|
|
1175
1101
|
)
|
|
1176
1102
|
|
|
1177
1103
|
|
|
1104
|
+
@component
|
|
1105
|
+
def _StatefulPressable(**p: Any) -> Element:
|
|
1106
|
+
"""Hook-driven Pressable used when ``style`` is a callable.
|
|
1107
|
+
|
|
1108
|
+
Tracks the pressed state with ``use_state`` and re-invokes the
|
|
1109
|
+
user's style function with ``{"pressed": bool}`` on every press
|
|
1110
|
+
transition, mirroring React Native's function-style ``style`` prop.
|
|
1111
|
+
"""
|
|
1112
|
+
pressed, set_pressed = use_state(False)
|
|
1113
|
+
style_fn = p["style_fn"]
|
|
1114
|
+
user_press_in = p.get("on_press_in")
|
|
1115
|
+
user_press_out = p.get("on_press_out")
|
|
1116
|
+
|
|
1117
|
+
def _press_in() -> None:
|
|
1118
|
+
set_pressed(True)
|
|
1119
|
+
if user_press_in is not None:
|
|
1120
|
+
user_press_in()
|
|
1121
|
+
|
|
1122
|
+
def _press_out() -> None:
|
|
1123
|
+
set_pressed(False)
|
|
1124
|
+
if user_press_out is not None:
|
|
1125
|
+
user_press_out()
|
|
1126
|
+
|
|
1127
|
+
forwarded = {k: v for k, v in p.items() if k not in ("children", "style_fn", "on_press_in", "on_press_out")}
|
|
1128
|
+
return _make_element(
|
|
1129
|
+
"Pressable",
|
|
1130
|
+
*(p.get("children") or []),
|
|
1131
|
+
style=resolve_style(style_fn({"pressed": pressed})),
|
|
1132
|
+
on_press_in=_press_in,
|
|
1133
|
+
on_press_out=_press_out,
|
|
1134
|
+
_defaults={"accessibility_role": "button"},
|
|
1135
|
+
**forwarded,
|
|
1136
|
+
)
|
|
1137
|
+
|
|
1138
|
+
|
|
1178
1139
|
def Pressable(
|
|
1179
1140
|
*children: Element,
|
|
1180
1141
|
on_press: Optional[Callable[[], None]] = None,
|
|
@@ -1183,12 +1144,15 @@ def Pressable(
|
|
|
1183
1144
|
on_press_out: Optional[Callable[[], None]] = None,
|
|
1184
1145
|
pressed_opacity: float = 0.6,
|
|
1185
1146
|
gestures: Optional[List[Any]] = None,
|
|
1186
|
-
style: StyleProp = None,
|
|
1147
|
+
style: Union[StyleProp, Callable[[Dict[str, bool]], StyleProp]] = None,
|
|
1187
1148
|
accessibility_label: Optional[str] = None,
|
|
1188
1149
|
accessibility_hint: Optional[str] = None,
|
|
1189
1150
|
accessibility_role: Optional[str] = None,
|
|
1190
1151
|
accessible: Optional[bool] = None,
|
|
1191
|
-
|
|
1152
|
+
accessibility_state: Optional[AccessibilityState] = None,
|
|
1153
|
+
accessibility_live_region: Optional[Literal["none", "polite", "assertive"]] = None,
|
|
1154
|
+
test_id: Optional[str] = None,
|
|
1155
|
+
ref: Optional[Ref] = None,
|
|
1192
1156
|
key: Optional[str] = None,
|
|
1193
1157
|
) -> Element:
|
|
1194
1158
|
"""Wrap children with tap / long-press / gesture handlers.
|
|
@@ -1210,17 +1174,60 @@ def Pressable(
|
|
|
1210
1174
|
gestures: Optional list of gesture descriptors from
|
|
1211
1175
|
`pythonnative.gestures` recognized natively on this view
|
|
1212
1176
|
(pan / swipe / pinch / rotation / multi-tap).
|
|
1213
|
-
style: Style dict applied to the wrapper
|
|
1177
|
+
style: Style dict applied to the wrapper, or a callable
|
|
1178
|
+
receiving the interaction state (``{"pressed": bool}``)
|
|
1179
|
+
and returning a style, re-evaluated on every press
|
|
1180
|
+
transition:
|
|
1181
|
+
|
|
1182
|
+
```python
|
|
1183
|
+
pn.Pressable(
|
|
1184
|
+
pn.Text("Tap"),
|
|
1185
|
+
style=lambda s: pn.style(
|
|
1186
|
+
background_color="#0051A8" if s["pressed"] else "#007AFF",
|
|
1187
|
+
),
|
|
1188
|
+
)
|
|
1189
|
+
```
|
|
1214
1190
|
accessibility_label: Spoken description for screen readers.
|
|
1215
1191
|
accessibility_hint: Spoken extra detail (iOS only).
|
|
1216
1192
|
accessibility_role: Override the default ``"button"`` role.
|
|
1217
1193
|
accessible: Override whether the element is exposed to AT.
|
|
1218
|
-
|
|
1194
|
+
accessibility_state: Current widget state for assistive tech,
|
|
1195
|
+
e.g. ``{"disabled": True, "selected": False}``. Recognized
|
|
1196
|
+
keys: ``disabled``, ``selected``, ``checked``, ``busy``,
|
|
1197
|
+
``expanded``.
|
|
1198
|
+
accessibility_live_region: How AT announces dynamic changes to
|
|
1199
|
+
this view: ``"none"``, ``"polite"``, or ``"assertive"``
|
|
1200
|
+
(Android only).
|
|
1201
|
+
test_id: Stable identifier for UI tests; exposed as
|
|
1202
|
+
``resource-id`` on Android and ``accessibilityIdentifier``
|
|
1203
|
+
on iOS.
|
|
1204
|
+
ref: Optional [`Ref`][pythonnative.Ref] from ``use_ref()``.
|
|
1219
1205
|
key: Stable identity for keyed reconciliation.
|
|
1220
1206
|
|
|
1221
1207
|
Returns:
|
|
1222
|
-
An [`Element`][pythonnative.Element] of type ``"Pressable"
|
|
1208
|
+
An [`Element`][pythonnative.Element] of type ``"Pressable"``
|
|
1209
|
+
(wrapped in a stateful composite when ``style`` is callable).
|
|
1223
1210
|
"""
|
|
1211
|
+
if callable(style):
|
|
1212
|
+
return _StatefulPressable(
|
|
1213
|
+
children=list(children),
|
|
1214
|
+
style_fn=style,
|
|
1215
|
+
ref=ref,
|
|
1216
|
+
key=key,
|
|
1217
|
+
on_press=on_press,
|
|
1218
|
+
on_long_press=on_long_press,
|
|
1219
|
+
on_press_in=on_press_in,
|
|
1220
|
+
on_press_out=on_press_out,
|
|
1221
|
+
pressed_opacity=pressed_opacity,
|
|
1222
|
+
gestures=gestures,
|
|
1223
|
+
accessibility_label=accessibility_label,
|
|
1224
|
+
accessibility_hint=accessibility_hint,
|
|
1225
|
+
accessibility_role=accessibility_role,
|
|
1226
|
+
accessible=accessible,
|
|
1227
|
+
accessibility_state=accessibility_state,
|
|
1228
|
+
accessibility_live_region=accessibility_live_region,
|
|
1229
|
+
test_id=test_id,
|
|
1230
|
+
)
|
|
1224
1231
|
return _make_element(
|
|
1225
1232
|
"Pressable",
|
|
1226
1233
|
*children,
|
|
@@ -1237,6 +1244,9 @@ def Pressable(
|
|
|
1237
1244
|
accessibility_hint=accessibility_hint,
|
|
1238
1245
|
accessibility_role=accessibility_role,
|
|
1239
1246
|
accessible=accessible,
|
|
1247
|
+
accessibility_state=accessibility_state,
|
|
1248
|
+
accessibility_live_region=accessibility_live_region,
|
|
1249
|
+
test_id=test_id,
|
|
1240
1250
|
_defaults={"accessibility_role": "button"},
|
|
1241
1251
|
)
|
|
1242
1252
|
|
|
@@ -1257,6 +1267,9 @@ def TouchableOpacity(
|
|
|
1257
1267
|
accessibility_hint: Optional[str] = None,
|
|
1258
1268
|
accessibility_role: Optional[str] = None,
|
|
1259
1269
|
accessible: Optional[bool] = None,
|
|
1270
|
+
accessibility_state: Optional[AccessibilityState] = None,
|
|
1271
|
+
accessibility_live_region: Optional[Literal["none", "polite", "assertive"]] = None,
|
|
1272
|
+
test_id: Optional[str] = None,
|
|
1260
1273
|
key: Optional[str] = None,
|
|
1261
1274
|
) -> Element:
|
|
1262
1275
|
"""Wrap children so they fade to ``active_opacity`` while pressed.
|
|
@@ -1278,6 +1291,16 @@ def TouchableOpacity(
|
|
|
1278
1291
|
accessibility_hint: Spoken extra detail (iOS only).
|
|
1279
1292
|
accessibility_role: Override the default ``"button"`` role.
|
|
1280
1293
|
accessible: Override whether the element is exposed to AT.
|
|
1294
|
+
accessibility_state: Current widget state for assistive tech,
|
|
1295
|
+
e.g. ``{"disabled": True, "selected": False}``. Recognized
|
|
1296
|
+
keys: ``disabled``, ``selected``, ``checked``, ``busy``,
|
|
1297
|
+
``expanded``.
|
|
1298
|
+
accessibility_live_region: How AT announces dynamic changes to
|
|
1299
|
+
this view: ``"none"``, ``"polite"``, or ``"assertive"``
|
|
1300
|
+
(Android only).
|
|
1301
|
+
test_id: Stable identifier for UI tests; exposed as
|
|
1302
|
+
``resource-id`` on Android and ``accessibilityIdentifier``
|
|
1303
|
+
on iOS.
|
|
1281
1304
|
key: Stable identity for keyed reconciliation.
|
|
1282
1305
|
|
|
1283
1306
|
Returns:
|
|
@@ -1300,6 +1323,9 @@ def TouchableOpacity(
|
|
|
1300
1323
|
accessibility_hint=accessibility_hint,
|
|
1301
1324
|
accessibility_role=accessibility_role,
|
|
1302
1325
|
accessible=accessible,
|
|
1326
|
+
accessibility_state=accessibility_state,
|
|
1327
|
+
accessibility_live_region=accessibility_live_region,
|
|
1328
|
+
test_id=test_id,
|
|
1303
1329
|
key=key,
|
|
1304
1330
|
)
|
|
1305
1331
|
|
|
@@ -1311,6 +1337,9 @@ def ImageBackground(
|
|
|
1311
1337
|
style: StyleProp = None,
|
|
1312
1338
|
accessibility_label: Optional[str] = None,
|
|
1313
1339
|
accessible: Optional[bool] = None,
|
|
1340
|
+
accessibility_state: Optional[AccessibilityState] = None,
|
|
1341
|
+
accessibility_live_region: Optional[Literal["none", "polite", "assertive"]] = None,
|
|
1342
|
+
test_id: Optional[str] = None,
|
|
1314
1343
|
key: Optional[str] = None,
|
|
1315
1344
|
) -> Element:
|
|
1316
1345
|
"""Render ``children`` layered on top of a background image.
|
|
@@ -1329,6 +1358,16 @@ def ImageBackground(
|
|
|
1329
1358
|
style: Style dict for the container (size, padding, alignment).
|
|
1330
1359
|
accessibility_label: Spoken description of the background image.
|
|
1331
1360
|
accessible: Override whether the image is exposed to AT.
|
|
1361
|
+
accessibility_state: Current widget state for assistive tech,
|
|
1362
|
+
e.g. ``{"disabled": True, "selected": False}``. Recognized
|
|
1363
|
+
keys: ``disabled``, ``selected``, ``checked``, ``busy``,
|
|
1364
|
+
``expanded``.
|
|
1365
|
+
accessibility_live_region: How AT announces dynamic changes to
|
|
1366
|
+
this view: ``"none"``, ``"polite"``, or ``"assertive"``
|
|
1367
|
+
(Android only).
|
|
1368
|
+
test_id: Stable identifier for UI tests; exposed as
|
|
1369
|
+
``resource-id`` on Android and ``accessibilityIdentifier``
|
|
1370
|
+
on iOS.
|
|
1332
1371
|
key: Stable identity for keyed reconciliation.
|
|
1333
1372
|
|
|
1334
1373
|
Returns:
|
|
@@ -1342,6 +1381,9 @@ def ImageBackground(
|
|
|
1342
1381
|
style=fill,
|
|
1343
1382
|
accessibility_label=accessibility_label,
|
|
1344
1383
|
accessible=accessible,
|
|
1384
|
+
accessibility_state=accessibility_state,
|
|
1385
|
+
accessibility_live_region=accessibility_live_region,
|
|
1386
|
+
test_id=test_id,
|
|
1345
1387
|
)
|
|
1346
1388
|
content = View(*children, style={"flex": 1})
|
|
1347
1389
|
return View(
|
|
@@ -1363,6 +1405,9 @@ def Checkbox(
|
|
|
1363
1405
|
accessibility_label: Optional[str] = None,
|
|
1364
1406
|
accessibility_hint: Optional[str] = None,
|
|
1365
1407
|
accessible: Optional[bool] = None,
|
|
1408
|
+
accessibility_state: Optional[AccessibilityState] = None,
|
|
1409
|
+
accessibility_live_region: Optional[Literal["none", "polite", "assertive"]] = None,
|
|
1410
|
+
test_id: Optional[str] = None,
|
|
1366
1411
|
key: Optional[str] = None,
|
|
1367
1412
|
) -> Element:
|
|
1368
1413
|
"""A boolean checkbox with an optional inline label.
|
|
@@ -1381,6 +1426,16 @@ def Checkbox(
|
|
|
1381
1426
|
accessibility_label: Spoken description for screen readers.
|
|
1382
1427
|
accessibility_hint: Spoken extra detail (iOS only).
|
|
1383
1428
|
accessible: Override whether the element is exposed to AT.
|
|
1429
|
+
accessibility_state: Current widget state for assistive tech,
|
|
1430
|
+
e.g. ``{"disabled": True, "selected": False}``. Recognized
|
|
1431
|
+
keys: ``disabled``, ``selected``, ``checked``, ``busy``,
|
|
1432
|
+
``expanded``.
|
|
1433
|
+
accessibility_live_region: How AT announces dynamic changes to
|
|
1434
|
+
this view: ``"none"``, ``"polite"``, or ``"assertive"``
|
|
1435
|
+
(Android only).
|
|
1436
|
+
test_id: Stable identifier for UI tests; exposed as
|
|
1437
|
+
``resource-id`` on Android and ``accessibilityIdentifier``
|
|
1438
|
+
on iOS.
|
|
1384
1439
|
key: Stable identity for keyed reconciliation.
|
|
1385
1440
|
|
|
1386
1441
|
Returns:
|
|
@@ -1398,6 +1453,9 @@ def Checkbox(
|
|
|
1398
1453
|
accessibility_label=accessibility_label,
|
|
1399
1454
|
accessibility_hint=accessibility_hint,
|
|
1400
1455
|
accessible=accessible,
|
|
1456
|
+
accessibility_state=accessibility_state,
|
|
1457
|
+
accessibility_live_region=accessibility_live_region,
|
|
1458
|
+
test_id=test_id,
|
|
1401
1459
|
_defaults={"accessibility_role": "checkbox"},
|
|
1402
1460
|
)
|
|
1403
1461
|
|
|
@@ -1412,6 +1470,9 @@ def SegmentedControl(
|
|
|
1412
1470
|
style: StyleProp = None,
|
|
1413
1471
|
accessibility_label: Optional[str] = None,
|
|
1414
1472
|
accessible: Optional[bool] = None,
|
|
1473
|
+
accessibility_state: Optional[AccessibilityState] = None,
|
|
1474
|
+
accessibility_live_region: Optional[Literal["none", "polite", "assertive"]] = None,
|
|
1475
|
+
test_id: Optional[str] = None,
|
|
1415
1476
|
key: Optional[str] = None,
|
|
1416
1477
|
) -> Element:
|
|
1417
1478
|
"""A horizontal multi-choice control (one selected segment at a time).
|
|
@@ -1428,6 +1489,16 @@ def SegmentedControl(
|
|
|
1428
1489
|
style: Style dict (or list of dicts).
|
|
1429
1490
|
accessibility_label: Spoken description for screen readers.
|
|
1430
1491
|
accessible: Override whether the element is exposed to AT.
|
|
1492
|
+
accessibility_state: Current widget state for assistive tech,
|
|
1493
|
+
e.g. ``{"disabled": True, "selected": False}``. Recognized
|
|
1494
|
+
keys: ``disabled``, ``selected``, ``checked``, ``busy``,
|
|
1495
|
+
``expanded``.
|
|
1496
|
+
accessibility_live_region: How AT announces dynamic changes to
|
|
1497
|
+
this view: ``"none"``, ``"polite"``, or ``"assertive"``
|
|
1498
|
+
(Android only).
|
|
1499
|
+
test_id: Stable identifier for UI tests; exposed as
|
|
1500
|
+
``resource-id`` on Android and ``accessibilityIdentifier``
|
|
1501
|
+
on iOS.
|
|
1431
1502
|
key: Stable identity for keyed reconciliation.
|
|
1432
1503
|
|
|
1433
1504
|
Returns:
|
|
@@ -1445,6 +1516,9 @@ def SegmentedControl(
|
|
|
1445
1516
|
tint_color=tint_color,
|
|
1446
1517
|
accessibility_label=accessibility_label,
|
|
1447
1518
|
accessible=accessible,
|
|
1519
|
+
accessibility_state=accessibility_state,
|
|
1520
|
+
accessibility_live_region=accessibility_live_region,
|
|
1521
|
+
test_id=test_id,
|
|
1448
1522
|
)
|
|
1449
1523
|
|
|
1450
1524
|
|
|
@@ -1459,6 +1533,9 @@ def DatePicker(
|
|
|
1459
1533
|
style: StyleProp = None,
|
|
1460
1534
|
accessibility_label: Optional[str] = None,
|
|
1461
1535
|
accessible: Optional[bool] = None,
|
|
1536
|
+
accessibility_state: Optional[AccessibilityState] = None,
|
|
1537
|
+
accessibility_live_region: Optional[Literal["none", "polite", "assertive"]] = None,
|
|
1538
|
+
test_id: Optional[str] = None,
|
|
1462
1539
|
key: Optional[str] = None,
|
|
1463
1540
|
) -> Element:
|
|
1464
1541
|
"""A native date / time picker.
|
|
@@ -1466,7 +1543,10 @@ def DatePicker(
|
|
|
1466
1543
|
Backed by ``UIDatePicker`` on iOS and a trigger button that opens
|
|
1467
1544
|
the platform ``DatePickerDialog`` / ``TimePickerDialog`` on
|
|
1468
1545
|
Android. ``value`` and the value reported to ``on_change`` are
|
|
1469
|
-
ISO-8601 strings (
|
|
1546
|
+
ISO-8601 strings (``"2026-05-31"`` for ``mode="date"``, ``"14:30"``
|
|
1547
|
+
for ``mode="time"``, ``"2026-05-31T14:30"`` for
|
|
1548
|
+
``mode="datetime"``), so values stay JSON-serializable and
|
|
1549
|
+
platform-agnostic.
|
|
1470
1550
|
|
|
1471
1551
|
Args:
|
|
1472
1552
|
value: Currently selected value as an ISO-8601 string.
|
|
@@ -1478,6 +1558,16 @@ def DatePicker(
|
|
|
1478
1558
|
style: Style dict (or list of dicts).
|
|
1479
1559
|
accessibility_label: Spoken description for screen readers.
|
|
1480
1560
|
accessible: Override whether the element is exposed to AT.
|
|
1561
|
+
accessibility_state: Current widget state for assistive tech,
|
|
1562
|
+
e.g. ``{"disabled": True, "selected": False}``. Recognized
|
|
1563
|
+
keys: ``disabled``, ``selected``, ``checked``, ``busy``,
|
|
1564
|
+
``expanded``.
|
|
1565
|
+
accessibility_live_region: How AT announces dynamic changes to
|
|
1566
|
+
this view: ``"none"``, ``"polite"``, or ``"assertive"``
|
|
1567
|
+
(Android only).
|
|
1568
|
+
test_id: Stable identifier for UI tests; exposed as
|
|
1569
|
+
``resource-id`` on Android and ``accessibilityIdentifier``
|
|
1570
|
+
on iOS.
|
|
1481
1571
|
key: Stable identity for keyed reconciliation.
|
|
1482
1572
|
|
|
1483
1573
|
Returns:
|
|
@@ -1495,6 +1585,9 @@ def DatePicker(
|
|
|
1495
1585
|
enabled=False if enabled is False else None,
|
|
1496
1586
|
accessibility_label=accessibility_label,
|
|
1497
1587
|
accessible=accessible,
|
|
1588
|
+
accessibility_state=accessibility_state,
|
|
1589
|
+
accessibility_live_region=accessibility_live_region,
|
|
1590
|
+
test_id=test_id,
|
|
1498
1591
|
_defaults={"accessibility_role": "button"},
|
|
1499
1592
|
)
|
|
1500
1593
|
|
|
@@ -1507,14 +1600,12 @@ def DatePicker(
|
|
|
1507
1600
|
def Fragment(*children: Optional[Element], key: Optional[str] = None) -> Element:
|
|
1508
1601
|
"""Group children without adding a wrapping native view.
|
|
1509
1602
|
|
|
1510
|
-
Like React's ``<></>``:
|
|
1511
|
-
|
|
1512
|
-
Fragment
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
[`memo`][pythonnative.memo] / conditional logic when grouping
|
|
1517
|
-
siblings inside another component's child list:
|
|
1603
|
+
Like React's ``<></>``: groups elements without introducing an
|
|
1604
|
+
extra container. Each child mounts as a direct sibling of the
|
|
1605
|
+
Fragment's position in the parent's child list. Components may
|
|
1606
|
+
also simply return a plain ``list`` of elements; ``Fragment``
|
|
1607
|
+
exists for when the group needs a ``key`` (e.g. rendering a list
|
|
1608
|
+
of pairs) or when a single expression reads better.
|
|
1518
1609
|
|
|
1519
1610
|
```python
|
|
1520
1611
|
pn.Column(
|
|
@@ -1528,24 +1619,54 @@ def Fragment(*children: Optional[Element], key: Optional[str] = None) -> Element
|
|
|
1528
1619
|
```
|
|
1529
1620
|
|
|
1530
1621
|
Args:
|
|
1531
|
-
*children: Child elements to expose at the parent level.
|
|
1532
|
-
children are dropped, which makes
|
|
1533
|
-
``cond and pn.Text(...)``
|
|
1534
|
-
|
|
1535
|
-
|
|
1622
|
+
*children: Child elements to expose at the parent level.
|
|
1623
|
+
``None`` and ``False`` children are dropped, which makes
|
|
1624
|
+
conditional rendering with ``cond and pn.Text(...)``
|
|
1625
|
+
ergonomic.
|
|
1626
|
+
key: Stable identity for keyed reconciliation. A keyed
|
|
1627
|
+
Fragment moves all of its children as one unit and
|
|
1628
|
+
preserves their state across reorders.
|
|
1536
1629
|
|
|
1537
1630
|
Returns:
|
|
1538
1631
|
An [`Element`][pythonnative.Element] of type ``"__Fragment__"``.
|
|
1632
|
+
"""
|
|
1633
|
+
kept = [c for c in children if c is not None and c is not False]
|
|
1634
|
+
return Element("__Fragment__", {}, kept, key=key)
|
|
1539
1635
|
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1636
|
+
|
|
1637
|
+
def Portal(*children: Element, key: Optional[str] = None) -> Element:
|
|
1638
|
+
"""Render ``children`` into a full-screen overlay above everything else.
|
|
1639
|
+
|
|
1640
|
+
Like React DOM's ``createPortal``: the children stay part of this
|
|
1641
|
+
component's tree for state, context, and events, but their native
|
|
1642
|
+
views mount in a transparent overlay attached to the window (above
|
|
1643
|
+
the screen's content) instead of inside the surrounding parent.
|
|
1644
|
+
Use it for toasts, dropdowns, tooltips, and lightweight custom
|
|
1645
|
+
overlays that must escape ``overflow: "hidden"`` ancestors. For a
|
|
1646
|
+
system-styled dialog with its own presentation and dismissal
|
|
1647
|
+
gestures, use [`Modal`][pythonnative.Modal] instead.
|
|
1648
|
+
|
|
1649
|
+
The overlay itself does not intercept touches; only the children
|
|
1650
|
+
themselves are hit-testable. Children are laid out against the
|
|
1651
|
+
full viewport, so position them with absolute insets:
|
|
1652
|
+
|
|
1653
|
+
```python
|
|
1654
|
+
pn.Portal(
|
|
1655
|
+
pn.View(
|
|
1656
|
+
pn.Text("Saved!"),
|
|
1657
|
+
style=pn.style(position="absolute", bottom=40, left=40, right=40),
|
|
1658
|
+
),
|
|
1659
|
+
)
|
|
1660
|
+
```
|
|
1661
|
+
|
|
1662
|
+
Args:
|
|
1663
|
+
*children: Overlay content.
|
|
1664
|
+
key: Stable identity for keyed reconciliation.
|
|
1665
|
+
|
|
1666
|
+
Returns:
|
|
1667
|
+
An [`Element`][pythonnative.Element] of type ``"Portal"``.
|
|
1546
1668
|
"""
|
|
1547
|
-
|
|
1548
|
-
return Element("__Fragment__", {}, filtered, key=key)
|
|
1669
|
+
return Element("Portal", {}, list(children), key=key)
|
|
1549
1670
|
|
|
1550
1671
|
|
|
1551
1672
|
# ======================================================================
|
|
@@ -1556,23 +1677,33 @@ def Fragment(*children: Optional[Element], key: Optional[str] = None) -> Element
|
|
|
1556
1677
|
def ErrorBoundary(
|
|
1557
1678
|
*children: Element,
|
|
1558
1679
|
fallback: Optional[Any] = None,
|
|
1680
|
+
on_error: Optional[Callable[[BaseException], None]] = None,
|
|
1559
1681
|
key: Optional[str] = None,
|
|
1560
1682
|
) -> Element:
|
|
1561
1683
|
"""Catch render errors in the wrapped subtree and display ``fallback`` instead.
|
|
1562
1684
|
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1685
|
+
When any descendant raises during render (initial mount, a parent
|
|
1686
|
+
re-render, or a local state-driven update), the failed subtree is
|
|
1687
|
+
torn down and ``fallback`` is mounted in its place. Without a
|
|
1688
|
+
boundary the error propagates to the screen host (which shows the
|
|
1689
|
+
dev error overlay in dev mode).
|
|
1567
1690
|
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1691
|
+
``fallback`` may be:
|
|
1692
|
+
|
|
1693
|
+
- An [`Element`][pythonnative.Element], shown as-is.
|
|
1694
|
+
- ``fallback(error) -> Element``.
|
|
1695
|
+
- ``fallback(error, reset) -> Element``, where ``reset`` is a
|
|
1696
|
+
zero-arg callable that clears the error and remounts the
|
|
1697
|
+
original children (fresh state), for retry buttons.
|
|
1571
1698
|
|
|
1572
1699
|
Args:
|
|
1573
1700
|
*children: Subtree to wrap.
|
|
1574
|
-
fallback:
|
|
1575
|
-
|
|
1701
|
+
fallback: Fallback content (see above). Required for the
|
|
1702
|
+
boundary to actually catch; without it errors propagate
|
|
1703
|
+
to the next boundary up.
|
|
1704
|
+
on_error: Callback invoked with the exception when the
|
|
1705
|
+
boundary catches, before the fallback mounts. Use it for
|
|
1706
|
+
error reporting.
|
|
1576
1707
|
key: Stable identity for keyed reconciliation.
|
|
1577
1708
|
|
|
1578
1709
|
Returns:
|
|
@@ -1585,32 +1716,43 @@ def ErrorBoundary(
|
|
|
1585
1716
|
|
|
1586
1717
|
pn.ErrorBoundary(
|
|
1587
1718
|
MyRiskyComponent(),
|
|
1588
|
-
fallback=lambda err: pn.
|
|
1719
|
+
fallback=lambda err, reset: pn.Column(
|
|
1720
|
+
pn.Text(f"Error: {err}"),
|
|
1721
|
+
pn.Button("Retry", on_press=reset),
|
|
1722
|
+
),
|
|
1723
|
+
on_error=lambda err: log.exception(err),
|
|
1589
1724
|
)
|
|
1590
1725
|
```
|
|
1591
1726
|
"""
|
|
1592
1727
|
props: Dict[str, Any] = {}
|
|
1593
1728
|
if fallback is not None:
|
|
1594
1729
|
props["__fallback__"] = fallback
|
|
1595
|
-
if
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
kids = [Fragment(*children)]
|
|
1599
|
-
return Element("__ErrorBoundary__", props, kids, key=key)
|
|
1730
|
+
if on_error is not None:
|
|
1731
|
+
props["__on_error__"] = on_error
|
|
1732
|
+
return Element("__ErrorBoundary__", props, list(children), key=key)
|
|
1600
1733
|
|
|
1601
1734
|
|
|
1602
1735
|
# ======================================================================
|
|
1603
|
-
# Lists (Python-windowed
|
|
1736
|
+
# Lists (native virtualization with a Python-windowed fallback)
|
|
1604
1737
|
# ======================================================================
|
|
1605
1738
|
#
|
|
1606
|
-
# FlatList and SectionList
|
|
1607
|
-
#
|
|
1608
|
-
#
|
|
1609
|
-
#
|
|
1610
|
-
#
|
|
1611
|
-
#
|
|
1612
|
-
#
|
|
1613
|
-
#
|
|
1739
|
+
# FlatList and SectionList pick between two engines:
|
|
1740
|
+
#
|
|
1741
|
+
# 1. **Native virtualization** (`_NativeList` -> the ``VirtualList``
|
|
1742
|
+
# element): on Android and iOS, when every row extent is known up
|
|
1743
|
+
# front and no windowed-only feature is requested, the list is
|
|
1744
|
+
# backed by a real ``RecyclerView`` / ``UITableView``. The platform
|
|
1745
|
+
# owns row recycling; each visible row hosts a nested-reconciler
|
|
1746
|
+
# subtree (see ``pythonnative.virtual_rows``).
|
|
1747
|
+
# 2. **Python windowing** (`_VirtualizedList`): a windowed slice of
|
|
1748
|
+
# rows rendered into a ScrollView (leading spacer, visible rows,
|
|
1749
|
+
# trailing spacer), the window shifting from scroll events (the
|
|
1750
|
+
# same architecture as React Native's VirtualizedList). Because
|
|
1751
|
+
# every windowed row lives in the *main* layout tree, rows may be
|
|
1752
|
+
# any height: estimates steer the spacer sizes and measured extents
|
|
1753
|
+
# correct them over time. This is the desktop path and the fallback
|
|
1754
|
+
# for variable-height rows, grids, horizontal lists, ornaments, and
|
|
1755
|
+
# pull-to-refresh.
|
|
1614
1756
|
|
|
1615
1757
|
_DEFAULT_ROW_EXTENT = 44.0
|
|
1616
1758
|
|
|
@@ -1637,7 +1779,7 @@ class _RowSpec:
|
|
|
1637
1779
|
|
|
1638
1780
|
def _dispatch_scroll_command(scroll_ref: Any, name: str, args: Dict[str, Any]) -> Any:
|
|
1639
1781
|
"""Send an imperative command to the ScrollView under ``scroll_ref``."""
|
|
1640
|
-
tag = scroll_ref
|
|
1782
|
+
tag = getattr(scroll_ref, "_pn_tag", None)
|
|
1641
1783
|
if tag is None:
|
|
1642
1784
|
return None
|
|
1643
1785
|
from .native_views import get_registry
|
|
@@ -1648,6 +1790,55 @@ def _dispatch_scroll_command(scroll_ref: Any, name: str, args: Dict[str, Any]) -
|
|
|
1648
1790
|
return None
|
|
1649
1791
|
|
|
1650
1792
|
|
|
1793
|
+
class ListController:
|
|
1794
|
+
"""Imperative scroll handle published on a list's ``ref``.
|
|
1795
|
+
|
|
1796
|
+
[`FlatList`][pythonnative.FlatList] and
|
|
1797
|
+
[`SectionList`][pythonnative.SectionList] install a
|
|
1798
|
+
``ListController`` on ``ref.current`` (via
|
|
1799
|
+
[`use_imperative_handle`][pythonnative.use_imperative_handle])
|
|
1800
|
+
after mount and clear it back to ``None`` on unmount.
|
|
1801
|
+
|
|
1802
|
+
Example:
|
|
1803
|
+
```python
|
|
1804
|
+
import pythonnative as pn
|
|
1805
|
+
|
|
1806
|
+
@pn.component
|
|
1807
|
+
def Chat(messages):
|
|
1808
|
+
list_ref = pn.use_ref()
|
|
1809
|
+
pn.use_layout_effect(
|
|
1810
|
+
lambda: list_ref.current and list_ref.current.scroll_to_end(animated=False),
|
|
1811
|
+
[len(messages)],
|
|
1812
|
+
)
|
|
1813
|
+
return pn.FlatList(data=messages, render_item=Bubble, ref=list_ref)
|
|
1814
|
+
```
|
|
1815
|
+
"""
|
|
1816
|
+
|
|
1817
|
+
__slots__ = ("_scroll_to_offset", "_scroll_to_index", "_scroll_to_end")
|
|
1818
|
+
|
|
1819
|
+
def __init__(
|
|
1820
|
+
self,
|
|
1821
|
+
scroll_to_offset: Callable[[float, bool], None],
|
|
1822
|
+
scroll_to_index: Callable[[int, bool], None],
|
|
1823
|
+
scroll_to_end: Callable[[bool], None],
|
|
1824
|
+
) -> None:
|
|
1825
|
+
self._scroll_to_offset = scroll_to_offset
|
|
1826
|
+
self._scroll_to_index = scroll_to_index
|
|
1827
|
+
self._scroll_to_end = scroll_to_end
|
|
1828
|
+
|
|
1829
|
+
def scroll_to_offset(self, offset: float, animated: bool = True) -> None:
|
|
1830
|
+
"""Scroll to an absolute content offset in points."""
|
|
1831
|
+
self._scroll_to_offset(offset, animated)
|
|
1832
|
+
|
|
1833
|
+
def scroll_to_index(self, index: int, animated: bool = True) -> None:
|
|
1834
|
+
"""Scroll so the row at ``index`` sits at the top of the viewport."""
|
|
1835
|
+
self._scroll_to_index(index, animated)
|
|
1836
|
+
|
|
1837
|
+
def scroll_to_end(self, animated: bool = True) -> None:
|
|
1838
|
+
"""Scroll to the end of the content."""
|
|
1839
|
+
self._scroll_to_end(animated)
|
|
1840
|
+
|
|
1841
|
+
|
|
1651
1842
|
@component
|
|
1652
1843
|
def _VirtualizedList(**p: Any) -> Element:
|
|
1653
1844
|
"""Shared windowing engine behind FlatList and SectionList."""
|
|
@@ -1659,18 +1850,18 @@ def _VirtualizedList(**p: Any) -> Element:
|
|
|
1659
1850
|
initial_extent: float = float(p.get("initial_window_extent") or 800.0)
|
|
1660
1851
|
|
|
1661
1852
|
window, set_window = use_state((0, -1))
|
|
1662
|
-
measured = use_ref({}) # row key -> measured extent (points)
|
|
1663
|
-
row_refs = use_ref({}) # row key ->
|
|
1853
|
+
measured: Ref[Dict[str, float]] = use_ref({}) # row key -> measured extent (points)
|
|
1854
|
+
row_refs: Ref[Dict[str, Ref]] = use_ref({}) # row key -> Ref for live rows
|
|
1664
1855
|
end_latch = use_ref({"fired_for": -1})
|
|
1665
|
-
viewable_ref = use_ref({"keys": ()})
|
|
1856
|
+
viewable_ref: Ref[Dict[str, Tuple[str, ...]]] = use_ref({"keys": ()})
|
|
1666
1857
|
scroll_pos = use_ref({"offset": 0.0})
|
|
1667
|
-
sv_ref = use_ref(None)
|
|
1858
|
+
sv_ref: Ref = use_ref(None)
|
|
1668
1859
|
|
|
1669
1860
|
# ------------------------------------------------------------------
|
|
1670
1861
|
# Extent model: measured > per-row hint > estimate. ``starts`` are
|
|
1671
1862
|
# prefix sums; ``starts[n]`` is the total content extent.
|
|
1672
1863
|
# ------------------------------------------------------------------
|
|
1673
|
-
measured_map: Dict[str, float] = measured
|
|
1864
|
+
measured_map: Dict[str, float] = measured.current
|
|
1674
1865
|
starts: List[float] = [0.0] * (n + 1)
|
|
1675
1866
|
acc = 0.0
|
|
1676
1867
|
for i, spec in enumerate(rows):
|
|
@@ -1683,7 +1874,7 @@ def _VirtualizedList(**p: Any) -> Element:
|
|
|
1683
1874
|
total_extent = acc
|
|
1684
1875
|
|
|
1685
1876
|
def _viewport_extent() -> float:
|
|
1686
|
-
frame = sv_ref.
|
|
1877
|
+
frame = sv_ref._pn_frame
|
|
1687
1878
|
if frame:
|
|
1688
1879
|
extent = frame[2] if horizontal else frame[3]
|
|
1689
1880
|
if extent and extent > 0:
|
|
@@ -1702,7 +1893,7 @@ def _VirtualizedList(**p: Any) -> Element:
|
|
|
1702
1893
|
|
|
1703
1894
|
first, last = window
|
|
1704
1895
|
if last < 0 or first >= n:
|
|
1705
|
-
first, last = _window_for(scroll_pos
|
|
1896
|
+
first, last = _window_for(scroll_pos.current["offset"], _viewport_extent())
|
|
1706
1897
|
last = min(last, n - 1)
|
|
1707
1898
|
first = max(0, min(first, max(0, n - 1)))
|
|
1708
1899
|
|
|
@@ -1718,8 +1909,8 @@ def _VirtualizedList(**p: Any) -> Element:
|
|
|
1718
1909
|
user_on_scroll = p.get("on_scroll")
|
|
1719
1910
|
|
|
1720
1911
|
def _sweep_measured() -> None:
|
|
1721
|
-
for row_key,
|
|
1722
|
-
frame =
|
|
1912
|
+
for row_key, row_ref in row_refs.current.items():
|
|
1913
|
+
frame = getattr(row_ref, "_pn_frame", None)
|
|
1723
1914
|
if frame:
|
|
1724
1915
|
extent = frame[2] if horizontal else frame[3]
|
|
1725
1916
|
if extent and extent > 0:
|
|
@@ -1730,7 +1921,7 @@ def _VirtualizedList(**p: Any) -> Element:
|
|
|
1730
1921
|
offset = float(payload.get("x" if horizontal else "y", 0.0) or 0.0)
|
|
1731
1922
|
else:
|
|
1732
1923
|
offset = float(payload or 0.0)
|
|
1733
|
-
scroll_pos
|
|
1924
|
+
scroll_pos.current["offset"] = offset
|
|
1734
1925
|
_sweep_measured()
|
|
1735
1926
|
viewport = _viewport_extent()
|
|
1736
1927
|
|
|
@@ -1741,18 +1932,18 @@ def _VirtualizedList(**p: Any) -> Element:
|
|
|
1741
1932
|
if on_end_reached is not None and total_extent > 0:
|
|
1742
1933
|
remaining = total_extent - (offset + viewport)
|
|
1743
1934
|
if remaining <= end_threshold * viewport:
|
|
1744
|
-
if end_latch
|
|
1745
|
-
end_latch
|
|
1935
|
+
if end_latch.current["fired_for"] != n:
|
|
1936
|
+
end_latch.current["fired_for"] = n
|
|
1746
1937
|
on_end_reached()
|
|
1747
1938
|
elif remaining > end_threshold * viewport + viewport:
|
|
1748
|
-
end_latch
|
|
1939
|
+
end_latch.current["fired_for"] = -1
|
|
1749
1940
|
|
|
1750
1941
|
if on_viewable is not None and n > 0:
|
|
1751
1942
|
v_first = max(0, bisect.bisect_right(starts, offset, 0, n) - 1)
|
|
1752
1943
|
v_last = min(n - 1, bisect.bisect_left(starts, offset + viewport, 0, n))
|
|
1753
1944
|
keys = tuple(rows[i].key for i in range(v_first, v_last + 1))
|
|
1754
|
-
if keys != viewable_ref
|
|
1755
|
-
viewable_ref
|
|
1945
|
+
if keys != viewable_ref.current["keys"]:
|
|
1946
|
+
viewable_ref.current["keys"] = keys
|
|
1756
1947
|
on_viewable(
|
|
1757
1948
|
[
|
|
1758
1949
|
{"index": rows[i].index, "key": rows[i].key, "item": rows[i].item}
|
|
@@ -1764,33 +1955,26 @@ def _VirtualizedList(**p: Any) -> Element:
|
|
|
1764
1955
|
user_on_scroll(payload)
|
|
1765
1956
|
|
|
1766
1957
|
# ------------------------------------------------------------------
|
|
1767
|
-
# Imperative controller (scroll_to_index / offset / end)
|
|
1768
|
-
# the user's ref
|
|
1769
|
-
# fresh extents
|
|
1770
|
-
# hook order stable.
|
|
1958
|
+
# Imperative controller (scroll_to_index / offset / end) published
|
|
1959
|
+
# on the user's ref. Rebuilt every render (deps=None) so the
|
|
1960
|
+
# closures see fresh extents.
|
|
1771
1961
|
# ------------------------------------------------------------------
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
if not isinstance(controller, dict):
|
|
1776
|
-
return
|
|
1777
|
-
|
|
1778
|
-
def scroll_to_offset(offset: float, animated: bool = True) -> None:
|
|
1779
|
-
axis = "x" if horizontal else "y"
|
|
1780
|
-
_dispatch_scroll_command(sv_ref, "scroll_to_offset", {axis: float(offset), "animated": animated})
|
|
1781
|
-
|
|
1782
|
-
def scroll_to_index(index: int, animated: bool = True) -> None:
|
|
1783
|
-
idx = max(0, min(int(index), n - 1)) if n else 0
|
|
1784
|
-
scroll_to_offset(starts[idx], animated)
|
|
1962
|
+
def _scroll_to_offset(offset: float, animated: bool = True) -> None:
|
|
1963
|
+
axis = "x" if horizontal else "y"
|
|
1964
|
+
_dispatch_scroll_command(sv_ref, "scroll_to_offset", {axis: float(offset), "animated": animated})
|
|
1785
1965
|
|
|
1786
|
-
|
|
1787
|
-
|
|
1966
|
+
def _scroll_to_index(index: int, animated: bool = True) -> None:
|
|
1967
|
+
idx = max(0, min(int(index), n - 1)) if n else 0
|
|
1968
|
+
_scroll_to_offset(starts[idx], animated)
|
|
1788
1969
|
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
controller["scroll_to_end"] = scroll_to_end
|
|
1970
|
+
def _scroll_to_end(animated: bool = True) -> None:
|
|
1971
|
+
_scroll_to_offset(max(0.0, total_extent - _viewport_extent()), animated)
|
|
1792
1972
|
|
|
1793
|
-
|
|
1973
|
+
use_imperative_handle(
|
|
1974
|
+
p.get("controller_ref"),
|
|
1975
|
+
lambda: ListController(_scroll_to_offset, _scroll_to_index, _scroll_to_end),
|
|
1976
|
+
None,
|
|
1977
|
+
)
|
|
1794
1978
|
|
|
1795
1979
|
# ------------------------------------------------------------------
|
|
1796
1980
|
# Children: header, leading spacer, windowed rows, trailing spacer,
|
|
@@ -1809,17 +1993,17 @@ def _VirtualizedList(**p: Any) -> Element:
|
|
|
1809
1993
|
if empty is not None:
|
|
1810
1994
|
children.append(View(empty, key="__pn_empty__"))
|
|
1811
1995
|
else:
|
|
1812
|
-
live_refs: Dict[str,
|
|
1996
|
+
live_refs: Dict[str, Ref] = {}
|
|
1813
1997
|
lead = starts[first]
|
|
1814
1998
|
if lead > 0:
|
|
1815
1999
|
lead_style: Dict[str, Any] = {spacer_key: lead}
|
|
1816
2000
|
children.append(View(style=lead_style, key="__pn_lead__"))
|
|
1817
2001
|
for i in range(first, last + 1):
|
|
1818
2002
|
spec = rows[i]
|
|
1819
|
-
row_ref = row_refs
|
|
2003
|
+
row_ref = row_refs.current.get(spec.key) or Ref()
|
|
1820
2004
|
live_refs[spec.key] = row_ref
|
|
1821
2005
|
children.append(View(spec.make(), ref=row_ref, key=spec.key))
|
|
1822
|
-
row_refs
|
|
2006
|
+
row_refs.current = live_refs
|
|
1823
2007
|
trail = total_extent - starts[last + 1]
|
|
1824
2008
|
if trail > 0:
|
|
1825
2009
|
trail_style: Dict[str, Any] = {spacer_key: trail}
|
|
@@ -1841,6 +2025,126 @@ def _VirtualizedList(**p: Any) -> Element:
|
|
|
1841
2025
|
)
|
|
1842
2026
|
|
|
1843
2027
|
|
|
2028
|
+
def _native_lists_supported() -> bool:
|
|
2029
|
+
"""Whether the natively virtualized list path is available.
|
|
2030
|
+
|
|
2031
|
+
Android (RecyclerView) and iOS (UITableView) have native handlers;
|
|
2032
|
+
the desktop preview and off-device tests use the Python-windowed
|
|
2033
|
+
engine. Patchable in tests to exercise the native routing.
|
|
2034
|
+
"""
|
|
2035
|
+
from .utils import IS_ANDROID, IS_IOS
|
|
2036
|
+
|
|
2037
|
+
return IS_ANDROID or IS_IOS
|
|
2038
|
+
|
|
2039
|
+
|
|
2040
|
+
@component
|
|
2041
|
+
def _NativeList(**p: Any) -> Element:
|
|
2042
|
+
"""Platform-virtualized list: emits a ``VirtualList`` native element.
|
|
2043
|
+
|
|
2044
|
+
The native side (RecyclerView / UITableView) owns row windowing and
|
|
2045
|
+
recycling; each visible row hosts a nested-reconciler subtree (see
|
|
2046
|
+
``pythonnative.virtual_rows``). This composite adapts the FlatList /
|
|
2047
|
+
SectionList surface onto that element: it forwards ``render_row``,
|
|
2048
|
+
derives ``on_end_reached`` and ``on_viewable_items_changed`` from
|
|
2049
|
+
native scroll reports, and wires the imperative scroll controller.
|
|
2050
|
+
|
|
2051
|
+
Requires every row's extent to be known up front (the native
|
|
2052
|
+
virtualizers need exact heights before rows are rendered); callers
|
|
2053
|
+
fall back to the Python-windowed engine otherwise.
|
|
2054
|
+
"""
|
|
2055
|
+
rows: List[_RowSpec] = p.get("rows") or []
|
|
2056
|
+
n = len(rows)
|
|
2057
|
+
heights: List[float] = [float(spec.extent or 0.0) for spec in rows]
|
|
2058
|
+
uniform = len(set(heights)) <= 1
|
|
2059
|
+
|
|
2060
|
+
internal_ref: Ref = use_ref(None)
|
|
2061
|
+
end_latch = use_ref({"fired_for": -1})
|
|
2062
|
+
viewable_ref: Ref[Dict[str, Tuple[str, ...]]] = use_ref({"keys": ()})
|
|
2063
|
+
|
|
2064
|
+
starts: List[float] = [0.0] * (n + 1)
|
|
2065
|
+
acc = 0.0
|
|
2066
|
+
for i, extent in enumerate(heights):
|
|
2067
|
+
starts[i] = acc
|
|
2068
|
+
acc += max(0.0, extent)
|
|
2069
|
+
starts[n] = acc
|
|
2070
|
+
total_extent = acc
|
|
2071
|
+
|
|
2072
|
+
def _render_row(index: int) -> Element:
|
|
2073
|
+
if 0 <= index < n:
|
|
2074
|
+
return rows[index].make()
|
|
2075
|
+
return View()
|
|
2076
|
+
|
|
2077
|
+
on_end_reached = p.get("on_end_reached")
|
|
2078
|
+
end_threshold = float(p.get("on_end_reached_threshold") or 0.5)
|
|
2079
|
+
on_viewable = p.get("on_viewable_items_changed")
|
|
2080
|
+
user_on_scroll = p.get("on_scroll")
|
|
2081
|
+
|
|
2082
|
+
def _handle_scroll(payload: Any) -> None:
|
|
2083
|
+
offset = float(payload.get("y", 0.0) or 0.0) if isinstance(payload, dict) else float(payload or 0.0)
|
|
2084
|
+
viewport = float(payload.get("extent", 0.0) or 0.0) if isinstance(payload, dict) else 0.0
|
|
2085
|
+
if viewport <= 0:
|
|
2086
|
+
viewport = 800.0
|
|
2087
|
+
|
|
2088
|
+
if on_end_reached is not None and total_extent > 0:
|
|
2089
|
+
remaining = total_extent - (offset + viewport)
|
|
2090
|
+
if remaining <= end_threshold * viewport:
|
|
2091
|
+
if end_latch.current["fired_for"] != n:
|
|
2092
|
+
end_latch.current["fired_for"] = n
|
|
2093
|
+
on_end_reached()
|
|
2094
|
+
elif remaining > end_threshold * viewport + viewport:
|
|
2095
|
+
end_latch.current["fired_for"] = -1
|
|
2096
|
+
|
|
2097
|
+
if on_viewable is not None and n > 0:
|
|
2098
|
+
v_first = max(0, bisect.bisect_right(starts, offset, 0, n) - 1)
|
|
2099
|
+
v_last = min(n - 1, bisect.bisect_left(starts, offset + viewport, 0, n))
|
|
2100
|
+
keys = tuple(rows[i].key for i in range(v_first, v_last + 1))
|
|
2101
|
+
if keys != viewable_ref.current["keys"]:
|
|
2102
|
+
viewable_ref.current["keys"] = keys
|
|
2103
|
+
on_viewable(
|
|
2104
|
+
[
|
|
2105
|
+
{"index": rows[i].index, "key": rows[i].key, "item": rows[i].item}
|
|
2106
|
+
for i in range(v_first, v_last + 1)
|
|
2107
|
+
]
|
|
2108
|
+
)
|
|
2109
|
+
|
|
2110
|
+
if user_on_scroll is not None:
|
|
2111
|
+
user_on_scroll({"x": 0.0, "y": offset})
|
|
2112
|
+
|
|
2113
|
+
def _scroll_to_offset(offset: float, animated: bool = True) -> None:
|
|
2114
|
+
_dispatch_scroll_command(internal_ref, "scroll_to_offset", {"y": float(offset), "animated": animated})
|
|
2115
|
+
|
|
2116
|
+
def _scroll_to_index(index: int, animated: bool = True) -> None:
|
|
2117
|
+
_dispatch_scroll_command(internal_ref, "scroll_to_index", {"index": int(index), "animated": animated})
|
|
2118
|
+
|
|
2119
|
+
def _scroll_to_end(animated: bool = True) -> None:
|
|
2120
|
+
_dispatch_scroll_command(internal_ref, "scroll_to_end", {"animated": animated})
|
|
2121
|
+
|
|
2122
|
+
use_imperative_handle(
|
|
2123
|
+
p.get("controller_ref"),
|
|
2124
|
+
lambda: ListController(_scroll_to_offset, _scroll_to_index, _scroll_to_end),
|
|
2125
|
+
None,
|
|
2126
|
+
)
|
|
2127
|
+
|
|
2128
|
+
props: Dict[str, Any] = dict(p.get("list_style") or {})
|
|
2129
|
+
props["count"] = n
|
|
2130
|
+
if uniform:
|
|
2131
|
+
props["row_height"] = heights[0] if heights else _DEFAULT_ROW_EXTENT
|
|
2132
|
+
else:
|
|
2133
|
+
props["row_heights"] = heights
|
|
2134
|
+
props["render_row"] = _render_row
|
|
2135
|
+
props["ref"] = internal_ref
|
|
2136
|
+
wants_scroll = on_end_reached is not None or on_viewable is not None or user_on_scroll is not None
|
|
2137
|
+
if wants_scroll:
|
|
2138
|
+
props["on_scroll"] = _handle_scroll
|
|
2139
|
+
if p.get("shows_scroll_indicator") is False:
|
|
2140
|
+
props["shows_scroll_indicator"] = False
|
|
2141
|
+
return Element("VirtualList", props, [])
|
|
2142
|
+
|
|
2143
|
+
|
|
2144
|
+
def _all_extents_known(rows: List[_RowSpec]) -> bool:
|
|
2145
|
+
return all(spec.extent is not None for spec in rows)
|
|
2146
|
+
|
|
2147
|
+
|
|
1844
2148
|
def FlatList(
|
|
1845
2149
|
*,
|
|
1846
2150
|
data: Optional[List[Any]] = None,
|
|
@@ -1863,7 +2167,7 @@ def FlatList(
|
|
|
1863
2167
|
shows_scroll_indicator: bool = True,
|
|
1864
2168
|
content_container_style: StyleProp = None,
|
|
1865
2169
|
style: StyleProp = None,
|
|
1866
|
-
ref: Optional[
|
|
2170
|
+
ref: Optional[Ref] = None,
|
|
1867
2171
|
key: Optional[str] = None,
|
|
1868
2172
|
) -> Element:
|
|
1869
2173
|
"""Virtualized scrollable list that renders items from ``data`` lazily.
|
|
@@ -1876,10 +2180,12 @@ def FlatList(
|
|
|
1876
2180
|
unknown rows start at ``estimated_item_height`` and are corrected
|
|
1877
2181
|
with their measured extent once they've been on screen.
|
|
1878
2182
|
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
2183
|
+
Pass a [`Ref`][pythonnative.Ref] (from
|
|
2184
|
+
[`use_ref`][pythonnative.use_ref]) to receive a
|
|
2185
|
+
[`ListController`][pythonnative.ListController] on ``ref.current``:
|
|
2186
|
+
``ref.current.scroll_to_index(i)``,
|
|
2187
|
+
``ref.current.scroll_to_offset(pts)``, and
|
|
2188
|
+
``ref.current.scroll_to_end()``.
|
|
1883
2189
|
|
|
1884
2190
|
Args:
|
|
1885
2191
|
data: List of arbitrary item values.
|
|
@@ -1914,8 +2220,9 @@ def FlatList(
|
|
|
1914
2220
|
content_container_style: Style applied to the inner content
|
|
1915
2221
|
wrapper.
|
|
1916
2222
|
style: Style for the outer scroll container.
|
|
1917
|
-
ref: Optional
|
|
1918
|
-
|
|
2223
|
+
ref: Optional [`Ref`][pythonnative.Ref]; receives a
|
|
2224
|
+
[`ListController`][pythonnative.ListController] on
|
|
2225
|
+
``ref.current`` after mount.
|
|
1919
2226
|
key: Stable identity for keyed reconciliation of the list.
|
|
1920
2227
|
|
|
1921
2228
|
Returns:
|
|
@@ -1994,6 +2301,32 @@ def FlatList(
|
|
|
1994
2301
|
|
|
1995
2302
|
estimated = estimated_item_height if estimated_item_height is not None else (item_height or _DEFAULT_ROW_EXTENT)
|
|
1996
2303
|
|
|
2304
|
+
# Route to the platform virtualizer (RecyclerView / UITableView)
|
|
2305
|
+
# when it can represent this list exactly: vertical, single-column,
|
|
2306
|
+
# every row extent known up front, and no features that only the
|
|
2307
|
+
# Python-windowed engine implements (ornaments, pull-to-refresh).
|
|
2308
|
+
if (
|
|
2309
|
+
_native_lists_supported()
|
|
2310
|
+
and not horizontal
|
|
2311
|
+
and num_columns == 1
|
|
2312
|
+
and list_header is None
|
|
2313
|
+
and list_footer is None
|
|
2314
|
+
and list_empty is None
|
|
2315
|
+
and refresh_control is None
|
|
2316
|
+
and _all_extents_known(rows)
|
|
2317
|
+
):
|
|
2318
|
+
return _NativeList(
|
|
2319
|
+
rows=rows,
|
|
2320
|
+
on_end_reached=on_end_reached,
|
|
2321
|
+
on_end_reached_threshold=on_end_reached_threshold,
|
|
2322
|
+
on_viewable_items_changed=on_viewable_items_changed,
|
|
2323
|
+
on_scroll=on_scroll,
|
|
2324
|
+
shows_scroll_indicator=shows_scroll_indicator,
|
|
2325
|
+
list_style=resolve_style(style) or None,
|
|
2326
|
+
controller_ref=ref,
|
|
2327
|
+
key=key,
|
|
2328
|
+
)
|
|
2329
|
+
|
|
1997
2330
|
return _VirtualizedList(
|
|
1998
2331
|
rows=rows,
|
|
1999
2332
|
horizontal=horizontal,
|
|
@@ -2033,7 +2366,7 @@ def SectionList(
|
|
|
2033
2366
|
on_end_reached_threshold: float = 0.5,
|
|
2034
2367
|
on_scroll: Optional[Callable[[Dict[str, float]], None]] = None,
|
|
2035
2368
|
style: StyleProp = None,
|
|
2036
|
-
ref: Optional[
|
|
2369
|
+
ref: Optional[Ref] = None,
|
|
2037
2370
|
key: Optional[str] = None,
|
|
2038
2371
|
) -> Element:
|
|
2039
2372
|
"""Virtualized list with section headers interleaved between row groups.
|
|
@@ -2067,8 +2400,9 @@ def SectionList(
|
|
|
2067
2400
|
multiples, at which ``on_end_reached`` fires.
|
|
2068
2401
|
on_scroll: Called with the raw scroll payload.
|
|
2069
2402
|
style: Style for the outer scroll container.
|
|
2070
|
-
ref: Optional
|
|
2071
|
-
|
|
2403
|
+
ref: Optional [`Ref`][pythonnative.Ref]; receives a
|
|
2404
|
+
[`ListController`][pythonnative.ListController] on
|
|
2405
|
+
``ref.current`` after mount.
|
|
2072
2406
|
key: Stable identity for keyed reconciliation of the list.
|
|
2073
2407
|
|
|
2074
2408
|
Returns:
|
|
@@ -2132,6 +2466,26 @@ def SectionList(
|
|
|
2132
2466
|
|
|
2133
2467
|
estimated = estimated_item_height if estimated_item_height is not None else (item_height or _DEFAULT_ROW_EXTENT)
|
|
2134
2468
|
|
|
2469
|
+
# Same native routing as FlatList: headers and items become one
|
|
2470
|
+
# flattened row sequence with per-row heights.
|
|
2471
|
+
if (
|
|
2472
|
+
_native_lists_supported()
|
|
2473
|
+
and list_header is None
|
|
2474
|
+
and list_footer is None
|
|
2475
|
+
and list_empty is None
|
|
2476
|
+
and refresh_control is None
|
|
2477
|
+
and _all_extents_known(rows)
|
|
2478
|
+
):
|
|
2479
|
+
return _NativeList(
|
|
2480
|
+
rows=rows,
|
|
2481
|
+
on_end_reached=on_end_reached,
|
|
2482
|
+
on_end_reached_threshold=on_end_reached_threshold,
|
|
2483
|
+
on_scroll=on_scroll,
|
|
2484
|
+
list_style=resolve_style(style) or None,
|
|
2485
|
+
controller_ref=ref,
|
|
2486
|
+
key=key,
|
|
2487
|
+
)
|
|
2488
|
+
|
|
2135
2489
|
return _VirtualizedList(
|
|
2136
2490
|
rows=rows,
|
|
2137
2491
|
horizontal=False,
|
|
@@ -2194,9 +2548,40 @@ def StatusBar(
|
|
|
2194
2548
|
return Element("StatusBar", props, [], key=key)
|
|
2195
2549
|
|
|
2196
2550
|
|
|
2551
|
+
@component
|
|
2552
|
+
def _KeyboardAvoidingContainer(**p: Any) -> Element:
|
|
2553
|
+
"""Hook-driven body of `KeyboardAvoidingView`.
|
|
2554
|
+
|
|
2555
|
+
Subscribes to the platform-reported keyboard height via
|
|
2556
|
+
[`use_keyboard_height`][pythonnative.use_keyboard_height] and
|
|
2557
|
+
applies the shift according to ``behavior``:
|
|
2558
|
+
|
|
2559
|
+
- ``"padding"``: adds the shift as bottom padding, resizing the
|
|
2560
|
+
content area (the default, and the right choice for forms
|
|
2561
|
+
inside a full-height container).
|
|
2562
|
+
- ``"position"``: translates the whole container upward without
|
|
2563
|
+
resizing it (useful for pinned footers/toolbars).
|
|
2564
|
+
"""
|
|
2565
|
+
height = use_keyboard_height()
|
|
2566
|
+
behavior = p.get("behavior") or "padding"
|
|
2567
|
+
offset = float(p.get("keyboard_vertical_offset") or 0.0)
|
|
2568
|
+
shift = max(0.0, height - offset) if height > 0 else 0.0
|
|
2569
|
+
style: Dict[str, Any] = dict(p.get("style") or {})
|
|
2570
|
+
if shift > 0:
|
|
2571
|
+
if behavior == "position":
|
|
2572
|
+
transform = list(style.get("transform") or [])
|
|
2573
|
+
transform.append({"translate_y": -shift})
|
|
2574
|
+
style["transform"] = transform
|
|
2575
|
+
else:
|
|
2576
|
+
style["padding_bottom"] = _numeric_edge_padding(style, "bottom") + shift
|
|
2577
|
+
children = p.get("children") or []
|
|
2578
|
+
return Element("KeyboardAvoidingView", style, list(children))
|
|
2579
|
+
|
|
2580
|
+
|
|
2197
2581
|
def KeyboardAvoidingView(
|
|
2198
2582
|
*children: Element,
|
|
2199
2583
|
behavior: Literal["padding", "position"] = "padding",
|
|
2584
|
+
keyboard_vertical_offset: float = 0.0,
|
|
2200
2585
|
style: StyleProp = None,
|
|
2201
2586
|
key: Optional[str] = None,
|
|
2202
2587
|
) -> Element:
|
|
@@ -2204,26 +2589,32 @@ def KeyboardAvoidingView(
|
|
|
2204
2589
|
|
|
2205
2590
|
Subscribes to the platform-reported keyboard height (via
|
|
2206
2591
|
[`use_keyboard_height`][pythonnative.use_keyboard_height]
|
|
2207
|
-
internally) and
|
|
2208
|
-
|
|
2592
|
+
internally) and shifts its content so the focused text input stays
|
|
2593
|
+
visible. On iOS the height comes from
|
|
2594
|
+
``UIKeyboardWillShowNotification``; on Android from the window's
|
|
2595
|
+
IME insets.
|
|
2209
2596
|
|
|
2210
2597
|
Args:
|
|
2211
2598
|
*children: Children rendered inside the avoiding container.
|
|
2212
|
-
behavior: ``"padding"`` (adds bottom padding
|
|
2213
|
-
(translates the container
|
|
2599
|
+
behavior: ``"padding"`` (adds bottom padding, resizing the
|
|
2600
|
+
content) or ``"position"`` (translates the container
|
|
2601
|
+
upward without resizing).
|
|
2602
|
+
keyboard_vertical_offset: Distance in layout units already
|
|
2603
|
+
covered by other UI (e.g. a nav bar); subtracted from the
|
|
2604
|
+
keyboard height before applying the shift.
|
|
2214
2605
|
style: Style dict (or list of dicts).
|
|
2215
2606
|
key: Stable identity for keyed reconciliation.
|
|
2216
2607
|
|
|
2217
2608
|
Returns:
|
|
2218
|
-
An [`Element`][pythonnative.Element]
|
|
2219
|
-
``"KeyboardAvoidingView"
|
|
2609
|
+
An [`Element`][pythonnative.Element] that renders a
|
|
2610
|
+
``"KeyboardAvoidingView"`` container.
|
|
2220
2611
|
"""
|
|
2221
|
-
return
|
|
2222
|
-
|
|
2223
|
-
*children,
|
|
2224
|
-
style=style,
|
|
2225
|
-
key=key,
|
|
2612
|
+
return _KeyboardAvoidingContainer(
|
|
2613
|
+
children=list(children),
|
|
2226
2614
|
behavior=behavior,
|
|
2615
|
+
keyboard_vertical_offset=keyboard_vertical_offset,
|
|
2616
|
+
style=resolve_style(style),
|
|
2617
|
+
key=key,
|
|
2227
2618
|
)
|
|
2228
2619
|
|
|
2229
2620
|
|
|
@@ -2291,7 +2682,10 @@ def Picker(
|
|
|
2291
2682
|
accessibility_label: Optional[str] = None,
|
|
2292
2683
|
accessibility_hint: Optional[str] = None,
|
|
2293
2684
|
accessible: Optional[bool] = None,
|
|
2294
|
-
|
|
2685
|
+
accessibility_state: Optional[AccessibilityState] = None,
|
|
2686
|
+
accessibility_live_region: Optional[Literal["none", "polite", "assertive"]] = None,
|
|
2687
|
+
test_id: Optional[str] = None,
|
|
2688
|
+
ref: Optional[Ref] = None,
|
|
2295
2689
|
key: Optional[str] = None,
|
|
2296
2690
|
) -> Element:
|
|
2297
2691
|
"""A real native dropdown / select widget.
|
|
@@ -2314,7 +2708,17 @@ def Picker(
|
|
|
2314
2708
|
accessibility_label: Spoken description for screen readers.
|
|
2315
2709
|
accessibility_hint: Spoken extra detail (iOS only).
|
|
2316
2710
|
accessible: Override whether the element is exposed to AT.
|
|
2317
|
-
|
|
2711
|
+
accessibility_state: Current widget state for assistive tech,
|
|
2712
|
+
e.g. ``{"disabled": True, "selected": False}``. Recognized
|
|
2713
|
+
keys: ``disabled``, ``selected``, ``checked``, ``busy``,
|
|
2714
|
+
``expanded``.
|
|
2715
|
+
accessibility_live_region: How AT announces dynamic changes to
|
|
2716
|
+
this view: ``"none"``, ``"polite"``, or ``"assertive"``
|
|
2717
|
+
(Android only).
|
|
2718
|
+
test_id: Stable identifier for UI tests; exposed as
|
|
2719
|
+
``resource-id`` on Android and ``accessibilityIdentifier``
|
|
2720
|
+
on iOS.
|
|
2721
|
+
ref: Optional [`Ref`][pythonnative.Ref] from ``use_ref()``.
|
|
2318
2722
|
key: Stable identity for keyed reconciliation.
|
|
2319
2723
|
|
|
2320
2724
|
Returns:
|
|
@@ -2332,5 +2736,8 @@ def Picker(
|
|
|
2332
2736
|
accessibility_label=accessibility_label,
|
|
2333
2737
|
accessibility_hint=accessibility_hint,
|
|
2334
2738
|
accessible=accessible,
|
|
2739
|
+
accessibility_state=accessibility_state,
|
|
2740
|
+
accessibility_live_region=accessibility_live_region,
|
|
2741
|
+
test_id=test_id,
|
|
2335
2742
|
_defaults={"accessibility_role": "button"},
|
|
2336
2743
|
)
|