pythonnative 0.23.0__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 +29 -50
- pythonnative/animated.py +1 -1
- pythonnative/cli/pn.py +3 -3
- pythonnative/components.py +250 -513
- pythonnative/diagnostics.py +214 -0
- pythonnative/element.py +5 -2
- pythonnative/events.py +13 -8
- pythonnative/hooks.py +413 -49
- pythonnative/hot_reload.py +9 -1
- pythonnative/native_views/android.py +78 -1
- pythonnative/native_views/desktop.py +38 -1
- pythonnative/native_views/ios.py +241 -7
- pythonnative/navigation.py +41 -17
- pythonnative/preview.py +43 -7
- pythonnative/reconciler.py +863 -441
- pythonnative/screen.py +324 -27
- pythonnative/storage.py +3 -3
- pythonnative/style.py +83 -0
- pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/ScreenFragment.kt +22 -0
- {pythonnative-0.23.0.dist-info → pythonnative-0.24.0.dist-info}/METADATA +5 -4
- {pythonnative-0.23.0.dist-info → pythonnative-0.24.0.dist-info}/RECORD +25 -24
- {pythonnative-0.23.0.dist-info → pythonnative-0.24.0.dist-info}/WHEEL +0 -0
- {pythonnative-0.23.0.dist-info → pythonnative-0.24.0.dist-info}/entry_points.txt +0 -0
- {pythonnative-0.23.0.dist-info → pythonnative-0.24.0.dist-info}/licenses/LICENSE +0 -0
- {pythonnative-0.23.0.dist-info → pythonnative-0.24.0.dist-info}/top_level.txt +0 -0
pythonnative/hot_reload.py
CHANGED
|
@@ -576,7 +576,10 @@ class ModuleReloader:
|
|
|
576
576
|
if getattr(vnode, "element", None) is not None:
|
|
577
577
|
rewrite_element_tree(vnode.element)
|
|
578
578
|
rendered = getattr(vnode, "_rendered", None)
|
|
579
|
-
if rendered
|
|
579
|
+
if isinstance(rendered, (list, tuple)):
|
|
580
|
+
for rendered_el in rendered:
|
|
581
|
+
rewrite_element_tree(rendered_el)
|
|
582
|
+
elif rendered is not None:
|
|
580
583
|
rewrite_element_tree(rendered)
|
|
581
584
|
for child in getattr(vnode, "children", []) or []:
|
|
582
585
|
visit(child)
|
|
@@ -598,6 +601,11 @@ class ModuleReloader:
|
|
|
598
601
|
if not replacement_map:
|
|
599
602
|
return False
|
|
600
603
|
rewrites = ModuleReloader.swap_components_in_tree(reconciler, replacement_map)
|
|
604
|
+
if rewrites > 0 and hasattr(reconciler, "reset_hook_signatures"):
|
|
605
|
+
# New component bodies may legitimately call a different
|
|
606
|
+
# hook sequence; forget the recorded signatures so the
|
|
607
|
+
# dev-mode order guard doesn't flag the refresh itself.
|
|
608
|
+
reconciler.reset_hook_signatures()
|
|
601
609
|
return rewrites > 0
|
|
602
610
|
|
|
603
611
|
@staticmethod
|
|
@@ -1073,7 +1073,7 @@ class ButtonHandler(AndroidViewHandler):
|
|
|
1073
1073
|
|
|
1074
1074
|
class ClickProxy(dynamic_proxy(jclass("android.view.View").OnClickListener)):
|
|
1075
1075
|
def onClick(self, view: Any) -> None:
|
|
1076
|
-
_fire(view, "
|
|
1076
|
+
_fire(view, "on_press")
|
|
1077
1077
|
|
|
1078
1078
|
btn.setOnClickListener(ClickProxy())
|
|
1079
1079
|
return btn
|
|
@@ -2094,6 +2094,81 @@ class ModalHandler(AndroidViewHandler):
|
|
|
2094
2094
|
pass
|
|
2095
2095
|
|
|
2096
2096
|
|
|
2097
|
+
# ======================================================================
|
|
2098
|
+
# Portal: floats its children over the screen in a top-level overlay
|
|
2099
|
+
# ======================================================================
|
|
2100
|
+
|
|
2101
|
+
|
|
2102
|
+
class PortalHandler(AndroidViewHandler):
|
|
2103
|
+
"""Overlay container hosting [`Portal`][pythonnative.Portal] children.
|
|
2104
|
+
|
|
2105
|
+
The portal's native view is a full-size, non-clickable
|
|
2106
|
+
``FrameLayout`` added directly to the screen's fragment container
|
|
2107
|
+
(the same parent as the screen root, so portal coordinates equal
|
|
2108
|
+
viewport coordinates). It is appended last, which stacks it above
|
|
2109
|
+
the main content; because the overlay itself never consumes
|
|
2110
|
+
touches, taps on empty areas fall through to the screen below
|
|
2111
|
+
while the portal's own children stay interactive.
|
|
2112
|
+
|
|
2113
|
+
The overlay attaches lazily on the first child insert and re-homes
|
|
2114
|
+
itself if the fragment container was rebuilt (e.g. after popping
|
|
2115
|
+
back to a previously mounted screen).
|
|
2116
|
+
"""
|
|
2117
|
+
|
|
2118
|
+
def _build(self, props: Dict[str, Any]) -> Any:
|
|
2119
|
+
overlay = jclass("android.widget.FrameLayout")(_ctx())
|
|
2120
|
+
overlay.setClickable(False)
|
|
2121
|
+
return overlay
|
|
2122
|
+
|
|
2123
|
+
def _apply(self, overlay: Any, props: Dict[str, Any], initial: bool) -> None:
|
|
2124
|
+
_apply_common_visual(overlay, props)
|
|
2125
|
+
if not initial:
|
|
2126
|
+
self._ensure_attached(overlay)
|
|
2127
|
+
|
|
2128
|
+
def _ensure_attached(self, overlay: Any) -> None:
|
|
2129
|
+
try:
|
|
2130
|
+
from ..utils import get_android_fragment_container
|
|
2131
|
+
|
|
2132
|
+
container = get_android_fragment_container()
|
|
2133
|
+
except Exception:
|
|
2134
|
+
return
|
|
2135
|
+
try:
|
|
2136
|
+
parent = overlay.getParent()
|
|
2137
|
+
if parent is not None and _java_id(parent) == _java_id(container):
|
|
2138
|
+
overlay.bringToFront()
|
|
2139
|
+
return
|
|
2140
|
+
if parent is not None:
|
|
2141
|
+
parent.removeView(overlay)
|
|
2142
|
+
LayoutParams = jclass("android.view.ViewGroup$LayoutParams")
|
|
2143
|
+
lp = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
|
|
2144
|
+
container.addView(overlay, lp)
|
|
2145
|
+
except Exception:
|
|
2146
|
+
pass
|
|
2147
|
+
|
|
2148
|
+
def insert_child(self, parent: Any, child: Any, index: int) -> None:
|
|
2149
|
+
self._ensure_attached(parent)
|
|
2150
|
+
_insert_view(parent, child, index)
|
|
2151
|
+
|
|
2152
|
+
def remove_child(self, parent: Any, child: Any) -> None:
|
|
2153
|
+
try:
|
|
2154
|
+
parent.removeView(child)
|
|
2155
|
+
except Exception:
|
|
2156
|
+
pass
|
|
2157
|
+
|
|
2158
|
+
def set_frame(self, native_view: Any, x: float, y: float, width: float, height: float) -> None:
|
|
2159
|
+
# The overlay fills its container via MATCH_PARENT layout params;
|
|
2160
|
+
# the engine frames only the portal's children.
|
|
2161
|
+
return
|
|
2162
|
+
|
|
2163
|
+
def _teardown(self, overlay: Any) -> None:
|
|
2164
|
+
try:
|
|
2165
|
+
parent = overlay.getParent()
|
|
2166
|
+
if parent is not None:
|
|
2167
|
+
parent.removeView(overlay)
|
|
2168
|
+
except Exception:
|
|
2169
|
+
pass
|
|
2170
|
+
|
|
2171
|
+
|
|
2097
2172
|
class SliderHandler(AndroidViewHandler):
|
|
2098
2173
|
def _build(self, props: Dict[str, Any]) -> Any:
|
|
2099
2174
|
sb = jclass("android.widget.SeekBar")(_ctx())
|
|
@@ -3211,6 +3286,7 @@ def register_handlers(registry: Any) -> None:
|
|
|
3211
3286
|
registry.register("Spacer", SpacerHandler())
|
|
3212
3287
|
registry.register("SafeAreaView", SafeAreaViewHandler())
|
|
3213
3288
|
registry.register("Modal", ModalHandler())
|
|
3289
|
+
registry.register("Portal", PortalHandler())
|
|
3214
3290
|
registry.register("Slider", SliderHandler())
|
|
3215
3291
|
registry.register("TabBar", TabBarHandler())
|
|
3216
3292
|
registry.register("Pressable", PressableHandler())
|
|
@@ -3238,6 +3314,7 @@ __all__ = [
|
|
|
3238
3314
|
"SpacerHandler",
|
|
3239
3315
|
"SafeAreaViewHandler",
|
|
3240
3316
|
"ModalHandler",
|
|
3317
|
+
"PortalHandler",
|
|
3241
3318
|
"SliderHandler",
|
|
3242
3319
|
"TabBarHandler",
|
|
3243
3320
|
"PressableHandler",
|
|
@@ -803,7 +803,7 @@ class TextHandler(DesktopViewHandler):
|
|
|
803
803
|
class ButtonHandler(DesktopViewHandler):
|
|
804
804
|
def build(self, props: Dict[str, Any]) -> Any:
|
|
805
805
|
button = tk.Button(_master(), highlightthickness=0, takefocus=0)
|
|
806
|
-
button.configure(command=lambda: _fire(button, "
|
|
806
|
+
button.configure(command=lambda: _fire(button, "on_press"))
|
|
807
807
|
return button
|
|
808
808
|
|
|
809
809
|
def apply(self, button: Any, props: Dict[str, Any]) -> None:
|
|
@@ -1328,6 +1328,42 @@ class ModalHandler(DesktopViewHandler):
|
|
|
1328
1328
|
return
|
|
1329
1329
|
|
|
1330
1330
|
|
|
1331
|
+
# ======================================================================
|
|
1332
|
+
# Portal
|
|
1333
|
+
# ======================================================================
|
|
1334
|
+
|
|
1335
|
+
|
|
1336
|
+
class PortalHandler(DesktopViewHandler):
|
|
1337
|
+
"""Portal host that floats children over the whole stage.
|
|
1338
|
+
|
|
1339
|
+
Tkinter has no view that can cover the stage without also
|
|
1340
|
+
swallowing clicks, so the portal keeps its own frame unmapped and
|
|
1341
|
+
instead places each child directly against the stage (``_place``
|
|
1342
|
+
with no logical parent targets the root container). The detached
|
|
1343
|
+
layout pass produces viewport coordinates, which is exactly the
|
|
1344
|
+
stage coordinate space, and ``_place`` lifts children above the
|
|
1345
|
+
main content. Empty portal area has no widget at all, so clicks
|
|
1346
|
+
there reach whatever sits underneath.
|
|
1347
|
+
"""
|
|
1348
|
+
|
|
1349
|
+
def insert_child(self, parent: Any, child: Any, index: int) -> None:
|
|
1350
|
+
# No ``_pn_parent``: placement composes against the stage.
|
|
1351
|
+
child._pn_parent = None
|
|
1352
|
+
_register_child(parent, child, index)
|
|
1353
|
+
_place(child)
|
|
1354
|
+
|
|
1355
|
+
def remove_child(self, parent: Any, child: Any) -> None:
|
|
1356
|
+
_unregister_child(parent, child)
|
|
1357
|
+
try:
|
|
1358
|
+
child.place_forget()
|
|
1359
|
+
except Exception:
|
|
1360
|
+
pass
|
|
1361
|
+
|
|
1362
|
+
def set_frame(self, native_view: Any, x: float, y: float, width: float, height: float) -> None:
|
|
1363
|
+
# The portal frame itself stays unmapped; only children render.
|
|
1364
|
+
return
|
|
1365
|
+
|
|
1366
|
+
|
|
1331
1367
|
# ======================================================================
|
|
1332
1368
|
# TabBar
|
|
1333
1369
|
# ======================================================================
|
|
@@ -1580,6 +1616,7 @@ def register_handlers(registry: Any) -> None:
|
|
|
1580
1616
|
registry.register("ScrollView", ScrollViewHandler())
|
|
1581
1617
|
registry.register("SafeAreaView", SafeAreaViewHandler())
|
|
1582
1618
|
registry.register("Modal", ModalHandler())
|
|
1619
|
+
registry.register("Portal", PortalHandler())
|
|
1583
1620
|
registry.register("Slider", SliderHandler())
|
|
1584
1621
|
registry.register("TabBar", TabBarHandler())
|
|
1585
1622
|
registry.register("Pressable", PressableHandler())
|
pythonnative/native_views/ios.py
CHANGED
|
@@ -1589,7 +1589,7 @@ class ButtonHandler(IOSViewHandler):
|
|
|
1589
1589
|
btn.setTranslatesAutoresizingMaskIntoConstraints_(True)
|
|
1590
1590
|
btn.retain()
|
|
1591
1591
|
_pn_retained_views.append(btn)
|
|
1592
|
-
_register_control_action(btn, 1 << 6, lambda: _fire(btn, "
|
|
1592
|
+
_register_control_action(btn, 1 << 6, lambda: _fire(btn, "on_press")) # TouchUpInside
|
|
1593
1593
|
return btn
|
|
1594
1594
|
|
|
1595
1595
|
def measure_intrinsic(
|
|
@@ -3137,6 +3137,174 @@ class ModalHandler(IOSViewHandler):
|
|
|
3137
3137
|
_fire(placeholder, "on_dismiss")
|
|
3138
3138
|
|
|
3139
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
|
+
|
|
3140
3308
|
# ======================================================================
|
|
3141
3309
|
# StatusBar: global side effect, no view in the tree
|
|
3142
3310
|
# ======================================================================
|
|
@@ -3972,6 +4140,28 @@ def _table_row_height(info: dict, row: int) -> float:
|
|
|
3972
4140
|
return float(info.get("row_height", 44.0))
|
|
3973
4141
|
|
|
3974
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
|
+
|
|
3975
4165
|
def _table_num_sections_imp(self_ptr: int, cmd_ptr: int, tv_ptr: int) -> int:
|
|
3976
4166
|
return 1
|
|
3977
4167
|
|
|
@@ -4041,7 +4231,37 @@ def _table_cell_imp(self_ptr: int, cmd_ptr: int, tv_ptr: int, ip_ptr: int) -> in
|
|
|
4041
4231
|
except Exception:
|
|
4042
4232
|
pass
|
|
4043
4233
|
|
|
4044
|
-
|
|
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:
|
|
4045
4265
|
render_row = info.get("render_row")
|
|
4046
4266
|
pool = info.get("pool")
|
|
4047
4267
|
if render_row is not None and pool is not None:
|
|
@@ -4195,7 +4415,19 @@ class VirtualListHandler(IOSViewHandler):
|
|
|
4195
4415
|
tv.setTranslatesAutoresizingMaskIntoConstraints_(True)
|
|
4196
4416
|
tv.setSeparatorStyle_(0) # none by default; rows draw their own
|
|
4197
4417
|
try:
|
|
4198
|
-
|
|
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))
|
|
4199
4431
|
except Exception:
|
|
4200
4432
|
pass
|
|
4201
4433
|
tv.retain()
|
|
@@ -4205,14 +4437,15 @@ class VirtualListHandler(IOSViewHandler):
|
|
|
4205
4437
|
if source_ptr == 0:
|
|
4206
4438
|
raise RuntimeError("[VirtualList][iOS] dataSource alloc returned NULL")
|
|
4207
4439
|
|
|
4208
|
-
|
|
4440
|
+
state = {
|
|
4209
4441
|
"count": int(props.get("count", 0) or 0),
|
|
4210
4442
|
"row_height": float(props.get("row_height", 44.0) or 44.0),
|
|
4211
|
-
"row_heights": props.get("row_heights"),
|
|
4212
4443
|
"render_row": props.get("render_row"),
|
|
4213
4444
|
"pool": RowHostPool(),
|
|
4214
4445
|
"tv": tv,
|
|
4215
4446
|
}
|
|
4447
|
+
_table_set_heights(state, props.get("row_heights"))
|
|
4448
|
+
_pn_table_state[source_ptr] = state
|
|
4216
4449
|
|
|
4217
4450
|
_objc_msgSend.restype = None
|
|
4218
4451
|
_objc_msgSend.argtypes = [_ct.c_void_p, _ct.c_void_p, _ct.c_void_p]
|
|
@@ -4220,7 +4453,6 @@ class VirtualListHandler(IOSViewHandler):
|
|
|
4220
4453
|
_objc_msgSend(tv_ptr, _SEL_SET_DATA_SOURCE, _ct.c_void_p(source_ptr))
|
|
4221
4454
|
_objc_msgSend(tv_ptr, _SEL_SET_DELEGATE, _ct.c_void_p(source_ptr))
|
|
4222
4455
|
|
|
4223
|
-
state = _pn_table_state[source_ptr]
|
|
4224
4456
|
state["source_ptr"] = source_ptr
|
|
4225
4457
|
return tv
|
|
4226
4458
|
|
|
@@ -4245,7 +4477,7 @@ class VirtualListHandler(IOSViewHandler):
|
|
|
4245
4477
|
info["row_height"] = float(props["row_height"])
|
|
4246
4478
|
data_changed = True
|
|
4247
4479
|
if "row_heights" in props:
|
|
4248
|
-
info
|
|
4480
|
+
_table_set_heights(info, props.get("row_heights"))
|
|
4249
4481
|
data_changed = True
|
|
4250
4482
|
if "render_row" in props:
|
|
4251
4483
|
info["render_row"] = props.get("render_row")
|
|
@@ -4350,6 +4582,7 @@ def register_handlers(registry: Any) -> None:
|
|
|
4350
4582
|
registry.register("ScrollView", ScrollViewHandler())
|
|
4351
4583
|
registry.register("SafeAreaView", SafeAreaViewHandler())
|
|
4352
4584
|
registry.register("Modal", ModalHandler())
|
|
4585
|
+
registry.register("Portal", PortalHandler())
|
|
4353
4586
|
registry.register("Slider", SliderHandler())
|
|
4354
4587
|
registry.register("TabBar", TabBarHandler())
|
|
4355
4588
|
registry.register("Pressable", PressableHandler())
|
|
@@ -4377,6 +4610,7 @@ __all__ = [
|
|
|
4377
4610
|
"SpacerHandler",
|
|
4378
4611
|
"SafeAreaViewHandler",
|
|
4379
4612
|
"ModalHandler",
|
|
4613
|
+
"PortalHandler",
|
|
4380
4614
|
"SliderHandler",
|
|
4381
4615
|
"TabBarHandler",
|
|
4382
4616
|
"PressableHandler",
|
pythonnative/navigation.py
CHANGED
|
@@ -45,11 +45,12 @@ Example:
|
|
|
45
45
|
```
|
|
46
46
|
"""
|
|
47
47
|
|
|
48
|
-
from typing import Any, Callable, Dict, List, Optional
|
|
48
|
+
from typing import Any, Callable, Dict, List, Optional, TypedDict, Union
|
|
49
49
|
|
|
50
50
|
from .element import Element
|
|
51
51
|
from .hooks import (
|
|
52
52
|
Provider,
|
|
53
|
+
Ref,
|
|
53
54
|
_NavigationContext,
|
|
54
55
|
component,
|
|
55
56
|
create_context,
|
|
@@ -60,6 +61,29 @@ from .hooks import (
|
|
|
60
61
|
use_state,
|
|
61
62
|
)
|
|
62
63
|
|
|
64
|
+
|
|
65
|
+
class ScreenOptions(TypedDict, total=False):
|
|
66
|
+
"""Typed per-screen options accepted by ``Screen(..., options=...)``.
|
|
67
|
+
|
|
68
|
+
All keys are optional. Navigators ignore keys they don't use.
|
|
69
|
+
|
|
70
|
+
Attributes:
|
|
71
|
+
title: Human-readable screen title. Stack navigators forward it
|
|
72
|
+
to the host's native navigation bar; tab and drawer
|
|
73
|
+
navigators use it as the item label.
|
|
74
|
+
tab_bar_icon: Native system icon identifier for tab items. A
|
|
75
|
+
string is used on every platform; a dict like
|
|
76
|
+
``{"ios": "house.fill", "android": "ic_menu_home"}``
|
|
77
|
+
selects per platform. iOS values are resolved via
|
|
78
|
+
SF Symbols (``UIImage.systemImageNamed_``); Android values
|
|
79
|
+
are resolved against ``android.R.drawable.<name>``. Names
|
|
80
|
+
that don't resolve fall back to text-only.
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
title: str
|
|
84
|
+
tab_bar_icon: Union[str, Dict[str, str]]
|
|
85
|
+
|
|
86
|
+
|
|
63
87
|
# ======================================================================
|
|
64
88
|
# Focus context
|
|
65
89
|
# ======================================================================
|
|
@@ -85,14 +109,14 @@ class _ScreenDef:
|
|
|
85
109
|
name: Route name used by `navigate(name, ...)`.
|
|
86
110
|
component: A `@component` function rendered when this screen is
|
|
87
111
|
active.
|
|
88
|
-
options:
|
|
89
|
-
|
|
90
|
-
specific navigator implementation.
|
|
112
|
+
options: Per-screen [`ScreenOptions`][pythonnative.ScreenOptions]
|
|
113
|
+
(e.g., `title`, `tab_bar_icon`). Interpretation is up to
|
|
114
|
+
the specific navigator implementation.
|
|
91
115
|
"""
|
|
92
116
|
|
|
93
117
|
__slots__ = ("name", "component", "options")
|
|
94
118
|
|
|
95
|
-
def __init__(self, name: str, component_fn: Any, options: Optional[
|
|
119
|
+
def __init__(self, name: str, component_fn: Any, options: Optional[ScreenOptions] = None) -> None:
|
|
96
120
|
self.name = name
|
|
97
121
|
self.component = component_fn
|
|
98
122
|
self.options = options or {}
|
|
@@ -450,10 +474,10 @@ def _stack_navigator_impl(screens: Any = None, initial_route: Optional[str] = No
|
|
|
450
474
|
stack, set_stack = use_state(lambda: [_RouteEntry(first_route, first_params)])
|
|
451
475
|
|
|
452
476
|
stack_ref = use_ref(None)
|
|
453
|
-
stack_ref
|
|
477
|
+
stack_ref.current = stack
|
|
454
478
|
|
|
455
479
|
handle = use_memo(
|
|
456
|
-
lambda: _DeclarativeNavHandle(screen_map, lambda: stack_ref
|
|
480
|
+
lambda: _DeclarativeNavHandle(screen_map, lambda: stack_ref.current, set_stack, parent=parent_nav), []
|
|
457
481
|
)
|
|
458
482
|
handle._screen_map = screen_map
|
|
459
483
|
handle._parent = parent_nav
|
|
@@ -505,7 +529,7 @@ def create_stack_navigator() -> Any:
|
|
|
505
529
|
|
|
506
530
|
class _StackNavigator:
|
|
507
531
|
@staticmethod
|
|
508
|
-
def Screen(name: str, *, component: Any, options: Optional[
|
|
532
|
+
def Screen(name: str, *, component: Any, options: Optional[ScreenOptions] = None) -> "_ScreenDef":
|
|
509
533
|
"""Define a screen within this stack navigator.
|
|
510
534
|
|
|
511
535
|
Args:
|
|
@@ -556,8 +580,8 @@ def _tab_navigator_impl(screens: Any = None, initial_route: Optional[str] = None
|
|
|
556
580
|
active_tab, set_active_tab = use_state(first_route)
|
|
557
581
|
tab_params, set_tab_params = use_state(lambda: {first_route: {}})
|
|
558
582
|
|
|
559
|
-
params_ref = use_ref(None)
|
|
560
|
-
params_ref
|
|
583
|
+
params_ref: Ref[Dict[str, Dict[str, Any]]] = use_ref(None)
|
|
584
|
+
params_ref.current = tab_params
|
|
561
585
|
|
|
562
586
|
def switch_tab(name: str, params: Optional[Dict[str, Any]] = None) -> None:
|
|
563
587
|
set_active_tab(name)
|
|
@@ -565,7 +589,7 @@ def _tab_navigator_impl(screens: Any = None, initial_route: Optional[str] = None
|
|
|
565
589
|
set_tab_params(lambda p: {**p, name: params})
|
|
566
590
|
|
|
567
591
|
def get_stack() -> List[_RouteEntry]:
|
|
568
|
-
p = params_ref
|
|
592
|
+
p = params_ref.current or {}
|
|
569
593
|
return [_RouteEntry(active_tab, p.get(active_tab, {}))]
|
|
570
594
|
|
|
571
595
|
handle = use_memo(lambda: _TabNavHandle(screen_map, get_stack, lambda _: None, switch_tab, parent=parent_nav), [])
|
|
@@ -643,7 +667,7 @@ def create_tab_navigator() -> Any:
|
|
|
643
667
|
|
|
644
668
|
class _TabNavigator:
|
|
645
669
|
@staticmethod
|
|
646
|
-
def Screen(name: str, *, component: Any, options: Optional[
|
|
670
|
+
def Screen(name: str, *, component: Any, options: Optional[ScreenOptions] = None) -> "_ScreenDef":
|
|
647
671
|
"""Define a screen within this tab navigator.
|
|
648
672
|
|
|
649
673
|
Args:
|
|
@@ -704,8 +728,8 @@ def _drawer_navigator_impl(screens: Any = None, initial_route: Optional[str] = N
|
|
|
704
728
|
drawer_open, set_drawer_open = use_state(False)
|
|
705
729
|
screen_params, set_screen_params = use_state(lambda: {first_route: {}})
|
|
706
730
|
|
|
707
|
-
params_ref = use_ref(None)
|
|
708
|
-
params_ref
|
|
731
|
+
params_ref: Ref[Dict[str, Dict[str, Any]]] = use_ref(None)
|
|
732
|
+
params_ref.current = screen_params
|
|
709
733
|
|
|
710
734
|
def switch_screen(name: str, params: Optional[Dict[str, Any]] = None) -> None:
|
|
711
735
|
set_active_screen(name)
|
|
@@ -713,7 +737,7 @@ def _drawer_navigator_impl(screens: Any = None, initial_route: Optional[str] = N
|
|
|
713
737
|
set_screen_params(lambda p: {**p, name: params})
|
|
714
738
|
|
|
715
739
|
def get_stack() -> List[_RouteEntry]:
|
|
716
|
-
p = params_ref
|
|
740
|
+
p = params_ref.current or {}
|
|
717
741
|
return [_RouteEntry(active_screen, p.get(active_screen, {}))]
|
|
718
742
|
|
|
719
743
|
handle = use_memo(
|
|
@@ -763,7 +787,7 @@ def _drawer_navigator_impl(screens: Any = None, initial_route: Optional[str] = N
|
|
|
763
787
|
return _select
|
|
764
788
|
|
|
765
789
|
menu_items.append(
|
|
766
|
-
Element("Button", {"title": label, "
|
|
790
|
+
Element("Button", {"title": label, "on_press": make_select(item_name)}, [], key=f"__drawer_{item_name}")
|
|
767
791
|
)
|
|
768
792
|
|
|
769
793
|
drawer_panel = Element(
|
|
@@ -813,7 +837,7 @@ def create_drawer_navigator() -> Any:
|
|
|
813
837
|
|
|
814
838
|
class _DrawerNavigator:
|
|
815
839
|
@staticmethod
|
|
816
|
-
def Screen(name: str, *, component: Any, options: Optional[
|
|
840
|
+
def Screen(name: str, *, component: Any, options: Optional[ScreenOptions] = None) -> "_ScreenDef":
|
|
817
841
|
"""Define a screen within this drawer navigator.
|
|
818
842
|
|
|
819
843
|
Args:
|