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.
Files changed (33) hide show
  1. pythonnative/__init__.py +14 -3
  2. pythonnative/animated.py +420 -135
  3. pythonnative/cli/pn.py +450 -956
  4. pythonnative/components.py +519 -235
  5. pythonnative/events.py +210 -0
  6. pythonnative/gestures.py +875 -0
  7. pythonnative/layout.py +463 -149
  8. pythonnative/mutations.py +130 -0
  9. pythonnative/native_views/__init__.py +161 -97
  10. pythonnative/native_views/android.py +1050 -1124
  11. pythonnative/native_views/base.py +108 -18
  12. pythonnative/native_views/desktop.py +460 -417
  13. pythonnative/native_views/ios.py +1918 -1916
  14. pythonnative/project/__init__.py +68 -0
  15. pythonnative/project/android.py +504 -0
  16. pythonnative/project/builder.py +555 -0
  17. pythonnative/project/config.py +642 -0
  18. pythonnative/project/doctor.py +233 -0
  19. pythonnative/project/icons.py +247 -0
  20. pythonnative/project/ios.py +344 -0
  21. pythonnative/project/permissions.py +343 -0
  22. pythonnative/project/runtime_assets.py +272 -0
  23. pythonnative/reconciler.py +540 -470
  24. pythonnative/screen.py +5 -2
  25. pythonnative/sdk/_components.py +2 -2
  26. pythonnative/templates/android_template/app/build.gradle +2 -0
  27. {pythonnative-0.20.0.dist-info → pythonnative-0.22.0.dist-info}/METADATA +10 -2
  28. {pythonnative-0.20.0.dist-info → pythonnative-0.22.0.dist-info}/RECORD +32 -21
  29. pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/PNVirtualListView.java +0 -129
  30. {pythonnative-0.20.0.dist-info → pythonnative-0.22.0.dist-info}/WHEEL +0 -0
  31. {pythonnative-0.20.0.dist-info → pythonnative-0.22.0.dist-info}/entry_points.txt +0 -0
  32. {pythonnative-0.20.0.dist-info → pythonnative-0.22.0.dist-info}/licenses/LICENSE +0 -0
  33. {pythonnative-0.20.0.dist-info → pythonnative-0.22.0.dist-info}/top_level.txt +0 -0
@@ -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[[float, float], None]] = None
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``, ``padding``, ``background_color``, ``border_radius``,
900
- ``border_width``, ``border_color``, ``shadow_color``,
901
- ``shadow_offset``, ``shadow_opacity``, ``shadow_radius``,
902
- ``elevation``, ``opacity``, ``transform``.
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[[float, float], None]] = None,
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 ``(x, y)`` content offsets as
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 and long-press handlers.
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
- Backed by ``UITableView`` on iOS and ``RecyclerView`` on Android via
1584
- the ``VirtualList`` element. Each visible row is mounted on demand
1585
- by a nested
1586
- [`Reconciler`][pythonnative.reconciler.Reconciler] when
1587
- ``item_height`` is specified.
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
- When ``item_height`` is omitted the implementation falls back to an
1590
- eager (non-virtualized) ``ScrollView`` of every row — keep the data
1591
- set small in that mode (the fallback is convenient for short lists
1592
- where virtualization overhead would dominate).
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: Iterable of arbitrary item values.
1596
- render_item: Function called per item, returning an
1597
- [`Element`][pythonnative.Element]. Defaults to wrapping
1598
- each item in a [`Text`][pythonnative.Text].
1599
- key_extractor: Function returning a stable key per item.
1600
- item_height: Fixed row height in layout units. Required to
1601
- enable native virtualization. When omitted, the list
1602
- falls back to an eager scroll of every row (not
1603
- recommended for long lists).
1604
- separator_height: Vertical gap between items, in layout units.
1605
- Combined with ``item_height`` for the virtualized case.
1606
- refresh_control: Optional ``{"refreshing": bool, "on_refresh":
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
- on_item_press: Callback invoked with the row index when the
1610
- user taps a row (virtualized backend only).
1611
- horizontal: Lay rows out left-to-right instead of top-to-bottom
1612
- (forces the eager backend).
1613
- num_columns: Render items in a grid of this many columns
1614
- (forces the eager backend). ``1`` (default) is a plain list.
1615
- list_header: Optional element rendered once above all rows.
1616
- list_footer: Optional element rendered once below all rows.
1617
- list_empty: Optional element rendered when ``data`` is empty.
1618
- on_end_reached: Callback invoked when the user scrolls within
1619
- ``on_end_reached_threshold`` of the end (virtualized
1620
- backend).
1621
- on_end_reached_threshold: Fraction of the viewport from the end
1622
- at which ``on_end_reached`` fires.
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 (forces the eager backend).
1625
- style: Style dict (or list of dicts).
1626
- key: Stable identity for keyed reconciliation of the list
1627
- itself.
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
- An [`Element`][pythonnative.Element] of type ``"VirtualList"``
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
- has_ornaments = (
1650
- num_columns > 1
1651
- or horizontal
1652
- or list_header is not None
1653
- or list_footer is not None
1654
- or content_container_style is not None
1655
- or (not items_list and list_empty is not None)
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
- layout_w = float(cell_width) if cell_width and cell_width > 0 else 0.0
1740
- layout_h = float(cell_height) if cell_height and cell_height > 0 else float(item_height)
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
- bounds = content_view.bounds
1744
- layout_w = float(bounds.size.width)
1952
+ return float(get_item_height(item, index)) + sep
1745
1953
  except Exception:
1746
- layout_w = 0.0
1747
- if layout_w > 0 and layout_h > 0:
1748
- backend.set_frame(native_root, "View", 0.0, 0.0, layout_w, layout_h)
1749
- reconciler.set_viewport_size(layout_w, layout_h)
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
- backend.add_child(content_view, native_root, "View")
1995
+ estimated = estimated_item_height if estimated_item_height is not None else (item_height or _DEFAULT_ROW_EXTENT)
1752
1996
 
1753
- return _make_element(
1754
- "VirtualList",
1755
- style=style,
1756
- key=key,
1757
- count=len(items_list),
1758
- row_height=row_h,
1759
- mount_row=_mount_row,
1760
- on_row_press=on_item_press,
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
- refresh_control=refresh_control,
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
- section_header_height: float = 32.0,
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 that supports section headers.
2039
+ """Virtualized list with section headers interleaved between row groups.
1779
2040
 
1780
- Internally flattens ``sections`` into a single virtualized list
1781
- where each row is either a section header or a section item. The
1782
- row mounter dispatches to ``render_section_header`` or
1783
- ``render_item`` depending on the row's type.
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
- item_height: Fixed row height for items, in layout units.
1792
- section_header_height: Fixed header height in layout units.
1793
- separator_height: Gap appended below each item, in layout units.
1794
- style: Style dict (or list of dicts).
1795
- key: Stable identity for keyed reconciliation.
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
- An [`Element`][pythonnative.Element] of type ``"VirtualList"``
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
- flat: List[Dict[str, Any]] = []
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
- backend = get_registry()
1851
- reconciler = Reconciler(backend)
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
- return _make_element(
1859
- "VirtualList",
1860
- style=style,
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