pythonnative 0.22.1__py3-none-any.whl → 0.24.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pythonnative/__init__.py +43 -50
- pythonnative/animated.py +1 -1
- pythonnative/appearance.py +124 -0
- pythonnative/cli/pn.py +3 -3
- pythonnative/components.py +873 -466
- pythonnative/diagnostics.py +214 -0
- pythonnative/element.py +5 -2
- pythonnative/events.py +13 -8
- pythonnative/hooks.py +454 -49
- pythonnative/hot_reload.py +9 -1
- pythonnative/images.py +196 -0
- pythonnative/mutations.py +3 -12
- pythonnative/native_views/__init__.py +1 -7
- pythonnative/native_views/android.py +651 -46
- pythonnative/native_views/desktop.py +116 -20
- pythonnative/native_views/ios.py +974 -52
- pythonnative/navigation.py +41 -17
- pythonnative/preview.py +74 -7
- pythonnative/project/config.py +1 -9
- pythonnative/reconciler.py +863 -441
- pythonnative/screen.py +409 -27
- pythonnative/storage.py +3 -3
- pythonnative/style.py +148 -6
- pythonnative/templates/android_template/app/src/main/AndroidManifest.xml +2 -1
- pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/PNAccessibilityDelegate.kt +47 -0
- pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/PNBorderDrawable.kt +67 -0
- pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/PNVirtualListView.java +172 -0
- pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/ScreenFragment.kt +22 -0
- pythonnative/virtual_rows.py +137 -0
- {pythonnative-0.22.1.dist-info → pythonnative-0.24.0.dist-info}/METADATA +5 -4
- {pythonnative-0.22.1.dist-info → pythonnative-0.24.0.dist-info}/RECORD +35 -28
- {pythonnative-0.22.1.dist-info → pythonnative-0.24.0.dist-info}/WHEEL +1 -1
- {pythonnative-0.22.1.dist-info → pythonnative-0.24.0.dist-info}/entry_points.txt +0 -0
- {pythonnative-0.22.1.dist-info → pythonnative-0.24.0.dist-info}/licenses/LICENSE +0 -0
- {pythonnative-0.22.1.dist-info → pythonnative-0.24.0.dist-info}/top_level.txt +0 -0
pythonnative/native_views/ios.py
CHANGED
|
@@ -304,6 +304,93 @@ def _apply_view_border(view: Any, props: Dict[str, Any]) -> None:
|
|
|
304
304
|
_apply_border(view.layer, props)
|
|
305
305
|
|
|
306
306
|
|
|
307
|
+
# Per-side border state keyed by id(view): {"widths": (l, t, r, b),
|
|
308
|
+
# "colors": (l, t, r, b), "layers": [CALayer or None x4]}.
|
|
309
|
+
_pn_side_border_map: dict = {}
|
|
310
|
+
|
|
311
|
+
_SIDE_BORDER_WIDTH_KEYS = (
|
|
312
|
+
"border_left_width",
|
|
313
|
+
"border_top_width",
|
|
314
|
+
"border_right_width",
|
|
315
|
+
"border_bottom_width",
|
|
316
|
+
)
|
|
317
|
+
_SIDE_BORDER_COLOR_KEYS = (
|
|
318
|
+
"border_left_color",
|
|
319
|
+
"border_top_color",
|
|
320
|
+
"border_right_color",
|
|
321
|
+
"border_bottom_color",
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def _apply_side_borders(view: Any, props: Dict[str, Any]) -> None:
|
|
326
|
+
"""Record per-side border strips (drawn as CALayer sublayers).
|
|
327
|
+
|
|
328
|
+
Activated when any ``border_<side>_width`` key is present. In that
|
|
329
|
+
mode the strips own all four edges: each side's width falls back
|
|
330
|
+
to the uniform ``border_width`` (else 0) and its color to the
|
|
331
|
+
uniform ``border_color`` (else black), and the layer-level uniform
|
|
332
|
+
border is disabled so the two mechanisms don't double-draw. The
|
|
333
|
+
strips are (re)positioned at frame time when bounds are known.
|
|
334
|
+
"""
|
|
335
|
+
if not any(k in props and props[k] is not None for k in _SIDE_BORDER_WIDTH_KEYS):
|
|
336
|
+
return
|
|
337
|
+
base_width = float(props.get("border_width") or 0.0)
|
|
338
|
+
base_color = props.get("border_color") or "#000000"
|
|
339
|
+
widths = tuple(float(props[k]) if props.get(k) is not None else base_width for k in _SIDE_BORDER_WIDTH_KEYS)
|
|
340
|
+
colors = tuple(props[k] if props.get(k) is not None else base_color for k in _SIDE_BORDER_COLOR_KEYS)
|
|
341
|
+
entry = _pn_side_border_map.setdefault(id(view), {"layers": [None, None, None, None]})
|
|
342
|
+
entry["widths"] = widths
|
|
343
|
+
entry["colors"] = colors
|
|
344
|
+
try:
|
|
345
|
+
view.layer.setBorderWidth_(0.0)
|
|
346
|
+
except Exception:
|
|
347
|
+
pass
|
|
348
|
+
try:
|
|
349
|
+
bounds = view.bounds
|
|
350
|
+
w = float(bounds.size.width)
|
|
351
|
+
h = float(bounds.size.height)
|
|
352
|
+
if w > 0.0 and h > 0.0:
|
|
353
|
+
_update_side_border_layers(view, w, h)
|
|
354
|
+
except Exception:
|
|
355
|
+
pass
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def _update_side_border_layers(view: Any, width: float, height: float) -> None:
|
|
359
|
+
"""Size the four border-strip sublayers to the view's current bounds."""
|
|
360
|
+
entry = _pn_side_border_map.get(id(view))
|
|
361
|
+
if entry is None or "widths" not in entry:
|
|
362
|
+
return
|
|
363
|
+
left_w, top_w, right_w, bottom_w = entry["widths"]
|
|
364
|
+
frames = (
|
|
365
|
+
((0.0, 0.0), (left_w, height)),
|
|
366
|
+
((0.0, 0.0), (width, top_w)),
|
|
367
|
+
((width - right_w, 0.0), (right_w, height)),
|
|
368
|
+
((0.0, height - bottom_w), (width, bottom_w)),
|
|
369
|
+
)
|
|
370
|
+
CALayer = ObjCClass("CALayer")
|
|
371
|
+
layers = entry["layers"]
|
|
372
|
+
for i in range(4):
|
|
373
|
+
w_i = entry["widths"][i]
|
|
374
|
+
layer = layers[i]
|
|
375
|
+
if w_i <= 0.0:
|
|
376
|
+
if layer is not None:
|
|
377
|
+
try:
|
|
378
|
+
layer.removeFromSuperlayer()
|
|
379
|
+
except Exception:
|
|
380
|
+
pass
|
|
381
|
+
layers[i] = None
|
|
382
|
+
continue
|
|
383
|
+
try:
|
|
384
|
+
if layer is None:
|
|
385
|
+
layer = CALayer.layer()
|
|
386
|
+
view.layer.addSublayer_(layer)
|
|
387
|
+
layers[i] = layer
|
|
388
|
+
layer.setBackgroundColor_(_cgcolor(entry["colors"][i]))
|
|
389
|
+
layer.setFrame_(frames[i])
|
|
390
|
+
except Exception:
|
|
391
|
+
pass
|
|
392
|
+
|
|
393
|
+
|
|
307
394
|
def _clamp_view_corner_radius(view: Any, width: float, height: float) -> None:
|
|
308
395
|
"""Clamp oversized pill radii to the view's rendered bounds."""
|
|
309
396
|
requested = _pn_view_border_radius_map.get(id(view))
|
|
@@ -481,8 +568,38 @@ def _apply_transform(view: Any, props: Dict[str, Any]) -> None:
|
|
|
481
568
|
pass
|
|
482
569
|
|
|
483
570
|
|
|
571
|
+
# UIAccessibilityTraits bitmask values (UIKit constants).
|
|
572
|
+
_TRAIT_BUTTON = 0x1
|
|
573
|
+
_TRAIT_LINK = 0x2
|
|
574
|
+
_TRAIT_IMAGE = 0x4
|
|
575
|
+
_TRAIT_SELECTED = 0x8
|
|
576
|
+
_TRAIT_KEYBOARD_KEY = 0x20
|
|
577
|
+
_TRAIT_STATIC_TEXT = 0x40
|
|
578
|
+
_TRAIT_SUMMARY_ELEMENT = 0x80
|
|
579
|
+
_TRAIT_NOT_ENABLED = 0x100
|
|
580
|
+
_TRAIT_UPDATES_FREQUENTLY = 0x200
|
|
581
|
+
_TRAIT_SEARCH_FIELD = 0x400
|
|
582
|
+
_TRAIT_ADJUSTABLE = 0x1000
|
|
583
|
+
_TRAIT_HEADER = 0x10000
|
|
584
|
+
|
|
585
|
+
_TRAIT_BY_ROLE = {
|
|
586
|
+
"button": _TRAIT_BUTTON,
|
|
587
|
+
"link": _TRAIT_LINK,
|
|
588
|
+
"image": _TRAIT_IMAGE,
|
|
589
|
+
"search": _TRAIT_SEARCH_FIELD,
|
|
590
|
+
"keyboard_key": _TRAIT_KEYBOARD_KEY,
|
|
591
|
+
"static_text": _TRAIT_STATIC_TEXT,
|
|
592
|
+
"summary_element": _TRAIT_SUMMARY_ELEMENT,
|
|
593
|
+
"adjustable": _TRAIT_ADJUSTABLE,
|
|
594
|
+
"header": _TRAIT_HEADER,
|
|
595
|
+
"selected": _TRAIT_SELECTED,
|
|
596
|
+
"checkbox": _TRAIT_BUTTON,
|
|
597
|
+
"none": 0,
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
|
|
484
601
|
def _apply_accessibility(view: Any, props: Dict[str, Any]) -> None:
|
|
485
|
-
"""Apply
|
|
602
|
+
"""Apply accessibility props (label / hint / role / state / test_id)."""
|
|
486
603
|
if "accessible" in props:
|
|
487
604
|
try:
|
|
488
605
|
view.setIsAccessibilityElement_(bool(props["accessible"]))
|
|
@@ -500,25 +617,36 @@ def _apply_accessibility(view: Any, props: Dict[str, Any]) -> None:
|
|
|
500
617
|
view.setAccessibilityHint_(str(v) if v is not None else "")
|
|
501
618
|
except Exception:
|
|
502
619
|
pass
|
|
503
|
-
if "
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
620
|
+
if "test_id" in props:
|
|
621
|
+
v = props["test_id"]
|
|
622
|
+
try:
|
|
623
|
+
view.setAccessibilityIdentifier_(str(v) if v is not None else None)
|
|
624
|
+
except Exception:
|
|
625
|
+
pass
|
|
626
|
+
|
|
627
|
+
role = props.get("accessibility_role")
|
|
628
|
+
state = props.get("accessibility_state")
|
|
629
|
+
if role is None and state is None:
|
|
630
|
+
return
|
|
631
|
+
traits = 0
|
|
632
|
+
if role is not None:
|
|
633
|
+
traits |= _TRAIT_BY_ROLE.get(str(role).lower(), 0)
|
|
634
|
+
if isinstance(state, dict):
|
|
635
|
+
if state.get("selected") or state.get("checked"):
|
|
636
|
+
traits |= _TRAIT_SELECTED
|
|
637
|
+
if state.get("disabled"):
|
|
638
|
+
traits |= _TRAIT_NOT_ENABLED
|
|
639
|
+
if state.get("busy"):
|
|
640
|
+
traits |= _TRAIT_UPDATES_FREQUENTLY
|
|
641
|
+
if "expanded" in state:
|
|
518
642
|
try:
|
|
519
|
-
view.
|
|
643
|
+
view.setAccessibilityValue_("expanded" if state["expanded"] else "collapsed")
|
|
520
644
|
except Exception:
|
|
521
645
|
pass
|
|
646
|
+
try:
|
|
647
|
+
view.setAccessibilityTraits_(traits)
|
|
648
|
+
except Exception:
|
|
649
|
+
pass
|
|
522
650
|
|
|
523
651
|
|
|
524
652
|
def _apply_common_visual(view: Any, props: Dict[str, Any]) -> None:
|
|
@@ -538,6 +666,7 @@ def _apply_common_visual(view: Any, props: Dict[str, Any]) -> None:
|
|
|
538
666
|
except Exception:
|
|
539
667
|
pass
|
|
540
668
|
_apply_view_border(view, props)
|
|
669
|
+
_apply_side_borders(view, props)
|
|
541
670
|
_apply_shadow(view, props)
|
|
542
671
|
_apply_transform(view, props)
|
|
543
672
|
_apply_accessibility(view, props)
|
|
@@ -1037,6 +1166,8 @@ class IOSViewHandler(ViewHandler):
|
|
|
1037
1166
|
# Controls register their own pointer as the action-handler key
|
|
1038
1167
|
# (see _register_control_action); drop it with the view.
|
|
1039
1168
|
_pn_action_handlers.pop(_recognizer_ptr(native_view), None)
|
|
1169
|
+
_pn_view_border_radius_map.pop(id(native_view), None)
|
|
1170
|
+
_pn_side_border_map.pop(id(native_view), None)
|
|
1040
1171
|
try:
|
|
1041
1172
|
native_view.removeFromSuperview()
|
|
1042
1173
|
except Exception:
|
|
@@ -1086,6 +1217,7 @@ class IOSViewHandler(ViewHandler):
|
|
|
1086
1217
|
native_view.setTranslatesAutoresizingMaskIntoConstraints_(True)
|
|
1087
1218
|
native_view.setFrame_(((frame_x, frame_y), (frame_w, frame_h)))
|
|
1088
1219
|
_clamp_view_corner_radius(native_view, frame_w, frame_h)
|
|
1220
|
+
_update_side_border_layers(native_view, frame_w, frame_h)
|
|
1089
1221
|
try:
|
|
1090
1222
|
_clamp_layer_corner_radius(native_view.layer, frame_w, frame_h)
|
|
1091
1223
|
except Exception:
|
|
@@ -1271,8 +1403,24 @@ class TextHandler(IOSViewHandler):
|
|
|
1271
1403
|
pass
|
|
1272
1404
|
return font
|
|
1273
1405
|
|
|
1406
|
+
_SPAN_REBUILD_KEYS = (
|
|
1407
|
+
"spans",
|
|
1408
|
+
"text",
|
|
1409
|
+
"font_size",
|
|
1410
|
+
"font_weight",
|
|
1411
|
+
"font_family",
|
|
1412
|
+
"italic",
|
|
1413
|
+
"bold",
|
|
1414
|
+
"color",
|
|
1415
|
+
"letter_spacing",
|
|
1416
|
+
"line_height",
|
|
1417
|
+
"text_decoration",
|
|
1418
|
+
)
|
|
1419
|
+
|
|
1274
1420
|
def _apply(self, label: Any, props: Dict[str, Any], initial: bool) -> None:
|
|
1275
|
-
|
|
1421
|
+
merged_state = _state_of(label).get("props") or props
|
|
1422
|
+
has_spans = bool(merged_state.get("spans"))
|
|
1423
|
+
if "text" in props and not has_spans:
|
|
1276
1424
|
label.setText_(str(props["text"]) if props["text"] is not None else "")
|
|
1277
1425
|
# Font requires combining size + weight + family + italic + bold.
|
|
1278
1426
|
font_keys_present = any(k in props for k in ("font_size", "font_weight", "font_family", "italic", "bold"))
|
|
@@ -1299,9 +1447,14 @@ class TextHandler(IOSViewHandler):
|
|
|
1299
1447
|
if "text_align" in props:
|
|
1300
1448
|
mapping = {"left": 0, "center": 1, "right": 2, "natural": 4, "justify": 3}
|
|
1301
1449
|
label.setTextAlignment_(mapping.get(props["text_align"], 0))
|
|
1302
|
-
if
|
|
1303
|
-
|
|
1304
|
-
|
|
1450
|
+
if has_spans:
|
|
1451
|
+
if any(k in props for k in self._SPAN_REBUILD_KEYS):
|
|
1452
|
+
self._apply_spans(label, merged_state)
|
|
1453
|
+
elif "spans" in props:
|
|
1454
|
+
# Rich -> plain transition: restore the plain string.
|
|
1455
|
+
label.setText_(str(merged_state.get("text") or ""))
|
|
1456
|
+
elif "letter_spacing" in props or "line_height" in props or "text_decoration" in props:
|
|
1457
|
+
self._apply_attributed(label, merged_state)
|
|
1305
1458
|
_apply_view_border(label, props)
|
|
1306
1459
|
_apply_shadow(label, props)
|
|
1307
1460
|
_apply_transform(label, props)
|
|
@@ -1312,6 +1465,74 @@ class TextHandler(IOSViewHandler):
|
|
|
1312
1465
|
except Exception:
|
|
1313
1466
|
pass
|
|
1314
1467
|
|
|
1468
|
+
def _apply_spans(self, label: Any, merged: Dict[str, Any]) -> None:
|
|
1469
|
+
"""Render a rich-text span list as one NSAttributedString.
|
|
1470
|
+
|
|
1471
|
+
The label's base text styling (font, color, kerning, line
|
|
1472
|
+
height, decoration) is applied over the full string first,
|
|
1473
|
+
then each span's overrides are layered onto its own range, so
|
|
1474
|
+
line wrapping flows across spans inside a single ``UILabel``.
|
|
1475
|
+
"""
|
|
1476
|
+
try:
|
|
1477
|
+
spans = [s for s in (merged.get("spans") or []) if isinstance(s, dict)]
|
|
1478
|
+
full_text = "".join(str(s.get("text", "")) for s in spans)
|
|
1479
|
+
NSMutableAttributedString = ObjCClass("NSMutableAttributedString")
|
|
1480
|
+
NSMutableParagraphStyle = ObjCClass("NSMutableParagraphStyle")
|
|
1481
|
+
attr = NSMutableAttributedString.alloc().initWithString_(full_text)
|
|
1482
|
+
full_range = (0, len(full_text))
|
|
1483
|
+
|
|
1484
|
+
base_font = label.font
|
|
1485
|
+
base_size = float(base_font.pointSize) if base_font is not None else 17.0
|
|
1486
|
+
if base_font is not None:
|
|
1487
|
+
attr.addAttribute_value_range_("NSFont", base_font, full_range)
|
|
1488
|
+
if merged.get("letter_spacing") is not None:
|
|
1489
|
+
attr.addAttribute_value_range_("NSKern", float(merged["letter_spacing"]), full_range)
|
|
1490
|
+
if merged.get("line_height") is not None:
|
|
1491
|
+
p_style = NSMutableParagraphStyle.alloc().init()
|
|
1492
|
+
p_style.setMinimumLineHeight_(float(merged["line_height"]))
|
|
1493
|
+
p_style.setMaximumLineHeight_(float(merged["line_height"]))
|
|
1494
|
+
attr.addAttribute_value_range_("NSParagraphStyle", p_style, full_range)
|
|
1495
|
+
if merged.get("text_decoration") == "underline":
|
|
1496
|
+
attr.addAttribute_value_range_("NSUnderline", 1, full_range)
|
|
1497
|
+
elif merged.get("text_decoration") == "line_through":
|
|
1498
|
+
attr.addAttribute_value_range_("NSStrikethrough", 1, full_range)
|
|
1499
|
+
|
|
1500
|
+
pos = 0
|
|
1501
|
+
for span in spans:
|
|
1502
|
+
text = str(span.get("text", ""))
|
|
1503
|
+
rng = (pos, len(text))
|
|
1504
|
+
pos += len(text)
|
|
1505
|
+
if not text:
|
|
1506
|
+
continue
|
|
1507
|
+
try:
|
|
1508
|
+
if any(
|
|
1509
|
+
span.get(k) is not None for k in ("font_size", "font_weight", "font_family", "italic", "bold")
|
|
1510
|
+
):
|
|
1511
|
+
size = float(span["font_size"]) if span.get("font_size") is not None else base_size
|
|
1512
|
+
weight = span.get("font_weight")
|
|
1513
|
+
if weight is None and span.get("bold"):
|
|
1514
|
+
weight = "bold"
|
|
1515
|
+
font = self._font_for(size, weight, span.get("font_family"), bool(span.get("italic")))
|
|
1516
|
+
attr.addAttribute_value_range_("NSFont", font, rng)
|
|
1517
|
+
if span.get("color") is not None:
|
|
1518
|
+
attr.addAttribute_value_range_("NSColor", _uicolor(span["color"]), rng)
|
|
1519
|
+
if span.get("background_color") is not None:
|
|
1520
|
+
attr.addAttribute_value_range_("NSBackgroundColor", _uicolor(span["background_color"]), rng)
|
|
1521
|
+
if span.get("letter_spacing") is not None:
|
|
1522
|
+
attr.addAttribute_value_range_("NSKern", float(span["letter_spacing"]), rng)
|
|
1523
|
+
if span.get("text_decoration") == "underline":
|
|
1524
|
+
attr.addAttribute_value_range_("NSUnderline", 1, rng)
|
|
1525
|
+
elif span.get("text_decoration") == "line_through":
|
|
1526
|
+
attr.addAttribute_value_range_("NSStrikethrough", 1, rng)
|
|
1527
|
+
except Exception:
|
|
1528
|
+
pass
|
|
1529
|
+
label.setAttributedText_(attr)
|
|
1530
|
+
except Exception:
|
|
1531
|
+
try:
|
|
1532
|
+
label.setText_(str(merged.get("text") or ""))
|
|
1533
|
+
except Exception:
|
|
1534
|
+
pass
|
|
1535
|
+
|
|
1315
1536
|
def _apply_attributed(self, label: Any, props: Dict[str, Any]) -> None:
|
|
1316
1537
|
"""Re-render the label's text as an NSAttributedString.
|
|
1317
1538
|
|
|
@@ -1368,7 +1589,7 @@ class ButtonHandler(IOSViewHandler):
|
|
|
1368
1589
|
btn.setTranslatesAutoresizingMaskIntoConstraints_(True)
|
|
1369
1590
|
btn.retain()
|
|
1370
1591
|
_pn_retained_views.append(btn)
|
|
1371
|
-
_register_control_action(btn, 1 << 6, lambda: _fire(btn, "
|
|
1592
|
+
_register_control_action(btn, 1 << 6, lambda: _fire(btn, "on_press")) # TouchUpInside
|
|
1372
1593
|
return btn
|
|
1373
1594
|
|
|
1374
1595
|
def measure_intrinsic(
|
|
@@ -1622,6 +1843,11 @@ class ImageHandler(IOSViewHandler):
|
|
|
1622
1843
|
iv.setTintColor_(_uicolor(props["tint_color"]))
|
|
1623
1844
|
except Exception:
|
|
1624
1845
|
pass
|
|
1846
|
+
if "placeholder_color" in props and props["placeholder_color"] is not None:
|
|
1847
|
+
try:
|
|
1848
|
+
iv.setBackgroundColor_(_uicolor(props["placeholder_color"]))
|
|
1849
|
+
except Exception:
|
|
1850
|
+
pass
|
|
1625
1851
|
if "source" in props and props["source"]:
|
|
1626
1852
|
self._load_source(iv, str(props["source"]))
|
|
1627
1853
|
if "scale_type" in props and props["scale_type"]:
|
|
@@ -1652,7 +1878,9 @@ class ImageHandler(IOSViewHandler):
|
|
|
1652
1878
|
|
|
1653
1879
|
def _load_source(self, iv: Any, source: str) -> None:
|
|
1654
1880
|
try:
|
|
1655
|
-
if source.startswith(
|
|
1881
|
+
if source.startswith("data:"):
|
|
1882
|
+
self._load_data_uri(iv, source)
|
|
1883
|
+
elif source.startswith(("http://", "https://")):
|
|
1656
1884
|
self._load_async(iv, source)
|
|
1657
1885
|
else:
|
|
1658
1886
|
# Bundle resource or absolute file path.
|
|
@@ -1662,55 +1890,107 @@ class ImageHandler(IOSViewHandler):
|
|
|
1662
1890
|
image = UIImage.imageWithContentsOfFile_(source)
|
|
1663
1891
|
if image:
|
|
1664
1892
|
iv.setImage_(image)
|
|
1893
|
+
_fire(iv, "on_load")
|
|
1894
|
+
else:
|
|
1895
|
+
_fire(iv, "on_error", f"image {source!r} not found")
|
|
1665
1896
|
except Exception:
|
|
1666
1897
|
pass
|
|
1667
1898
|
|
|
1668
|
-
def
|
|
1669
|
-
"""
|
|
1899
|
+
def _load_data_uri(self, iv: Any, source: str) -> None:
|
|
1900
|
+
"""Decode an inline ``data:`` URI (base64 payload) into the view."""
|
|
1901
|
+
try:
|
|
1902
|
+
import base64
|
|
1903
|
+
|
|
1904
|
+
payload = source.split(",", 1)[1] if "," in source else ""
|
|
1905
|
+
raw = base64.b64decode(payload)
|
|
1906
|
+
NSData = ObjCClass("NSData")
|
|
1907
|
+
data = NSData.dataWithBytes_length_(raw, len(raw))
|
|
1908
|
+
image = ObjCClass("UIImage").imageWithData_(data)
|
|
1909
|
+
if image is not None:
|
|
1910
|
+
iv.setImage_(image)
|
|
1911
|
+
_fire(iv, "on_load")
|
|
1912
|
+
else:
|
|
1913
|
+
_fire(iv, "on_error", "data URI decode failed")
|
|
1914
|
+
except Exception:
|
|
1915
|
+
_fire(iv, "on_error", "data URI decode failed")
|
|
1670
1916
|
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1917
|
+
def _load_async(self, iv: Any, source: str) -> None:
|
|
1918
|
+
"""Load a remote image through the shared image pipeline.
|
|
1919
|
+
|
|
1920
|
+
`pythonnative.images` downloads on a background thread with
|
|
1921
|
+
memory + disk caching and request deduplication, then hands a
|
|
1922
|
+
local file path back on the main queue; the decoded image is
|
|
1923
|
+
downsampled to the view's bounds so oversized photos don't pin
|
|
1924
|
+
full-resolution bitmaps in memory. The latest requested URI
|
|
1925
|
+
wins if several loads race.
|
|
1676
1926
|
"""
|
|
1677
1927
|
state = _state_of(iv)
|
|
1678
1928
|
state["pending_uri"] = source
|
|
1679
1929
|
try:
|
|
1680
1930
|
iv.retain()
|
|
1681
1931
|
_pn_retained_views.append(iv)
|
|
1682
|
-
|
|
1683
|
-
NSURLSession = ObjCClass("NSURLSession")
|
|
1684
|
-
UIImage = ObjCClass("UIImage")
|
|
1685
|
-
url = NSURL.URLWithString_(source)
|
|
1686
|
-
session = NSURLSession.sharedSession
|
|
1932
|
+
from ..images import fetch
|
|
1687
1933
|
|
|
1688
|
-
def
|
|
1689
|
-
if error is not None or data is None:
|
|
1690
|
-
return
|
|
1934
|
+
def on_ready(path: str) -> None:
|
|
1691
1935
|
try:
|
|
1692
|
-
|
|
1936
|
+
if _state_of(iv).get("pending_uri") != source:
|
|
1937
|
+
return
|
|
1938
|
+
image = self._decode_downsampled(iv, path)
|
|
1693
1939
|
if image is None:
|
|
1940
|
+
_fire(iv, "on_error", "decode failed")
|
|
1694
1941
|
return
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
try:
|
|
1698
|
-
if _state_of(iv).get("pending_uri") == source:
|
|
1699
|
-
iv.setImage_(image)
|
|
1700
|
-
except Exception:
|
|
1701
|
-
pass
|
|
1702
|
-
|
|
1703
|
-
from ..runtime import call_on_main_thread
|
|
1704
|
-
|
|
1705
|
-
call_on_main_thread(apply)
|
|
1942
|
+
iv.setImage_(image)
|
|
1943
|
+
_fire(iv, "on_load")
|
|
1706
1944
|
except Exception:
|
|
1707
1945
|
pass
|
|
1708
1946
|
|
|
1709
|
-
|
|
1710
|
-
|
|
1947
|
+
def on_error(message: str) -> None:
|
|
1948
|
+
if _state_of(iv).get("pending_uri") == source:
|
|
1949
|
+
_fire(iv, "on_error", message)
|
|
1950
|
+
|
|
1951
|
+
fetch(source, on_ready, on_error)
|
|
1711
1952
|
except Exception:
|
|
1712
1953
|
pass
|
|
1713
1954
|
|
|
1955
|
+
def _decode_downsampled(self, iv: Any, path: str) -> Any:
|
|
1956
|
+
"""Decode a file, scaling down to the view's bounds if oversized.
|
|
1957
|
+
|
|
1958
|
+
Uses ``UIGraphicsImageRenderer`` to re-rasterize when the
|
|
1959
|
+
source is more than 2x the target's pixel size, keeping memory
|
|
1960
|
+
proportional to what's on screen.
|
|
1961
|
+
"""
|
|
1962
|
+
try:
|
|
1963
|
+
UIImage = ObjCClass("UIImage")
|
|
1964
|
+
image = UIImage.imageWithContentsOfFile_(path)
|
|
1965
|
+
if image is None:
|
|
1966
|
+
return None
|
|
1967
|
+
bounds = iv.bounds
|
|
1968
|
+
target_w = float(bounds.size.width)
|
|
1969
|
+
target_h = float(bounds.size.height)
|
|
1970
|
+
if target_w <= 0.0 or target_h <= 0.0:
|
|
1971
|
+
return image
|
|
1972
|
+
size = image.size
|
|
1973
|
+
src_w, src_h = float(size.width), float(size.height)
|
|
1974
|
+
if src_w <= target_w * 2.0 and src_h <= target_h * 2.0:
|
|
1975
|
+
return image
|
|
1976
|
+
try:
|
|
1977
|
+
scale = min(target_w * 2.0 / src_w, target_h * 2.0 / src_h)
|
|
1978
|
+
new_w, new_h = src_w * scale, src_h * scale
|
|
1979
|
+
UIGraphicsImageRenderer = ObjCClass("UIGraphicsImageRenderer")
|
|
1980
|
+
renderer = UIGraphicsImageRenderer.alloc().initWithSize_((new_w, new_h))
|
|
1981
|
+
|
|
1982
|
+
def draw(_ctx_obj: Any) -> None:
|
|
1983
|
+
image.drawInRect_(((0.0, 0.0), (new_w, new_h)))
|
|
1984
|
+
|
|
1985
|
+
resized = renderer.imageWithActions_(draw)
|
|
1986
|
+
return resized if resized is not None else image
|
|
1987
|
+
except Exception:
|
|
1988
|
+
# Resize is an optimization; the full-size image is
|
|
1989
|
+
# still correct if the renderer path fails.
|
|
1990
|
+
return image
|
|
1991
|
+
except Exception:
|
|
1992
|
+
return None
|
|
1993
|
+
|
|
1714
1994
|
|
|
1715
1995
|
# ----------------------------------------------------------------------
|
|
1716
1996
|
# TextInput: raw libobjc target/delegate
|
|
@@ -2857,6 +3137,174 @@ class ModalHandler(IOSViewHandler):
|
|
|
2857
3137
|
_fire(placeholder, "on_dismiss")
|
|
2858
3138
|
|
|
2859
3139
|
|
|
3140
|
+
# ======================================================================
|
|
3141
|
+
# Portal: floats its children over the screen in a top-level overlay
|
|
3142
|
+
# ======================================================================
|
|
3143
|
+
#
|
|
3144
|
+
# The overlay is an instance of ``_PNPortalViewCTypes``, a raw libobjc
|
|
3145
|
+
# subclass of ``UIView`` overriding ``pointInside:withEvent:`` so the
|
|
3146
|
+
# overlay only claims touches that land on one of its subviews. UIKit's
|
|
3147
|
+
# default hit-testing skips a view (and its whole subtree) when
|
|
3148
|
+
# ``pointInside:`` returns NO, so taps on empty portal area fall
|
|
3149
|
+
# through to the screen content below while the portal's own children
|
|
3150
|
+
# stay fully interactive. Registered raw (not via rubicon's
|
|
3151
|
+
# ``@objc_method``) for the same arm64 FFI reliability reasons as the
|
|
3152
|
+
# other delegate classes in this module.
|
|
3153
|
+
|
|
3154
|
+
|
|
3155
|
+
class _PNCGPoint(_ct.Structure):
|
|
3156
|
+
_fields_ = [("x", _ct.c_double), ("y", _ct.c_double)]
|
|
3157
|
+
|
|
3158
|
+
|
|
3159
|
+
_PORTAL_POINT_INSIDE_TYPE = _ct.CFUNCTYPE(_ct.c_bool, _ct.c_void_p, _ct.c_void_p, _PNCGPoint, _ct.c_void_p)
|
|
3160
|
+
|
|
3161
|
+
|
|
3162
|
+
def _portal_point_inside_imp(self_ptr: int, _cmd_ptr: int, point: Any, _event_ptr: int) -> bool:
|
|
3163
|
+
"""Return whether ``point`` lands on any visible, interactive subview."""
|
|
3164
|
+
try:
|
|
3165
|
+
view = ObjCInstance(_ct.c_void_p(self_ptr))
|
|
3166
|
+
px = float(point.x)
|
|
3167
|
+
py = float(point.y)
|
|
3168
|
+
# On instances of this raw-registered class rubicon resolves
|
|
3169
|
+
# ``subviews`` as a bound method rather than a property, so it
|
|
3170
|
+
# must be called to get the NSArray.
|
|
3171
|
+
subs = view.subviews
|
|
3172
|
+
if callable(subs):
|
|
3173
|
+
subs = subs()
|
|
3174
|
+
for sub in list(subs or []):
|
|
3175
|
+
try:
|
|
3176
|
+
if bool(sub.isHidden()) or float(sub.alpha) <= 0.01:
|
|
3177
|
+
continue
|
|
3178
|
+
if not bool(sub.isUserInteractionEnabled()):
|
|
3179
|
+
continue
|
|
3180
|
+
frame = sub.frame
|
|
3181
|
+
fx = float(frame.origin.x)
|
|
3182
|
+
fy = float(frame.origin.y)
|
|
3183
|
+
fw = float(frame.size.width)
|
|
3184
|
+
fh = float(frame.size.height)
|
|
3185
|
+
if fx <= px <= fx + fw and fy <= py <= fy + fh:
|
|
3186
|
+
return True
|
|
3187
|
+
except Exception:
|
|
3188
|
+
continue
|
|
3189
|
+
return False
|
|
3190
|
+
except Exception:
|
|
3191
|
+
return False
|
|
3192
|
+
|
|
3193
|
+
|
|
3194
|
+
_portal_point_inside_imp_ref = _PORTAL_POINT_INSIDE_TYPE(_portal_point_inside_imp)
|
|
3195
|
+
|
|
3196
|
+
_UIVIEW_CLS = _get_cls(b"UIView")
|
|
3197
|
+
_PN_PORTAL_VIEW_CLS = _alloc_cls(_UIVIEW_CLS, b"_PNPortalViewCTypes", 0) if _UIVIEW_CLS else None
|
|
3198
|
+
if _PN_PORTAL_VIEW_CLS:
|
|
3199
|
+
_add_method(
|
|
3200
|
+
_PN_PORTAL_VIEW_CLS,
|
|
3201
|
+
_sel_reg(b"pointInside:withEvent:"),
|
|
3202
|
+
_ct.cast(_portal_point_inside_imp_ref, _ct.c_void_p),
|
|
3203
|
+
b"c@:{CGPoint=dd}@",
|
|
3204
|
+
)
|
|
3205
|
+
_reg_cls(_PN_PORTAL_VIEW_CLS)
|
|
3206
|
+
|
|
3207
|
+
|
|
3208
|
+
class PortalHandler(IOSViewHandler):
|
|
3209
|
+
"""Overlay container hosting [`Portal`][pythonnative.Portal] children.
|
|
3210
|
+
|
|
3211
|
+
The portal's native view is a transparent, full-size overlay added
|
|
3212
|
+
to the topmost view controller's view (the same view the screen
|
|
3213
|
+
root is attached to). Its frame mirrors the screen host's root
|
|
3214
|
+
frame (below the top safe-area inset), so portal coordinates equal
|
|
3215
|
+
viewport coordinates. Attachment happens lazily on the first child
|
|
3216
|
+
insert and re-homes itself if the host view changed (e.g. after a
|
|
3217
|
+
native modal closed).
|
|
3218
|
+
|
|
3219
|
+
When the ``_PNPortalViewCTypes`` registration is unavailable the
|
|
3220
|
+
handler degrades to a plain ``UIView`` overlay: portal children
|
|
3221
|
+
still render and receive touches, but empty portal area blocks the
|
|
3222
|
+
content below instead of passing touches through.
|
|
3223
|
+
"""
|
|
3224
|
+
|
|
3225
|
+
def _build(self, props: Dict[str, Any]) -> Any:
|
|
3226
|
+
overlay = None
|
|
3227
|
+
if _PN_PORTAL_VIEW_CLS:
|
|
3228
|
+
try:
|
|
3229
|
+
overlay = ObjCClass("_PNPortalViewCTypes").alloc().init()
|
|
3230
|
+
except Exception:
|
|
3231
|
+
overlay = None
|
|
3232
|
+
if overlay is None:
|
|
3233
|
+
overlay = ObjCClass("UIView").alloc().init()
|
|
3234
|
+
overlay.setTranslatesAutoresizingMaskIntoConstraints_(True)
|
|
3235
|
+
overlay.setBackgroundColor_(_uicolor("#00000000"))
|
|
3236
|
+
overlay.retain()
|
|
3237
|
+
_pn_retained_views.append(overlay)
|
|
3238
|
+
return overlay
|
|
3239
|
+
|
|
3240
|
+
def _apply(self, overlay: Any, props: Dict[str, Any], initial: bool) -> None:
|
|
3241
|
+
_apply_common_visual(overlay, props)
|
|
3242
|
+
if not initial:
|
|
3243
|
+
self._ensure_attached(overlay)
|
|
3244
|
+
|
|
3245
|
+
def _ensure_attached(self, overlay: Any) -> None:
|
|
3246
|
+
try:
|
|
3247
|
+
UIApplication = ObjCClass("UIApplication")
|
|
3248
|
+
top = _top_view_controller_for_alert(UIApplication.sharedApplication)
|
|
3249
|
+
host = top.view if top is not None else None
|
|
3250
|
+
if host is None:
|
|
3251
|
+
return
|
|
3252
|
+
current = overlay.superview
|
|
3253
|
+
if current is not None and _objc_ptr(current) == _objc_ptr(host):
|
|
3254
|
+
host.bringSubviewToFront_(overlay)
|
|
3255
|
+
self._sync_overlay_frame(overlay, host)
|
|
3256
|
+
return
|
|
3257
|
+
if current is not None:
|
|
3258
|
+
overlay.removeFromSuperview()
|
|
3259
|
+
host.addSubview_(overlay)
|
|
3260
|
+
self._sync_overlay_frame(overlay, host)
|
|
3261
|
+
except Exception:
|
|
3262
|
+
pass
|
|
3263
|
+
|
|
3264
|
+
@staticmethod
|
|
3265
|
+
def _sync_overlay_frame(overlay: Any, host: Any) -> None:
|
|
3266
|
+
"""Mirror the screen host's root frame (see ``_sync_root_frame``)."""
|
|
3267
|
+
try:
|
|
3268
|
+
bounds = host.bounds
|
|
3269
|
+
insets = host.safeAreaInsets
|
|
3270
|
+
top = float(insets.top)
|
|
3271
|
+
left = float(insets.left)
|
|
3272
|
+
right = float(insets.right)
|
|
3273
|
+
w = max(0.0, float(bounds.size.width) - left - right)
|
|
3274
|
+
h = max(0.0, float(bounds.size.height) - top)
|
|
3275
|
+
overlay.setFrame_(((left, top), (w, h)))
|
|
3276
|
+
overlay.setAutoresizingMask_(2 | 16) # FlexibleWidth | FlexibleHeight
|
|
3277
|
+
except Exception:
|
|
3278
|
+
pass
|
|
3279
|
+
|
|
3280
|
+
def insert_child(self, parent: Any, child: Any, index: int) -> None:
|
|
3281
|
+
self._ensure_attached(parent)
|
|
3282
|
+
super().insert_child(parent, child, index)
|
|
3283
|
+
|
|
3284
|
+
def set_frame(self, native_view: Any, x: float, y: float, width: float, height: float) -> None:
|
|
3285
|
+
# The overlay frames itself against the host view; the engine
|
|
3286
|
+
# frames only the portal's children.
|
|
3287
|
+
return
|
|
3288
|
+
|
|
3289
|
+
def measure_intrinsic(
|
|
3290
|
+
self,
|
|
3291
|
+
native_view: Any,
|
|
3292
|
+
max_width: float,
|
|
3293
|
+
max_height: float,
|
|
3294
|
+
) -> Tuple[float, float]:
|
|
3295
|
+
return (0.0, 0.0)
|
|
3296
|
+
|
|
3297
|
+
def _teardown(self, overlay: Any) -> None:
|
|
3298
|
+
try:
|
|
3299
|
+
_pn_retained_views.remove(overlay)
|
|
3300
|
+
except ValueError:
|
|
3301
|
+
pass
|
|
3302
|
+
try:
|
|
3303
|
+
overlay.release()
|
|
3304
|
+
except Exception:
|
|
3305
|
+
pass
|
|
3306
|
+
|
|
3307
|
+
|
|
2860
3308
|
# ======================================================================
|
|
2861
3309
|
# StatusBar: global side effect, no view in the tree
|
|
2862
3310
|
# ======================================================================
|
|
@@ -3641,6 +4089,476 @@ class DatePickerHandler(IOSViewHandler):
|
|
|
3641
4089
|
return (0.0, 0.0)
|
|
3642
4090
|
|
|
3643
4091
|
|
|
4092
|
+
# ======================================================================
|
|
4093
|
+
# VirtualList — UITableView-backed native virtualization
|
|
4094
|
+
# ======================================================================
|
|
4095
|
+
#
|
|
4096
|
+
# We register a raw libobjc class ``_PNTableSourceCTypes`` rather than
|
|
4097
|
+
# using rubicon-objc's ``@objc_method`` because UIKit invokes
|
|
4098
|
+
# ``tableView:cellForRowAtIndexPath:`` with a tagged-pointer
|
|
4099
|
+
# NSIndexPath that crashes inside CPython's ``_ctypes.O_get`` when
|
|
4100
|
+
# rubicon-objc's FFI closure tries to wrap it as a PyObject*.
|
|
4101
|
+
#
|
|
4102
|
+
# Each UITableView gets its own dataSource instance; per-instance state
|
|
4103
|
+
# lives in ``_pn_table_state`` keyed by the dataSource's raw pointer
|
|
4104
|
+
# (the integer passed as ``self`` to every IMP). Rows host nested
|
|
4105
|
+
# reconciler subtrees pooled per cell ``contentView`` (see
|
|
4106
|
+
# ``pythonnative.virtual_rows``); recycled cells rebind their existing
|
|
4107
|
+
# subtree instead of remounting, so steady scrolling reuses both the
|
|
4108
|
+
# native cell and the Python-side row tree.
|
|
4109
|
+
|
|
4110
|
+
_pn_table_state: Dict[int, dict] = {}
|
|
4111
|
+
|
|
4112
|
+
_PN_CELL_REUSE_ID = "PNCell"
|
|
4113
|
+
|
|
4114
|
+
_SEL_ROW = _sel_reg(b"row")
|
|
4115
|
+
_SEL_DESELECT_ROW = _sel_reg(b"deselectRowAtIndexPath:animated:")
|
|
4116
|
+
_SEL_SET_DATA_SOURCE = _sel_reg(b"setDataSource:")
|
|
4117
|
+
|
|
4118
|
+
_TABLE_NUM_SECTIONS_TYPE = _ct.CFUNCTYPE(_ct.c_long, _ct.c_void_p, _ct.c_void_p, _ct.c_void_p)
|
|
4119
|
+
_TABLE_NUM_ROWS_TYPE = _ct.CFUNCTYPE(_ct.c_long, _ct.c_void_p, _ct.c_void_p, _ct.c_void_p, _ct.c_long)
|
|
4120
|
+
_TABLE_HEIGHT_TYPE = _ct.CFUNCTYPE(_ct.c_double, _ct.c_void_p, _ct.c_void_p, _ct.c_void_p, _ct.c_void_p)
|
|
4121
|
+
_TABLE_CELL_TYPE = _ct.CFUNCTYPE(_ct.c_void_p, _ct.c_void_p, _ct.c_void_p, _ct.c_void_p, _ct.c_void_p)
|
|
4122
|
+
_TABLE_DID_SELECT_TYPE = _ct.CFUNCTYPE(None, _ct.c_void_p, _ct.c_void_p, _ct.c_void_p, _ct.c_void_p)
|
|
4123
|
+
_TABLE_SCROLL_TYPE = _ct.CFUNCTYPE(None, _ct.c_void_p, _ct.c_void_p, _ct.c_void_p)
|
|
4124
|
+
|
|
4125
|
+
|
|
4126
|
+
def _table_row_of(ip_ptr: int) -> int:
|
|
4127
|
+
"""Read ``[indexPath row]`` via raw msgSend (tagged-pointer safe)."""
|
|
4128
|
+
try:
|
|
4129
|
+
_objc_msgSend.restype = _ct.c_long
|
|
4130
|
+
_objc_msgSend.argtypes = [_ct.c_void_p, _ct.c_void_p]
|
|
4131
|
+
return int(_objc_msgSend(_ct.c_void_p(ip_ptr), _SEL_ROW))
|
|
4132
|
+
except Exception:
|
|
4133
|
+
return 0
|
|
4134
|
+
|
|
4135
|
+
|
|
4136
|
+
def _table_row_height(info: dict, row: int) -> float:
|
|
4137
|
+
heights = info.get("row_heights")
|
|
4138
|
+
if heights and 0 <= row < len(heights):
|
|
4139
|
+
return float(heights[row])
|
|
4140
|
+
return float(info.get("row_height", 44.0))
|
|
4141
|
+
|
|
4142
|
+
|
|
4143
|
+
def _table_row_start(info: dict, row: int) -> float:
|
|
4144
|
+
"""Content-coordinate y where ``row`` begins."""
|
|
4145
|
+
starts = info.get("row_starts")
|
|
4146
|
+
if starts is not None and 0 <= row < len(starts):
|
|
4147
|
+
return float(starts[row])
|
|
4148
|
+
return float(info.get("row_height", 44.0)) * row
|
|
4149
|
+
|
|
4150
|
+
|
|
4151
|
+
def _table_set_heights(info: dict, heights: Optional[List[float]]) -> None:
|
|
4152
|
+
"""Store per-row heights plus their prefix sums (for row starts)."""
|
|
4153
|
+
info["row_heights"] = heights
|
|
4154
|
+
if not heights:
|
|
4155
|
+
info["row_starts"] = None
|
|
4156
|
+
return
|
|
4157
|
+
starts = [0.0] * len(heights)
|
|
4158
|
+
acc = 0.0
|
|
4159
|
+
for i, h in enumerate(heights):
|
|
4160
|
+
starts[i] = acc
|
|
4161
|
+
acc += float(h)
|
|
4162
|
+
info["row_starts"] = starts
|
|
4163
|
+
|
|
4164
|
+
|
|
4165
|
+
def _table_num_sections_imp(self_ptr: int, cmd_ptr: int, tv_ptr: int) -> int:
|
|
4166
|
+
return 1
|
|
4167
|
+
|
|
4168
|
+
|
|
4169
|
+
def _table_num_rows_imp(self_ptr: int, cmd_ptr: int, tv_ptr: int, section: int) -> int:
|
|
4170
|
+
info = _pn_table_state.get(int(self_ptr))
|
|
4171
|
+
return int(info.get("count", 0)) if info else 0
|
|
4172
|
+
|
|
4173
|
+
|
|
4174
|
+
def _table_height_imp(self_ptr: int, cmd_ptr: int, tv_ptr: int, ip_ptr: int) -> float:
|
|
4175
|
+
info = _pn_table_state.get(int(self_ptr))
|
|
4176
|
+
if info is None:
|
|
4177
|
+
return 44.0
|
|
4178
|
+
return _table_row_height(info, _table_row_of(ip_ptr))
|
|
4179
|
+
|
|
4180
|
+
|
|
4181
|
+
def _table_cell_imp(self_ptr: int, cmd_ptr: int, tv_ptr: int, ip_ptr: int) -> int:
|
|
4182
|
+
"""Build (or rebind) a cell for ``tableView:cellForRowAtIndexPath:``.
|
|
4183
|
+
|
|
4184
|
+
``ip_ptr`` is read raw via ``[indexPath row]`` to avoid the
|
|
4185
|
+
rubicon-objc tagged-pointer crash. The table view itself is a real
|
|
4186
|
+
heap object so wrapping it as an ObjCInstance is safe. Freshly
|
|
4187
|
+
allocated cells are retained explicitly so they survive the Python
|
|
4188
|
+
wrapper's release.
|
|
4189
|
+
"""
|
|
4190
|
+
import traceback as _tb
|
|
4191
|
+
|
|
4192
|
+
row = _table_row_of(ip_ptr)
|
|
4193
|
+
try:
|
|
4194
|
+
UITableViewCell = ObjCClass("UITableViewCell")
|
|
4195
|
+
tv = ObjCInstance(_ct.c_void_p(tv_ptr))
|
|
4196
|
+
info = _pn_table_state.get(int(self_ptr))
|
|
4197
|
+
row_h = _table_row_height(info, row) if info else 44.0
|
|
4198
|
+
|
|
4199
|
+
try:
|
|
4200
|
+
cell_w = float(tv.bounds.size.width)
|
|
4201
|
+
except Exception:
|
|
4202
|
+
cell_w = 0.0
|
|
4203
|
+
if cell_w <= 0:
|
|
4204
|
+
try:
|
|
4205
|
+
cell_w = float(ObjCClass("UIScreen").mainScreen().bounds.size.width)
|
|
4206
|
+
except Exception:
|
|
4207
|
+
cell_w = 320.0
|
|
4208
|
+
|
|
4209
|
+
cell = tv.dequeueReusableCellWithIdentifier_(_PN_CELL_REUSE_ID)
|
|
4210
|
+
if cell is None or (hasattr(cell, "ptr") and cell.ptr.value == 0):
|
|
4211
|
+
cell = UITableViewCell.alloc().initWithStyle_reuseIdentifier_(0, _PN_CELL_REUSE_ID)
|
|
4212
|
+
transparent = _uicolor("#00000000")
|
|
4213
|
+
cell.setBackgroundColor_(transparent)
|
|
4214
|
+
cell.contentView.setBackgroundColor_(transparent)
|
|
4215
|
+
cell.setSelectionStyle_(0)
|
|
4216
|
+
cell.retain() # offset the Python wrapper's release on __del__
|
|
4217
|
+
|
|
4218
|
+
try:
|
|
4219
|
+
cell.setFrame_(((0, 0), (cell_w, row_h)))
|
|
4220
|
+
cell.contentView.setFrame_(((0, 0), (cell_w, row_h)))
|
|
4221
|
+
except Exception:
|
|
4222
|
+
pass
|
|
4223
|
+
|
|
4224
|
+
content = cell.contentView
|
|
4225
|
+
try:
|
|
4226
|
+
existing_subs = content.subviews
|
|
4227
|
+
if callable(existing_subs):
|
|
4228
|
+
existing_subs = existing_subs()
|
|
4229
|
+
for sub in list(existing_subs or []):
|
|
4230
|
+
sub.removeFromSuperview()
|
|
4231
|
+
except Exception:
|
|
4232
|
+
pass
|
|
4233
|
+
|
|
4234
|
+
# Only rows in (or near) the visible content window get a
|
|
4235
|
+
# mounted subtree. UITableView's accessibility container calls
|
|
4236
|
+
# ``cellForRowAtIndexPath:`` for *every* row whenever an
|
|
4237
|
+
# accessibility client (VoiceOver, XCUITest / UI-test drivers)
|
|
4238
|
+
# snapshots the hierarchy, no matter the estimated heights.
|
|
4239
|
+
# Mounting those would defeat virtualization -- a 10k-row list
|
|
4240
|
+
# would mount 10k Python subtrees on the first snapshot -- and
|
|
4241
|
+
# the never-displayed cells would sit stacked at the table
|
|
4242
|
+
# origin where tests read them as visible. Off-window requests
|
|
4243
|
+
# get an empty accessibility-hidden placeholder; when the row
|
|
4244
|
+
# really scrolls on screen UIKit asks again and the content
|
|
4245
|
+
# window has moved, so the real subtree mounts.
|
|
4246
|
+
try:
|
|
4247
|
+
offset_y = float(tv.contentOffset.y)
|
|
4248
|
+
except Exception:
|
|
4249
|
+
offset_y = 0.0
|
|
4250
|
+
try:
|
|
4251
|
+
viewport_h = float(tv.bounds.size.height)
|
|
4252
|
+
except Exception:
|
|
4253
|
+
viewport_h = 0.0
|
|
4254
|
+
row_start = _table_row_start(info, row) if info else 0.0
|
|
4255
|
+
margin = viewport_h if viewport_h > 0 else 800.0
|
|
4256
|
+
in_window = (row_start + row_h >= offset_y - margin) and (
|
|
4257
|
+
row_start <= offset_y + max(viewport_h, row_h) + margin
|
|
4258
|
+
)
|
|
4259
|
+
try:
|
|
4260
|
+
cell.setAccessibilityElementsHidden_(not in_window)
|
|
4261
|
+
except Exception:
|
|
4262
|
+
pass
|
|
4263
|
+
|
|
4264
|
+
if in_window and info is not None:
|
|
4265
|
+
render_row = info.get("render_row")
|
|
4266
|
+
pool = info.get("pool")
|
|
4267
|
+
if render_row is not None and pool is not None:
|
|
4268
|
+
try:
|
|
4269
|
+
content_key = int(content.ptr.value)
|
|
4270
|
+
root = pool.bind(content_key, lambda: render_row(row), cell_w, row_h)
|
|
4271
|
+
if root is not None:
|
|
4272
|
+
content.addSubview_(root)
|
|
4273
|
+
# The layout engine frames only the *descendants*
|
|
4274
|
+
# of a subtree root (screen hosts normally size
|
|
4275
|
+
# the root themselves), so the row root would
|
|
4276
|
+
# otherwise keep its zero frame and render as
|
|
4277
|
+
# nothing. Fill the cell.
|
|
4278
|
+
try:
|
|
4279
|
+
root.setFrame_(((0, 0), (cell_w, row_h)))
|
|
4280
|
+
except Exception:
|
|
4281
|
+
pass
|
|
4282
|
+
except Exception:
|
|
4283
|
+
print(f"[VirtualList][iOS] mount for row={row} raised:")
|
|
4284
|
+
_tb.print_exc()
|
|
4285
|
+
|
|
4286
|
+
cell_ptr = cell.ptr.value
|
|
4287
|
+
return int(cell_ptr) if cell_ptr is not None else 0
|
|
4288
|
+
except Exception:
|
|
4289
|
+
print(f"[VirtualList][iOS] _table_cell_imp raised for row={row}:")
|
|
4290
|
+
_tb.print_exc()
|
|
4291
|
+
try:
|
|
4292
|
+
cell = ObjCClass("UITableViewCell").alloc().initWithStyle_reuseIdentifier_(0, _PN_CELL_REUSE_ID)
|
|
4293
|
+
cell.retain()
|
|
4294
|
+
return int(cell.ptr.value)
|
|
4295
|
+
except Exception:
|
|
4296
|
+
return 0
|
|
4297
|
+
|
|
4298
|
+
|
|
4299
|
+
def _table_did_select_imp(self_ptr: int, cmd_ptr: int, tv_ptr: int, ip_ptr: int) -> None:
|
|
4300
|
+
row = _table_row_of(ip_ptr)
|
|
4301
|
+
try:
|
|
4302
|
+
_objc_msgSend.restype = None
|
|
4303
|
+
_objc_msgSend.argtypes = [_ct.c_void_p, _ct.c_void_p, _ct.c_void_p, _ct.c_bool]
|
|
4304
|
+
_objc_msgSend(_ct.c_void_p(tv_ptr), _SEL_DESELECT_ROW, _ct.c_void_p(ip_ptr), True)
|
|
4305
|
+
except Exception:
|
|
4306
|
+
pass
|
|
4307
|
+
_fire_ptr(int(tv_ptr or 0), "on_row_press", row)
|
|
4308
|
+
|
|
4309
|
+
|
|
4310
|
+
def _table_scroll_imp(self_ptr: int, cmd_ptr: int, sv_ptr: int) -> None:
|
|
4311
|
+
"""Raw C callback for ``scrollViewDidScroll:`` on the table source."""
|
|
4312
|
+
info = _pn_table_state.get(int(self_ptr))
|
|
4313
|
+
if not info:
|
|
4314
|
+
return
|
|
4315
|
+
tv = info.get("tv")
|
|
4316
|
+
if tv is None:
|
|
4317
|
+
return
|
|
4318
|
+
try:
|
|
4319
|
+
offset = float(tv.contentOffset.y)
|
|
4320
|
+
extent = float(tv.bounds.size.height)
|
|
4321
|
+
content = float(tv.contentSize.height)
|
|
4322
|
+
except Exception:
|
|
4323
|
+
return
|
|
4324
|
+
_fire(tv, "on_scroll", {"x": 0.0, "y": offset, "extent": extent, "range": content})
|
|
4325
|
+
|
|
4326
|
+
|
|
4327
|
+
_table_num_sections_imp_ref = _TABLE_NUM_SECTIONS_TYPE(_table_num_sections_imp)
|
|
4328
|
+
_table_num_rows_imp_ref = _TABLE_NUM_ROWS_TYPE(_table_num_rows_imp)
|
|
4329
|
+
_table_height_imp_ref = _TABLE_HEIGHT_TYPE(_table_height_imp)
|
|
4330
|
+
_table_cell_imp_ref = _TABLE_CELL_TYPE(_table_cell_imp)
|
|
4331
|
+
_table_did_select_imp_ref = _TABLE_DID_SELECT_TYPE(_table_did_select_imp)
|
|
4332
|
+
_table_scroll_imp_ref = _TABLE_SCROLL_TYPE(_table_scroll_imp)
|
|
4333
|
+
|
|
4334
|
+
_PN_TABLE_SOURCE_CLS = _alloc_cls(_NS_OBJECT_CLS, b"_PNTableSourceCTypes", 0)
|
|
4335
|
+
if _PN_TABLE_SOURCE_CLS:
|
|
4336
|
+
_add_method(
|
|
4337
|
+
_PN_TABLE_SOURCE_CLS,
|
|
4338
|
+
_sel_reg(b"numberOfSectionsInTableView:"),
|
|
4339
|
+
_ct.cast(_table_num_sections_imp_ref, _ct.c_void_p),
|
|
4340
|
+
b"q@:@",
|
|
4341
|
+
)
|
|
4342
|
+
_add_method(
|
|
4343
|
+
_PN_TABLE_SOURCE_CLS,
|
|
4344
|
+
_sel_reg(b"tableView:numberOfRowsInSection:"),
|
|
4345
|
+
_ct.cast(_table_num_rows_imp_ref, _ct.c_void_p),
|
|
4346
|
+
b"q@:@q",
|
|
4347
|
+
)
|
|
4348
|
+
_add_method(
|
|
4349
|
+
_PN_TABLE_SOURCE_CLS,
|
|
4350
|
+
_sel_reg(b"tableView:heightForRowAtIndexPath:"),
|
|
4351
|
+
_ct.cast(_table_height_imp_ref, _ct.c_void_p),
|
|
4352
|
+
b"d@:@@",
|
|
4353
|
+
)
|
|
4354
|
+
_add_method(
|
|
4355
|
+
_PN_TABLE_SOURCE_CLS,
|
|
4356
|
+
_sel_reg(b"tableView:cellForRowAtIndexPath:"),
|
|
4357
|
+
_ct.cast(_table_cell_imp_ref, _ct.c_void_p),
|
|
4358
|
+
b"@@:@@",
|
|
4359
|
+
)
|
|
4360
|
+
_add_method(
|
|
4361
|
+
_PN_TABLE_SOURCE_CLS,
|
|
4362
|
+
_sel_reg(b"tableView:didSelectRowAtIndexPath:"),
|
|
4363
|
+
_ct.cast(_table_did_select_imp_ref, _ct.c_void_p),
|
|
4364
|
+
b"v@:@@",
|
|
4365
|
+
)
|
|
4366
|
+
_add_method(
|
|
4367
|
+
_PN_TABLE_SOURCE_CLS,
|
|
4368
|
+
_sel_reg(b"scrollViewDidScroll:"),
|
|
4369
|
+
_ct.cast(_table_scroll_imp_ref, _ct.c_void_p),
|
|
4370
|
+
b"v@:@",
|
|
4371
|
+
)
|
|
4372
|
+
_reg_cls(_PN_TABLE_SOURCE_CLS)
|
|
4373
|
+
|
|
4374
|
+
|
|
4375
|
+
def _alloc_table_source_instance() -> int:
|
|
4376
|
+
"""Allocate and retain a fresh ``_PNTableSourceCTypes`` instance.
|
|
4377
|
+
|
|
4378
|
+
Returns the raw pointer for the new dataSource. Callers must keep
|
|
4379
|
+
the pointer alive themselves; UITableView's dataSource and
|
|
4380
|
+
delegate relationships are non-retaining.
|
|
4381
|
+
"""
|
|
4382
|
+
if not _PN_TABLE_SOURCE_CLS:
|
|
4383
|
+
raise RuntimeError("_PNTableSourceCTypes class registration failed")
|
|
4384
|
+
_objc_msgSend.restype = _ct.c_void_p
|
|
4385
|
+
_objc_msgSend.argtypes = [_ct.c_void_p, _ct.c_void_p]
|
|
4386
|
+
raw = _objc_msgSend(_PN_TABLE_SOURCE_CLS, _SEL_ALLOC)
|
|
4387
|
+
raw = _objc_msgSend(raw, _SEL_INIT)
|
|
4388
|
+
raw = _objc_msgSend(raw, _SEL_RETAIN)
|
|
4389
|
+
return int(raw) if raw is not None else 0
|
|
4390
|
+
|
|
4391
|
+
|
|
4392
|
+
class VirtualListHandler(IOSViewHandler):
|
|
4393
|
+
"""Natively virtualized list backed by ``UITableView``.
|
|
4394
|
+
|
|
4395
|
+
Expects props:
|
|
4396
|
+
|
|
4397
|
+
- ``count``: total number of rows.
|
|
4398
|
+
- ``row_height``: uniform row extent in points, or
|
|
4399
|
+
- ``row_heights``: per-row extents.
|
|
4400
|
+
- ``render_row``: ``render_row(index) -> Element`` producing one
|
|
4401
|
+
row's subtree. Called lazily as rows enter the viewport.
|
|
4402
|
+
- ``shows_scroll_indicator``: hide the scroll bar when ``False``.
|
|
4403
|
+
|
|
4404
|
+
Events (dispatched by tag): ``on_row_press`` with the row index,
|
|
4405
|
+
``on_scroll`` with ``{"x", "y", "extent", "range"}`` in points.
|
|
4406
|
+
|
|
4407
|
+
Commands: ``scroll_to_offset`` / ``scroll_to_index`` /
|
|
4408
|
+
``scroll_to_end`` / ``get_scroll_offset``.
|
|
4409
|
+
"""
|
|
4410
|
+
|
|
4411
|
+
def _build(self, props: Dict[str, Any]) -> Any:
|
|
4412
|
+
from ..virtual_rows import RowHostPool
|
|
4413
|
+
|
|
4414
|
+
tv = ObjCClass("UITableView").alloc().initWithFrame_style_(((0, 0), (0, 0)), 0)
|
|
4415
|
+
tv.setTranslatesAutoresizingMaskIntoConstraints_(True)
|
|
4416
|
+
tv.setSeparatorStyle_(0) # none by default; rows draw their own
|
|
4417
|
+
try:
|
|
4418
|
+
# A *nonzero* estimate keeps UITableView lazy: with
|
|
4419
|
+
# ``estimatedRowHeight = 0`` modern UIKit builds cells for
|
|
4420
|
+
# every row up front to size its content, which mounts a
|
|
4421
|
+
# Python subtree per row (defeating virtualization) and
|
|
4422
|
+
# leaves the never-displayed cells visible to the
|
|
4423
|
+
# accessibility tree. ``heightForRowAtIndexPath:`` still
|
|
4424
|
+
# returns exact heights for the rows that do come on screen.
|
|
4425
|
+
heights = props.get("row_heights")
|
|
4426
|
+
if heights:
|
|
4427
|
+
estimate = sum(float(h) for h in heights) / max(1, len(heights))
|
|
4428
|
+
else:
|
|
4429
|
+
estimate = float(props.get("row_height", 44.0) or 44.0)
|
|
4430
|
+
tv.setEstimatedRowHeight_(max(1.0, estimate))
|
|
4431
|
+
except Exception:
|
|
4432
|
+
pass
|
|
4433
|
+
tv.retain()
|
|
4434
|
+
_pn_retained_views.append(tv)
|
|
4435
|
+
|
|
4436
|
+
source_ptr = _alloc_table_source_instance()
|
|
4437
|
+
if source_ptr == 0:
|
|
4438
|
+
raise RuntimeError("[VirtualList][iOS] dataSource alloc returned NULL")
|
|
4439
|
+
|
|
4440
|
+
state = {
|
|
4441
|
+
"count": int(props.get("count", 0) or 0),
|
|
4442
|
+
"row_height": float(props.get("row_height", 44.0) or 44.0),
|
|
4443
|
+
"render_row": props.get("render_row"),
|
|
4444
|
+
"pool": RowHostPool(),
|
|
4445
|
+
"tv": tv,
|
|
4446
|
+
}
|
|
4447
|
+
_table_set_heights(state, props.get("row_heights"))
|
|
4448
|
+
_pn_table_state[source_ptr] = state
|
|
4449
|
+
|
|
4450
|
+
_objc_msgSend.restype = None
|
|
4451
|
+
_objc_msgSend.argtypes = [_ct.c_void_p, _ct.c_void_p, _ct.c_void_p]
|
|
4452
|
+
tv_ptr = tv.ptr if hasattr(tv, "ptr") else tv
|
|
4453
|
+
_objc_msgSend(tv_ptr, _SEL_SET_DATA_SOURCE, _ct.c_void_p(source_ptr))
|
|
4454
|
+
_objc_msgSend(tv_ptr, _SEL_SET_DELEGATE, _ct.c_void_p(source_ptr))
|
|
4455
|
+
|
|
4456
|
+
state["source_ptr"] = source_ptr
|
|
4457
|
+
return tv
|
|
4458
|
+
|
|
4459
|
+
def _source_state(self, tv: Any) -> Optional[dict]:
|
|
4460
|
+
tv_ptr = _objc_ptr(tv)
|
|
4461
|
+
for info in _pn_table_state.values():
|
|
4462
|
+
candidate = info.get("tv")
|
|
4463
|
+
if candidate is not None and _objc_ptr(candidate) == tv_ptr:
|
|
4464
|
+
return info
|
|
4465
|
+
return None
|
|
4466
|
+
|
|
4467
|
+
def _apply(self, tv: Any, props: Dict[str, Any], initial: bool) -> None:
|
|
4468
|
+
_apply_common_visual(tv, props)
|
|
4469
|
+
info = self._source_state(tv)
|
|
4470
|
+
if info is None:
|
|
4471
|
+
return
|
|
4472
|
+
data_changed = False
|
|
4473
|
+
if "count" in props:
|
|
4474
|
+
info["count"] = int(props.get("count") or 0)
|
|
4475
|
+
data_changed = True
|
|
4476
|
+
if "row_height" in props and props["row_height"] is not None:
|
|
4477
|
+
info["row_height"] = float(props["row_height"])
|
|
4478
|
+
data_changed = True
|
|
4479
|
+
if "row_heights" in props:
|
|
4480
|
+
_table_set_heights(info, props.get("row_heights"))
|
|
4481
|
+
data_changed = True
|
|
4482
|
+
if "render_row" in props:
|
|
4483
|
+
info["render_row"] = props.get("render_row")
|
|
4484
|
+
data_changed = True
|
|
4485
|
+
if "shows_scroll_indicator" in props:
|
|
4486
|
+
visible = props["shows_scroll_indicator"] is not False
|
|
4487
|
+
try:
|
|
4488
|
+
tv.setShowsVerticalScrollIndicator_(visible)
|
|
4489
|
+
except Exception:
|
|
4490
|
+
pass
|
|
4491
|
+
if data_changed and not initial:
|
|
4492
|
+
try:
|
|
4493
|
+
tv.reloadData()
|
|
4494
|
+
except Exception:
|
|
4495
|
+
pass
|
|
4496
|
+
|
|
4497
|
+
def _teardown(self, tv: Any) -> None:
|
|
4498
|
+
info = self._source_state(tv)
|
|
4499
|
+
if info is None:
|
|
4500
|
+
return
|
|
4501
|
+
try:
|
|
4502
|
+
info["pool"].release_all()
|
|
4503
|
+
except Exception:
|
|
4504
|
+
pass
|
|
4505
|
+
info["tv"] = None
|
|
4506
|
+
_pn_table_state.pop(info.get("source_ptr"), None)
|
|
4507
|
+
|
|
4508
|
+
def measure_intrinsic(
|
|
4509
|
+
self,
|
|
4510
|
+
native_view: Any,
|
|
4511
|
+
max_width: float,
|
|
4512
|
+
max_height: float,
|
|
4513
|
+
) -> Tuple[float, float]:
|
|
4514
|
+
# Fill the available space, like a ScrollView clamped to its
|
|
4515
|
+
# parent; collapse to 0 on unbounded axes (nested lists don't
|
|
4516
|
+
# scroll, matching React Native).
|
|
4517
|
+
w = max_width if math.isfinite(max_width) else 0.0
|
|
4518
|
+
h = max_height if math.isfinite(max_height) else 0.0
|
|
4519
|
+
return (max(0.0, w), max(0.0, h))
|
|
4520
|
+
|
|
4521
|
+
def command(self, native_view: Any, name: str, args: Dict[str, Any]) -> Any:
|
|
4522
|
+
if name == "scroll_to_offset":
|
|
4523
|
+
y = float(args.get("y", 0.0) or 0.0)
|
|
4524
|
+
animated = args.get("animated", True) is not False
|
|
4525
|
+
try:
|
|
4526
|
+
native_view.setContentOffset_animated_((0.0, y), animated)
|
|
4527
|
+
except Exception:
|
|
4528
|
+
pass
|
|
4529
|
+
return None
|
|
4530
|
+
if name == "scroll_to_index":
|
|
4531
|
+
index = int(args.get("index", 0) or 0)
|
|
4532
|
+
animated = args.get("animated", True) is not False
|
|
4533
|
+
info = self._source_state(native_view)
|
|
4534
|
+
count = int(info.get("count", 0)) if info else 0
|
|
4535
|
+
index = max(0, min(index, max(0, count - 1)))
|
|
4536
|
+
try:
|
|
4537
|
+
path = ObjCClass("NSIndexPath").indexPathForRow_inSection_(index, 0)
|
|
4538
|
+
# 1 = UITableViewScrollPositionTop
|
|
4539
|
+
native_view.scrollToRowAtIndexPath_atScrollPosition_animated_(path, 1, animated)
|
|
4540
|
+
except Exception:
|
|
4541
|
+
pass
|
|
4542
|
+
return None
|
|
4543
|
+
if name == "scroll_to_end":
|
|
4544
|
+
animated = args.get("animated", True) is not False
|
|
4545
|
+
try:
|
|
4546
|
+
content_h = float(native_view.contentSize.height)
|
|
4547
|
+
bounds_h = float(native_view.bounds.size.height)
|
|
4548
|
+
target = max(0.0, content_h - bounds_h)
|
|
4549
|
+
native_view.setContentOffset_animated_((0.0, target), animated)
|
|
4550
|
+
except Exception:
|
|
4551
|
+
pass
|
|
4552
|
+
return None
|
|
4553
|
+
if name == "get_scroll_offset":
|
|
4554
|
+
try:
|
|
4555
|
+
offset = native_view.contentOffset
|
|
4556
|
+
return {"x": float(offset.x), "y": float(offset.y)}
|
|
4557
|
+
except Exception:
|
|
4558
|
+
return {"x": 0.0, "y": 0.0}
|
|
4559
|
+
return None
|
|
4560
|
+
|
|
4561
|
+
|
|
3644
4562
|
# ======================================================================
|
|
3645
4563
|
# Registration
|
|
3646
4564
|
# ======================================================================
|
|
@@ -3664,6 +4582,7 @@ def register_handlers(registry: Any) -> None:
|
|
|
3664
4582
|
registry.register("ScrollView", ScrollViewHandler())
|
|
3665
4583
|
registry.register("SafeAreaView", SafeAreaViewHandler())
|
|
3666
4584
|
registry.register("Modal", ModalHandler())
|
|
4585
|
+
registry.register("Portal", PortalHandler())
|
|
3667
4586
|
registry.register("Slider", SliderHandler())
|
|
3668
4587
|
registry.register("TabBar", TabBarHandler())
|
|
3669
4588
|
registry.register("Pressable", PressableHandler())
|
|
@@ -3673,6 +4592,7 @@ def register_handlers(registry: Any) -> None:
|
|
|
3673
4592
|
registry.register("Checkbox", CheckboxHandler())
|
|
3674
4593
|
registry.register("SegmentedControl", SegmentedControlHandler())
|
|
3675
4594
|
registry.register("DatePicker", DatePickerHandler())
|
|
4595
|
+
registry.register("VirtualList", VirtualListHandler())
|
|
3676
4596
|
|
|
3677
4597
|
|
|
3678
4598
|
__all__ = [
|
|
@@ -3690,6 +4610,7 @@ __all__ = [
|
|
|
3690
4610
|
"SpacerHandler",
|
|
3691
4611
|
"SafeAreaViewHandler",
|
|
3692
4612
|
"ModalHandler",
|
|
4613
|
+
"PortalHandler",
|
|
3693
4614
|
"SliderHandler",
|
|
3694
4615
|
"TabBarHandler",
|
|
3695
4616
|
"PressableHandler",
|
|
@@ -3699,5 +4620,6 @@ __all__ = [
|
|
|
3699
4620
|
"CheckboxHandler",
|
|
3700
4621
|
"SegmentedControlHandler",
|
|
3701
4622
|
"DatePickerHandler",
|
|
4623
|
+
"VirtualListHandler",
|
|
3702
4624
|
"register_handlers",
|
|
3703
4625
|
]
|