pythonnative 0.20.0__py3-none-any.whl → 0.22.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 +14 -3
- pythonnative/animated.py +420 -135
- pythonnative/cli/pn.py +450 -956
- pythonnative/components.py +519 -235
- pythonnative/events.py +210 -0
- pythonnative/gestures.py +875 -0
- pythonnative/layout.py +463 -149
- pythonnative/mutations.py +130 -0
- pythonnative/native_views/__init__.py +161 -97
- pythonnative/native_views/android.py +1050 -1124
- pythonnative/native_views/base.py +108 -18
- pythonnative/native_views/desktop.py +460 -417
- pythonnative/native_views/ios.py +1918 -1916
- pythonnative/project/__init__.py +68 -0
- pythonnative/project/android.py +504 -0
- pythonnative/project/builder.py +555 -0
- pythonnative/project/config.py +642 -0
- pythonnative/project/doctor.py +233 -0
- pythonnative/project/icons.py +247 -0
- pythonnative/project/ios.py +344 -0
- pythonnative/project/permissions.py +343 -0
- pythonnative/project/runtime_assets.py +272 -0
- pythonnative/reconciler.py +540 -470
- pythonnative/screen.py +5 -2
- pythonnative/sdk/_components.py +2 -2
- pythonnative/templates/android_template/app/build.gradle +2 -0
- {pythonnative-0.20.0.dist-info → pythonnative-0.22.0.dist-info}/METADATA +10 -2
- {pythonnative-0.20.0.dist-info → pythonnative-0.22.0.dist-info}/RECORD +32 -21
- pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/PNVirtualListView.java +0 -129
- {pythonnative-0.20.0.dist-info → pythonnative-0.22.0.dist-info}/WHEEL +0 -0
- {pythonnative-0.20.0.dist-info → pythonnative-0.22.0.dist-info}/entry_points.txt +0 -0
- {pythonnative-0.20.0.dist-info → pythonnative-0.22.0.dist-info}/licenses/LICENSE +0 -0
- {pythonnative-0.20.0.dist-info → pythonnative-0.22.0.dist-info}/top_level.txt +0 -0
pythonnative/components.py
CHANGED
|
@@ -24,10 +24,12 @@ Example:
|
|
|
24
24
|
```
|
|
25
25
|
"""
|
|
26
26
|
|
|
27
|
+
import bisect
|
|
27
28
|
from dataclasses import dataclass, field
|
|
28
|
-
from typing import Any, Callable, Dict, List, Literal, Optional
|
|
29
|
+
from typing import Any, Callable, Dict, List, Literal, Optional, Tuple
|
|
29
30
|
|
|
30
31
|
from .element import Element
|
|
32
|
+
from .hooks import component, use_effect, use_ref, use_state
|
|
31
33
|
from .sdk import Props
|
|
32
34
|
from .style import (
|
|
33
35
|
AutoCapitalize,
|
|
@@ -180,6 +182,7 @@ class SwitchProps(Props):
|
|
|
180
182
|
|
|
181
183
|
value: bool = False
|
|
182
184
|
on_change: Optional[Callable[[bool], None]] = None
|
|
185
|
+
accessibility_label: Optional[str] = None
|
|
183
186
|
|
|
184
187
|
|
|
185
188
|
@dataclass(frozen=True)
|
|
@@ -230,12 +233,14 @@ class SliderProps(Props):
|
|
|
230
233
|
min_value: float = 0.0
|
|
231
234
|
max_value: float = 1.0
|
|
232
235
|
on_change: Optional[Callable[[float], None]] = None
|
|
236
|
+
accessibility_label: Optional[str] = None
|
|
233
237
|
|
|
234
238
|
|
|
235
239
|
@dataclass(frozen=True)
|
|
236
240
|
class ViewProps(Props):
|
|
237
241
|
"""Props for [`View`][pythonnative.View], [`Column`][pythonnative.Column], and [`Row`][pythonnative.Row]."""
|
|
238
242
|
|
|
243
|
+
gestures: Optional[List[Any]] = None
|
|
239
244
|
accessibility_label: Optional[str] = None
|
|
240
245
|
accessibility_hint: Optional[str] = None
|
|
241
246
|
accessibility_role: Optional[str] = None
|
|
@@ -244,11 +249,15 @@ class ViewProps(Props):
|
|
|
244
249
|
|
|
245
250
|
@dataclass(frozen=True)
|
|
246
251
|
class ScrollViewProps(Props):
|
|
247
|
-
"""Props for [`ScrollView`][pythonnative.ScrollView].
|
|
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
|
+
"""
|
|
248
257
|
|
|
249
258
|
refresh_control: Optional[Dict[str, Any]] = None
|
|
250
259
|
scroll_axis: Optional[Literal["vertical", "horizontal"]] = None
|
|
251
|
-
on_scroll: Optional[Callable[[
|
|
260
|
+
on_scroll: Optional[Callable[[Dict[str, float]], None]] = None
|
|
252
261
|
shows_scroll_indicator: bool = True
|
|
253
262
|
paging_enabled: bool = False
|
|
254
263
|
bounces: bool = True
|
|
@@ -281,7 +290,10 @@ class PressableProps(Props):
|
|
|
281
290
|
|
|
282
291
|
on_press: Optional[Callable[[], None]] = None
|
|
283
292
|
on_long_press: Optional[Callable[[], None]] = None
|
|
293
|
+
on_press_in: Optional[Callable[[], None]] = None
|
|
294
|
+
on_press_out: Optional[Callable[[], None]] = None
|
|
284
295
|
pressed_opacity: float = 0.6
|
|
296
|
+
gestures: Optional[List[Any]] = None
|
|
285
297
|
accessibility_label: Optional[str] = None
|
|
286
298
|
accessibility_hint: Optional[str] = None
|
|
287
299
|
accessibility_role: Optional[str] = None
|
|
@@ -656,6 +668,7 @@ def Switch(
|
|
|
656
668
|
*,
|
|
657
669
|
value: bool = False,
|
|
658
670
|
on_change: Optional[Callable[[bool], None]] = None,
|
|
671
|
+
accessibility_label: Optional[str] = None,
|
|
659
672
|
style: StyleProp = None,
|
|
660
673
|
key: Optional[str] = None,
|
|
661
674
|
) -> Element:
|
|
@@ -664,6 +677,8 @@ def Switch(
|
|
|
664
677
|
Args:
|
|
665
678
|
value: Current on/off state.
|
|
666
679
|
on_change: Callback invoked with the new boolean state.
|
|
680
|
+
accessibility_label: Label exposed to assistive technology (and
|
|
681
|
+
UI test drivers) for the switch.
|
|
667
682
|
style: Style dict (or list of dicts).
|
|
668
683
|
key: Stable identity for keyed reconciliation.
|
|
669
684
|
|
|
@@ -676,6 +691,7 @@ def Switch(
|
|
|
676
691
|
key=key,
|
|
677
692
|
value=value,
|
|
678
693
|
on_change=on_change,
|
|
694
|
+
accessibility_label=accessibility_label,
|
|
679
695
|
)
|
|
680
696
|
|
|
681
697
|
|
|
@@ -839,6 +855,7 @@ def Slider(
|
|
|
839
855
|
min_value: float = 0.0,
|
|
840
856
|
max_value: float = 1.0,
|
|
841
857
|
on_change: Optional[Callable[[float], None]] = None,
|
|
858
|
+
accessibility_label: Optional[str] = None,
|
|
842
859
|
style: StyleProp = None,
|
|
843
860
|
key: Optional[str] = None,
|
|
844
861
|
) -> Element:
|
|
@@ -850,6 +867,8 @@ def Slider(
|
|
|
850
867
|
max_value: Upper bound.
|
|
851
868
|
on_change: Callback invoked with the new value as the user
|
|
852
869
|
drags.
|
|
870
|
+
accessibility_label: Label exposed to assistive technology (and
|
|
871
|
+
UI test drivers) for the slider.
|
|
853
872
|
style: Style dict (or list of dicts).
|
|
854
873
|
key: Stable identity for keyed reconciliation.
|
|
855
874
|
|
|
@@ -864,6 +883,7 @@ def Slider(
|
|
|
864
883
|
min_value=min_value,
|
|
865
884
|
max_value=max_value,
|
|
866
885
|
on_change=on_change,
|
|
886
|
+
accessibility_label=accessibility_label,
|
|
867
887
|
)
|
|
868
888
|
|
|
869
889
|
|
|
@@ -875,6 +895,7 @@ def Slider(
|
|
|
875
895
|
def View(
|
|
876
896
|
*children: Element,
|
|
877
897
|
style: StyleProp = None,
|
|
898
|
+
gestures: Optional[List[Any]] = None,
|
|
878
899
|
accessibility_label: Optional[str] = None,
|
|
879
900
|
accessibility_hint: Optional[str] = None,
|
|
880
901
|
accessibility_role: Optional[str] = None,
|
|
@@ -890,20 +911,30 @@ def View(
|
|
|
890
911
|
|
|
891
912
|
- ``flex_direction``: ``"column"`` (default), ``"row"``,
|
|
892
913
|
``"column_reverse"``, ``"row_reverse"``.
|
|
914
|
+
- ``flex_wrap``: ``"nowrap"`` (default), ``"wrap"``,
|
|
915
|
+
``"wrap_reverse"`` — with ``align_content`` controlling how
|
|
916
|
+
wrapped lines share leftover cross-axis space.
|
|
893
917
|
- ``justify_content``: main-axis distribution. Accepts
|
|
894
918
|
``"flex_start"`` (default), ``"center"``, ``"flex_end"``,
|
|
895
919
|
``"space_between"``, ``"space_around"``, ``"space_evenly"``.
|
|
896
920
|
- ``align_items``: cross-axis alignment. Accepts ``"stretch"``
|
|
897
921
|
(default), ``"flex_start"``, ``"center"``, ``"flex_end"``.
|
|
922
|
+
- ``direction``: ``"ltr"`` (default) or ``"rtl"`` — flips rows and
|
|
923
|
+
resolves ``margin_start`` / ``padding_end`` / absolute ``start``
|
|
924
|
+
/ ``end`` insets.
|
|
898
925
|
- ``overflow``: ``"visible"`` (default) or ``"hidden"``.
|
|
899
|
-
- ``spacing
|
|
900
|
-
``
|
|
901
|
-
``
|
|
902
|
-
``
|
|
926
|
+
- ``spacing`` (alias ``gap``; per-axis ``row_gap`` /
|
|
927
|
+
``column_gap``), ``padding``, ``background_color``,
|
|
928
|
+
``border_radius``, ``border_width``, ``border_color``,
|
|
929
|
+
``shadow_color``, ``shadow_offset``, ``shadow_opacity``,
|
|
930
|
+
``shadow_radius``, ``elevation``, ``opacity``, ``transform``.
|
|
903
931
|
|
|
904
932
|
Args:
|
|
905
933
|
*children: Child elements rendered inside the container.
|
|
906
934
|
style: Style dict (or list of dicts).
|
|
935
|
+
gestures: Optional list of gesture descriptors from
|
|
936
|
+
`pythonnative.gestures` (e.g. ``[gestures.Pan(on_change=…)]``)
|
|
937
|
+
recognized natively on this view.
|
|
907
938
|
accessibility_label: Spoken description for screen readers.
|
|
908
939
|
accessibility_hint: Spoken extra detail (iOS only).
|
|
909
940
|
accessibility_role: Semantic role for assistive tech.
|
|
@@ -920,6 +951,7 @@ def View(
|
|
|
920
951
|
style=style,
|
|
921
952
|
ref=ref,
|
|
922
953
|
key=key,
|
|
954
|
+
gestures=gestures,
|
|
923
955
|
accessibility_label=accessibility_label,
|
|
924
956
|
accessibility_hint=accessibility_hint,
|
|
925
957
|
accessibility_role=accessibility_role,
|
|
@@ -994,7 +1026,7 @@ def ScrollView(
|
|
|
994
1026
|
*children: Element,
|
|
995
1027
|
refresh_control: Optional[Dict[str, Any]] = None,
|
|
996
1028
|
scroll_axis: Optional[Literal["vertical", "horizontal"]] = None,
|
|
997
|
-
on_scroll: Optional[Callable[[
|
|
1029
|
+
on_scroll: Optional[Callable[[Dict[str, float]], None]] = None,
|
|
998
1030
|
shows_scroll_indicator: bool = True,
|
|
999
1031
|
paging_enabled: bool = False,
|
|
1000
1032
|
bounces: bool = True,
|
|
@@ -1019,8 +1051,8 @@ def ScrollView(
|
|
|
1019
1051
|
must have ``refreshing`` (bool) and ``on_refresh``
|
|
1020
1052
|
(callable).
|
|
1021
1053
|
scroll_axis: ``"vertical"`` (default) or ``"horizontal"``.
|
|
1022
|
-
on_scroll: Callback invoked with ``
|
|
1023
|
-
the user scrolls.
|
|
1054
|
+
on_scroll: Callback invoked with ``{"x": …, "y": …}`` content
|
|
1055
|
+
offsets as the user scrolls.
|
|
1024
1056
|
shows_scroll_indicator: When ``False``, hides the scroll bar.
|
|
1025
1057
|
paging_enabled: When ``True``, the scroll view snaps to
|
|
1026
1058
|
multiples of its own size (carousel behavior).
|
|
@@ -1147,15 +1179,19 @@ def Pressable(
|
|
|
1147
1179
|
*children: Element,
|
|
1148
1180
|
on_press: Optional[Callable[[], None]] = None,
|
|
1149
1181
|
on_long_press: Optional[Callable[[], None]] = None,
|
|
1182
|
+
on_press_in: Optional[Callable[[], None]] = None,
|
|
1183
|
+
on_press_out: Optional[Callable[[], None]] = None,
|
|
1150
1184
|
pressed_opacity: float = 0.6,
|
|
1185
|
+
gestures: Optional[List[Any]] = None,
|
|
1151
1186
|
style: StyleProp = None,
|
|
1152
1187
|
accessibility_label: Optional[str] = None,
|
|
1153
1188
|
accessibility_hint: Optional[str] = None,
|
|
1154
1189
|
accessibility_role: Optional[str] = None,
|
|
1155
1190
|
accessible: Optional[bool] = None,
|
|
1191
|
+
ref: Optional[Dict[str, Any]] = None,
|
|
1156
1192
|
key: Optional[str] = None,
|
|
1157
1193
|
) -> Element:
|
|
1158
|
-
"""Wrap children with tap
|
|
1194
|
+
"""Wrap children with tap / long-press / gesture handlers.
|
|
1159
1195
|
|
|
1160
1196
|
Useful for making non-button elements (text, images, custom views)
|
|
1161
1197
|
respond to user taps. The wrapper view fades to ``pressed_opacity``
|
|
@@ -1167,13 +1203,19 @@ def Pressable(
|
|
|
1167
1203
|
*children: Elements to make pressable.
|
|
1168
1204
|
on_press: Callback invoked on a normal tap.
|
|
1169
1205
|
on_long_press: Callback invoked on a sustained press.
|
|
1206
|
+
on_press_in: Callback invoked the moment the press starts.
|
|
1207
|
+
on_press_out: Callback invoked when the press lifts or cancels.
|
|
1170
1208
|
pressed_opacity: Opacity (0–1) applied while the user's finger
|
|
1171
1209
|
is down. Set to ``1.0`` for no visual feedback.
|
|
1210
|
+
gestures: Optional list of gesture descriptors from
|
|
1211
|
+
`pythonnative.gestures` recognized natively on this view
|
|
1212
|
+
(pan / swipe / pinch / rotation / multi-tap).
|
|
1172
1213
|
style: Style dict applied to the wrapper.
|
|
1173
1214
|
accessibility_label: Spoken description for screen readers.
|
|
1174
1215
|
accessibility_hint: Spoken extra detail (iOS only).
|
|
1175
1216
|
accessibility_role: Override the default ``"button"`` role.
|
|
1176
1217
|
accessible: Override whether the element is exposed to AT.
|
|
1218
|
+
ref: Optional ``use_ref()`` dict.
|
|
1177
1219
|
key: Stable identity for keyed reconciliation.
|
|
1178
1220
|
|
|
1179
1221
|
Returns:
|
|
@@ -1183,10 +1225,14 @@ def Pressable(
|
|
|
1183
1225
|
"Pressable",
|
|
1184
1226
|
*children,
|
|
1185
1227
|
style=style,
|
|
1228
|
+
ref=ref,
|
|
1186
1229
|
key=key,
|
|
1187
1230
|
on_press=on_press,
|
|
1188
1231
|
on_long_press=on_long_press,
|
|
1232
|
+
on_press_in=on_press_in,
|
|
1233
|
+
on_press_out=on_press_out,
|
|
1189
1234
|
pressed_opacity=pressed_opacity,
|
|
1235
|
+
gestures=gestures,
|
|
1190
1236
|
accessibility_label=accessibility_label,
|
|
1191
1237
|
accessibility_hint=accessibility_hint,
|
|
1192
1238
|
accessibility_role=accessibility_role,
|
|
@@ -1554,8 +1600,245 @@ def ErrorBoundary(
|
|
|
1554
1600
|
|
|
1555
1601
|
|
|
1556
1602
|
# ======================================================================
|
|
1557
|
-
# Lists
|
|
1603
|
+
# Lists (Python-windowed virtualization over ScrollView)
|
|
1558
1604
|
# ======================================================================
|
|
1605
|
+
#
|
|
1606
|
+
# FlatList and SectionList are pure Python components, not native
|
|
1607
|
+
# elements. They render a windowed slice of rows into a ScrollView —
|
|
1608
|
+
# leading spacer, visible rows, trailing spacer — and shift the window
|
|
1609
|
+
# from scroll events (the same architecture as React Native's
|
|
1610
|
+
# VirtualizedList). Because every windowed row lives in the *main*
|
|
1611
|
+
# layout tree, rows may be any height: estimates only steer the spacer
|
|
1612
|
+
# sizes, and each row's measured extent is fed back from the layout
|
|
1613
|
+
# pass through its ref to correct the estimates over time.
|
|
1614
|
+
|
|
1615
|
+
_DEFAULT_ROW_EXTENT = 44.0
|
|
1616
|
+
|
|
1617
|
+
|
|
1618
|
+
class _RowSpec:
|
|
1619
|
+
"""One virtualized row: a stable key, a lazy renderer, and an extent hint."""
|
|
1620
|
+
|
|
1621
|
+
__slots__ = ("key", "make", "extent", "item", "index")
|
|
1622
|
+
|
|
1623
|
+
def __init__(
|
|
1624
|
+
self,
|
|
1625
|
+
key: str,
|
|
1626
|
+
make: Callable[[], Element],
|
|
1627
|
+
extent: Optional[float],
|
|
1628
|
+
item: Any = None,
|
|
1629
|
+
index: int = 0,
|
|
1630
|
+
) -> None:
|
|
1631
|
+
self.key = key
|
|
1632
|
+
self.make = make
|
|
1633
|
+
self.extent = extent
|
|
1634
|
+
self.item = item
|
|
1635
|
+
self.index = index
|
|
1636
|
+
|
|
1637
|
+
|
|
1638
|
+
def _dispatch_scroll_command(scroll_ref: Any, name: str, args: Dict[str, Any]) -> Any:
|
|
1639
|
+
"""Send an imperative command to the ScrollView under ``scroll_ref``."""
|
|
1640
|
+
tag = scroll_ref.get("_pn_tag") if isinstance(scroll_ref, dict) else None
|
|
1641
|
+
if tag is None:
|
|
1642
|
+
return None
|
|
1643
|
+
from .native_views import get_registry
|
|
1644
|
+
|
|
1645
|
+
try:
|
|
1646
|
+
return get_registry().command(tag, name, args)
|
|
1647
|
+
except Exception:
|
|
1648
|
+
return None
|
|
1649
|
+
|
|
1650
|
+
|
|
1651
|
+
@component
|
|
1652
|
+
def _VirtualizedList(**p: Any) -> Element:
|
|
1653
|
+
"""Shared windowing engine behind FlatList and SectionList."""
|
|
1654
|
+
rows: List[_RowSpec] = p.get("rows") or []
|
|
1655
|
+
n = len(rows)
|
|
1656
|
+
horizontal: bool = bool(p.get("horizontal"))
|
|
1657
|
+
estimated: float = float(p.get("estimated_row_extent") or _DEFAULT_ROW_EXTENT)
|
|
1658
|
+
overscan: float = float(p.get("overscan_extent") or 0.0)
|
|
1659
|
+
initial_extent: float = float(p.get("initial_window_extent") or 800.0)
|
|
1660
|
+
|
|
1661
|
+
window, set_window = use_state((0, -1))
|
|
1662
|
+
measured = use_ref({}) # row key -> measured extent (points)
|
|
1663
|
+
row_refs = use_ref({}) # row key -> ref dict for live rows
|
|
1664
|
+
end_latch = use_ref({"fired_for": -1})
|
|
1665
|
+
viewable_ref = use_ref({"keys": ()})
|
|
1666
|
+
scroll_pos = use_ref({"offset": 0.0})
|
|
1667
|
+
sv_ref = use_ref(None)
|
|
1668
|
+
|
|
1669
|
+
# ------------------------------------------------------------------
|
|
1670
|
+
# Extent model: measured > per-row hint > estimate. ``starts`` are
|
|
1671
|
+
# prefix sums; ``starts[n]`` is the total content extent.
|
|
1672
|
+
# ------------------------------------------------------------------
|
|
1673
|
+
measured_map: Dict[str, float] = measured["current"]
|
|
1674
|
+
starts: List[float] = [0.0] * (n + 1)
|
|
1675
|
+
acc = 0.0
|
|
1676
|
+
for i, spec in enumerate(rows):
|
|
1677
|
+
starts[i] = acc
|
|
1678
|
+
extent = measured_map.get(spec.key)
|
|
1679
|
+
if extent is None:
|
|
1680
|
+
extent = spec.extent if spec.extent is not None else estimated
|
|
1681
|
+
acc += max(0.0, float(extent))
|
|
1682
|
+
starts[n] = acc
|
|
1683
|
+
total_extent = acc
|
|
1684
|
+
|
|
1685
|
+
def _viewport_extent() -> float:
|
|
1686
|
+
frame = sv_ref.get("_pn_frame") if isinstance(sv_ref, dict) else None
|
|
1687
|
+
if frame:
|
|
1688
|
+
extent = frame[2] if horizontal else frame[3]
|
|
1689
|
+
if extent and extent > 0:
|
|
1690
|
+
return float(extent)
|
|
1691
|
+
return initial_extent
|
|
1692
|
+
|
|
1693
|
+
def _window_for(offset: float, viewport: float) -> Tuple[int, int]:
|
|
1694
|
+
if n == 0:
|
|
1695
|
+
return (0, -1)
|
|
1696
|
+
pad = overscan if overscan > 0 else viewport
|
|
1697
|
+
lo = max(0.0, offset - pad)
|
|
1698
|
+
hi = offset + viewport + pad
|
|
1699
|
+
first = max(0, bisect.bisect_right(starts, lo, 0, n) - 1)
|
|
1700
|
+
last = min(n - 1, bisect.bisect_left(starts, hi, 0, n))
|
|
1701
|
+
return (first, last)
|
|
1702
|
+
|
|
1703
|
+
first, last = window
|
|
1704
|
+
if last < 0 or first >= n:
|
|
1705
|
+
first, last = _window_for(scroll_pos["current"]["offset"], _viewport_extent())
|
|
1706
|
+
last = min(last, n - 1)
|
|
1707
|
+
first = max(0, min(first, max(0, n - 1)))
|
|
1708
|
+
|
|
1709
|
+
# ------------------------------------------------------------------
|
|
1710
|
+
# Scroll handling: sweep measured extents, shift the window, fire
|
|
1711
|
+
# end-reached / viewability callbacks. State only changes when the
|
|
1712
|
+
# window actually moves, so steady scrolling inside the overscan
|
|
1713
|
+
# region costs no re-render.
|
|
1714
|
+
# ------------------------------------------------------------------
|
|
1715
|
+
on_end_reached = p.get("on_end_reached")
|
|
1716
|
+
end_threshold = float(p.get("on_end_reached_threshold") or 0.5)
|
|
1717
|
+
on_viewable = p.get("on_viewable_items_changed")
|
|
1718
|
+
user_on_scroll = p.get("on_scroll")
|
|
1719
|
+
|
|
1720
|
+
def _sweep_measured() -> None:
|
|
1721
|
+
for row_key, ref in row_refs["current"].items():
|
|
1722
|
+
frame = ref.get("_pn_frame") if isinstance(ref, dict) else None
|
|
1723
|
+
if frame:
|
|
1724
|
+
extent = frame[2] if horizontal else frame[3]
|
|
1725
|
+
if extent and extent > 0:
|
|
1726
|
+
measured_map[row_key] = float(extent)
|
|
1727
|
+
|
|
1728
|
+
def _handle_scroll(payload: Any) -> None:
|
|
1729
|
+
if isinstance(payload, dict):
|
|
1730
|
+
offset = float(payload.get("x" if horizontal else "y", 0.0) or 0.0)
|
|
1731
|
+
else:
|
|
1732
|
+
offset = float(payload or 0.0)
|
|
1733
|
+
scroll_pos["current"]["offset"] = offset
|
|
1734
|
+
_sweep_measured()
|
|
1735
|
+
viewport = _viewport_extent()
|
|
1736
|
+
|
|
1737
|
+
new_window = _window_for(offset, viewport)
|
|
1738
|
+
if new_window != (first, last):
|
|
1739
|
+
set_window(new_window)
|
|
1740
|
+
|
|
1741
|
+
if on_end_reached is not None and total_extent > 0:
|
|
1742
|
+
remaining = total_extent - (offset + viewport)
|
|
1743
|
+
if remaining <= end_threshold * viewport:
|
|
1744
|
+
if end_latch["current"]["fired_for"] != n:
|
|
1745
|
+
end_latch["current"]["fired_for"] = n
|
|
1746
|
+
on_end_reached()
|
|
1747
|
+
elif remaining > end_threshold * viewport + viewport:
|
|
1748
|
+
end_latch["current"]["fired_for"] = -1
|
|
1749
|
+
|
|
1750
|
+
if on_viewable is not None and n > 0:
|
|
1751
|
+
v_first = max(0, bisect.bisect_right(starts, offset, 0, n) - 1)
|
|
1752
|
+
v_last = min(n - 1, bisect.bisect_left(starts, offset + viewport, 0, n))
|
|
1753
|
+
keys = tuple(rows[i].key for i in range(v_first, v_last + 1))
|
|
1754
|
+
if keys != viewable_ref["current"]["keys"]:
|
|
1755
|
+
viewable_ref["current"]["keys"] = keys
|
|
1756
|
+
on_viewable(
|
|
1757
|
+
[
|
|
1758
|
+
{"index": rows[i].index, "key": rows[i].key, "item": rows[i].item}
|
|
1759
|
+
for i in range(v_first, v_last + 1)
|
|
1760
|
+
]
|
|
1761
|
+
)
|
|
1762
|
+
|
|
1763
|
+
if user_on_scroll is not None:
|
|
1764
|
+
user_on_scroll(payload)
|
|
1765
|
+
|
|
1766
|
+
# ------------------------------------------------------------------
|
|
1767
|
+
# Imperative controller (scroll_to_index / offset / end) exposed on
|
|
1768
|
+
# the user's ref dict. Re-attached every render so the closures see
|
|
1769
|
+
# fresh extents; the effect itself must run unconditionally to keep
|
|
1770
|
+
# hook order stable.
|
|
1771
|
+
# ------------------------------------------------------------------
|
|
1772
|
+
controller = p.get("controller_ref")
|
|
1773
|
+
|
|
1774
|
+
def _attach_controller() -> None:
|
|
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)
|
|
1785
|
+
|
|
1786
|
+
def scroll_to_end(animated: bool = True) -> None:
|
|
1787
|
+
scroll_to_offset(max(0.0, total_extent - _viewport_extent()), animated)
|
|
1788
|
+
|
|
1789
|
+
controller["scroll_to_offset"] = scroll_to_offset
|
|
1790
|
+
controller["scroll_to_index"] = scroll_to_index
|
|
1791
|
+
controller["scroll_to_end"] = scroll_to_end
|
|
1792
|
+
|
|
1793
|
+
use_effect(_attach_controller, None)
|
|
1794
|
+
|
|
1795
|
+
# ------------------------------------------------------------------
|
|
1796
|
+
# Children: header, leading spacer, windowed rows, trailing spacer,
|
|
1797
|
+
# footer. Rows keep per-key refs so their measured extents survive
|
|
1798
|
+
# recycling.
|
|
1799
|
+
# ------------------------------------------------------------------
|
|
1800
|
+
spacer_key = "width" if horizontal else "height"
|
|
1801
|
+
children: List[Element] = []
|
|
1802
|
+
header = p.get("header")
|
|
1803
|
+
footer = p.get("footer")
|
|
1804
|
+
if header is not None:
|
|
1805
|
+
children.append(View(header, key="__pn_header__"))
|
|
1806
|
+
|
|
1807
|
+
if n == 0:
|
|
1808
|
+
empty = p.get("empty")
|
|
1809
|
+
if empty is not None:
|
|
1810
|
+
children.append(View(empty, key="__pn_empty__"))
|
|
1811
|
+
else:
|
|
1812
|
+
live_refs: Dict[str, Any] = {}
|
|
1813
|
+
lead = starts[first]
|
|
1814
|
+
if lead > 0:
|
|
1815
|
+
lead_style: Dict[str, Any] = {spacer_key: lead}
|
|
1816
|
+
children.append(View(style=lead_style, key="__pn_lead__"))
|
|
1817
|
+
for i in range(first, last + 1):
|
|
1818
|
+
spec = rows[i]
|
|
1819
|
+
row_ref = row_refs["current"].get(spec.key) or {"current": None}
|
|
1820
|
+
live_refs[spec.key] = row_ref
|
|
1821
|
+
children.append(View(spec.make(), ref=row_ref, key=spec.key))
|
|
1822
|
+
row_refs["current"] = live_refs
|
|
1823
|
+
trail = total_extent - starts[last + 1]
|
|
1824
|
+
if trail > 0:
|
|
1825
|
+
trail_style: Dict[str, Any] = {spacer_key: trail}
|
|
1826
|
+
children.append(View(style=trail_style, key="__pn_trail__"))
|
|
1827
|
+
|
|
1828
|
+
if footer is not None:
|
|
1829
|
+
children.append(View(footer, key="__pn_footer__"))
|
|
1830
|
+
|
|
1831
|
+
wrapper = Row if horizontal else Column
|
|
1832
|
+
inner = wrapper(*children, style=p.get("content_container_style"))
|
|
1833
|
+
return ScrollView(
|
|
1834
|
+
inner,
|
|
1835
|
+
scroll_axis="horizontal" if horizontal else "vertical",
|
|
1836
|
+
on_scroll=_handle_scroll,
|
|
1837
|
+
refresh_control=p.get("refresh_control"),
|
|
1838
|
+
shows_scroll_indicator=p.get("shows_scroll_indicator", True),
|
|
1839
|
+
style=p.get("list_style"),
|
|
1840
|
+
ref=sv_ref,
|
|
1841
|
+
)
|
|
1559
1842
|
|
|
1560
1843
|
|
|
1561
1844
|
def FlatList(
|
|
@@ -1564,9 +1847,10 @@ def FlatList(
|
|
|
1564
1847
|
render_item: Optional[Callable[[Any, int], Element]] = None,
|
|
1565
1848
|
key_extractor: Optional[Callable[[Any, int], str]] = None,
|
|
1566
1849
|
item_height: Optional[float] = None,
|
|
1850
|
+
get_item_height: Optional[Callable[[Any, int], float]] = None,
|
|
1851
|
+
estimated_item_height: Optional[float] = None,
|
|
1567
1852
|
separator_height: float = 0,
|
|
1568
1853
|
refresh_control: Optional[Dict[str, Any]] = None,
|
|
1569
|
-
on_item_press: Optional[Callable[[int], None]] = None,
|
|
1570
1854
|
horizontal: bool = False,
|
|
1571
1855
|
num_columns: int = 1,
|
|
1572
1856
|
list_header: Optional[Element] = None,
|
|
@@ -1574,61 +1858,68 @@ def FlatList(
|
|
|
1574
1858
|
list_empty: Optional[Element] = None,
|
|
1575
1859
|
on_end_reached: Optional[Callable[[], None]] = None,
|
|
1576
1860
|
on_end_reached_threshold: float = 0.5,
|
|
1861
|
+
on_viewable_items_changed: Optional[Callable[[List[Dict[str, Any]]], None]] = None,
|
|
1862
|
+
on_scroll: Optional[Callable[[Dict[str, float]], None]] = None,
|
|
1863
|
+
shows_scroll_indicator: bool = True,
|
|
1577
1864
|
content_container_style: StyleProp = None,
|
|
1578
1865
|
style: StyleProp = None,
|
|
1866
|
+
ref: Optional[Dict[str, Any]] = None,
|
|
1579
1867
|
key: Optional[str] = None,
|
|
1580
1868
|
) -> Element:
|
|
1581
1869
|
"""Virtualized scrollable list that renders items from ``data`` lazily.
|
|
1582
1870
|
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
``
|
|
1871
|
+
Only the rows inside (and just beyond) the viewport are mounted;
|
|
1872
|
+
leading and trailing spacers stand in for everything else, and the
|
|
1873
|
+
window shifts as the user scrolls. Rows may have **variable
|
|
1874
|
+
heights**: pass ``item_height`` when rows are uniform,
|
|
1875
|
+
``get_item_height`` for exact per-item extents, or nothing at all —
|
|
1876
|
+
unknown rows start at ``estimated_item_height`` and are corrected
|
|
1877
|
+
with their measured extent once they've been on screen.
|
|
1588
1878
|
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1879
|
+
The ``ref`` dict (from [`use_ref`][pythonnative.use_ref]) is
|
|
1880
|
+
populated with an imperative controller:
|
|
1881
|
+
``ref["scroll_to_index"](i)``, ``ref["scroll_to_offset"](pts)``,
|
|
1882
|
+
and ``ref["scroll_to_end"]()``.
|
|
1593
1883
|
|
|
1594
1884
|
Args:
|
|
1595
|
-
data:
|
|
1596
|
-
render_item:
|
|
1597
|
-
[`
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
item_height:
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
refresh_control: Optional
|
|
1607
|
-
callable}`` for pull-to-refresh; see
|
|
1885
|
+
data: List of arbitrary item values.
|
|
1886
|
+
render_item: ``render_item(item, index) -> Element``. Defaults
|
|
1887
|
+
to wrapping each item in a [`Text`][pythonnative.Text].
|
|
1888
|
+
key_extractor: Function returning a stable key per item
|
|
1889
|
+
(recommended whenever ``data`` can reorder).
|
|
1890
|
+
item_height: Uniform row extent in points, when known.
|
|
1891
|
+
get_item_height: ``get_item_height(item, index) -> float`` for
|
|
1892
|
+
exact variable extents without measurement.
|
|
1893
|
+
estimated_item_height: Starting extent estimate for rows whose
|
|
1894
|
+
true size isn't known yet (default 44).
|
|
1895
|
+
separator_height: Gap below each row, in points.
|
|
1896
|
+
refresh_control: Optional pull-to-refresh spec from
|
|
1608
1897
|
[`RefreshControl`][pythonnative.RefreshControl].
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1898
|
+
horizontal: Scroll horizontally (extents become widths).
|
|
1899
|
+
num_columns: Render items in a grid of this many columns.
|
|
1900
|
+
list_header: Element rendered once before all rows.
|
|
1901
|
+
list_footer: Element rendered once after all rows.
|
|
1902
|
+
list_empty: Element rendered when ``data`` is empty.
|
|
1903
|
+
on_end_reached: Called when the user scrolls within
|
|
1904
|
+
``on_end_reached_threshold`` viewports of the end (fires
|
|
1905
|
+
once per data length).
|
|
1906
|
+
on_end_reached_threshold: Distance from the end, in viewport
|
|
1907
|
+
multiples, at which ``on_end_reached`` fires.
|
|
1908
|
+
on_viewable_items_changed: Called with a list of
|
|
1909
|
+
``{"index", "key", "item"}`` dicts whenever the set of
|
|
1910
|
+
visible rows changes.
|
|
1911
|
+
on_scroll: Called with the raw scroll payload
|
|
1912
|
+
(``{"x": …, "y": …}``).
|
|
1913
|
+
shows_scroll_indicator: When ``False``, hides the scroll bar.
|
|
1623
1914
|
content_container_style: Style applied to the inner content
|
|
1624
|
-
wrapper
|
|
1625
|
-
style: Style
|
|
1626
|
-
|
|
1627
|
-
|
|
1915
|
+
wrapper.
|
|
1916
|
+
style: Style for the outer scroll container.
|
|
1917
|
+
ref: Optional ``use_ref()`` dict; receives the scroll
|
|
1918
|
+
controller functions.
|
|
1919
|
+
key: Stable identity for keyed reconciliation of the list.
|
|
1628
1920
|
|
|
1629
1921
|
Returns:
|
|
1630
|
-
|
|
1631
|
-
(virtualized) or ``"ScrollView"`` (eager fallback).
|
|
1922
|
+
A virtualized list element (a function component instance).
|
|
1632
1923
|
|
|
1633
1924
|
Example:
|
|
1634
1925
|
```python
|
|
@@ -1645,122 +1936,81 @@ def FlatList(
|
|
|
1645
1936
|
```
|
|
1646
1937
|
"""
|
|
1647
1938
|
items_list = list(data or [])
|
|
1939
|
+
sep = float(separator_height or 0.0)
|
|
1648
1940
|
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
)
|
|
1657
|
-
|
|
1658
|
-
if item_height is None or has_ornaments:
|
|
1659
|
-
# Eager fallback for short lists, grids, and lists with
|
|
1660
|
-
# header/footer/empty ornaments (which the fixed-height
|
|
1661
|
-
# virtualizer can't express).
|
|
1662
|
-
rendered: List[Element] = []
|
|
1663
|
-
for i, item in enumerate(items_list):
|
|
1664
|
-
el = render_item(item, i) if render_item else Text(str(item))
|
|
1665
|
-
if key_extractor is not None:
|
|
1666
|
-
el = Element(el.type, el.props, el.children, key=key_extractor(item, i))
|
|
1667
|
-
rendered.append(el)
|
|
1668
|
-
|
|
1669
|
-
sep = separator_height or None
|
|
1670
|
-
|
|
1671
|
-
if not has_ornaments:
|
|
1672
|
-
# Backward-compatible shape: ScrollView wrapping a single
|
|
1673
|
-
# Column of the rendered rows.
|
|
1674
|
-
inner = Column(*rendered, style={"spacing": sep} if sep else None)
|
|
1675
|
-
return ScrollView(inner, refresh_control=refresh_control, style=style, key=key)
|
|
1676
|
-
|
|
1677
|
-
if not rendered and list_empty is not None:
|
|
1678
|
-
content: List[Element] = [list_empty]
|
|
1679
|
-
elif num_columns > 1:
|
|
1680
|
-
rows: List[Element] = []
|
|
1681
|
-
for start in range(0, len(rendered), num_columns):
|
|
1682
|
-
chunk = rendered[start : start + num_columns]
|
|
1683
|
-
rows.append(
|
|
1684
|
-
Row(
|
|
1685
|
-
*chunk,
|
|
1686
|
-
style={"spacing": separator_height, "flex": 1} if separator_height else {"flex": 1},
|
|
1687
|
-
key=f"row-{start}",
|
|
1688
|
-
)
|
|
1689
|
-
)
|
|
1690
|
-
content = rows
|
|
1691
|
-
elif horizontal:
|
|
1692
|
-
content = [Row(*rendered, style={"spacing": sep} if sep else None)]
|
|
1693
|
-
else:
|
|
1694
|
-
content = [Column(*rendered, style={"spacing": sep} if sep else None)]
|
|
1695
|
-
|
|
1696
|
-
body: List[Element] = []
|
|
1697
|
-
if list_header is not None:
|
|
1698
|
-
body.append(list_header)
|
|
1699
|
-
body.extend(content)
|
|
1700
|
-
if list_footer is not None:
|
|
1701
|
-
body.append(list_footer)
|
|
1702
|
-
|
|
1703
|
-
axis: Literal["vertical", "horizontal"] = "horizontal" if horizontal else "vertical"
|
|
1704
|
-
wrapper = Row if horizontal else Column
|
|
1705
|
-
inner = wrapper(*body, style=content_container_style)
|
|
1706
|
-
return ScrollView(
|
|
1707
|
-
inner,
|
|
1708
|
-
refresh_control=refresh_control,
|
|
1709
|
-
scroll_axis=axis,
|
|
1710
|
-
style=style,
|
|
1711
|
-
key=key,
|
|
1712
|
-
)
|
|
1713
|
-
|
|
1714
|
-
# Virtualized path: render_item is invoked lazily by the native
|
|
1715
|
-
# cell mount callback when each row scrolls into view.
|
|
1716
|
-
row_h = float(item_height) + float(separator_height)
|
|
1717
|
-
|
|
1718
|
-
def _mount_row(
|
|
1719
|
-
index: int,
|
|
1720
|
-
content_view: Any,
|
|
1721
|
-
cell_width: float = 0.0,
|
|
1722
|
-
cell_height: float = 0.0,
|
|
1723
|
-
) -> None:
|
|
1724
|
-
# Imported lazily so the components module stays importable in
|
|
1725
|
-
# off-device test environments.
|
|
1726
|
-
from .native_views import get_registry
|
|
1727
|
-
from .reconciler import Reconciler
|
|
1728
|
-
|
|
1729
|
-
try:
|
|
1730
|
-
item = items_list[index]
|
|
1731
|
-
except IndexError:
|
|
1732
|
-
return
|
|
1733
|
-
|
|
1734
|
-
element = render_item(item, index) if render_item else Text(str(item))
|
|
1735
|
-
backend = get_registry()
|
|
1736
|
-
reconciler = Reconciler(backend)
|
|
1737
|
-
native_root = reconciler.mount(element)
|
|
1941
|
+
def _row_key(item: Any, index: int) -> str:
|
|
1942
|
+
if key_extractor is not None:
|
|
1943
|
+
try:
|
|
1944
|
+
return str(key_extractor(item, index))
|
|
1945
|
+
except Exception:
|
|
1946
|
+
pass
|
|
1947
|
+
return f"__pn_row_{index}__"
|
|
1738
1948
|
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
if layout_w <= 0:
|
|
1949
|
+
def _row_extent(item: Any, index: int) -> Optional[float]:
|
|
1950
|
+
if get_item_height is not None:
|
|
1742
1951
|
try:
|
|
1743
|
-
|
|
1744
|
-
layout_w = float(bounds.size.width)
|
|
1952
|
+
return float(get_item_height(item, index)) + sep
|
|
1745
1953
|
except Exception:
|
|
1746
|
-
|
|
1747
|
-
if
|
|
1748
|
-
|
|
1749
|
-
|
|
1954
|
+
return None
|
|
1955
|
+
if item_height is not None:
|
|
1956
|
+
return float(item_height) + sep
|
|
1957
|
+
return None
|
|
1958
|
+
|
|
1959
|
+
def _make_row(item: Any, index: int) -> Callable[[], Element]:
|
|
1960
|
+
def _make() -> Element:
|
|
1961
|
+
el = render_item(item, index) if render_item else Text(str(item))
|
|
1962
|
+
if sep > 0:
|
|
1963
|
+
pad_style: Dict[str, Any] = {"padding_end" if horizontal else "padding_bottom": sep}
|
|
1964
|
+
return View(el, style=pad_style)
|
|
1965
|
+
return el
|
|
1966
|
+
|
|
1967
|
+
return _make
|
|
1968
|
+
|
|
1969
|
+
rows: List[_RowSpec] = []
|
|
1970
|
+
if num_columns > 1 and not horizontal:
|
|
1971
|
+
for start in range(0, len(items_list), num_columns):
|
|
1972
|
+
chunk = items_list[start : start + num_columns]
|
|
1973
|
+
|
|
1974
|
+
def _make_group(group: List[Any] = chunk, base: int = start) -> Element:
|
|
1975
|
+
cells = [
|
|
1976
|
+
View(
|
|
1977
|
+
render_item(it, base + j) if render_item else Text(str(it)),
|
|
1978
|
+
style={"flex": 1},
|
|
1979
|
+
key=_row_key(it, base + j),
|
|
1980
|
+
)
|
|
1981
|
+
for j, it in enumerate(group)
|
|
1982
|
+
]
|
|
1983
|
+
row = Row(*cells)
|
|
1984
|
+
if sep > 0:
|
|
1985
|
+
return View(row, style={"padding_bottom": sep})
|
|
1986
|
+
return row
|
|
1987
|
+
|
|
1988
|
+
group_key = "__pn_grp_" + "|".join(_row_key(it, start + j) for j, it in enumerate(chunk))
|
|
1989
|
+
extent = (float(item_height) + sep) if item_height is not None else None
|
|
1990
|
+
rows.append(_RowSpec(group_key, _make_group, extent, item=chunk, index=start))
|
|
1991
|
+
else:
|
|
1992
|
+
for i, item in enumerate(items_list):
|
|
1993
|
+
rows.append(_RowSpec(_row_key(item, i), _make_row(item, i), _row_extent(item, i), item=item, index=i))
|
|
1750
1994
|
|
|
1751
|
-
|
|
1995
|
+
estimated = estimated_item_height if estimated_item_height is not None else (item_height or _DEFAULT_ROW_EXTENT)
|
|
1752
1996
|
|
|
1753
|
-
return
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1997
|
+
return _VirtualizedList(
|
|
1998
|
+
rows=rows,
|
|
1999
|
+
horizontal=horizontal,
|
|
2000
|
+
estimated_row_extent=float(estimated) + sep,
|
|
2001
|
+
header=list_header,
|
|
2002
|
+
footer=list_footer,
|
|
2003
|
+
empty=list_empty,
|
|
2004
|
+
refresh_control=refresh_control,
|
|
1761
2005
|
on_end_reached=on_end_reached,
|
|
1762
2006
|
on_end_reached_threshold=on_end_reached_threshold,
|
|
1763
|
-
|
|
2007
|
+
on_viewable_items_changed=on_viewable_items_changed,
|
|
2008
|
+
on_scroll=on_scroll,
|
|
2009
|
+
shows_scroll_indicator=shows_scroll_indicator,
|
|
2010
|
+
content_container_style=resolve_style(content_container_style) or None,
|
|
2011
|
+
list_style=resolve_style(style) or None,
|
|
2012
|
+
controller_ref=ref,
|
|
2013
|
+
key=key,
|
|
1764
2014
|
)
|
|
1765
2015
|
|
|
1766
2016
|
|
|
@@ -1769,99 +2019,133 @@ def SectionList(
|
|
|
1769
2019
|
sections: Optional[List[Dict[str, Any]]] = None,
|
|
1770
2020
|
render_item: Optional[Callable[[Any, int, int], Element]] = None,
|
|
1771
2021
|
render_section_header: Optional[Callable[[Dict[str, Any], int], Element]] = None,
|
|
2022
|
+
key_extractor: Optional[Callable[[Any, int], str]] = None,
|
|
1772
2023
|
item_height: Optional[float] = None,
|
|
1773
|
-
|
|
2024
|
+
get_item_height: Optional[Callable[[Any, int, int], float]] = None,
|
|
2025
|
+
estimated_item_height: Optional[float] = None,
|
|
2026
|
+
section_header_height: Optional[float] = None,
|
|
1774
2027
|
separator_height: float = 0,
|
|
2028
|
+
refresh_control: Optional[Dict[str, Any]] = None,
|
|
2029
|
+
list_header: Optional[Element] = None,
|
|
2030
|
+
list_footer: Optional[Element] = None,
|
|
2031
|
+
list_empty: Optional[Element] = None,
|
|
2032
|
+
on_end_reached: Optional[Callable[[], None]] = None,
|
|
2033
|
+
on_end_reached_threshold: float = 0.5,
|
|
2034
|
+
on_scroll: Optional[Callable[[Dict[str, float]], None]] = None,
|
|
1775
2035
|
style: StyleProp = None,
|
|
2036
|
+
ref: Optional[Dict[str, Any]] = None,
|
|
1776
2037
|
key: Optional[str] = None,
|
|
1777
2038
|
) -> Element:
|
|
1778
|
-
"""Virtualized list
|
|
2039
|
+
"""Virtualized list with section headers interleaved between row groups.
|
|
1779
2040
|
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
2041
|
+
Flattens ``sections`` into a single virtualized sequence where each
|
|
2042
|
+
entry is either a header or an item, then reuses the same windowing
|
|
2043
|
+
engine as [`FlatList`][pythonnative.FlatList] — headers and items
|
|
2044
|
+
may have different (and variable) heights.
|
|
1784
2045
|
|
|
1785
2046
|
Args:
|
|
1786
2047
|
sections: Each section is ``{"title": ..., "data": [...]}``.
|
|
1787
2048
|
render_item: ``render_item(item, item_index, section_index) ->
|
|
1788
2049
|
Element``.
|
|
1789
2050
|
render_section_header: ``render_section_header(section,
|
|
1790
|
-
section_index) -> Element``.
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
2051
|
+
section_index) -> Element``. Defaults to a bold
|
|
2052
|
+
[`Text`][pythonnative.Text] of the section title.
|
|
2053
|
+
key_extractor: Stable key per item: ``key_extractor(item,
|
|
2054
|
+
item_index) -> str``.
|
|
2055
|
+
item_height: Uniform item extent in points, when known.
|
|
2056
|
+
get_item_height: ``get_item_height(item, item_index,
|
|
2057
|
+
section_index) -> float`` for exact variable extents.
|
|
2058
|
+
estimated_item_height: Starting estimate for unmeasured rows.
|
|
2059
|
+
section_header_height: Header extent in points, when known.
|
|
2060
|
+
separator_height: Gap below each item, in points.
|
|
2061
|
+
refresh_control: Optional pull-to-refresh spec.
|
|
2062
|
+
list_header: Element rendered once before everything.
|
|
2063
|
+
list_footer: Element rendered once after everything.
|
|
2064
|
+
list_empty: Element rendered when there are no sections.
|
|
2065
|
+
on_end_reached: Called near the end of the content.
|
|
2066
|
+
on_end_reached_threshold: Distance from the end, in viewport
|
|
2067
|
+
multiples, at which ``on_end_reached`` fires.
|
|
2068
|
+
on_scroll: Called with the raw scroll payload.
|
|
2069
|
+
style: Style for the outer scroll container.
|
|
2070
|
+
ref: Optional ``use_ref()`` dict; receives the scroll
|
|
2071
|
+
controller functions.
|
|
2072
|
+
key: Stable identity for keyed reconciliation of the list.
|
|
1796
2073
|
|
|
1797
2074
|
Returns:
|
|
1798
|
-
|
|
1799
|
-
(virtualized). When ``item_height`` is omitted the layout falls
|
|
1800
|
-
back to an eager column.
|
|
2075
|
+
A virtualized list element (a function component instance).
|
|
1801
2076
|
"""
|
|
1802
2077
|
sections_list = list(sections or [])
|
|
2078
|
+
sep = float(separator_height or 0.0)
|
|
2079
|
+
|
|
2080
|
+
def _header_el(section: Dict[str, Any], s_idx: int) -> Element:
|
|
2081
|
+
if render_section_header is not None:
|
|
2082
|
+
return render_section_header(section, s_idx)
|
|
2083
|
+
return Text(str(section.get("title", "")), style={"bold": True, "padding": 8})
|
|
2084
|
+
|
|
2085
|
+
def _item_el(item: Any, i_idx: int, s_idx: int) -> Element:
|
|
2086
|
+
if render_item is not None:
|
|
2087
|
+
return render_item(item, i_idx, s_idx)
|
|
2088
|
+
return Text(str(item))
|
|
1803
2089
|
|
|
1804
|
-
|
|
2090
|
+
rows: List[_RowSpec] = []
|
|
2091
|
+
flat_index = 0
|
|
1805
2092
|
for s_idx, section in enumerate(sections_list):
|
|
1806
|
-
flat.append({"_kind": "header", "section": section, "section_index": s_idx})
|
|
1807
|
-
for i_idx, item in enumerate(section.get("data", []) or []):
|
|
1808
|
-
flat.append({"_kind": "item", "item": item, "item_index": i_idx, "section_index": s_idx})
|
|
1809
|
-
|
|
1810
|
-
if item_height is None:
|
|
1811
|
-
# Eager fallback.
|
|
1812
|
-
children: List[Element] = []
|
|
1813
|
-
for entry in flat:
|
|
1814
|
-
if entry["_kind"] == "header":
|
|
1815
|
-
if render_section_header is not None:
|
|
1816
|
-
children.append(render_section_header(entry["section"], entry["section_index"]))
|
|
1817
|
-
else:
|
|
1818
|
-
children.append(Text(str(entry["section"].get("title", ""))))
|
|
1819
|
-
else:
|
|
1820
|
-
if render_item is not None:
|
|
1821
|
-
children.append(render_item(entry["item"], entry["item_index"], entry["section_index"]))
|
|
1822
|
-
else:
|
|
1823
|
-
children.append(Text(str(entry["item"])))
|
|
1824
|
-
inner = Column(*children, style={"spacing": separator_height} if separator_height else None)
|
|
1825
|
-
return ScrollView(inner, style=style, key=key)
|
|
1826
|
-
|
|
1827
|
-
# Virtualized: mixed row heights aren't supported in v1, so we
|
|
1828
|
-
# use the larger of section_header_height and item_height + sep.
|
|
1829
|
-
row_h = max(float(section_header_height), float(item_height) + float(separator_height))
|
|
1830
|
-
|
|
1831
|
-
def _mount_row(index: int, content_view: Any) -> None:
|
|
1832
|
-
from .native_views import get_registry
|
|
1833
|
-
from .reconciler import Reconciler
|
|
1834
|
-
|
|
1835
|
-
try:
|
|
1836
|
-
entry = flat[index]
|
|
1837
|
-
except IndexError:
|
|
1838
|
-
return
|
|
1839
|
-
if entry["_kind"] == "header":
|
|
1840
|
-
if render_section_header is not None:
|
|
1841
|
-
element = render_section_header(entry["section"], entry["section_index"])
|
|
1842
|
-
else:
|
|
1843
|
-
element = Text(str(entry["section"].get("title", "")))
|
|
1844
|
-
else:
|
|
1845
|
-
if render_item is not None:
|
|
1846
|
-
element = render_item(entry["item"], entry["item_index"], entry["section_index"])
|
|
1847
|
-
else:
|
|
1848
|
-
element = Text(str(entry["item"]))
|
|
1849
2093
|
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
native_root = reconciler.mount(element)
|
|
1853
|
-
try:
|
|
1854
|
-
backend.add_child(content_view, native_root, "View")
|
|
1855
|
-
except Exception:
|
|
1856
|
-
pass
|
|
2094
|
+
def _make_header(sec: Dict[str, Any] = section, si: int = s_idx) -> Element:
|
|
2095
|
+
return _header_el(sec, si)
|
|
1857
2096
|
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
2097
|
+
rows.append(
|
|
2098
|
+
_RowSpec(
|
|
2099
|
+
f"__pn_sec_{s_idx}__",
|
|
2100
|
+
_make_header,
|
|
2101
|
+
float(section_header_height) if section_header_height is not None else None,
|
|
2102
|
+
item=section,
|
|
2103
|
+
index=flat_index,
|
|
2104
|
+
)
|
|
2105
|
+
)
|
|
2106
|
+
flat_index += 1
|
|
2107
|
+
for i_idx, item in enumerate(section.get("data", []) or []):
|
|
2108
|
+
if key_extractor is not None:
|
|
2109
|
+
try:
|
|
2110
|
+
row_key = f"s{s_idx}:" + str(key_extractor(item, i_idx))
|
|
2111
|
+
except Exception:
|
|
2112
|
+
row_key = f"__pn_row_{s_idx}_{i_idx}__"
|
|
2113
|
+
else:
|
|
2114
|
+
row_key = f"__pn_row_{s_idx}_{i_idx}__"
|
|
2115
|
+
|
|
2116
|
+
def _make_item(it: Any = item, ii: int = i_idx, si: int = s_idx) -> Element:
|
|
2117
|
+
el = _item_el(it, ii, si)
|
|
2118
|
+
if sep > 0:
|
|
2119
|
+
return View(el, style={"padding_bottom": sep})
|
|
2120
|
+
return el
|
|
2121
|
+
|
|
2122
|
+
extent: Optional[float] = None
|
|
2123
|
+
if get_item_height is not None:
|
|
2124
|
+
try:
|
|
2125
|
+
extent = float(get_item_height(item, i_idx, s_idx)) + sep
|
|
2126
|
+
except Exception:
|
|
2127
|
+
extent = None
|
|
2128
|
+
elif item_height is not None:
|
|
2129
|
+
extent = float(item_height) + sep
|
|
2130
|
+
rows.append(_RowSpec(row_key, _make_item, extent, item=item, index=flat_index))
|
|
2131
|
+
flat_index += 1
|
|
2132
|
+
|
|
2133
|
+
estimated = estimated_item_height if estimated_item_height is not None else (item_height or _DEFAULT_ROW_EXTENT)
|
|
2134
|
+
|
|
2135
|
+
return _VirtualizedList(
|
|
2136
|
+
rows=rows,
|
|
2137
|
+
horizontal=False,
|
|
2138
|
+
estimated_row_extent=float(estimated) + sep,
|
|
2139
|
+
header=list_header,
|
|
2140
|
+
footer=list_footer,
|
|
2141
|
+
empty=list_empty,
|
|
2142
|
+
refresh_control=refresh_control,
|
|
2143
|
+
on_end_reached=on_end_reached,
|
|
2144
|
+
on_end_reached_threshold=on_end_reached_threshold,
|
|
2145
|
+
on_scroll=on_scroll,
|
|
2146
|
+
list_style=resolve_style(style) or None,
|
|
2147
|
+
controller_ref=ref,
|
|
1861
2148
|
key=key,
|
|
1862
|
-
count=len(flat),
|
|
1863
|
-
row_height=row_h,
|
|
1864
|
-
mount_row=_mount_row,
|
|
1865
2149
|
)
|
|
1866
2150
|
|
|
1867
2151
|
|