pythonnative 0.17.0__tar.gz → 0.17.1__tar.gz
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-0.17.0/src/pythonnative.egg-info → pythonnative-0.17.1}/PKG-INFO +1 -1
- {pythonnative-0.17.0 → pythonnative-0.17.1}/pyproject.toml +2 -1
- {pythonnative-0.17.0 → pythonnative-0.17.1}/src/pythonnative/__init__.py +1 -1
- {pythonnative-0.17.0 → pythonnative-0.17.1}/src/pythonnative/animated.py +33 -7
- {pythonnative-0.17.0 → pythonnative-0.17.1}/src/pythonnative/layout.py +35 -1
- {pythonnative-0.17.0 → pythonnative-0.17.1}/src/pythonnative/native_views/android.py +94 -7
- {pythonnative-0.17.0 → pythonnative-0.17.1}/src/pythonnative/native_views/ios.py +68 -6
- {pythonnative-0.17.0 → pythonnative-0.17.1}/src/pythonnative/navigation.py +57 -2
- {pythonnative-0.17.0 → pythonnative-0.17.1}/src/pythonnative/reconciler.py +38 -0
- {pythonnative-0.17.0 → pythonnative-0.17.1}/src/pythonnative/screen.py +52 -3
- {pythonnative-0.17.0 → pythonnative-0.17.1}/src/pythonnative/storage.py +23 -14
- {pythonnative-0.17.0 → pythonnative-0.17.1/src/pythonnative.egg-info}/PKG-INFO +1 -1
- {pythonnative-0.17.0 → pythonnative-0.17.1}/tests/test_layout.py +80 -0
- {pythonnative-0.17.0 → pythonnative-0.17.1}/tests/test_reconciler.py +39 -0
- {pythonnative-0.17.0 → pythonnative-0.17.1}/LICENSE +0 -0
- {pythonnative-0.17.0 → pythonnative-0.17.1}/README.md +0 -0
- {pythonnative-0.17.0 → pythonnative-0.17.1}/setup.cfg +0 -0
- {pythonnative-0.17.0 → pythonnative-0.17.1}/src/pythonnative/_ios_log.py +0 -0
- {pythonnative-0.17.0 → pythonnative-0.17.1}/src/pythonnative/alerts.py +0 -0
- {pythonnative-0.17.0 → pythonnative-0.17.1}/src/pythonnative/cli/__init__.py +0 -0
- {pythonnative-0.17.0 → pythonnative-0.17.1}/src/pythonnative/cli/pn.py +0 -0
- {pythonnative-0.17.0 → pythonnative-0.17.1}/src/pythonnative/components.py +0 -0
- {pythonnative-0.17.0 → pythonnative-0.17.1}/src/pythonnative/element.py +0 -0
- {pythonnative-0.17.0 → pythonnative-0.17.1}/src/pythonnative/hooks.py +0 -0
- {pythonnative-0.17.0 → pythonnative-0.17.1}/src/pythonnative/hot_reload.py +0 -0
- {pythonnative-0.17.0 → pythonnative-0.17.1}/src/pythonnative/native_modules/__init__.py +0 -0
- {pythonnative-0.17.0 → pythonnative-0.17.1}/src/pythonnative/native_modules/camera.py +0 -0
- {pythonnative-0.17.0 → pythonnative-0.17.1}/src/pythonnative/native_modules/file_system.py +0 -0
- {pythonnative-0.17.0 → pythonnative-0.17.1}/src/pythonnative/native_modules/location.py +0 -0
- {pythonnative-0.17.0 → pythonnative-0.17.1}/src/pythonnative/native_modules/notifications.py +0 -0
- {pythonnative-0.17.0 → pythonnative-0.17.1}/src/pythonnative/native_views/__init__.py +0 -0
- {pythonnative-0.17.0 → pythonnative-0.17.1}/src/pythonnative/native_views/base.py +0 -0
- {pythonnative-0.17.0 → pythonnative-0.17.1}/src/pythonnative/net.py +0 -0
- {pythonnative-0.17.0 → pythonnative-0.17.1}/src/pythonnative/platform.py +0 -0
- {pythonnative-0.17.0 → pythonnative-0.17.1}/src/pythonnative/platform_metrics.py +0 -0
- {pythonnative-0.17.0 → pythonnative-0.17.1}/src/pythonnative/runtime.py +0 -0
- {pythonnative-0.17.0 → pythonnative-0.17.1}/src/pythonnative/sdk/__init__.py +0 -0
- {pythonnative-0.17.0 → pythonnative-0.17.1}/src/pythonnative/sdk/_components.py +0 -0
- {pythonnative-0.17.0 → pythonnative-0.17.1}/src/pythonnative/style.py +0 -0
- {pythonnative-0.17.0 → pythonnative-0.17.1}/src/pythonnative/templates/android_template/app/build.gradle +0 -0
- {pythonnative-0.17.0 → pythonnative-0.17.1}/src/pythonnative/templates/android_template/app/proguard-rules.pro +0 -0
- {pythonnative-0.17.0 → pythonnative-0.17.1}/src/pythonnative/templates/android_template/app/src/androidTest/java/com/pythonnative/android_template/ExampleInstrumentedTest.kt +0 -0
- {pythonnative-0.17.0 → pythonnative-0.17.1}/src/pythonnative/templates/android_template/app/src/main/AndroidManifest.xml +0 -0
- {pythonnative-0.17.0 → pythonnative-0.17.1}/src/pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/MainActivity.kt +0 -0
- {pythonnative-0.17.0 → pythonnative-0.17.1}/src/pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/Navigator.kt +0 -0
- {pythonnative-0.17.0 → pythonnative-0.17.1}/src/pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/PNVirtualListView.java +0 -0
- {pythonnative-0.17.0 → pythonnative-0.17.1}/src/pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/ScreenFragment.kt +0 -0
- {pythonnative-0.17.0 → pythonnative-0.17.1}/src/pythonnative/templates/android_template/app/src/main/res/drawable/ic_launcher_background.xml +0 -0
- {pythonnative-0.17.0 → pythonnative-0.17.1}/src/pythonnative/templates/android_template/app/src/main/res/drawable-v24/ic_launcher_foreground.xml +0 -0
- {pythonnative-0.17.0 → pythonnative-0.17.1}/src/pythonnative/templates/android_template/app/src/main/res/layout/activity_main.xml +0 -0
- {pythonnative-0.17.0 → pythonnative-0.17.1}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +0 -0
- {pythonnative-0.17.0 → pythonnative-0.17.1}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +0 -0
- {pythonnative-0.17.0 → pythonnative-0.17.1}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-hdpi/ic_launcher.webp +0 -0
- {pythonnative-0.17.0 → pythonnative-0.17.1}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp +0 -0
- {pythonnative-0.17.0 → pythonnative-0.17.1}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-mdpi/ic_launcher.webp +0 -0
- {pythonnative-0.17.0 → pythonnative-0.17.1}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp +0 -0
- {pythonnative-0.17.0 → pythonnative-0.17.1}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-xhdpi/ic_launcher.webp +0 -0
- {pythonnative-0.17.0 → pythonnative-0.17.1}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp +0 -0
- {pythonnative-0.17.0 → pythonnative-0.17.1}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp +0 -0
- {pythonnative-0.17.0 → pythonnative-0.17.1}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp +0 -0
- {pythonnative-0.17.0 → pythonnative-0.17.1}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp +0 -0
- {pythonnative-0.17.0 → pythonnative-0.17.1}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp +0 -0
- {pythonnative-0.17.0 → pythonnative-0.17.1}/src/pythonnative/templates/android_template/app/src/main/res/navigation/nav_graph.xml +0 -0
- {pythonnative-0.17.0 → pythonnative-0.17.1}/src/pythonnative/templates/android_template/app/src/main/res/values/colors.xml +0 -0
- {pythonnative-0.17.0 → pythonnative-0.17.1}/src/pythonnative/templates/android_template/app/src/main/res/values/strings.xml +0 -0
- {pythonnative-0.17.0 → pythonnative-0.17.1}/src/pythonnative/templates/android_template/app/src/main/res/values/themes.xml +0 -0
- {pythonnative-0.17.0 → pythonnative-0.17.1}/src/pythonnative/templates/android_template/app/src/main/res/values-night/themes.xml +0 -0
- {pythonnative-0.17.0 → pythonnative-0.17.1}/src/pythonnative/templates/android_template/app/src/main/res/xml/backup_rules.xml +0 -0
- {pythonnative-0.17.0 → pythonnative-0.17.1}/src/pythonnative/templates/android_template/app/src/main/res/xml/data_extraction_rules.xml +0 -0
- {pythonnative-0.17.0 → pythonnative-0.17.1}/src/pythonnative/templates/android_template/app/src/test/java/com/pythonnative/android_template/ExampleUnitTest.kt +0 -0
- {pythonnative-0.17.0 → pythonnative-0.17.1}/src/pythonnative/templates/android_template/build.gradle +0 -0
- {pythonnative-0.17.0 → pythonnative-0.17.1}/src/pythonnative/templates/android_template/gradle/wrapper/gradle-wrapper.jar +0 -0
- {pythonnative-0.17.0 → pythonnative-0.17.1}/src/pythonnative/templates/android_template/gradle/wrapper/gradle-wrapper.properties +0 -0
- {pythonnative-0.17.0 → pythonnative-0.17.1}/src/pythonnative/templates/android_template/gradle.properties +0 -0
- {pythonnative-0.17.0 → pythonnative-0.17.1}/src/pythonnative/templates/android_template/gradlew +0 -0
- {pythonnative-0.17.0 → pythonnative-0.17.1}/src/pythonnative/templates/android_template/gradlew.bat +0 -0
- {pythonnative-0.17.0 → pythonnative-0.17.1}/src/pythonnative/templates/android_template/settings.gradle +0 -0
- {pythonnative-0.17.0 → pythonnative-0.17.1}/src/pythonnative/templates/ios_template/ios_template/AppDelegate.swift +0 -0
- {pythonnative-0.17.0 → pythonnative-0.17.1}/src/pythonnative/templates/ios_template/ios_template/Assets.xcassets/AccentColor.colorset/Contents.json +0 -0
- {pythonnative-0.17.0 → pythonnative-0.17.1}/src/pythonnative/templates/ios_template/ios_template/Assets.xcassets/AppIcon.appiconset/Contents.json +0 -0
- {pythonnative-0.17.0 → pythonnative-0.17.1}/src/pythonnative/templates/ios_template/ios_template/Assets.xcassets/Contents.json +0 -0
- {pythonnative-0.17.0 → pythonnative-0.17.1}/src/pythonnative/templates/ios_template/ios_template/Base.lproj/LaunchScreen.storyboard +0 -0
- {pythonnative-0.17.0 → pythonnative-0.17.1}/src/pythonnative/templates/ios_template/ios_template/Base.lproj/Main.storyboard +0 -0
- {pythonnative-0.17.0 → pythonnative-0.17.1}/src/pythonnative/templates/ios_template/ios_template/Info.plist +0 -0
- {pythonnative-0.17.0 → pythonnative-0.17.1}/src/pythonnative/templates/ios_template/ios_template/SceneDelegate.swift +0 -0
- {pythonnative-0.17.0 → pythonnative-0.17.1}/src/pythonnative/templates/ios_template/ios_template/ViewController.swift +0 -0
- {pythonnative-0.17.0 → pythonnative-0.17.1}/src/pythonnative/templates/ios_template/ios_template.xcodeproj/project.pbxproj +0 -0
- {pythonnative-0.17.0 → pythonnative-0.17.1}/src/pythonnative/templates/ios_template/ios_template.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +0 -0
- {pythonnative-0.17.0 → pythonnative-0.17.1}/src/pythonnative/templates/ios_template/ios_templateTests/ios_templateTests.swift +0 -0
- {pythonnative-0.17.0 → pythonnative-0.17.1}/src/pythonnative/templates/ios_template/ios_templateUITests/ios_templateUITests.swift +0 -0
- {pythonnative-0.17.0 → pythonnative-0.17.1}/src/pythonnative/templates/ios_template/ios_templateUITests/ios_templateUITestsLaunchTests.swift +0 -0
- {pythonnative-0.17.0 → pythonnative-0.17.1}/src/pythonnative/utils.py +0 -0
- {pythonnative-0.17.0 → pythonnative-0.17.1}/src/pythonnative.egg-info/SOURCES.txt +0 -0
- {pythonnative-0.17.0 → pythonnative-0.17.1}/src/pythonnative.egg-info/dependency_links.txt +0 -0
- {pythonnative-0.17.0 → pythonnative-0.17.1}/src/pythonnative.egg-info/entry_points.txt +0 -0
- {pythonnative-0.17.0 → pythonnative-0.17.1}/src/pythonnative.egg-info/requires.txt +0 -0
- {pythonnative-0.17.0 → pythonnative-0.17.1}/src/pythonnative.egg-info/top_level.txt +0 -0
- {pythonnative-0.17.0 → pythonnative-0.17.1}/tests/test_alert.py +0 -0
- {pythonnative-0.17.0 → pythonnative-0.17.1}/tests/test_animated.py +0 -0
- {pythonnative-0.17.0 → pythonnative-0.17.1}/tests/test_async_hooks.py +0 -0
- {pythonnative-0.17.0 → pythonnative-0.17.1}/tests/test_cli.py +0 -0
- {pythonnative-0.17.0 → pythonnative-0.17.1}/tests/test_components.py +0 -0
- {pythonnative-0.17.0 → pythonnative-0.17.1}/tests/test_element.py +0 -0
- {pythonnative-0.17.0 → pythonnative-0.17.1}/tests/test_hooks.py +0 -0
- {pythonnative-0.17.0 → pythonnative-0.17.1}/tests/test_hot_reload.py +0 -0
- {pythonnative-0.17.0 → pythonnative-0.17.1}/tests/test_ios_log.py +0 -0
- {pythonnative-0.17.0 → pythonnative-0.17.1}/tests/test_metric_hooks.py +0 -0
- {pythonnative-0.17.0 → pythonnative-0.17.1}/tests/test_native_views.py +0 -0
- {pythonnative-0.17.0 → pythonnative-0.17.1}/tests/test_navigation.py +0 -0
- {pythonnative-0.17.0 → pythonnative-0.17.1}/tests/test_net.py +0 -0
- {pythonnative-0.17.0 → pythonnative-0.17.1}/tests/test_new_components.py +0 -0
- {pythonnative-0.17.0 → pythonnative-0.17.1}/tests/test_platform.py +0 -0
- {pythonnative-0.17.0 → pythonnative-0.17.1}/tests/test_platform_metrics.py +0 -0
- {pythonnative-0.17.0 → pythonnative-0.17.1}/tests/test_ref.py +0 -0
- {pythonnative-0.17.0 → pythonnative-0.17.1}/tests/test_runtime.py +0 -0
- {pythonnative-0.17.0 → pythonnative-0.17.1}/tests/test_screen.py +0 -0
- {pythonnative-0.17.0 → pythonnative-0.17.1}/tests/test_sdk.py +0 -0
- {pythonnative-0.17.0 → pythonnative-0.17.1}/tests/test_smoke.py +0 -0
- {pythonnative-0.17.0 → pythonnative-0.17.1}/tests/test_storage.py +0 -0
- {pythonnative-0.17.0 → pythonnative-0.17.1}/tests/test_style.py +0 -0
- {pythonnative-0.17.0 → pythonnative-0.17.1}/tests/test_utils.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "pythonnative"
|
|
7
|
-
version = "0.17.
|
|
7
|
+
version = "0.17.1"
|
|
8
8
|
description = "Cross-platform native UI toolkit for Android and iOS"
|
|
9
9
|
authors = [
|
|
10
10
|
{ name = "Owen Carey" }
|
|
@@ -69,6 +69,7 @@ Documentation = "https://docs.pythonnative.com/"
|
|
|
69
69
|
|
|
70
70
|
|
|
71
71
|
|
|
72
|
+
|
|
72
73
|
[tool.setuptools.packages.find]
|
|
73
74
|
where = ["src"]
|
|
74
75
|
|
|
@@ -62,6 +62,12 @@ from .style import StyleProp, resolve_style
|
|
|
62
62
|
_TARGET_FPS = 60.0
|
|
63
63
|
_FRAME_DT = 1.0 / _TARGET_FPS
|
|
64
64
|
|
|
65
|
+
# Upper bound on how much wall-clock time the animation loop will try to
|
|
66
|
+
# catch up on in a single iteration after thread starvation. At 60 fps
|
|
67
|
+
# this is ~333 ms of simulated motion; further drift is dropped to keep
|
|
68
|
+
# the loop responsive.
|
|
69
|
+
_MAX_CATCHUP_FRAMES = 20
|
|
70
|
+
|
|
65
71
|
_EASINGS: Dict[str, Callable[[float], float]] = {
|
|
66
72
|
"linear": lambda t: t,
|
|
67
73
|
"ease_in": lambda t: t * t,
|
|
@@ -199,6 +205,20 @@ class _AnimationManager:
|
|
|
199
205
|
|
|
200
206
|
def _loop(self) -> None:
|
|
201
207
|
last = time.monotonic()
|
|
208
|
+
# Clamping the per-tick dt is important for numerical stability:
|
|
209
|
+
# an underdamped spring with a 0.3 s step explodes immediately,
|
|
210
|
+
# and on iOS/Android the animation thread can be starved for
|
|
211
|
+
# several frames during render bursts. We integrate physics on a
|
|
212
|
+
# clamped dt (max 2 target frames) and sub-step when wall-clock
|
|
213
|
+
# has advanced more than that, so the perceived motion still
|
|
214
|
+
# tracks real time at most a couple of frames behind. After an
|
|
215
|
+
# extreme starvation (e.g. the app was backgrounded for seconds)
|
|
216
|
+
# we cap the catch-up at ``_MAX_CATCHUP_FRAMES`` worth of
|
|
217
|
+
# physics; any further wall-clock drift is dropped on the floor,
|
|
218
|
+
# which keeps the loop responsive instead of spinning forward
|
|
219
|
+
# through hundreds of substeps.
|
|
220
|
+
max_step = _FRAME_DT * 2.0
|
|
221
|
+
max_catchup = _FRAME_DT * _MAX_CATCHUP_FRAMES
|
|
202
222
|
while not self._stopped:
|
|
203
223
|
now = time.monotonic()
|
|
204
224
|
dt = now - last
|
|
@@ -209,13 +229,19 @@ class _AnimationManager:
|
|
|
209
229
|
time.sleep(0.05)
|
|
210
230
|
last = time.monotonic()
|
|
211
231
|
continue
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
232
|
+
remaining = min(dt, max_catchup)
|
|
233
|
+
while remaining > 0.0:
|
|
234
|
+
step = remaining if remaining <= max_step else max_step
|
|
235
|
+
remaining -= step
|
|
236
|
+
for anim in active:
|
|
237
|
+
if getattr(anim, "_completed", False):
|
|
238
|
+
continue
|
|
239
|
+
try:
|
|
240
|
+
finished = anim.advance(step)
|
|
241
|
+
except Exception:
|
|
242
|
+
finished = True
|
|
243
|
+
if finished:
|
|
244
|
+
self.remove(anim)
|
|
219
245
|
time.sleep(_FRAME_DT)
|
|
220
246
|
|
|
221
247
|
|
|
@@ -401,7 +401,17 @@ class LayoutNode:
|
|
|
401
401
|
height: Computed height in points.
|
|
402
402
|
"""
|
|
403
403
|
|
|
404
|
-
__slots__ = (
|
|
404
|
+
__slots__ = (
|
|
405
|
+
"style",
|
|
406
|
+
"children",
|
|
407
|
+
"measure",
|
|
408
|
+
"user_data",
|
|
409
|
+
"x",
|
|
410
|
+
"y",
|
|
411
|
+
"width",
|
|
412
|
+
"height",
|
|
413
|
+
"_pn_scroll_axis",
|
|
414
|
+
)
|
|
405
415
|
|
|
406
416
|
def __init__(
|
|
407
417
|
self,
|
|
@@ -418,6 +428,14 @@ class LayoutNode:
|
|
|
418
428
|
self.y: float = 0.0
|
|
419
429
|
self.width: float = 0.0
|
|
420
430
|
self.height: float = 0.0
|
|
431
|
+
# ``"x"``/``"y"`` for scroll containers; ``None`` for everything
|
|
432
|
+
# else. Consumed by ``_measure_container`` to clamp the node's
|
|
433
|
+
# own main-axis size to the parent's available space while still
|
|
434
|
+
# measuring children unbounded on the scroll axis (which is what
|
|
435
|
+
# makes the native ``UIScrollView`` / Android ``ScrollView``
|
|
436
|
+
# actually scroll). The reconciler stamps this when building the
|
|
437
|
+
# layout tree for ``ScrollView`` elements.
|
|
438
|
+
self._pn_scroll_axis: Optional[str] = None
|
|
421
439
|
|
|
422
440
|
def __repr__(self) -> str:
|
|
423
441
|
return (
|
|
@@ -576,6 +594,22 @@ def _measure_container(
|
|
|
576
594
|
|
|
577
595
|
width = explicit_w if explicit_w is not None else (used_w + pad_x)
|
|
578
596
|
height = explicit_h if explicit_h is not None else (used_h + pad_y)
|
|
597
|
+
|
|
598
|
+
# Scroll containers: clamp the container's own main-axis size to the
|
|
599
|
+
# parent's available space when no explicit size was provided. The
|
|
600
|
+
# children are still measured against an unbounded main-axis (handled
|
|
601
|
+
# via the wrapper inserted in ``Reconciler._build_layout_tree``) so the
|
|
602
|
+
# overflow becomes the scrollable region. Without this clamp, the
|
|
603
|
+
# container would grow to fit its content and there would be no
|
|
604
|
+
# overflow for the native ScrollView to scroll. Skipped when the
|
|
605
|
+
# parent is itself unbounded, so nested scroll views still fall back
|
|
606
|
+
# to natural sizing (the inner scroll is unscrollable in that case,
|
|
607
|
+
# which matches the behavior in React Native).
|
|
608
|
+
scroll_axis = getattr(node, "_pn_scroll_axis", None)
|
|
609
|
+
if scroll_axis == "y" and explicit_h is None and math.isfinite(avail_h):
|
|
610
|
+
height = avail_h
|
|
611
|
+
elif scroll_axis == "x" and explicit_w is None and math.isfinite(avail_w):
|
|
612
|
+
width = avail_w
|
|
579
613
|
return width, height
|
|
580
614
|
|
|
581
615
|
|
|
@@ -35,6 +35,7 @@ _pn_text_input_suppress_callbacks: dict = {}
|
|
|
35
35
|
_pn_view_visual_props: dict = {}
|
|
36
36
|
_DRAWABLE_STYLE_KEYS = ("background_color", "border_radius", "border_width", "border_color")
|
|
37
37
|
|
|
38
|
+
|
|
38
39
|
# ======================================================================
|
|
39
40
|
# Shared helpers
|
|
40
41
|
# ======================================================================
|
|
@@ -509,18 +510,27 @@ class ButtonHandler(AndroidViewHandler):
|
|
|
509
510
|
class ScrollViewHandler(AndroidViewHandler):
|
|
510
511
|
"""Scroll container — wraps a single child whose height is unbounded.
|
|
511
512
|
|
|
513
|
+
Uses ``androidx.core.widget.NestedScrollView`` rather than the
|
|
514
|
+
framework ``android.widget.ScrollView`` because the framework
|
|
515
|
+
ScrollView always intercepts vertical gestures, even when it has
|
|
516
|
+
no overflow. That breaks the common case of nesting a small
|
|
517
|
+
fixed-height scroll view inside a screen-level scroll view (the
|
|
518
|
+
outer steals every gesture and the inner never scrolls).
|
|
519
|
+
``NestedScrollView`` implements the standard
|
|
520
|
+
``NestedScrollingParent2`` / ``NestedScrollingChild2`` protocol so
|
|
521
|
+
the outer cooperates with any nested scroll, only consuming
|
|
522
|
+
leftover scroll when its child reaches its limit.
|
|
523
|
+
|
|
512
524
|
When a ``refresh_control`` prop is provided, wraps the scroll in
|
|
513
525
|
a `SwipeRefreshLayout` and forwards the on-refresh callback.
|
|
514
526
|
"""
|
|
515
527
|
|
|
516
528
|
def create(self, props: Dict[str, Any]) -> Any:
|
|
517
|
-
|
|
529
|
+
try:
|
|
530
|
+
sv = jclass("androidx.core.widget.NestedScrollView")(_ctx())
|
|
531
|
+
except Exception:
|
|
532
|
+
sv = jclass("android.widget.ScrollView")(_ctx())
|
|
518
533
|
_apply_common_visual(sv, props)
|
|
519
|
-
# Wrap the inner ScrollView in a SwipeRefreshLayout when
|
|
520
|
-
# ``refresh_control`` is asked for. Implementing this cleanly
|
|
521
|
-
# would require returning a different parent; for v1, we
|
|
522
|
-
# attach the listener via a wrapper that we expose to
|
|
523
|
-
# add_child callers below.
|
|
524
534
|
return sv
|
|
525
535
|
|
|
526
536
|
def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
|
|
@@ -536,6 +546,17 @@ class ScrollViewHandler(AndroidViewHandler):
|
|
|
536
546
|
class TextInputHandler(AndroidViewHandler):
|
|
537
547
|
def create(self, props: Dict[str, Any]) -> Any:
|
|
538
548
|
et = jclass("android.widget.EditText")(_ctx())
|
|
549
|
+
# Default to single-line so pressing Enter triggers IME_ACTION_DONE
|
|
550
|
+
# (submit / dismiss) instead of inserting a newline. The
|
|
551
|
+
# ``_apply`` path will override this if ``multiline=True`` is
|
|
552
|
+
# set in props. Without this, every TextInput without an
|
|
553
|
+
# explicit ``multiline`` value falls back to Android's
|
|
554
|
+
# multi-line default and Enter inserts ``\n``.
|
|
555
|
+
try:
|
|
556
|
+
if not props.get("multiline"):
|
|
557
|
+
et.setSingleLine(True)
|
|
558
|
+
except Exception:
|
|
559
|
+
pass
|
|
539
560
|
self._apply(et, props)
|
|
540
561
|
return et
|
|
541
562
|
|
|
@@ -661,7 +682,73 @@ class TextInputHandler(AndroidViewHandler):
|
|
|
661
682
|
et.addTextChangedListener(watcher)
|
|
662
683
|
else:
|
|
663
684
|
_pn_text_input_callbacks[key] = None
|
|
664
|
-
if "
|
|
685
|
+
if "return_key_type" in props and props["return_key_type"] is not None:
|
|
686
|
+
# Map the cross-platform ``return_key_type`` to Android's
|
|
687
|
+
# ``EditorInfo.IME_ACTION_*`` so the soft keyboard renders the
|
|
688
|
+
# right action key (Done / Go / Search / Send / Next), which
|
|
689
|
+
# is what triggers the ``OnEditorActionListener`` below. iOS
|
|
690
|
+
# has a richer set (Google / Yahoo / Join / Route) with no
|
|
691
|
+
# direct AOSP equivalents — fall back to ``IME_ACTION_DONE``
|
|
692
|
+
# for those so the keyboard at least dismisses cleanly.
|
|
693
|
+
try:
|
|
694
|
+
EditorInfo = jclass("android.view.inputmethod.EditorInfo")
|
|
695
|
+
rkt_mapping = {
|
|
696
|
+
"default": EditorInfo.IME_ACTION_UNSPECIFIED,
|
|
697
|
+
"go": EditorInfo.IME_ACTION_GO,
|
|
698
|
+
"google": EditorInfo.IME_ACTION_DONE,
|
|
699
|
+
"join": EditorInfo.IME_ACTION_DONE,
|
|
700
|
+
"next": EditorInfo.IME_ACTION_NEXT,
|
|
701
|
+
"route": EditorInfo.IME_ACTION_DONE,
|
|
702
|
+
"search": EditorInfo.IME_ACTION_SEARCH,
|
|
703
|
+
"send": EditorInfo.IME_ACTION_SEND,
|
|
704
|
+
"yahoo": EditorInfo.IME_ACTION_DONE,
|
|
705
|
+
"done": EditorInfo.IME_ACTION_DONE,
|
|
706
|
+
}
|
|
707
|
+
action = rkt_mapping.get(props["return_key_type"], EditorInfo.IME_ACTION_DONE)
|
|
708
|
+
et.setImeOptions(action)
|
|
709
|
+
except Exception:
|
|
710
|
+
pass
|
|
711
|
+
if not props.get("multiline"):
|
|
712
|
+
# Always install an editor-action listener on single-line
|
|
713
|
+
# inputs so pressing the IME action key (Done / Go / etc.)
|
|
714
|
+
# *or* the Enter key on a single-line ``EditText`` dismisses
|
|
715
|
+
# the soft keyboard. Without this the keyboard stays up after
|
|
716
|
+
# ``inputText`` + ``pressKey: Enter`` in Maestro and on smaller
|
|
717
|
+
# screens hides the rest of the layout — and matches React
|
|
718
|
+
# Native's default Android behavior. ``on_submit`` (if any) is
|
|
719
|
+
# fired before dismissal so the callback sees the final text.
|
|
720
|
+
try:
|
|
721
|
+
on_submit_cb = props.get("on_submit")
|
|
722
|
+
EditorListener = jclass("android.widget.TextView$OnEditorActionListener")
|
|
723
|
+
Context = jclass("android.content.Context")
|
|
724
|
+
|
|
725
|
+
class SubmitProxy(dynamic_proxy(EditorListener)):
|
|
726
|
+
def __init__(self, callback: Optional[Callable[[str], None]]) -> None:
|
|
727
|
+
super().__init__()
|
|
728
|
+
self.callback = callback
|
|
729
|
+
|
|
730
|
+
def onEditorAction(self, view: Any, action_id: int, event: Any) -> bool:
|
|
731
|
+
if self.callback is not None:
|
|
732
|
+
try:
|
|
733
|
+
self.callback(str(view.getText()))
|
|
734
|
+
except Exception:
|
|
735
|
+
pass
|
|
736
|
+
try:
|
|
737
|
+
view.clearFocus()
|
|
738
|
+
ctx = view.getContext()
|
|
739
|
+
imm = ctx.getSystemService(Context.INPUT_METHOD_SERVICE)
|
|
740
|
+
imm.hideSoftInputFromWindow(view.getWindowToken(), 0)
|
|
741
|
+
except Exception:
|
|
742
|
+
pass
|
|
743
|
+
return True
|
|
744
|
+
|
|
745
|
+
et.setOnEditorActionListener(SubmitProxy(on_submit_cb))
|
|
746
|
+
except Exception:
|
|
747
|
+
pass
|
|
748
|
+
elif "on_submit" in props and props["on_submit"] is not None:
|
|
749
|
+
# Multi-line inputs: only install the listener when an explicit
|
|
750
|
+
# ``on_submit`` is provided. Enter inserts a newline by default
|
|
751
|
+
# on multi-line ``EditText`` and we don't want to override that.
|
|
665
752
|
try:
|
|
666
753
|
cb = props["on_submit"]
|
|
667
754
|
EditorListener = jclass("android.widget.TextView$OnEditorActionListener")
|
|
@@ -54,6 +54,19 @@ NSObject = ObjCClass("NSObject")
|
|
|
54
54
|
UIColor = ObjCClass("UIColor")
|
|
55
55
|
UIFont = ObjCClass("UIFont")
|
|
56
56
|
|
|
57
|
+
# Declare ``superview`` as a property on UIView so rubicon-objc returns
|
|
58
|
+
# the actual UIView (or None) on attribute access, instead of an
|
|
59
|
+
# ObjCBoundMethod. Without this, accessing ``view.superview`` returns a
|
|
60
|
+
# method handle and the entire codepath that updates UIScrollView's
|
|
61
|
+
# ``contentSize`` would raise silently. See rubicon-objc docs on
|
|
62
|
+
# ``declare_property`` for why some ``@property`` declarations aren't
|
|
63
|
+
# auto-detected by the runtime introspection.
|
|
64
|
+
try:
|
|
65
|
+
_UIView = ObjCClass("UIView")
|
|
66
|
+
_UIView.declare_property("superview")
|
|
67
|
+
except Exception:
|
|
68
|
+
pass
|
|
69
|
+
|
|
57
70
|
|
|
58
71
|
def _objc_ptr(obj: Any) -> Optional[int]:
|
|
59
72
|
"""Return the raw Objective-C pointer for a Rubicon object."""
|
|
@@ -131,6 +144,8 @@ _SEL_UTF8STRING = _sel_reg(b"UTF8String")
|
|
|
131
144
|
_SEL_ADD_TARGET_ACTION_EVENTS = _sel_reg(b"addTarget:action:forControlEvents:")
|
|
132
145
|
_SEL_ON_EDIT = _sel_reg(b"onEdit:")
|
|
133
146
|
_SEL_ON_SUBMIT = _sel_reg(b"onSubmit:")
|
|
147
|
+
_SEL_RESIGN_FIRST_RESPONDER = _sel_reg(b"resignFirstResponder")
|
|
148
|
+
_SEL_TEXT_FIELD_SHOULD_RETURN = _sel_reg(b"textFieldShouldReturn:")
|
|
134
149
|
|
|
135
150
|
_NS_OBJECT_CLS = _get_cls(b"NSObject")
|
|
136
151
|
|
|
@@ -507,12 +522,19 @@ class IOSViewHandler(ViewHandler):
|
|
|
507
522
|
pass
|
|
508
523
|
try:
|
|
509
524
|
parent = native_view.superview
|
|
510
|
-
|
|
511
|
-
|
|
525
|
+
parent_cls = ""
|
|
526
|
+
try:
|
|
527
|
+
parent_cls = str(parent.objc_class.name) if parent is not None else ""
|
|
528
|
+
except Exception:
|
|
529
|
+
parent_cls = ""
|
|
530
|
+
# Expand the parent UIScrollView's contentSize whenever a
|
|
531
|
+
# child's frame extends past the visible bounds, so the
|
|
532
|
+
# scroll view can actually scroll to reveal it.
|
|
533
|
+
if "UIScrollView" in parent_cls:
|
|
512
534
|
bounds = parent.bounds
|
|
513
535
|
content_w = max(float(bounds.size.width), frame_x + frame_w)
|
|
514
536
|
content_h = max(float(bounds.size.height), frame_y + frame_h)
|
|
515
|
-
|
|
537
|
+
parent.setContentSize_((content_w, content_h))
|
|
516
538
|
except Exception:
|
|
517
539
|
pass
|
|
518
540
|
except Exception:
|
|
@@ -651,6 +673,7 @@ _pn_tf_raw_target_map: dict = {}
|
|
|
651
673
|
_PN_TEXTFIELD_TARGET_CLS: Optional[int] = None
|
|
652
674
|
_textfield_edit_imp_ref: Any = None
|
|
653
675
|
_textfield_submit_imp_ref: Any = None
|
|
676
|
+
_textfield_should_return_imp_ref: Any = None
|
|
654
677
|
|
|
655
678
|
|
|
656
679
|
def _textfield_text(sender_ptr: int) -> str:
|
|
@@ -694,8 +717,27 @@ def _textfield_on_submit_imp(self_ptr: int, _cmd: int, sender_ptr: int) -> None:
|
|
|
694
717
|
pass
|
|
695
718
|
|
|
696
719
|
|
|
720
|
+
def _textfield_should_return_imp(self_ptr: int, _cmd: int, tf_ptr: int) -> bool:
|
|
721
|
+
"""``UITextFieldDelegate.textFieldShouldReturn:`` — dismiss the keyboard.
|
|
722
|
+
|
|
723
|
+
iOS doesn't dismiss the keyboard on Return by default; the standard
|
|
724
|
+
pattern is for the delegate to call ``resignFirstResponder`` and
|
|
725
|
+
return ``YES``. Matching that here brings PythonNative's
|
|
726
|
+
``TextInput`` in line with React Native's default behavior and with
|
|
727
|
+
what users expect from a ``return_key_type="done"`` style.
|
|
728
|
+
"""
|
|
729
|
+
try:
|
|
730
|
+
_objc_msgSend.restype = None
|
|
731
|
+
_objc_msgSend.argtypes = [_ct.c_void_p, _ct.c_void_p]
|
|
732
|
+
_objc_msgSend(_ct.c_void_p(int(tf_ptr or 0)), _SEL_RESIGN_FIRST_RESPONDER)
|
|
733
|
+
except Exception:
|
|
734
|
+
pass
|
|
735
|
+
return True
|
|
736
|
+
|
|
737
|
+
|
|
697
738
|
def _ensure_textfield_target_class() -> Optional[int]:
|
|
698
|
-
global _PN_TEXTFIELD_TARGET_CLS
|
|
739
|
+
global _PN_TEXTFIELD_TARGET_CLS
|
|
740
|
+
global _textfield_edit_imp_ref, _textfield_submit_imp_ref, _textfield_should_return_imp_ref
|
|
699
741
|
if _PN_TEXTFIELD_TARGET_CLS is not None:
|
|
700
742
|
return _PN_TEXTFIELD_TARGET_CLS
|
|
701
743
|
existing = _get_cls(b"PNTextFieldActionTarget")
|
|
@@ -706,10 +748,18 @@ def _ensure_textfield_target_class() -> Optional[int]:
|
|
|
706
748
|
if not cls:
|
|
707
749
|
return None
|
|
708
750
|
action_type = _ct.CFUNCTYPE(None, _ct.c_void_p, _ct.c_void_p, _ct.c_void_p)
|
|
751
|
+
bool_type = _ct.CFUNCTYPE(_ct.c_bool, _ct.c_void_p, _ct.c_void_p, _ct.c_void_p)
|
|
709
752
|
_textfield_edit_imp_ref = action_type(_textfield_on_edit_imp)
|
|
710
753
|
_textfield_submit_imp_ref = action_type(_textfield_on_submit_imp)
|
|
754
|
+
_textfield_should_return_imp_ref = bool_type(_textfield_should_return_imp)
|
|
711
755
|
_add_method(cls, _SEL_ON_EDIT, _ct.cast(_textfield_edit_imp_ref, _ct.c_void_p), b"v@:@")
|
|
712
756
|
_add_method(cls, _SEL_ON_SUBMIT, _ct.cast(_textfield_submit_imp_ref, _ct.c_void_p), b"v@:@")
|
|
757
|
+
_add_method(
|
|
758
|
+
cls,
|
|
759
|
+
_SEL_TEXT_FIELD_SHOULD_RETURN,
|
|
760
|
+
_ct.cast(_textfield_should_return_imp_ref, _ct.c_void_p),
|
|
761
|
+
b"c@:@",
|
|
762
|
+
)
|
|
713
763
|
_reg_cls(cls)
|
|
714
764
|
_PN_TEXTFIELD_TARGET_CLS = int(cls)
|
|
715
765
|
return _PN_TEXTFIELD_TARGET_CLS
|
|
@@ -760,6 +810,16 @@ def _attach_textfield_raw_target(tf: Any, props: Dict[str, Any]) -> None:
|
|
|
760
810
|
_SEL_ON_SUBMIT,
|
|
761
811
|
1 << 6,
|
|
762
812
|
)
|
|
813
|
+
# Wire the same object as the UITextFieldDelegate so its
|
|
814
|
+
# ``textFieldShouldReturn:`` runs and resigns first responder
|
|
815
|
+
# — without this iOS keeps the keyboard up after Return.
|
|
816
|
+
_objc_msgSend.restype = None
|
|
817
|
+
_objc_msgSend.argtypes = [_ct.c_void_p, _ct.c_void_p, _ct.c_void_p]
|
|
818
|
+
_objc_msgSend(
|
|
819
|
+
_ct.c_void_p(tf_ptr),
|
|
820
|
+
_SEL_SET_DELEGATE,
|
|
821
|
+
_ct.c_void_p(target_ptr),
|
|
822
|
+
)
|
|
763
823
|
if "on_change" in props:
|
|
764
824
|
_pn_tf_change_callback_map[int(target_ptr)] = props["on_change"]
|
|
765
825
|
if "on_submit" in props:
|
|
@@ -1313,8 +1373,10 @@ class TextInputHandler(IOSViewHandler):
|
|
|
1313
1373
|
except Exception:
|
|
1314
1374
|
pass
|
|
1315
1375
|
self._common_apply(tf, props)
|
|
1316
|
-
|
|
1317
|
-
|
|
1376
|
+
# Always wire the action target — even without ``on_change`` /
|
|
1377
|
+
# ``on_submit`` we want the textfield's delegate set so Return
|
|
1378
|
+
# dismisses the keyboard (textFieldShouldReturn:).
|
|
1379
|
+
_attach_textfield_raw_target(tf, props)
|
|
1318
1380
|
|
|
1319
1381
|
def _apply_textview(self, tv: Any, props: Dict[str, Any]) -> None:
|
|
1320
1382
|
if "value" in props:
|
|
@@ -64,7 +64,14 @@ from .hooks import (
|
|
|
64
64
|
# Focus context
|
|
65
65
|
# ======================================================================
|
|
66
66
|
|
|
67
|
-
|
|
67
|
+
# Defaults to True: components rendered outside any declarative
|
|
68
|
+
# navigator (e.g. the root component of a screen pushed via the host's
|
|
69
|
+
# native nav stack) are by definition focused — the host's own
|
|
70
|
+
# ``on_resume`` / ``on_pause`` lifecycle drives the focus state for
|
|
71
|
+
# those. Declarative navigators override this provider on the active
|
|
72
|
+
# subtree (always True today; reserved for future inactive-screen
|
|
73
|
+
# rendering).
|
|
74
|
+
_FocusContext = create_context(True)
|
|
68
75
|
|
|
69
76
|
# ======================================================================
|
|
70
77
|
# Data structures
|
|
@@ -925,6 +932,15 @@ def use_focus_effect(effect: Callable, deps: Optional[list] = None) -> None:
|
|
|
925
932
|
one. Useful for starting subscriptions, refreshing data, or
|
|
926
933
|
pausing animations on the inactive screen.
|
|
927
934
|
|
|
935
|
+
The focus state combines two sources of truth:
|
|
936
|
+
|
|
937
|
+
- The screen host's lifecycle (``on_resume`` / ``on_pause``), so
|
|
938
|
+
pushing a sibling onto the navigation stack blurs this screen and
|
|
939
|
+
popping back to it refocuses it.
|
|
940
|
+
- The in-tree ``_FocusContext`` value, which lets declarative
|
|
941
|
+
navigators (e.g. tabs, drawers) mark only the active subtree as
|
|
942
|
+
focused even when both screens are part of the same host.
|
|
943
|
+
|
|
928
944
|
Args:
|
|
929
945
|
effect: A zero-arg callable invoked when focused. Optionally
|
|
930
946
|
returns a cleanup callable.
|
|
@@ -941,7 +957,46 @@ def use_focus_effect(effect: Callable, deps: Optional[list] = None) -> None:
|
|
|
941
957
|
return pn.Text("Home")
|
|
942
958
|
```
|
|
943
959
|
"""
|
|
944
|
-
|
|
960
|
+
context_focused = use_context(_FocusContext)
|
|
961
|
+
|
|
962
|
+
nav = use_context(_NavigationContext)
|
|
963
|
+
# Walk the navigator parent chain to find the screen host. Declarative
|
|
964
|
+
# navigators (Stack/Tab/Drawer) wrap the host's ``NavigationHandle``
|
|
965
|
+
# as ``_parent``; only the host-level handle has ``_host``.
|
|
966
|
+
host = None
|
|
967
|
+
cursor = nav
|
|
968
|
+
while cursor is not None:
|
|
969
|
+
candidate = getattr(cursor, "_host", None)
|
|
970
|
+
if candidate is not None:
|
|
971
|
+
host = candidate
|
|
972
|
+
break
|
|
973
|
+
cursor = getattr(cursor, "_parent", None)
|
|
974
|
+
initial_host_focus = bool(getattr(host, "_is_focused", True)) if host is not None else True
|
|
975
|
+
host_focused, set_host_focused = use_state(initial_host_focus)
|
|
976
|
+
|
|
977
|
+
def subscribe_to_host_focus() -> Any:
|
|
978
|
+
if host is None:
|
|
979
|
+
return None
|
|
980
|
+
subscribers = getattr(host, "_focus_subscribers", None)
|
|
981
|
+
if subscribers is None:
|
|
982
|
+
return None
|
|
983
|
+
subscribers.append(set_host_focused)
|
|
984
|
+
# The host may have changed focus state between the initial
|
|
985
|
+
# ``use_state`` call and this effect running (e.g. mid-render
|
|
986
|
+
# lifecycle event); resync once to avoid stale state.
|
|
987
|
+
set_host_focused(bool(host._is_focused))
|
|
988
|
+
|
|
989
|
+
def cleanup() -> None:
|
|
990
|
+
try:
|
|
991
|
+
subscribers.remove(set_host_focused)
|
|
992
|
+
except ValueError:
|
|
993
|
+
pass
|
|
994
|
+
|
|
995
|
+
return cleanup
|
|
996
|
+
|
|
997
|
+
use_effect(subscribe_to_host_focus, [])
|
|
998
|
+
|
|
999
|
+
is_focused = context_focused and host_focused
|
|
945
1000
|
all_deps = [is_focused] + (list(deps) if deps is not None else [])
|
|
946
1001
|
|
|
947
1002
|
def wrapped_effect() -> Any:
|
|
@@ -803,8 +803,37 @@ class Reconciler:
|
|
|
803
803
|
# root in the screen.
|
|
804
804
|
for child in layout_root.children:
|
|
805
805
|
self._apply_layout(child, 0.0, 0.0)
|
|
806
|
+
# Lay out the children of every visible ``Modal`` as a fresh
|
|
807
|
+
# subtree sized to the viewport. Modals are excluded from the
|
|
808
|
+
# main layout tree (their content lives in a separately
|
|
809
|
+
# presented native container) so without this pass the
|
|
810
|
+
# children's frames never get computed and the modal renders
|
|
811
|
+
# blank.
|
|
812
|
+
self._layout_visible_modals(self._tree, viewport_w, viewport_h)
|
|
806
813
|
self._log_viewport(f"_run_layout: pass#{layout_pass} done")
|
|
807
814
|
|
|
815
|
+
def _layout_visible_modals(
|
|
816
|
+
self,
|
|
817
|
+
vnode: VNode,
|
|
818
|
+
viewport_w: float,
|
|
819
|
+
viewport_h: float,
|
|
820
|
+
) -> None:
|
|
821
|
+
element = vnode.element
|
|
822
|
+
if isinstance(element.type, str) and element.type == "Modal":
|
|
823
|
+
if element.props.get("visible") and vnode.children:
|
|
824
|
+
child_layout = self._build_layout_tree(vnode.children[0])
|
|
825
|
+
if child_layout is not None:
|
|
826
|
+
viewport = LayoutNode(
|
|
827
|
+
style={"width": viewport_w, "height": viewport_h},
|
|
828
|
+
children=[child_layout],
|
|
829
|
+
)
|
|
830
|
+
calculate_layout(viewport, viewport_w, viewport_h)
|
|
831
|
+
for c in viewport.children:
|
|
832
|
+
self._apply_layout(c, 0.0, 0.0)
|
|
833
|
+
return
|
|
834
|
+
for child in vnode.children:
|
|
835
|
+
self._layout_visible_modals(child, viewport_w, viewport_h)
|
|
836
|
+
|
|
808
837
|
def _build_layout_tree(self, vnode: VNode) -> Optional[LayoutNode]:
|
|
809
838
|
"""Walk `vnode` and build a parallel `LayoutNode` tree of native nodes.
|
|
810
839
|
|
|
@@ -829,6 +858,15 @@ class Reconciler:
|
|
|
829
858
|
|
|
830
859
|
style = extract_layout_style(element.props)
|
|
831
860
|
layout = LayoutNode(style=style, user_data=vnode)
|
|
861
|
+
if element.type == "ScrollView":
|
|
862
|
+
# Mark the scroll axis so the layout engine clamps the
|
|
863
|
+
# container's own main-axis size to its parent's available
|
|
864
|
+
# space (otherwise the container grows to fit its content
|
|
865
|
+
# and there is no overflow for the native ScrollView to
|
|
866
|
+
# actually scroll). The children are still wrapped below so
|
|
867
|
+
# they see an unbounded main axis when measured.
|
|
868
|
+
scroll_axis = element.props.get("scroll_axis", "vertical")
|
|
869
|
+
layout._pn_scroll_axis = "x" if scroll_axis == "horizontal" else "y"
|
|
832
870
|
self._log_viewport(
|
|
833
871
|
f"_build_layout_tree: node type={element.type!r} view={self._obj_debug(vnode.native_view)} "
|
|
834
872
|
f"style={style!r} children={len(vnode.children)}"
|
|
@@ -165,6 +165,25 @@ def _init_host_common(host: Any, component_path: str, component_func: Any) -> No
|
|
|
165
165
|
host._hot_reload_manifest_path = None
|
|
166
166
|
host._hot_reload_last_version = None
|
|
167
167
|
host._layout_listener = None # retained on Android to prevent GC
|
|
168
|
+
# Focus state — drives ``use_focus_effect``. Starts focused because
|
|
169
|
+
# a host is only created when the screen is being presented; the
|
|
170
|
+
# platform lifecycle hooks (``on_resume`` / ``on_pause``) flip this
|
|
171
|
+
# when the user navigates to / from another screen.
|
|
172
|
+
host._is_focused = True
|
|
173
|
+
host._focus_subscribers = []
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _set_host_focused(host: Any, focused: bool) -> None:
|
|
177
|
+
"""Update ``host._is_focused`` and notify ``use_focus_effect`` subscribers."""
|
|
178
|
+
if getattr(host, "_is_focused", True) == focused:
|
|
179
|
+
return
|
|
180
|
+
host._is_focused = focused
|
|
181
|
+
subscribers = list(getattr(host, "_focus_subscribers", ()) or ())
|
|
182
|
+
for callback in subscribers:
|
|
183
|
+
try:
|
|
184
|
+
callback(focused)
|
|
185
|
+
except Exception:
|
|
186
|
+
pass
|
|
168
187
|
|
|
169
188
|
|
|
170
189
|
def _push_viewport_size(host: Any, width: float, height: float) -> None:
|
|
@@ -232,6 +251,23 @@ def _flush_scheduled_renders(hosts: Sequence[Any]) -> None:
|
|
|
232
251
|
def _on_create(host: Any) -> None:
|
|
233
252
|
from .hooks import NavigationHandle, Provider, _NavigationContext
|
|
234
253
|
|
|
254
|
+
# ``on_create`` is idempotent across native-view recreations. On
|
|
255
|
+
# Android the FragmentManager destroys and recreates a screen's
|
|
256
|
+
# view every time the user pops back to it, and the platform
|
|
257
|
+
# template calls ``screen.on_create()`` again from
|
|
258
|
+
# ``onViewCreated`` — but the Python screen object (and therefore
|
|
259
|
+
# the reconciler, hook state, focus subscribers, etc.) persists
|
|
260
|
+
# across that. Re-running the full mount path here would reset
|
|
261
|
+
# use_state, clobber use_focus_effect subscriptions, and break
|
|
262
|
+
# navigation handles held by existing components, which is why
|
|
263
|
+
# the focus counter never advanced past ``1`` before this guard.
|
|
264
|
+
# If we're already mounted, just re-attach the existing root view
|
|
265
|
+
# to the (newly created) native container — ``on_resume`` will
|
|
266
|
+
# fire the focus subscribers separately.
|
|
267
|
+
if host._reconciler is not None and host._root_native_view is not None:
|
|
268
|
+
host._attach_root(host._root_native_view)
|
|
269
|
+
return
|
|
270
|
+
|
|
235
271
|
host._nav_handle = NavigationHandle(host)
|
|
236
272
|
host._reconciler = _new_reconciler(host)
|
|
237
273
|
|
|
@@ -711,7 +747,7 @@ if IS_ANDROID:
|
|
|
711
747
|
pass
|
|
712
748
|
|
|
713
749
|
def on_resume(self) -> None:
|
|
714
|
-
|
|
750
|
+
_set_host_focused(self, True)
|
|
715
751
|
|
|
716
752
|
def on_layout(self) -> None:
|
|
717
753
|
# Android pushes viewport changes through the
|
|
@@ -721,7 +757,7 @@ if IS_ANDROID:
|
|
|
721
757
|
pass
|
|
722
758
|
|
|
723
759
|
def on_pause(self) -> None:
|
|
724
|
-
|
|
760
|
+
_set_host_focused(self, False)
|
|
725
761
|
|
|
726
762
|
def on_stop(self) -> None:
|
|
727
763
|
pass
|
|
@@ -796,6 +832,18 @@ if IS_ANDROID:
|
|
|
796
832
|
container.removeAllViews()
|
|
797
833
|
except Exception:
|
|
798
834
|
pass
|
|
835
|
+
# When the user pops back to a previously mounted screen,
|
|
836
|
+
# ``native_view`` is the root from the prior mount and may
|
|
837
|
+
# still be parented under the old (destroyed) FrameLayout.
|
|
838
|
+
# ViewGroup.addView() throws if a view already has a
|
|
839
|
+
# parent, so detach it from the old one before re-attaching
|
|
840
|
+
# to the freshly created container.
|
|
841
|
+
try:
|
|
842
|
+
old_parent = native_view.getParent()
|
|
843
|
+
if old_parent is not None:
|
|
844
|
+
old_parent.removeView(native_view)
|
|
845
|
+
except Exception:
|
|
846
|
+
pass
|
|
799
847
|
LayoutParams = jclass("android.view.ViewGroup$LayoutParams")
|
|
800
848
|
lp = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
|
|
801
849
|
container.addView(native_view, lp)
|
|
@@ -1052,7 +1100,7 @@ else:
|
|
|
1052
1100
|
pass
|
|
1053
1101
|
|
|
1054
1102
|
def on_pause(self) -> None:
|
|
1055
|
-
|
|
1103
|
+
_set_host_focused(self, False)
|
|
1056
1104
|
|
|
1057
1105
|
def on_stop(self) -> None:
|
|
1058
1106
|
pass
|
|
@@ -1267,6 +1315,7 @@ else:
|
|
|
1267
1315
|
# ``viewDidAppear`` always follows ``viewDidLayoutSubviews``,
|
|
1268
1316
|
# but trigger one extra sync here for safety in case a
|
|
1269
1317
|
# template overrides the layout call without forwarding.
|
|
1318
|
+
_set_host_focused(self, True)
|
|
1270
1319
|
if self._root_native_view is None:
|
|
1271
1320
|
_log_pn("on_resume: no root_native_view yet, skipping")
|
|
1272
1321
|
return
|