pythonnative 0.22.1__py3-none-any.whl → 0.23.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 (25) hide show
  1. pythonnative/__init__.py +16 -2
  2. pythonnative/appearance.py +124 -0
  3. pythonnative/components.py +708 -38
  4. pythonnative/hooks.py +41 -0
  5. pythonnative/images.py +196 -0
  6. pythonnative/mutations.py +3 -12
  7. pythonnative/native_views/__init__.py +1 -7
  8. pythonnative/native_views/android.py +573 -45
  9. pythonnative/native_views/desktop.py +78 -19
  10. pythonnative/native_views/ios.py +739 -51
  11. pythonnative/preview.py +31 -0
  12. pythonnative/project/config.py +1 -9
  13. pythonnative/screen.py +85 -0
  14. pythonnative/style.py +65 -6
  15. pythonnative/templates/android_template/app/src/main/AndroidManifest.xml +2 -1
  16. pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/PNAccessibilityDelegate.kt +47 -0
  17. pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/PNBorderDrawable.kt +67 -0
  18. pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/PNVirtualListView.java +172 -0
  19. pythonnative/virtual_rows.py +137 -0
  20. {pythonnative-0.22.1.dist-info → pythonnative-0.23.0.dist-info}/METADATA +1 -1
  21. {pythonnative-0.22.1.dist-info → pythonnative-0.23.0.dist-info}/RECORD +25 -19
  22. {pythonnative-0.22.1.dist-info → pythonnative-0.23.0.dist-info}/WHEEL +1 -1
  23. {pythonnative-0.22.1.dist-info → pythonnative-0.23.0.dist-info}/entry_points.txt +0 -0
  24. {pythonnative-0.22.1.dist-info → pythonnative-0.23.0.dist-info}/licenses/LICENSE +0 -0
  25. {pythonnative-0.22.1.dist-info → pythonnative-0.23.0.dist-info}/top_level.txt +0 -0
@@ -26,10 +26,17 @@ Example:
26
26
 
27
27
  import bisect
28
28
  from dataclasses import dataclass, field
29
- from typing import Any, Callable, Dict, List, Literal, Optional, Tuple
29
+ from typing import Any, Callable, Dict, List, Literal, Optional, Tuple, Union
30
30
 
31
31
  from .element import Element
32
- from .hooks import component, use_effect, use_ref, use_state
32
+ from .hooks import (
33
+ component,
34
+ use_effect,
35
+ use_keyboard_height,
36
+ use_ref,
37
+ use_safe_area_insets,
38
+ use_state,
39
+ )
33
40
  from .sdk import Props
34
41
  from .style import (
35
42
  AutoCapitalize,
@@ -117,10 +124,14 @@ class TextProps(Props):
117
124
  """Props for [`Text`][pythonnative.Text]."""
118
125
 
119
126
  text: str = ""
127
+ spans: Optional[List[Dict[str, Any]]] = None
120
128
  accessibility_label: Optional[str] = None
121
129
  accessibility_hint: Optional[str] = None
122
130
  accessibility_role: Optional[str] = None
123
131
  accessible: Optional[bool] = None
132
+ accessibility_state: Optional[Dict[str, Any]] = None
133
+ accessibility_live_region: Optional[Literal["none", "polite", "assertive"]] = None
134
+ test_id: Optional[str] = None
124
135
 
125
136
 
126
137
  @dataclass(frozen=True)
@@ -134,6 +145,9 @@ class ButtonProps(Props):
134
145
  accessibility_hint: Optional[str] = None
135
146
  accessibility_role: Optional[str] = None
136
147
  accessible: Optional[bool] = None
148
+ accessibility_state: Optional[Dict[str, Any]] = None
149
+ accessibility_live_region: Optional[Literal["none", "polite", "assertive"]] = None
150
+ test_id: Optional[str] = None
137
151
 
138
152
 
139
153
  @dataclass(frozen=True)
@@ -162,6 +176,9 @@ class TextInputProps(Props):
162
176
  accessibility_label: Optional[str] = None
163
177
  accessibility_hint: Optional[str] = None
164
178
  accessible: Optional[bool] = None
179
+ accessibility_state: Optional[Dict[str, Any]] = None
180
+ accessibility_live_region: Optional[Literal["none", "polite", "assertive"]] = None
181
+ test_id: Optional[str] = None
165
182
 
166
183
 
167
184
  @dataclass(frozen=True)
@@ -171,9 +188,15 @@ class ImageProps(Props):
171
188
  source: Optional[str] = None
172
189
  scale_type: Optional[ScaleType] = None
173
190
  tint_color: Optional[Color] = None
191
+ placeholder_color: Optional[Color] = None
192
+ on_load: Optional[Callable[[], None]] = None
193
+ on_error: Optional[Callable[[str], None]] = None
174
194
  accessibility_label: Optional[str] = None
175
195
  accessibility_role: Optional[str] = None
176
196
  accessible: Optional[bool] = None
197
+ accessibility_state: Optional[Dict[str, Any]] = None
198
+ accessibility_live_region: Optional[Literal["none", "polite", "assertive"]] = None
199
+ test_id: Optional[str] = None
177
200
 
178
201
 
179
202
  @dataclass(frozen=True)
@@ -245,6 +268,9 @@ class ViewProps(Props):
245
268
  accessibility_hint: Optional[str] = None
246
269
  accessibility_role: Optional[str] = None
247
270
  accessible: Optional[bool] = None
271
+ accessibility_state: Optional[Dict[str, Any]] = None
272
+ accessibility_live_region: Optional[Literal["none", "polite", "assertive"]] = None
273
+ test_id: Optional[str] = None
248
274
 
249
275
 
250
276
  @dataclass(frozen=True)
@@ -269,6 +295,8 @@ class ScrollViewProps(Props):
269
295
  class SafeAreaViewProps(Props):
270
296
  """Props for [`SafeAreaView`][pythonnative.SafeAreaView]."""
271
297
 
298
+ edges: Optional[Tuple[Literal["top", "left", "bottom", "right"], ...]] = None
299
+
272
300
 
273
301
  @dataclass(frozen=True)
274
302
  class ModalProps(Props):
@@ -298,6 +326,9 @@ class PressableProps(Props):
298
326
  accessibility_hint: Optional[str] = None
299
327
  accessibility_role: Optional[str] = None
300
328
  accessible: Optional[bool] = None
329
+ accessibility_state: Optional[Dict[str, Any]] = None
330
+ accessibility_live_region: Optional[Literal["none", "polite", "assertive"]] = None
331
+ test_id: Optional[str] = None
301
332
 
302
333
 
303
334
  @dataclass(frozen=True)
@@ -314,6 +345,7 @@ class KeyboardAvoidingViewProps(Props):
314
345
  """Props for [`KeyboardAvoidingView`][pythonnative.KeyboardAvoidingView]."""
315
346
 
316
347
  behavior: Literal["padding", "position"] = "padding"
348
+ keyboard_vertical_offset: float = 0.0
317
349
 
318
350
 
319
351
  @dataclass(frozen=True)
@@ -332,6 +364,9 @@ class PickerProps(Props):
332
364
  accessibility_label: Optional[str] = None
333
365
  accessibility_hint: Optional[str] = None
334
366
  accessible: Optional[bool] = None
367
+ accessibility_state: Optional[Dict[str, Any]] = None
368
+ accessibility_live_region: Optional[Literal["none", "polite", "assertive"]] = None
369
+ test_id: Optional[str] = None
335
370
 
336
371
 
337
372
  @dataclass(frozen=True)
@@ -346,6 +381,9 @@ class TouchableOpacityProps(Props):
346
381
  accessibility_hint: Optional[str] = None
347
382
  accessibility_role: Optional[str] = None
348
383
  accessible: Optional[bool] = None
384
+ accessibility_state: Optional[Dict[str, Any]] = None
385
+ accessibility_live_region: Optional[Literal["none", "polite", "assertive"]] = None
386
+ test_id: Optional[str] = None
349
387
 
350
388
 
351
389
  @dataclass(frozen=True)
@@ -356,6 +394,9 @@ class ImageBackgroundProps(Props):
356
394
  scale_type: Optional[ScaleType] = None
357
395
  accessibility_label: Optional[str] = None
358
396
  accessible: Optional[bool] = None
397
+ accessibility_state: Optional[Dict[str, Any]] = None
398
+ accessibility_live_region: Optional[Literal["none", "polite", "assertive"]] = None
399
+ test_id: Optional[str] = None
359
400
 
360
401
 
361
402
  @dataclass(frozen=True)
@@ -370,6 +411,9 @@ class CheckboxProps(Props):
370
411
  accessibility_label: Optional[str] = None
371
412
  accessibility_hint: Optional[str] = None
372
413
  accessible: Optional[bool] = None
414
+ accessibility_state: Optional[Dict[str, Any]] = None
415
+ accessibility_live_region: Optional[Literal["none", "polite", "assertive"]] = None
416
+ test_id: Optional[str] = None
373
417
 
374
418
 
375
419
  @dataclass(frozen=True)
@@ -383,6 +427,9 @@ class SegmentedControlProps(Props):
383
427
  tint_color: Optional[Color] = None
384
428
  accessibility_label: Optional[str] = None
385
429
  accessible: Optional[bool] = None
430
+ accessibility_state: Optional[Dict[str, Any]] = None
431
+ accessibility_live_region: Optional[Literal["none", "polite", "assertive"]] = None
432
+ test_id: Optional[str] = None
386
433
 
387
434
 
388
435
  @dataclass(frozen=True)
@@ -403,6 +450,9 @@ class DatePickerProps(Props):
403
450
  enabled: bool = True
404
451
  accessibility_label: Optional[str] = None
405
452
  accessible: Optional[bool] = None
453
+ accessibility_state: Optional[Dict[str, Any]] = None
454
+ accessibility_live_region: Optional[Literal["none", "polite", "assertive"]] = None
455
+ test_id: Optional[str] = None
406
456
 
407
457
 
408
458
  # ======================================================================
@@ -410,18 +460,71 @@ class DatePickerProps(Props):
410
460
  # ======================================================================
411
461
 
412
462
 
463
+ # Style keys that can vary per span inside a rich-text run. Layout
464
+ # keys are excluded: spans are inline fragments of one paragraph and
465
+ # have no boxes of their own.
466
+ _SPAN_STYLE_KEYS = (
467
+ "color",
468
+ "background_color",
469
+ "font_size",
470
+ "font_family",
471
+ "font_weight",
472
+ "bold",
473
+ "italic",
474
+ "text_decoration",
475
+ "letter_spacing",
476
+ )
477
+
478
+
479
+ def _flatten_text_spans(
480
+ parts: Tuple[Any, ...],
481
+ inherited: Dict[str, Any],
482
+ ) -> List[Dict[str, Any]]:
483
+ """Flatten nested ``Text`` parts into a flat list of styled spans.
484
+
485
+ Strings become spans carrying only the inherited style overrides;
486
+ nested ``Text`` elements contribute their own span-style keys
487
+ (merged over what they inherit) and recurse into their parts.
488
+ """
489
+ spans: List[Dict[str, Any]] = []
490
+ for part in parts:
491
+ if part is None:
492
+ continue
493
+ if isinstance(part, Element):
494
+ props = part.props or {}
495
+ child_style = dict(inherited)
496
+ for k in _SPAN_STYLE_KEYS:
497
+ if props.get(k) is not None:
498
+ child_style[k] = props[k]
499
+ nested = props.get("spans")
500
+ if nested:
501
+ for span in nested:
502
+ merged = dict(child_style)
503
+ for k in _SPAN_STYLE_KEYS:
504
+ if span.get(k) is not None:
505
+ merged[k] = span[k]
506
+ spans.append({"text": span.get("text", ""), **merged})
507
+ else:
508
+ spans.append({"text": str(props.get("text", "")), **child_style})
509
+ else:
510
+ spans.append({"text": str(part), **inherited})
511
+ return spans
512
+
513
+
413
514
  def Text(
414
- text: str = "",
415
- *,
515
+ *parts: Any,
416
516
  style: StyleProp = None,
417
517
  accessibility_label: Optional[str] = None,
418
518
  accessibility_hint: Optional[str] = None,
419
519
  accessibility_role: Optional[str] = None,
420
520
  accessible: Optional[bool] = None,
521
+ accessibility_state: Optional[Dict[str, Any]] = None,
522
+ accessibility_live_region: Optional[Literal["none", "polite", "assertive"]] = None,
523
+ test_id: Optional[str] = None,
421
524
  ref: Optional[Dict[str, Any]] = None,
422
525
  key: Optional[str] = None,
423
526
  ) -> Element:
424
- """Display a string of text.
527
+ """Display a string of text, optionally with styled nested spans.
425
528
 
426
529
  Style properties: ``font_size``, ``color``, ``bold``,
427
530
  ``font_weight``, ``font_family``, ``italic``, ``text_align``,
@@ -431,29 +534,70 @@ def Text(
431
534
  ``border_color``, ``shadow_*``, ``opacity``, ``transform``, plus
432
535
  the common layout props.
433
536
 
537
+ **Rich text**: pass multiple parts, mixing plain strings and
538
+ nested ``Text`` elements, to render one paragraph with per-span
539
+ styling (a single ``TextView`` / ``UILabel`` natively, so line
540
+ wrapping flows across spans):
541
+
542
+ ```python
543
+ pn.Text(
544
+ "Hello, ",
545
+ pn.Text("world", style=pn.style(bold=True, color="#0A84FF")),
546
+ "!",
547
+ style=pn.style(font_size=18),
548
+ )
549
+ ```
550
+
551
+ Nested spans inherit the outer element's text styling and may
552
+ override ``color``, ``background_color``, ``font_size``,
553
+ ``font_family``, ``font_weight``, ``bold``, ``italic``,
554
+ ``text_decoration``, and ``letter_spacing``.
555
+
434
556
  Args:
435
- text: Text content to display.
557
+ *parts: Text content: a single string, or any mix of strings
558
+ and nested ``Text`` elements for rich text.
436
559
  style: Style dict (or list of dicts).
437
560
  accessibility_label: Spoken description for screen readers.
438
561
  accessibility_hint: Spoken extra detail (iOS only).
439
562
  accessibility_role: Semantic role for assistive tech.
440
563
  accessible: Override whether the element is exposed to AT.
564
+ accessibility_state: Current widget state for assistive tech,
565
+ e.g. ``{"disabled": True, "selected": False}``. Recognized
566
+ keys: ``disabled``, ``selected``, ``checked``, ``busy``,
567
+ ``expanded``.
568
+ accessibility_live_region: How AT announces dynamic changes to
569
+ this view: ``"none"``, ``"polite"``, or ``"assertive"``
570
+ (Android only).
571
+ test_id: Stable identifier for UI tests; exposed as
572
+ ``resource-id`` on Android and ``accessibilityIdentifier``
573
+ on iOS.
441
574
  ref: Optional ``use_ref()`` dict.
442
575
  key: Stable identity for keyed reconciliation.
443
576
 
444
577
  Returns:
445
578
  An [`Element`][pythonnative.Element] of type ``"Text"``.
446
579
  """
580
+ rich = any(isinstance(p, Element) for p in parts) or len(parts) > 1
581
+ if rich:
582
+ spans = _flatten_text_spans(parts, {})
583
+ text = "".join(s["text"] for s in spans)
584
+ else:
585
+ spans = None
586
+ text = str(parts[0]) if parts else ""
447
587
  return _make_element(
448
588
  "Text",
449
589
  style=style,
450
590
  ref=ref,
451
591
  key=key,
452
592
  text=text,
593
+ spans=spans,
453
594
  accessibility_label=accessibility_label,
454
595
  accessibility_hint=accessibility_hint,
455
596
  accessibility_role=accessibility_role,
456
597
  accessible=accessible,
598
+ accessibility_state=accessibility_state,
599
+ accessibility_live_region=accessibility_live_region,
600
+ test_id=test_id,
457
601
  )
458
602
 
459
603
 
@@ -467,6 +611,9 @@ def Button(
467
611
  accessibility_hint: Optional[str] = None,
468
612
  accessibility_role: Optional[str] = None,
469
613
  accessible: Optional[bool] = None,
614
+ accessibility_state: Optional[Dict[str, Any]] = None,
615
+ accessibility_live_region: Optional[Literal["none", "polite", "assertive"]] = None,
616
+ test_id: Optional[str] = None,
470
617
  ref: Optional[Dict[str, Any]] = None,
471
618
  key: Optional[str] = None,
472
619
  ) -> Element:
@@ -488,6 +635,16 @@ def Button(
488
635
  accessibility_hint: Spoken extra detail (iOS only).
489
636
  accessibility_role: Override the default ``"button"`` role.
490
637
  accessible: Override whether the element is exposed to AT.
638
+ accessibility_state: Current widget state for assistive tech,
639
+ e.g. ``{"disabled": True, "selected": False}``. Recognized
640
+ keys: ``disabled``, ``selected``, ``checked``, ``busy``,
641
+ ``expanded``.
642
+ accessibility_live_region: How AT announces dynamic changes to
643
+ this view: ``"none"``, ``"polite"``, or ``"assertive"``
644
+ (Android only).
645
+ test_id: Stable identifier for UI tests; exposed as
646
+ ``resource-id`` on Android and ``accessibilityIdentifier``
647
+ on iOS.
491
648
  ref: Optional ``use_ref()`` dict.
492
649
  key: Stable identity for keyed reconciliation.
493
650
 
@@ -506,6 +663,9 @@ def Button(
506
663
  accessibility_hint=accessibility_hint,
507
664
  accessibility_role=accessibility_role,
508
665
  accessible=accessible,
666
+ accessibility_state=accessibility_state,
667
+ accessibility_live_region=accessibility_live_region,
668
+ test_id=test_id,
509
669
  _defaults={"accessibility_role": "button"},
510
670
  )
511
671
 
@@ -535,6 +695,9 @@ def TextInput(
535
695
  accessibility_label: Optional[str] = None,
536
696
  accessibility_hint: Optional[str] = None,
537
697
  accessible: Optional[bool] = None,
698
+ accessibility_state: Optional[Dict[str, Any]] = None,
699
+ accessibility_live_region: Optional[Literal["none", "polite", "assertive"]] = None,
700
+ test_id: Optional[str] = None,
538
701
  ref: Optional[Dict[str, Any]] = None,
539
702
  key: Optional[str] = None,
540
703
  ) -> Element:
@@ -575,6 +738,16 @@ def TextInput(
575
738
  accessibility_label: Spoken description for screen readers.
576
739
  accessibility_hint: Spoken extra detail (iOS only).
577
740
  accessible: Override whether the element is exposed to AT.
741
+ accessibility_state: Current widget state for assistive tech,
742
+ e.g. ``{"disabled": True, "selected": False}``. Recognized
743
+ keys: ``disabled``, ``selected``, ``checked``, ``busy``,
744
+ ``expanded``.
745
+ accessibility_live_region: How AT announces dynamic changes to
746
+ this view: ``"none"``, ``"polite"``, or ``"assertive"``
747
+ (Android only).
748
+ test_id: Stable identifier for UI tests; exposed as
749
+ ``resource-id`` on Android and ``accessibilityIdentifier``
750
+ on iOS.
578
751
  ref: Optional ``use_ref()`` dict.
579
752
  key: Stable identity for keyed reconciliation.
580
753
 
@@ -608,6 +781,9 @@ def TextInput(
608
781
  accessibility_label=accessibility_label,
609
782
  accessibility_hint=accessibility_hint,
610
783
  accessible=accessible,
784
+ accessibility_state=accessibility_state,
785
+ accessibility_live_region=accessibility_live_region,
786
+ test_id=test_id,
611
787
  )
612
788
 
613
789
 
@@ -616,10 +792,16 @@ def Image(
616
792
  *,
617
793
  scale_type: Optional[ScaleType] = None,
618
794
  tint_color: Optional[Color] = None,
795
+ placeholder_color: Optional[Color] = None,
796
+ on_load: Optional[Callable[[], None]] = None,
797
+ on_error: Optional[Callable[[str], None]] = None,
619
798
  style: StyleProp = None,
620
799
  accessibility_label: Optional[str] = None,
621
800
  accessibility_role: Optional[str] = None,
622
801
  accessible: Optional[bool] = None,
802
+ accessibility_state: Optional[Dict[str, Any]] = None,
803
+ accessibility_live_region: Optional[Literal["none", "polite", "assertive"]] = None,
804
+ test_id: Optional[str] = None,
623
805
  ref: Optional[Dict[str, Any]] = None,
624
806
  key: Optional[str] = None,
625
807
  ) -> Element:
@@ -628,10 +810,11 @@ def Image(
628
810
  Style properties: ``background_color``, ``border_*``, ``opacity``,
629
811
  ``transform``, plus the common layout props.
630
812
 
631
- Network images (``http://`` / ``https://``) are loaded
632
- asynchronously off the main thread on both iOS (via
633
- ``NSURLSession``) and Android (via a worker thread plus
634
- ``BitmapFactory``).
813
+ Network images (``http://`` / ``https://``) go through the shared
814
+ image pipeline (`pythonnative.images`): downloads happen on a
815
+ background thread, bytes are cached in memory and on disk keyed by
816
+ URL, concurrent requests for the same URL share one download, and
817
+ large bitmaps are downsampled to the view size when decoded.
635
818
 
636
819
  Args:
637
820
  source: Image resource name or URL.
@@ -639,10 +822,26 @@ def Image(
639
822
  ``"center"``.
640
823
  tint_color: Color overlay applied to template images
641
824
  (monochrome icons).
825
+ placeholder_color: Background color shown while a remote image
826
+ is loading (and left in place if it fails).
827
+ on_load: Callback invoked once the image has been decoded and
828
+ displayed.
829
+ on_error: Callback invoked with an error message when a remote
830
+ image fails to download or decode.
642
831
  style: Style dict (or list of dicts).
643
832
  accessibility_label: Spoken description for screen readers.
644
833
  accessibility_role: Override the default ``"image"`` role.
645
834
  accessible: Override whether the element is exposed to AT.
835
+ accessibility_state: Current widget state for assistive tech,
836
+ e.g. ``{"disabled": True, "selected": False}``. Recognized
837
+ keys: ``disabled``, ``selected``, ``checked``, ``busy``,
838
+ ``expanded``.
839
+ accessibility_live_region: How AT announces dynamic changes to
840
+ this view: ``"none"``, ``"polite"``, or ``"assertive"``
841
+ (Android only).
842
+ test_id: Stable identifier for UI tests; exposed as
843
+ ``resource-id`` on Android and ``accessibilityIdentifier``
844
+ on iOS.
646
845
  ref: Optional ``use_ref()`` dict.
647
846
  key: Stable identity for keyed reconciliation.
648
847
 
@@ -657,9 +856,15 @@ def Image(
657
856
  source=source or None,
658
857
  scale_type=scale_type,
659
858
  tint_color=tint_color,
859
+ placeholder_color=placeholder_color,
860
+ on_load=on_load,
861
+ on_error=on_error,
660
862
  accessibility_label=accessibility_label,
661
863
  accessibility_role=accessibility_role,
662
864
  accessible=accessible,
865
+ accessibility_state=accessibility_state,
866
+ accessibility_live_region=accessibility_live_region,
867
+ test_id=test_id,
663
868
  _defaults={"accessibility_role": "image"},
664
869
  )
665
870
 
@@ -900,6 +1105,9 @@ def View(
900
1105
  accessibility_hint: Optional[str] = None,
901
1106
  accessibility_role: Optional[str] = None,
902
1107
  accessible: Optional[bool] = None,
1108
+ accessibility_state: Optional[Dict[str, Any]] = None,
1109
+ accessibility_live_region: Optional[Literal["none", "polite", "assertive"]] = None,
1110
+ test_id: Optional[str] = None,
903
1111
  ref: Optional[Dict[str, Any]] = None,
904
1112
  key: Optional[str] = None,
905
1113
  ) -> Element:
@@ -939,6 +1147,16 @@ def View(
939
1147
  accessibility_hint: Spoken extra detail (iOS only).
940
1148
  accessibility_role: Semantic role for assistive tech.
941
1149
  accessible: Override whether the element is exposed to AT.
1150
+ accessibility_state: Current widget state for assistive tech,
1151
+ e.g. ``{"disabled": True, "selected": False}``. Recognized
1152
+ keys: ``disabled``, ``selected``, ``checked``, ``busy``,
1153
+ ``expanded``.
1154
+ accessibility_live_region: How AT announces dynamic changes to
1155
+ this view: ``"none"``, ``"polite"``, or ``"assertive"``
1156
+ (Android only).
1157
+ test_id: Stable identifier for UI tests; exposed as
1158
+ ``resource-id`` on Android and ``accessibilityIdentifier``
1159
+ on iOS.
942
1160
  ref: Optional ``use_ref()`` dict.
943
1161
  key: Stable identity for keyed reconciliation.
944
1162
 
@@ -956,6 +1174,9 @@ def View(
956
1174
  accessibility_hint=accessibility_hint,
957
1175
  accessibility_role=accessibility_role,
958
1176
  accessible=accessible,
1177
+ accessibility_state=accessibility_state,
1178
+ accessibility_live_region=accessibility_live_region,
1179
+ test_id=test_id,
959
1180
  _defaults={"flex_direction": "column"},
960
1181
  )
961
1182
 
@@ -1087,25 +1308,76 @@ def ScrollView(
1087
1308
  )
1088
1309
 
1089
1310
 
1311
+ _SAFE_AREA_EDGES: Tuple[str, ...] = ("top", "left", "bottom", "right")
1312
+
1313
+
1314
+ def _numeric_edge_padding(style: Dict[str, Any], edge: str) -> float:
1315
+ """Return the numeric padding already declared for ``edge`` in ``style``.
1316
+
1317
+ Only numeric values participate; percentage strings and dict
1318
+ shorthands are left alone (the inset simply overrides them for
1319
+ that edge). Resolution order matches the layout engine:
1320
+ ``padding_{edge}`` beats the axis shorthand, which beats
1321
+ ``padding``.
1322
+ """
1323
+ axis_key = "padding_vertical" if edge in ("top", "bottom") else "padding_horizontal"
1324
+ for key in (f"padding_{edge}", axis_key, "padding"):
1325
+ value = style.get(key)
1326
+ if isinstance(value, (int, float)) and not isinstance(value, bool):
1327
+ return float(value)
1328
+ return 0.0
1329
+
1330
+
1331
+ @component
1332
+ def _SafeAreaContainer(**p: Any) -> Element:
1333
+ """Hook-driven body of `SafeAreaView`.
1334
+
1335
+ Reads the live insets via
1336
+ [`use_safe_area_insets`][pythonnative.use_safe_area_insets] (so the
1337
+ subtree re-renders when the platform publishes new values, e.g. on
1338
+ rotation) and adds each selected edge's inset on top of any padding
1339
+ the user declared for that edge.
1340
+ """
1341
+ insets = use_safe_area_insets()
1342
+ style: Dict[str, Any] = dict(p.get("style") or {})
1343
+ edges = p.get("edges") or _SAFE_AREA_EDGES
1344
+ for edge in _SAFE_AREA_EDGES:
1345
+ if edge not in edges:
1346
+ continue
1347
+ inset = float(insets.get(edge, 0.0) or 0.0)
1348
+ if inset > 0:
1349
+ style[f"padding_{edge}"] = _numeric_edge_padding(style, edge) + inset
1350
+ children = p.get("children") or []
1351
+ return Element("SafeAreaView", style, list(children))
1352
+
1353
+
1090
1354
  def SafeAreaView(
1091
1355
  *children: Element,
1356
+ edges: Optional[Tuple[Literal["top", "left", "bottom", "right"], ...]] = None,
1092
1357
  style: StyleProp = None,
1093
1358
  key: Optional[str] = None,
1094
1359
  ) -> Element:
1095
1360
  """Container that respects safe-area insets (notch, status bar, home indicator).
1096
1361
 
1362
+ Applies the platform-reported insets as extra padding on the
1363
+ selected edges and re-renders automatically when the insets change
1364
+ (rotation, split view). User padding on an inset edge is added to
1365
+ the inset, matching ``react-native-safe-area-context``.
1366
+
1097
1367
  Args:
1098
1368
  *children: Child elements that should avoid system UI overlays.
1369
+ edges: Which edges to pad; defaults to all four.
1099
1370
  style: Style dict (or list of dicts).
1100
1371
  key: Stable identity for keyed reconciliation.
1101
1372
 
1102
1373
  Returns:
1103
- An [`Element`][pythonnative.Element] of type ``"SafeAreaView"``.
1374
+ An [`Element`][pythonnative.Element] that renders a
1375
+ ``"SafeAreaView"`` container.
1104
1376
  """
1105
- return _make_element(
1106
- "SafeAreaView",
1107
- *children,
1108
- style=style,
1377
+ return _SafeAreaContainer(
1378
+ children=list(children),
1379
+ edges=tuple(edges) if edges else None,
1380
+ style=resolve_style(style),
1109
1381
  key=key,
1110
1382
  )
1111
1383
 
@@ -1175,6 +1447,41 @@ def Modal(
1175
1447
  )
1176
1448
 
1177
1449
 
1450
+ @component
1451
+ def _StatefulPressable(**p: Any) -> Element:
1452
+ """Hook-driven Pressable used when ``style`` is a callable.
1453
+
1454
+ Tracks the pressed state with ``use_state`` and re-invokes the
1455
+ user's style function with ``{"pressed": bool}`` on every press
1456
+ transition, mirroring React Native's function-style ``style`` prop.
1457
+ """
1458
+ pressed, set_pressed = use_state(False)
1459
+ style_fn = p["style_fn"]
1460
+ user_press_in = p.get("on_press_in")
1461
+ user_press_out = p.get("on_press_out")
1462
+
1463
+ def _press_in() -> None:
1464
+ set_pressed(True)
1465
+ if user_press_in is not None:
1466
+ user_press_in()
1467
+
1468
+ def _press_out() -> None:
1469
+ set_pressed(False)
1470
+ if user_press_out is not None:
1471
+ user_press_out()
1472
+
1473
+ forwarded = {k: v for k, v in p.items() if k not in ("children", "style_fn", "on_press_in", "on_press_out")}
1474
+ return _make_element(
1475
+ "Pressable",
1476
+ *(p.get("children") or []),
1477
+ style=resolve_style(style_fn({"pressed": pressed})),
1478
+ on_press_in=_press_in,
1479
+ on_press_out=_press_out,
1480
+ _defaults={"accessibility_role": "button"},
1481
+ **forwarded,
1482
+ )
1483
+
1484
+
1178
1485
  def Pressable(
1179
1486
  *children: Element,
1180
1487
  on_press: Optional[Callable[[], None]] = None,
@@ -1183,11 +1490,14 @@ def Pressable(
1183
1490
  on_press_out: Optional[Callable[[], None]] = None,
1184
1491
  pressed_opacity: float = 0.6,
1185
1492
  gestures: Optional[List[Any]] = None,
1186
- style: StyleProp = None,
1493
+ style: Union[StyleProp, Callable[[Dict[str, bool]], StyleProp]] = None,
1187
1494
  accessibility_label: Optional[str] = None,
1188
1495
  accessibility_hint: Optional[str] = None,
1189
1496
  accessibility_role: Optional[str] = None,
1190
1497
  accessible: Optional[bool] = None,
1498
+ accessibility_state: Optional[Dict[str, Any]] = None,
1499
+ accessibility_live_region: Optional[Literal["none", "polite", "assertive"]] = None,
1500
+ test_id: Optional[str] = None,
1191
1501
  ref: Optional[Dict[str, Any]] = None,
1192
1502
  key: Optional[str] = None,
1193
1503
  ) -> Element:
@@ -1210,17 +1520,60 @@ def Pressable(
1210
1520
  gestures: Optional list of gesture descriptors from
1211
1521
  `pythonnative.gestures` recognized natively on this view
1212
1522
  (pan / swipe / pinch / rotation / multi-tap).
1213
- style: Style dict applied to the wrapper.
1523
+ style: Style dict applied to the wrapper, or a callable
1524
+ receiving the interaction state (``{"pressed": bool}``)
1525
+ and returning a style, re-evaluated on every press
1526
+ transition:
1527
+
1528
+ ```python
1529
+ pn.Pressable(
1530
+ pn.Text("Tap"),
1531
+ style=lambda s: pn.style(
1532
+ background_color="#0051A8" if s["pressed"] else "#007AFF",
1533
+ ),
1534
+ )
1535
+ ```
1214
1536
  accessibility_label: Spoken description for screen readers.
1215
1537
  accessibility_hint: Spoken extra detail (iOS only).
1216
1538
  accessibility_role: Override the default ``"button"`` role.
1217
1539
  accessible: Override whether the element is exposed to AT.
1540
+ accessibility_state: Current widget state for assistive tech,
1541
+ e.g. ``{"disabled": True, "selected": False}``. Recognized
1542
+ keys: ``disabled``, ``selected``, ``checked``, ``busy``,
1543
+ ``expanded``.
1544
+ accessibility_live_region: How AT announces dynamic changes to
1545
+ this view: ``"none"``, ``"polite"``, or ``"assertive"``
1546
+ (Android only).
1547
+ test_id: Stable identifier for UI tests; exposed as
1548
+ ``resource-id`` on Android and ``accessibilityIdentifier``
1549
+ on iOS.
1218
1550
  ref: Optional ``use_ref()`` dict.
1219
1551
  key: Stable identity for keyed reconciliation.
1220
1552
 
1221
1553
  Returns:
1222
- An [`Element`][pythonnative.Element] of type ``"Pressable"``.
1554
+ An [`Element`][pythonnative.Element] of type ``"Pressable"``
1555
+ (wrapped in a stateful composite when ``style`` is callable).
1223
1556
  """
1557
+ if callable(style):
1558
+ return _StatefulPressable(
1559
+ children=list(children),
1560
+ style_fn=style,
1561
+ ref=ref,
1562
+ key=key,
1563
+ on_press=on_press,
1564
+ on_long_press=on_long_press,
1565
+ on_press_in=on_press_in,
1566
+ on_press_out=on_press_out,
1567
+ pressed_opacity=pressed_opacity,
1568
+ gestures=gestures,
1569
+ accessibility_label=accessibility_label,
1570
+ accessibility_hint=accessibility_hint,
1571
+ accessibility_role=accessibility_role,
1572
+ accessible=accessible,
1573
+ accessibility_state=accessibility_state,
1574
+ accessibility_live_region=accessibility_live_region,
1575
+ test_id=test_id,
1576
+ )
1224
1577
  return _make_element(
1225
1578
  "Pressable",
1226
1579
  *children,
@@ -1237,6 +1590,9 @@ def Pressable(
1237
1590
  accessibility_hint=accessibility_hint,
1238
1591
  accessibility_role=accessibility_role,
1239
1592
  accessible=accessible,
1593
+ accessibility_state=accessibility_state,
1594
+ accessibility_live_region=accessibility_live_region,
1595
+ test_id=test_id,
1240
1596
  _defaults={"accessibility_role": "button"},
1241
1597
  )
1242
1598
 
@@ -1257,6 +1613,9 @@ def TouchableOpacity(
1257
1613
  accessibility_hint: Optional[str] = None,
1258
1614
  accessibility_role: Optional[str] = None,
1259
1615
  accessible: Optional[bool] = None,
1616
+ accessibility_state: Optional[Dict[str, Any]] = None,
1617
+ accessibility_live_region: Optional[Literal["none", "polite", "assertive"]] = None,
1618
+ test_id: Optional[str] = None,
1260
1619
  key: Optional[str] = None,
1261
1620
  ) -> Element:
1262
1621
  """Wrap children so they fade to ``active_opacity`` while pressed.
@@ -1278,6 +1637,16 @@ def TouchableOpacity(
1278
1637
  accessibility_hint: Spoken extra detail (iOS only).
1279
1638
  accessibility_role: Override the default ``"button"`` role.
1280
1639
  accessible: Override whether the element is exposed to AT.
1640
+ accessibility_state: Current widget state for assistive tech,
1641
+ e.g. ``{"disabled": True, "selected": False}``. Recognized
1642
+ keys: ``disabled``, ``selected``, ``checked``, ``busy``,
1643
+ ``expanded``.
1644
+ accessibility_live_region: How AT announces dynamic changes to
1645
+ this view: ``"none"``, ``"polite"``, or ``"assertive"``
1646
+ (Android only).
1647
+ test_id: Stable identifier for UI tests; exposed as
1648
+ ``resource-id`` on Android and ``accessibilityIdentifier``
1649
+ on iOS.
1281
1650
  key: Stable identity for keyed reconciliation.
1282
1651
 
1283
1652
  Returns:
@@ -1300,6 +1669,9 @@ def TouchableOpacity(
1300
1669
  accessibility_hint=accessibility_hint,
1301
1670
  accessibility_role=accessibility_role,
1302
1671
  accessible=accessible,
1672
+ accessibility_state=accessibility_state,
1673
+ accessibility_live_region=accessibility_live_region,
1674
+ test_id=test_id,
1303
1675
  key=key,
1304
1676
  )
1305
1677
 
@@ -1311,6 +1683,9 @@ def ImageBackground(
1311
1683
  style: StyleProp = None,
1312
1684
  accessibility_label: Optional[str] = None,
1313
1685
  accessible: Optional[bool] = None,
1686
+ accessibility_state: Optional[Dict[str, Any]] = None,
1687
+ accessibility_live_region: Optional[Literal["none", "polite", "assertive"]] = None,
1688
+ test_id: Optional[str] = None,
1314
1689
  key: Optional[str] = None,
1315
1690
  ) -> Element:
1316
1691
  """Render ``children`` layered on top of a background image.
@@ -1329,6 +1704,16 @@ def ImageBackground(
1329
1704
  style: Style dict for the container (size, padding, alignment).
1330
1705
  accessibility_label: Spoken description of the background image.
1331
1706
  accessible: Override whether the image is exposed to AT.
1707
+ accessibility_state: Current widget state for assistive tech,
1708
+ e.g. ``{"disabled": True, "selected": False}``. Recognized
1709
+ keys: ``disabled``, ``selected``, ``checked``, ``busy``,
1710
+ ``expanded``.
1711
+ accessibility_live_region: How AT announces dynamic changes to
1712
+ this view: ``"none"``, ``"polite"``, or ``"assertive"``
1713
+ (Android only).
1714
+ test_id: Stable identifier for UI tests; exposed as
1715
+ ``resource-id`` on Android and ``accessibilityIdentifier``
1716
+ on iOS.
1332
1717
  key: Stable identity for keyed reconciliation.
1333
1718
 
1334
1719
  Returns:
@@ -1342,6 +1727,9 @@ def ImageBackground(
1342
1727
  style=fill,
1343
1728
  accessibility_label=accessibility_label,
1344
1729
  accessible=accessible,
1730
+ accessibility_state=accessibility_state,
1731
+ accessibility_live_region=accessibility_live_region,
1732
+ test_id=test_id,
1345
1733
  )
1346
1734
  content = View(*children, style={"flex": 1})
1347
1735
  return View(
@@ -1363,6 +1751,9 @@ def Checkbox(
1363
1751
  accessibility_label: Optional[str] = None,
1364
1752
  accessibility_hint: Optional[str] = None,
1365
1753
  accessible: Optional[bool] = None,
1754
+ accessibility_state: Optional[Dict[str, Any]] = None,
1755
+ accessibility_live_region: Optional[Literal["none", "polite", "assertive"]] = None,
1756
+ test_id: Optional[str] = None,
1366
1757
  key: Optional[str] = None,
1367
1758
  ) -> Element:
1368
1759
  """A boolean checkbox with an optional inline label.
@@ -1381,6 +1772,16 @@ def Checkbox(
1381
1772
  accessibility_label: Spoken description for screen readers.
1382
1773
  accessibility_hint: Spoken extra detail (iOS only).
1383
1774
  accessible: Override whether the element is exposed to AT.
1775
+ accessibility_state: Current widget state for assistive tech,
1776
+ e.g. ``{"disabled": True, "selected": False}``. Recognized
1777
+ keys: ``disabled``, ``selected``, ``checked``, ``busy``,
1778
+ ``expanded``.
1779
+ accessibility_live_region: How AT announces dynamic changes to
1780
+ this view: ``"none"``, ``"polite"``, or ``"assertive"``
1781
+ (Android only).
1782
+ test_id: Stable identifier for UI tests; exposed as
1783
+ ``resource-id`` on Android and ``accessibilityIdentifier``
1784
+ on iOS.
1384
1785
  key: Stable identity for keyed reconciliation.
1385
1786
 
1386
1787
  Returns:
@@ -1398,6 +1799,9 @@ def Checkbox(
1398
1799
  accessibility_label=accessibility_label,
1399
1800
  accessibility_hint=accessibility_hint,
1400
1801
  accessible=accessible,
1802
+ accessibility_state=accessibility_state,
1803
+ accessibility_live_region=accessibility_live_region,
1804
+ test_id=test_id,
1401
1805
  _defaults={"accessibility_role": "checkbox"},
1402
1806
  )
1403
1807
 
@@ -1412,6 +1816,9 @@ def SegmentedControl(
1412
1816
  style: StyleProp = None,
1413
1817
  accessibility_label: Optional[str] = None,
1414
1818
  accessible: Optional[bool] = None,
1819
+ accessibility_state: Optional[Dict[str, Any]] = None,
1820
+ accessibility_live_region: Optional[Literal["none", "polite", "assertive"]] = None,
1821
+ test_id: Optional[str] = None,
1415
1822
  key: Optional[str] = None,
1416
1823
  ) -> Element:
1417
1824
  """A horizontal multi-choice control (one selected segment at a time).
@@ -1428,6 +1835,16 @@ def SegmentedControl(
1428
1835
  style: Style dict (or list of dicts).
1429
1836
  accessibility_label: Spoken description for screen readers.
1430
1837
  accessible: Override whether the element is exposed to AT.
1838
+ accessibility_state: Current widget state for assistive tech,
1839
+ e.g. ``{"disabled": True, "selected": False}``. Recognized
1840
+ keys: ``disabled``, ``selected``, ``checked``, ``busy``,
1841
+ ``expanded``.
1842
+ accessibility_live_region: How AT announces dynamic changes to
1843
+ this view: ``"none"``, ``"polite"``, or ``"assertive"``
1844
+ (Android only).
1845
+ test_id: Stable identifier for UI tests; exposed as
1846
+ ``resource-id`` on Android and ``accessibilityIdentifier``
1847
+ on iOS.
1431
1848
  key: Stable identity for keyed reconciliation.
1432
1849
 
1433
1850
  Returns:
@@ -1445,6 +1862,9 @@ def SegmentedControl(
1445
1862
  tint_color=tint_color,
1446
1863
  accessibility_label=accessibility_label,
1447
1864
  accessible=accessible,
1865
+ accessibility_state=accessibility_state,
1866
+ accessibility_live_region=accessibility_live_region,
1867
+ test_id=test_id,
1448
1868
  )
1449
1869
 
1450
1870
 
@@ -1459,6 +1879,9 @@ def DatePicker(
1459
1879
  style: StyleProp = None,
1460
1880
  accessibility_label: Optional[str] = None,
1461
1881
  accessible: Optional[bool] = None,
1882
+ accessibility_state: Optional[Dict[str, Any]] = None,
1883
+ accessibility_live_region: Optional[Literal["none", "polite", "assertive"]] = None,
1884
+ test_id: Optional[str] = None,
1462
1885
  key: Optional[str] = None,
1463
1886
  ) -> Element:
1464
1887
  """A native date / time picker.
@@ -1478,6 +1901,16 @@ def DatePicker(
1478
1901
  style: Style dict (or list of dicts).
1479
1902
  accessibility_label: Spoken description for screen readers.
1480
1903
  accessible: Override whether the element is exposed to AT.
1904
+ accessibility_state: Current widget state for assistive tech,
1905
+ e.g. ``{"disabled": True, "selected": False}``. Recognized
1906
+ keys: ``disabled``, ``selected``, ``checked``, ``busy``,
1907
+ ``expanded``.
1908
+ accessibility_live_region: How AT announces dynamic changes to
1909
+ this view: ``"none"``, ``"polite"``, or ``"assertive"``
1910
+ (Android only).
1911
+ test_id: Stable identifier for UI tests; exposed as
1912
+ ``resource-id`` on Android and ``accessibilityIdentifier``
1913
+ on iOS.
1481
1914
  key: Stable identity for keyed reconciliation.
1482
1915
 
1483
1916
  Returns:
@@ -1495,6 +1928,9 @@ def DatePicker(
1495
1928
  enabled=False if enabled is False else None,
1496
1929
  accessibility_label=accessibility_label,
1497
1930
  accessible=accessible,
1931
+ accessibility_state=accessibility_state,
1932
+ accessibility_live_region=accessibility_live_region,
1933
+ test_id=test_id,
1498
1934
  _defaults={"accessibility_role": "button"},
1499
1935
  )
1500
1936
 
@@ -1600,17 +2036,26 @@ def ErrorBoundary(
1600
2036
 
1601
2037
 
1602
2038
  # ======================================================================
1603
- # Lists (Python-windowed virtualization over ScrollView)
2039
+ # Lists (native virtualization with a Python-windowed fallback)
1604
2040
  # ======================================================================
1605
2041
  #
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.
2042
+ # FlatList and SectionList pick between two engines:
2043
+ #
2044
+ # 1. **Native virtualization** (`_NativeList` -> the ``VirtualList``
2045
+ # element): on Android and iOS, when every row extent is known up
2046
+ # front and no windowed-only feature is requested, the list is
2047
+ # backed by a real ``RecyclerView`` / ``UITableView``. The platform
2048
+ # owns row recycling; each visible row hosts a nested-reconciler
2049
+ # subtree (see ``pythonnative.virtual_rows``).
2050
+ # 2. **Python windowing** (`_VirtualizedList`): a windowed slice of
2051
+ # rows rendered into a ScrollView (leading spacer, visible rows,
2052
+ # trailing spacer), the window shifting from scroll events (the
2053
+ # same architecture as React Native's VirtualizedList). Because
2054
+ # every windowed row lives in the *main* layout tree, rows may be
2055
+ # any height: estimates steer the spacer sizes and measured extents
2056
+ # correct them over time. This is the desktop path and the fallback
2057
+ # for variable-height rows, grids, horizontal lists, ornaments, and
2058
+ # pull-to-refresh.
1614
2059
 
1615
2060
  _DEFAULT_ROW_EXTENT = 44.0
1616
2061
 
@@ -1841,6 +2286,132 @@ def _VirtualizedList(**p: Any) -> Element:
1841
2286
  )
1842
2287
 
1843
2288
 
2289
+ def _native_lists_supported() -> bool:
2290
+ """Whether the natively virtualized list path is available.
2291
+
2292
+ Android (RecyclerView) and iOS (UITableView) have native handlers;
2293
+ the desktop preview and off-device tests use the Python-windowed
2294
+ engine. Patchable in tests to exercise the native routing.
2295
+ """
2296
+ from .utils import IS_ANDROID, IS_IOS
2297
+
2298
+ return IS_ANDROID or IS_IOS
2299
+
2300
+
2301
+ @component
2302
+ def _NativeList(**p: Any) -> Element:
2303
+ """Platform-virtualized list: emits a ``VirtualList`` native element.
2304
+
2305
+ The native side (RecyclerView / UITableView) owns row windowing and
2306
+ recycling; each visible row hosts a nested-reconciler subtree (see
2307
+ ``pythonnative.virtual_rows``). This composite adapts the FlatList /
2308
+ SectionList surface onto that element: it forwards ``render_row``,
2309
+ derives ``on_end_reached`` and ``on_viewable_items_changed`` from
2310
+ native scroll reports, and wires the imperative scroll controller.
2311
+
2312
+ Requires every row's extent to be known up front (the native
2313
+ virtualizers need exact heights before rows are rendered); callers
2314
+ fall back to the Python-windowed engine otherwise.
2315
+ """
2316
+ rows: List[_RowSpec] = p.get("rows") or []
2317
+ n = len(rows)
2318
+ heights: List[float] = [float(spec.extent or 0.0) for spec in rows]
2319
+ uniform = len(set(heights)) <= 1
2320
+
2321
+ internal_ref = use_ref(None)
2322
+ end_latch = use_ref({"fired_for": -1})
2323
+ viewable_ref = use_ref({"keys": ()})
2324
+
2325
+ starts: List[float] = [0.0] * (n + 1)
2326
+ acc = 0.0
2327
+ for i, extent in enumerate(heights):
2328
+ starts[i] = acc
2329
+ acc += max(0.0, extent)
2330
+ starts[n] = acc
2331
+ total_extent = acc
2332
+
2333
+ def _render_row(index: int) -> Element:
2334
+ if 0 <= index < n:
2335
+ return rows[index].make()
2336
+ return View()
2337
+
2338
+ on_end_reached = p.get("on_end_reached")
2339
+ end_threshold = float(p.get("on_end_reached_threshold") or 0.5)
2340
+ on_viewable = p.get("on_viewable_items_changed")
2341
+ user_on_scroll = p.get("on_scroll")
2342
+
2343
+ def _handle_scroll(payload: Any) -> None:
2344
+ offset = float(payload.get("y", 0.0) or 0.0) if isinstance(payload, dict) else float(payload or 0.0)
2345
+ viewport = float(payload.get("extent", 0.0) or 0.0) if isinstance(payload, dict) else 0.0
2346
+ if viewport <= 0:
2347
+ viewport = 800.0
2348
+
2349
+ if on_end_reached is not None and total_extent > 0:
2350
+ remaining = total_extent - (offset + viewport)
2351
+ if remaining <= end_threshold * viewport:
2352
+ if end_latch["current"]["fired_for"] != n:
2353
+ end_latch["current"]["fired_for"] = n
2354
+ on_end_reached()
2355
+ elif remaining > end_threshold * viewport + viewport:
2356
+ end_latch["current"]["fired_for"] = -1
2357
+
2358
+ if on_viewable is not None and n > 0:
2359
+ v_first = max(0, bisect.bisect_right(starts, offset, 0, n) - 1)
2360
+ v_last = min(n - 1, bisect.bisect_left(starts, offset + viewport, 0, n))
2361
+ keys = tuple(rows[i].key for i in range(v_first, v_last + 1))
2362
+ if keys != viewable_ref["current"]["keys"]:
2363
+ viewable_ref["current"]["keys"] = keys
2364
+ on_viewable(
2365
+ [
2366
+ {"index": rows[i].index, "key": rows[i].key, "item": rows[i].item}
2367
+ for i in range(v_first, v_last + 1)
2368
+ ]
2369
+ )
2370
+
2371
+ if user_on_scroll is not None:
2372
+ user_on_scroll({"x": 0.0, "y": offset})
2373
+
2374
+ controller = p.get("controller_ref")
2375
+
2376
+ def _attach_controller() -> None:
2377
+ if not isinstance(controller, dict):
2378
+ return
2379
+
2380
+ def scroll_to_offset(offset: float, animated: bool = True) -> None:
2381
+ _dispatch_scroll_command(internal_ref, "scroll_to_offset", {"y": float(offset), "animated": animated})
2382
+
2383
+ def scroll_to_index(index: int, animated: bool = True) -> None:
2384
+ _dispatch_scroll_command(internal_ref, "scroll_to_index", {"index": int(index), "animated": animated})
2385
+
2386
+ def scroll_to_end(animated: bool = True) -> None:
2387
+ _dispatch_scroll_command(internal_ref, "scroll_to_end", {"animated": animated})
2388
+
2389
+ controller["scroll_to_offset"] = scroll_to_offset
2390
+ controller["scroll_to_index"] = scroll_to_index
2391
+ controller["scroll_to_end"] = scroll_to_end
2392
+
2393
+ use_effect(_attach_controller, None)
2394
+
2395
+ props: Dict[str, Any] = dict(p.get("list_style") or {})
2396
+ props["count"] = n
2397
+ if uniform:
2398
+ props["row_height"] = heights[0] if heights else _DEFAULT_ROW_EXTENT
2399
+ else:
2400
+ props["row_heights"] = heights
2401
+ props["render_row"] = _render_row
2402
+ props["ref"] = internal_ref
2403
+ wants_scroll = on_end_reached is not None or on_viewable is not None or user_on_scroll is not None
2404
+ if wants_scroll:
2405
+ props["on_scroll"] = _handle_scroll
2406
+ if p.get("shows_scroll_indicator") is False:
2407
+ props["shows_scroll_indicator"] = False
2408
+ return Element("VirtualList", props, [])
2409
+
2410
+
2411
+ def _all_extents_known(rows: List[_RowSpec]) -> bool:
2412
+ return all(spec.extent is not None for spec in rows)
2413
+
2414
+
1844
2415
  def FlatList(
1845
2416
  *,
1846
2417
  data: Optional[List[Any]] = None,
@@ -1994,6 +2565,32 @@ def FlatList(
1994
2565
 
1995
2566
  estimated = estimated_item_height if estimated_item_height is not None else (item_height or _DEFAULT_ROW_EXTENT)
1996
2567
 
2568
+ # Route to the platform virtualizer (RecyclerView / UITableView)
2569
+ # when it can represent this list exactly: vertical, single-column,
2570
+ # every row extent known up front, and no features that only the
2571
+ # Python-windowed engine implements (ornaments, pull-to-refresh).
2572
+ if (
2573
+ _native_lists_supported()
2574
+ and not horizontal
2575
+ and num_columns == 1
2576
+ and list_header is None
2577
+ and list_footer is None
2578
+ and list_empty is None
2579
+ and refresh_control is None
2580
+ and _all_extents_known(rows)
2581
+ ):
2582
+ return _NativeList(
2583
+ rows=rows,
2584
+ on_end_reached=on_end_reached,
2585
+ on_end_reached_threshold=on_end_reached_threshold,
2586
+ on_viewable_items_changed=on_viewable_items_changed,
2587
+ on_scroll=on_scroll,
2588
+ shows_scroll_indicator=shows_scroll_indicator,
2589
+ list_style=resolve_style(style) or None,
2590
+ controller_ref=ref,
2591
+ key=key,
2592
+ )
2593
+
1997
2594
  return _VirtualizedList(
1998
2595
  rows=rows,
1999
2596
  horizontal=horizontal,
@@ -2132,6 +2729,26 @@ def SectionList(
2132
2729
 
2133
2730
  estimated = estimated_item_height if estimated_item_height is not None else (item_height or _DEFAULT_ROW_EXTENT)
2134
2731
 
2732
+ # Same native routing as FlatList: headers and items become one
2733
+ # flattened row sequence with per-row heights.
2734
+ if (
2735
+ _native_lists_supported()
2736
+ and list_header is None
2737
+ and list_footer is None
2738
+ and list_empty is None
2739
+ and refresh_control is None
2740
+ and _all_extents_known(rows)
2741
+ ):
2742
+ return _NativeList(
2743
+ rows=rows,
2744
+ on_end_reached=on_end_reached,
2745
+ on_end_reached_threshold=on_end_reached_threshold,
2746
+ on_scroll=on_scroll,
2747
+ list_style=resolve_style(style) or None,
2748
+ controller_ref=ref,
2749
+ key=key,
2750
+ )
2751
+
2135
2752
  return _VirtualizedList(
2136
2753
  rows=rows,
2137
2754
  horizontal=False,
@@ -2194,9 +2811,40 @@ def StatusBar(
2194
2811
  return Element("StatusBar", props, [], key=key)
2195
2812
 
2196
2813
 
2814
+ @component
2815
+ def _KeyboardAvoidingContainer(**p: Any) -> Element:
2816
+ """Hook-driven body of `KeyboardAvoidingView`.
2817
+
2818
+ Subscribes to the platform-reported keyboard height via
2819
+ [`use_keyboard_height`][pythonnative.use_keyboard_height] and
2820
+ applies the shift according to ``behavior``:
2821
+
2822
+ - ``"padding"``: adds the shift as bottom padding, resizing the
2823
+ content area (the default, and the right choice for forms
2824
+ inside a full-height container).
2825
+ - ``"position"``: translates the whole container upward without
2826
+ resizing it (useful for pinned footers/toolbars).
2827
+ """
2828
+ height = use_keyboard_height()
2829
+ behavior = p.get("behavior") or "padding"
2830
+ offset = float(p.get("keyboard_vertical_offset") or 0.0)
2831
+ shift = max(0.0, height - offset) if height > 0 else 0.0
2832
+ style: Dict[str, Any] = dict(p.get("style") or {})
2833
+ if shift > 0:
2834
+ if behavior == "position":
2835
+ transform = list(style.get("transform") or [])
2836
+ transform.append({"translate_y": -shift})
2837
+ style["transform"] = transform
2838
+ else:
2839
+ style["padding_bottom"] = _numeric_edge_padding(style, "bottom") + shift
2840
+ children = p.get("children") or []
2841
+ return Element("KeyboardAvoidingView", style, list(children))
2842
+
2843
+
2197
2844
  def KeyboardAvoidingView(
2198
2845
  *children: Element,
2199
2846
  behavior: Literal["padding", "position"] = "padding",
2847
+ keyboard_vertical_offset: float = 0.0,
2200
2848
  style: StyleProp = None,
2201
2849
  key: Optional[str] = None,
2202
2850
  ) -> Element:
@@ -2204,26 +2852,32 @@ def KeyboardAvoidingView(
2204
2852
 
2205
2853
  Subscribes to the platform-reported keyboard height (via
2206
2854
  [`use_keyboard_height`][pythonnative.use_keyboard_height]
2207
- internally) and applies it as bottom padding so the focused text
2208
- input stays visible.
2855
+ internally) and shifts its content so the focused text input stays
2856
+ visible. On iOS the height comes from
2857
+ ``UIKeyboardWillShowNotification``; on Android from the window's
2858
+ IME insets.
2209
2859
 
2210
2860
  Args:
2211
2861
  *children: Children rendered inside the avoiding container.
2212
- behavior: ``"padding"`` (adds bottom padding) or ``"position"``
2213
- (translates the container upward).
2862
+ behavior: ``"padding"`` (adds bottom padding, resizing the
2863
+ content) or ``"position"`` (translates the container
2864
+ upward without resizing).
2865
+ keyboard_vertical_offset: Distance in layout units already
2866
+ covered by other UI (e.g. a nav bar); subtracted from the
2867
+ keyboard height before applying the shift.
2214
2868
  style: Style dict (or list of dicts).
2215
2869
  key: Stable identity for keyed reconciliation.
2216
2870
 
2217
2871
  Returns:
2218
- An [`Element`][pythonnative.Element] of type
2219
- ``"KeyboardAvoidingView"``.
2872
+ An [`Element`][pythonnative.Element] that renders a
2873
+ ``"KeyboardAvoidingView"`` container.
2220
2874
  """
2221
- return _make_element(
2222
- "KeyboardAvoidingView",
2223
- *children,
2224
- style=style,
2225
- key=key,
2875
+ return _KeyboardAvoidingContainer(
2876
+ children=list(children),
2226
2877
  behavior=behavior,
2878
+ keyboard_vertical_offset=keyboard_vertical_offset,
2879
+ style=resolve_style(style),
2880
+ key=key,
2227
2881
  )
2228
2882
 
2229
2883
 
@@ -2291,6 +2945,9 @@ def Picker(
2291
2945
  accessibility_label: Optional[str] = None,
2292
2946
  accessibility_hint: Optional[str] = None,
2293
2947
  accessible: Optional[bool] = None,
2948
+ accessibility_state: Optional[Dict[str, Any]] = None,
2949
+ accessibility_live_region: Optional[Literal["none", "polite", "assertive"]] = None,
2950
+ test_id: Optional[str] = None,
2294
2951
  ref: Optional[Dict[str, Any]] = None,
2295
2952
  key: Optional[str] = None,
2296
2953
  ) -> Element:
@@ -2314,6 +2971,16 @@ def Picker(
2314
2971
  accessibility_label: Spoken description for screen readers.
2315
2972
  accessibility_hint: Spoken extra detail (iOS only).
2316
2973
  accessible: Override whether the element is exposed to AT.
2974
+ accessibility_state: Current widget state for assistive tech,
2975
+ e.g. ``{"disabled": True, "selected": False}``. Recognized
2976
+ keys: ``disabled``, ``selected``, ``checked``, ``busy``,
2977
+ ``expanded``.
2978
+ accessibility_live_region: How AT announces dynamic changes to
2979
+ this view: ``"none"``, ``"polite"``, or ``"assertive"``
2980
+ (Android only).
2981
+ test_id: Stable identifier for UI tests; exposed as
2982
+ ``resource-id`` on Android and ``accessibilityIdentifier``
2983
+ on iOS.
2317
2984
  ref: Optional ``use_ref()`` dict.
2318
2985
  key: Stable identity for keyed reconciliation.
2319
2986
 
@@ -2332,5 +2999,8 @@ def Picker(
2332
2999
  accessibility_label=accessibility_label,
2333
3000
  accessibility_hint=accessibility_hint,
2334
3001
  accessible=accessible,
3002
+ accessibility_state=accessibility_state,
3003
+ accessibility_live_region=accessibility_live_region,
3004
+ test_id=test_id,
2335
3005
  _defaults={"accessibility_role": "button"},
2336
3006
  )