pythonnative 0.25.0__py3-none-any.whl → 0.27.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 +1 -1
- pythonnative/animated.py +697 -62
- pythonnative/cli/pn.py +172 -12
- pythonnative/components.py +61 -7
- pythonnative/diagnostics.py +27 -0
- pythonnative/gestures.py +446 -34
- pythonnative/native_modules/app_state.py +2 -1
- pythonnative/native_modules/battery.py +3 -2
- pythonnative/native_modules/biometrics.py +2 -1
- pythonnative/native_modules/camera.py +16 -7
- pythonnative/native_modules/clipboard.py +3 -2
- pythonnative/native_modules/file_system.py +3 -2
- pythonnative/native_modules/haptics.py +7 -6
- pythonnative/native_modules/linking.py +62 -6
- pythonnative/native_modules/location.py +11 -8
- pythonnative/native_modules/net_info.py +75 -5
- pythonnative/native_modules/notifications.py +134 -15
- pythonnative/native_modules/permissions.py +3 -2
- pythonnative/native_modules/share.py +2 -1
- pythonnative/native_views/android.py +461 -137
- pythonnative/native_views/desktop.py +816 -96
- pythonnative/native_views/ios.py +750 -193
- pythonnative/project/android.py +15 -3
- pythonnative/project/builder.py +172 -70
- pythonnative/project/config.py +28 -6
- pythonnative/project/devices.py +291 -0
- pythonnative/project/doctor.py +5 -4
- pythonnative/project/ios.py +51 -77
- pythonnative/project/permissions.py +19 -1
- pythonnative/project/runtime_assets.py +78 -166
- pythonnative/reconciler.py +28 -0
- pythonnative/style.py +19 -0
- pythonnative/templates/android_template/app/build.gradle +9 -0
- pythonnative/templates/android_template/app/src/main/AndroidManifest.xml +5 -0
- pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/MainActivity.kt +148 -12
- pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/PNFrameLayout.kt +50 -0
- pythonnative/templates/ios_template/ios_template/AppDelegate.swift +41 -17
- pythonnative/templates/ios_template/ios_template/BridgingHeader.h +14 -0
- pythonnative/templates/ios_template/ios_template/Info.plist +5 -2
- pythonnative/templates/ios_template/ios_template/PythonRuntime.swift +397 -0
- pythonnative/templates/ios_template/ios_template/SceneDelegate.swift +31 -20
- pythonnative/templates/ios_template/ios_template/ViewController.swift +77 -198
- pythonnative/templates/ios_template/ios_template.xcodeproj/project.pbxproj +72 -47
- {pythonnative-0.25.0.dist-info → pythonnative-0.27.0.dist-info}/METADATA +3 -3
- {pythonnative-0.25.0.dist-info → pythonnative-0.27.0.dist-info}/RECORD +49 -46
- pythonnative/templates/ios_template/ios_template/Base.lproj/Main.storyboard +0 -24
- {pythonnative-0.25.0.dist-info → pythonnative-0.27.0.dist-info}/WHEEL +0 -0
- {pythonnative-0.25.0.dist-info → pythonnative-0.27.0.dist-info}/entry_points.txt +0 -0
- {pythonnative-0.25.0.dist-info → pythonnative-0.27.0.dist-info}/licenses/LICENSE +0 -0
- {pythonnative-0.25.0.dist-info → pythonnative-0.27.0.dist-info}/top_level.txt +0 -0
pythonnative/cli/pn.py
CHANGED
|
@@ -6,10 +6,14 @@ The console script `pn` (declared in `pyproject.toml`) dispatches to:
|
|
|
6
6
|
- `pn doctor [platform]`: diagnose the local toolchain and config.
|
|
7
7
|
- `pn preview [component]`: render the app in a desktop (Tkinter) window
|
|
8
8
|
with Fast Refresh, the fast inner dev loop, no device required.
|
|
9
|
-
- `pn
|
|
10
|
-
|
|
9
|
+
- `pn devices [platform]`: list connected devices, emulators, and
|
|
10
|
+
simulators.
|
|
11
|
+
- `pn run android|ios [--device D]`: stage + build + install + launch on
|
|
12
|
+
a device, emulator, or simulator, with optional on-device hot reload.
|
|
13
|
+
- `pn logs android|ios`: stream logs from the running app without
|
|
14
|
+
rebuilding.
|
|
11
15
|
- `pn build android|ios`: produce standalone artifacts (signed APK/AAB,
|
|
12
|
-
or an iOS archive/IPA).
|
|
16
|
+
or an iOS archive/IPA, optionally uploaded to App Store Connect).
|
|
13
17
|
- `pn app-id android|ios`: print the resolved application/bundle id
|
|
14
18
|
(handy for scripts and CI).
|
|
15
19
|
- `pn clean`: remove the local `build/` directory.
|
|
@@ -34,6 +38,7 @@ from pathlib import Path
|
|
|
34
38
|
from typing import Any, Dict, List, Optional
|
|
35
39
|
|
|
36
40
|
from ..project import builder as builder_mod
|
|
41
|
+
from ..project import devices as devices_mod
|
|
37
42
|
from ..project import doctor as doctor_mod
|
|
38
43
|
from ..project.android import collect_logcat_filters
|
|
39
44
|
from ..project.config import CONFIG_FILENAME, AppConfig, ConfigError, render_default_toml
|
|
@@ -241,6 +246,41 @@ def _preview_entry(project_dir: Path) -> str:
|
|
|
241
246
|
return "app.main"
|
|
242
247
|
|
|
243
248
|
|
|
249
|
+
# ======================================================================
|
|
250
|
+
# devices
|
|
251
|
+
# ======================================================================
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def devices_command(args: argparse.Namespace) -> None:
|
|
255
|
+
"""List connected devices, emulators, and simulators.
|
|
256
|
+
|
|
257
|
+
Args:
|
|
258
|
+
args: Parsed namespace with optional ``platform``.
|
|
259
|
+
"""
|
|
260
|
+
platform: Optional[str] = getattr(args, "platform", None)
|
|
261
|
+
devices = devices_mod.list_devices(platform)
|
|
262
|
+
if not devices:
|
|
263
|
+
print("No devices found.")
|
|
264
|
+
print("Android: start an emulator or connect a device with USB debugging enabled.")
|
|
265
|
+
print("iOS: open Xcode once to install Simulators, or plug in a device.")
|
|
266
|
+
sys.exit(1)
|
|
267
|
+
print(f" {'IDENTIFIER':<40} {'KIND':<10} {'STATE':<10} NAME")
|
|
268
|
+
for device in devices:
|
|
269
|
+
print(device.format())
|
|
270
|
+
print("\nTarget one with: pn run <platform> --device <identifier or name>")
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def _resolve_device(platform: str, query: Optional[str]) -> Optional[devices_mod.Device]:
|
|
274
|
+
"""Resolve ``--device`` to a concrete target, exiting on a bad query."""
|
|
275
|
+
if not query:
|
|
276
|
+
return None
|
|
277
|
+
device = devices_mod.find_device(devices_mod.list_devices(platform), query)
|
|
278
|
+
if device is None:
|
|
279
|
+
print(f"Error: no {platform} device matches {query!r}. Run 'pn devices {platform}' to list targets.")
|
|
280
|
+
sys.exit(1)
|
|
281
|
+
return device
|
|
282
|
+
|
|
283
|
+
|
|
244
284
|
# ======================================================================
|
|
245
285
|
# run
|
|
246
286
|
# ======================================================================
|
|
@@ -250,13 +290,14 @@ def run_project(args: argparse.Namespace) -> None:
|
|
|
250
290
|
"""Stage, build, install, and launch the app on a device/simulator.
|
|
251
291
|
|
|
252
292
|
Args:
|
|
253
|
-
args: Parsed namespace (``platform``, ``
|
|
254
|
-
``hot_reload``, ``no_logs``).
|
|
293
|
+
args: Parsed namespace (``platform``, ``device``,
|
|
294
|
+
``prepare_only``, ``hot_reload``, ``no_logs``).
|
|
255
295
|
"""
|
|
256
296
|
platform: str = args.platform
|
|
257
297
|
prepare_only: bool = getattr(args, "prepare_only", False)
|
|
258
298
|
hot_reload: bool = getattr(args, "hot_reload", False)
|
|
259
299
|
show_logs: bool = not getattr(args, "no_logs", False)
|
|
300
|
+
device = _resolve_device(platform, getattr(args, "device", None))
|
|
260
301
|
|
|
261
302
|
config = _load_config_or_exit()
|
|
262
303
|
builder = builder_mod.Builder(config, log=print)
|
|
@@ -273,9 +314,14 @@ def run_project(args: argparse.Namespace) -> None:
|
|
|
273
314
|
|
|
274
315
|
try:
|
|
275
316
|
if platform == "android":
|
|
276
|
-
_run_android(builder, prepared, hot_reload=hot_reload, show_logs=show_logs)
|
|
317
|
+
_run_android(builder, prepared, device=device, hot_reload=hot_reload, show_logs=show_logs)
|
|
318
|
+
elif device is not None and device.kind == "device":
|
|
319
|
+
_run_ios_device(builder, prepared, device=device)
|
|
320
|
+
if hot_reload:
|
|
321
|
+
print("Note: hot reload targets the iOS Simulator; skipping it for a physical device.")
|
|
322
|
+
return
|
|
277
323
|
else:
|
|
278
|
-
_run_ios(builder, prepared, hot_reload=hot_reload, show_logs=show_logs)
|
|
324
|
+
_run_ios(builder, prepared, device=device, hot_reload=hot_reload, show_logs=show_logs)
|
|
279
325
|
except builder_mod.BuildError as exc:
|
|
280
326
|
print(f"Error: {exc}")
|
|
281
327
|
sys.exit(1)
|
|
@@ -295,9 +341,14 @@ def _run_android(
|
|
|
295
341
|
builder: builder_mod.Builder,
|
|
296
342
|
prepared: builder_mod.PreparedProject,
|
|
297
343
|
*,
|
|
344
|
+
device: Optional[devices_mod.Device],
|
|
298
345
|
hot_reload: bool,
|
|
299
346
|
show_logs: bool,
|
|
300
347
|
) -> None:
|
|
348
|
+
if device is not None:
|
|
349
|
+
# Both Gradle's install task and every adb call below honor
|
|
350
|
+
# ANDROID_SERIAL, so exporting it targets the whole run.
|
|
351
|
+
os.environ["ANDROID_SERIAL"] = device.identifier
|
|
301
352
|
builder.install_android_debug(prepared)
|
|
302
353
|
_clear_android_hot_reload_overlay(prepared.app_id)
|
|
303
354
|
subprocess.run(
|
|
@@ -319,11 +370,12 @@ def _run_ios(
|
|
|
319
370
|
builder: builder_mod.Builder,
|
|
320
371
|
prepared: builder_mod.PreparedProject,
|
|
321
372
|
*,
|
|
373
|
+
device: Optional[devices_mod.Device],
|
|
322
374
|
hot_reload: bool,
|
|
323
375
|
show_logs: bool,
|
|
324
376
|
) -> None:
|
|
325
377
|
app_path = builder.build_ios_simulator(prepared)
|
|
326
|
-
udid = _select_ios_simulator()
|
|
378
|
+
udid = device.identifier if device is not None else _select_ios_simulator()
|
|
327
379
|
if udid is None:
|
|
328
380
|
print("No available iOS Simulators found; open the project in Xcode to run.")
|
|
329
381
|
return
|
|
@@ -352,6 +404,45 @@ def _run_ios(
|
|
|
352
404
|
print("Launched iOS app on Simulator.")
|
|
353
405
|
|
|
354
406
|
|
|
407
|
+
def _run_ios_device(
|
|
408
|
+
builder: builder_mod.Builder,
|
|
409
|
+
prepared: builder_mod.PreparedProject,
|
|
410
|
+
*,
|
|
411
|
+
device: devices_mod.Device,
|
|
412
|
+
) -> None:
|
|
413
|
+
"""Build, install, and launch on a physical iOS device via devicectl."""
|
|
414
|
+
app_path = builder.build_ios_device(prepared)
|
|
415
|
+
print(f"Installing on {device.name}...")
|
|
416
|
+
install = subprocess.run(
|
|
417
|
+
["xcrun", "devicectl", "device", "install", "app", "--device", device.identifier, str(app_path)],
|
|
418
|
+
check=False,
|
|
419
|
+
)
|
|
420
|
+
if install.returncode != 0:
|
|
421
|
+
print(
|
|
422
|
+
"Error: install failed. Make sure the device is unlocked, paired with this Mac, "
|
|
423
|
+
"and has Developer Mode enabled (Settings > Privacy & Security > Developer Mode)."
|
|
424
|
+
)
|
|
425
|
+
sys.exit(1)
|
|
426
|
+
launch = subprocess.run(
|
|
427
|
+
[
|
|
428
|
+
"xcrun",
|
|
429
|
+
"devicectl",
|
|
430
|
+
"device",
|
|
431
|
+
"process",
|
|
432
|
+
"launch",
|
|
433
|
+
"--terminate-existing",
|
|
434
|
+
"--device",
|
|
435
|
+
device.identifier,
|
|
436
|
+
prepared.app_id,
|
|
437
|
+
],
|
|
438
|
+
check=False,
|
|
439
|
+
)
|
|
440
|
+
if launch.returncode != 0:
|
|
441
|
+
print("Error: launch failed. Launch the app from the home screen to see details.")
|
|
442
|
+
sys.exit(1)
|
|
443
|
+
print(f"Launched on {device.name}. Use Console.app (or Xcode > Devices) for device logs.")
|
|
444
|
+
|
|
445
|
+
|
|
355
446
|
# ======================================================================
|
|
356
447
|
# build
|
|
357
448
|
# ======================================================================
|
|
@@ -361,16 +452,24 @@ def build_project(args: argparse.Namespace) -> None:
|
|
|
361
452
|
"""Build standalone, distributable artifacts for ``platform``.
|
|
362
453
|
|
|
363
454
|
Args:
|
|
364
|
-
args: Parsed namespace (``platform``, ``debug``).
|
|
455
|
+
args: Parsed namespace (``platform``, ``debug``, ``upload``).
|
|
365
456
|
"""
|
|
366
457
|
platform: str = args.platform
|
|
367
458
|
debug: bool = getattr(args, "debug", False)
|
|
459
|
+
upload: bool = getattr(args, "upload", False)
|
|
368
460
|
|
|
369
461
|
config = _load_config_or_exit()
|
|
370
462
|
builder = builder_mod.Builder(config, log=print)
|
|
371
463
|
|
|
464
|
+
if upload and (platform != "ios" or debug):
|
|
465
|
+
print("Error: --upload applies to 'pn build ios' release builds only.")
|
|
466
|
+
sys.exit(1)
|
|
467
|
+
if upload and config.ios.signing.export_method != "app-store":
|
|
468
|
+
print('Error: --upload requires [ios.signing] export_method = "app-store" in pythonnative.toml.')
|
|
469
|
+
sys.exit(1)
|
|
470
|
+
|
|
372
471
|
try:
|
|
373
|
-
prepared = builder.prepare(platform)
|
|
472
|
+
prepared = builder.prepare(platform, release=not debug)
|
|
374
473
|
if platform == "android":
|
|
375
474
|
artifacts = builder.build_android(prepared, debug=debug)
|
|
376
475
|
else:
|
|
@@ -378,19 +477,61 @@ def build_project(args: argparse.Namespace) -> None:
|
|
|
378
477
|
app_path = builder.build_ios_simulator(prepared)
|
|
379
478
|
artifacts = builder_mod.BuildArtifacts(paths=[app_path])
|
|
380
479
|
else:
|
|
381
|
-
artifacts = builder.build_ios_archive(prepared)
|
|
480
|
+
artifacts = builder.build_ios_archive(prepared, upload=upload)
|
|
382
481
|
except builder_mod.BuildError as exc:
|
|
383
482
|
print(f"Error: {exc}")
|
|
384
483
|
sys.exit(1)
|
|
385
484
|
|
|
485
|
+
if upload:
|
|
486
|
+
print("\nUploaded to App Store Connect. Check the build's status at appstoreconnect.apple.com.")
|
|
386
487
|
if not artifacts.paths:
|
|
387
|
-
|
|
488
|
+
if not upload:
|
|
489
|
+
print("Build completed, but no artifacts were found. Check the build output above.")
|
|
388
490
|
return
|
|
389
491
|
print("\nBuilt artifacts:")
|
|
390
492
|
for path in artifacts.paths:
|
|
391
493
|
print(f" {path}")
|
|
392
494
|
|
|
393
495
|
|
|
496
|
+
# ======================================================================
|
|
497
|
+
# logs
|
|
498
|
+
# ======================================================================
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
def logs_command(args: argparse.Namespace) -> None:
|
|
502
|
+
"""Stream logs from the running app without rebuilding.
|
|
503
|
+
|
|
504
|
+
Args:
|
|
505
|
+
args: Parsed namespace (``platform``).
|
|
506
|
+
"""
|
|
507
|
+
platform: str = args.platform
|
|
508
|
+
if platform == "android":
|
|
509
|
+
proc = _start_android_log_stream()
|
|
510
|
+
if proc is None:
|
|
511
|
+
sys.exit(1)
|
|
512
|
+
try:
|
|
513
|
+
proc.wait()
|
|
514
|
+
except KeyboardInterrupt:
|
|
515
|
+
print()
|
|
516
|
+
_terminate_subprocess(proc)
|
|
517
|
+
print("Stopped log streaming.")
|
|
518
|
+
return
|
|
519
|
+
|
|
520
|
+
# iOS: relaunch the app on the booted simulator with a console PTY so
|
|
521
|
+
# Python's stdout/stderr stream to this terminal.
|
|
522
|
+
config = _load_config_or_exit()
|
|
523
|
+
proc = _start_ios_log_stream(config.bundle_id)
|
|
524
|
+
if proc is None:
|
|
525
|
+
print("For a physical device, use Console.app or Xcode > Devices and Simulators.")
|
|
526
|
+
sys.exit(1)
|
|
527
|
+
try:
|
|
528
|
+
proc.wait()
|
|
529
|
+
except KeyboardInterrupt:
|
|
530
|
+
print()
|
|
531
|
+
_terminate_subprocess(proc)
|
|
532
|
+
print("Stopped log streaming.")
|
|
533
|
+
|
|
534
|
+
|
|
394
535
|
# ======================================================================
|
|
395
536
|
# clean
|
|
396
537
|
# ======================================================================
|
|
@@ -747,16 +888,35 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
747
888
|
parser_preview.add_argument("--no-hot-reload", action="store_true", help="Disable file watching / Fast Refresh")
|
|
748
889
|
parser_preview.set_defaults(func=preview_project)
|
|
749
890
|
|
|
891
|
+
parser_devices = subparsers.add_parser("devices", help="List devices, emulators, and simulators")
|
|
892
|
+
parser_devices.add_argument("platform", nargs="?", choices=["android", "ios"], help="Restrict to a platform")
|
|
893
|
+
parser_devices.set_defaults(func=devices_command)
|
|
894
|
+
|
|
750
895
|
parser_run = subparsers.add_parser("run", help="Build, install, and launch on a device/simulator")
|
|
751
896
|
parser_run.add_argument("platform", choices=["android", "ios"])
|
|
897
|
+
parser_run.add_argument(
|
|
898
|
+
"--device",
|
|
899
|
+
"-d",
|
|
900
|
+
help="Target device: an identifier or name from 'pn devices' "
|
|
901
|
+
"(physical iOS devices need [ios].development_team)",
|
|
902
|
+
)
|
|
752
903
|
parser_run.add_argument("--prepare-only", action="store_true", help="Stage + configure without building")
|
|
753
904
|
parser_run.add_argument("--hot-reload", action="store_true", help="Watch app/ and push updates to the running app")
|
|
754
905
|
parser_run.add_argument("--no-logs", action="store_true", help="Don't stream device logs after launch")
|
|
755
906
|
parser_run.set_defaults(func=run_project)
|
|
756
907
|
|
|
908
|
+
parser_logs = subparsers.add_parser("logs", help="Stream logs from the running app")
|
|
909
|
+
parser_logs.add_argument("platform", choices=["android", "ios"])
|
|
910
|
+
parser_logs.set_defaults(func=logs_command)
|
|
911
|
+
|
|
757
912
|
parser_build = subparsers.add_parser("build", help="Build distributable artifacts")
|
|
758
913
|
parser_build.add_argument("platform", choices=["android", "ios"])
|
|
759
914
|
parser_build.add_argument("--debug", action="store_true", help="Build the debug variant instead of release")
|
|
915
|
+
parser_build.add_argument(
|
|
916
|
+
"--upload",
|
|
917
|
+
action="store_true",
|
|
918
|
+
help='Upload the iOS release build to App Store Connect (needs export_method = "app-store")',
|
|
919
|
+
)
|
|
760
920
|
parser_build.set_defaults(func=build_project)
|
|
761
921
|
|
|
762
922
|
parser_app_id = subparsers.add_parser("app-id", help="Print the resolved application/bundle id")
|
pythonnative/components.py
CHANGED
|
@@ -755,6 +755,8 @@ def View(
|
|
|
755
755
|
*children: Element,
|
|
756
756
|
style: StyleProp = None,
|
|
757
757
|
gestures: Optional[List[Any]] = None,
|
|
758
|
+
hit_slop: Optional[Union[float, Dict[str, float]]] = None,
|
|
759
|
+
on_layout: Optional[Callable[[Dict[str, float]], None]] = None,
|
|
758
760
|
accessibility_label: Optional[str] = None,
|
|
759
761
|
accessibility_hint: Optional[str] = None,
|
|
760
762
|
accessibility_role: Optional[str] = None,
|
|
@@ -797,6 +799,13 @@ def View(
|
|
|
797
799
|
gestures: Optional list of gesture descriptors from
|
|
798
800
|
`pythonnative.gestures` (e.g. ``[gestures.Pan(on_change=…)]``)
|
|
799
801
|
recognized natively on this view.
|
|
802
|
+
hit_slop: Extend the touch target beyond the view's bounds
|
|
803
|
+
without changing layout: a uniform number of points, or a
|
|
804
|
+
dict with any of ``top`` / ``left`` / ``bottom`` /
|
|
805
|
+
``right``.
|
|
806
|
+
on_layout: Callback invoked with
|
|
807
|
+
``{"x", "y", "width", "height"}`` after this view is laid
|
|
808
|
+
out, and again whenever its frame changes.
|
|
800
809
|
accessibility_label: Spoken description for screen readers.
|
|
801
810
|
accessibility_hint: Spoken extra detail (iOS only).
|
|
802
811
|
accessibility_role: Semantic role for assistive tech.
|
|
@@ -824,6 +833,8 @@ def View(
|
|
|
824
833
|
ref=ref,
|
|
825
834
|
key=key,
|
|
826
835
|
gestures=gestures,
|
|
836
|
+
hit_slop=hit_slop,
|
|
837
|
+
on_layout=on_layout,
|
|
827
838
|
accessibility_label=accessibility_label,
|
|
828
839
|
accessibility_hint=accessibility_hint,
|
|
829
840
|
accessibility_role=accessibility_role,
|
|
@@ -838,6 +849,7 @@ def View(
|
|
|
838
849
|
def Column(
|
|
839
850
|
*children: Element,
|
|
840
851
|
style: StyleProp = None,
|
|
852
|
+
on_layout: Optional[Callable[[Dict[str, float]], None]] = None,
|
|
841
853
|
ref: Optional[Ref] = None,
|
|
842
854
|
key: Optional[str] = None,
|
|
843
855
|
) -> Element:
|
|
@@ -850,6 +862,9 @@ def Column(
|
|
|
850
862
|
Args:
|
|
851
863
|
*children: Child elements stacked top to bottom.
|
|
852
864
|
style: Style dict (or list of dicts).
|
|
865
|
+
on_layout: Callback invoked with
|
|
866
|
+
``{"x", "y", "width", "height"}`` after layout and on
|
|
867
|
+
frame changes.
|
|
853
868
|
ref: Optional [`Ref`][pythonnative.Ref] for native-view access.
|
|
854
869
|
key: Stable identity for keyed reconciliation.
|
|
855
870
|
|
|
@@ -860,6 +875,7 @@ def Column(
|
|
|
860
875
|
"Column",
|
|
861
876
|
*children,
|
|
862
877
|
style=style,
|
|
878
|
+
on_layout=on_layout,
|
|
863
879
|
ref=ref,
|
|
864
880
|
key=key,
|
|
865
881
|
_forced={"flex_direction": "column"},
|
|
@@ -869,6 +885,7 @@ def Column(
|
|
|
869
885
|
def Row(
|
|
870
886
|
*children: Element,
|
|
871
887
|
style: StyleProp = None,
|
|
888
|
+
on_layout: Optional[Callable[[Dict[str, float]], None]] = None,
|
|
872
889
|
ref: Optional[Ref] = None,
|
|
873
890
|
key: Optional[str] = None,
|
|
874
891
|
) -> Element:
|
|
@@ -881,6 +898,9 @@ def Row(
|
|
|
881
898
|
Args:
|
|
882
899
|
*children: Child elements arranged left to right.
|
|
883
900
|
style: Style dict (or list of dicts).
|
|
901
|
+
on_layout: Callback invoked with
|
|
902
|
+
``{"x", "y", "width", "height"}`` after layout and on
|
|
903
|
+
frame changes.
|
|
884
904
|
ref: Optional [`Ref`][pythonnative.Ref] for native-view access.
|
|
885
905
|
key: Stable identity for keyed reconciliation.
|
|
886
906
|
|
|
@@ -891,6 +911,7 @@ def Row(
|
|
|
891
911
|
"Row",
|
|
892
912
|
*children,
|
|
893
913
|
style=style,
|
|
914
|
+
on_layout=on_layout,
|
|
894
915
|
ref=ref,
|
|
895
916
|
key=key,
|
|
896
917
|
_forced={"flex_direction": "row"},
|
|
@@ -1144,6 +1165,8 @@ def Pressable(
|
|
|
1144
1165
|
on_press_out: Optional[Callable[[], None]] = None,
|
|
1145
1166
|
pressed_opacity: float = 0.6,
|
|
1146
1167
|
gestures: Optional[List[Any]] = None,
|
|
1168
|
+
hit_slop: Optional[Union[float, Dict[str, float]]] = None,
|
|
1169
|
+
on_layout: Optional[Callable[[Dict[str, float]], None]] = None,
|
|
1147
1170
|
style: Union[StyleProp, Callable[[Dict[str, bool]], StyleProp]] = None,
|
|
1148
1171
|
accessibility_label: Optional[str] = None,
|
|
1149
1172
|
accessibility_hint: Optional[str] = None,
|
|
@@ -1174,6 +1197,14 @@ def Pressable(
|
|
|
1174
1197
|
gestures: Optional list of gesture descriptors from
|
|
1175
1198
|
`pythonnative.gestures` recognized natively on this view
|
|
1176
1199
|
(pan / swipe / pinch / rotation / multi-tap).
|
|
1200
|
+
hit_slop: Extend the touch target beyond the view's bounds
|
|
1201
|
+
without changing layout: a uniform number of points, or a
|
|
1202
|
+
dict with any of ``top`` / ``left`` / ``bottom`` /
|
|
1203
|
+
``right``. Essential for small touch targets (icons,
|
|
1204
|
+
chips) that should honor the 44-point guideline.
|
|
1205
|
+
on_layout: Callback invoked with
|
|
1206
|
+
``{"x", "y", "width", "height"}`` after layout and on
|
|
1207
|
+
frame changes.
|
|
1177
1208
|
style: Style dict applied to the wrapper, or a callable
|
|
1178
1209
|
receiving the interaction state (``{"pressed": bool}``)
|
|
1179
1210
|
and returning a style, re-evaluated on every press
|
|
@@ -1220,6 +1251,8 @@ def Pressable(
|
|
|
1220
1251
|
on_press_out=on_press_out,
|
|
1221
1252
|
pressed_opacity=pressed_opacity,
|
|
1222
1253
|
gestures=gestures,
|
|
1254
|
+
hit_slop=hit_slop,
|
|
1255
|
+
on_layout=on_layout,
|
|
1223
1256
|
accessibility_label=accessibility_label,
|
|
1224
1257
|
accessibility_hint=accessibility_hint,
|
|
1225
1258
|
accessibility_role=accessibility_role,
|
|
@@ -1240,6 +1273,8 @@ def Pressable(
|
|
|
1240
1273
|
on_press_out=on_press_out,
|
|
1241
1274
|
pressed_opacity=pressed_opacity,
|
|
1242
1275
|
gestures=gestures,
|
|
1276
|
+
hit_slop=hit_slop,
|
|
1277
|
+
on_layout=on_layout,
|
|
1243
1278
|
accessibility_label=accessibility_label,
|
|
1244
1279
|
accessibility_hint=accessibility_hint,
|
|
1245
1280
|
accessibility_role=accessibility_role,
|
|
@@ -2620,13 +2655,30 @@ def _KeyboardAvoidingContainer(**p: Any) -> Element:
|
|
|
2620
2655
|
inside a full-height container).
|
|
2621
2656
|
- ``"position"``: translates the whole container upward without
|
|
2622
2657
|
resizing it (useful for pinned footers/toolbars).
|
|
2658
|
+
- ``"height"``: shrinks the container's own height by the shift.
|
|
2659
|
+
The resting height is captured via ``on_layout`` while the
|
|
2660
|
+
keyboard is hidden, so flex-sized containers work too.
|
|
2623
2661
|
"""
|
|
2624
|
-
|
|
2662
|
+
keyboard = use_keyboard_height()
|
|
2663
|
+
base_height, set_base_height = use_state(0.0)
|
|
2625
2664
|
behavior = p.get("behavior") or "padding"
|
|
2626
2665
|
offset = float(p.get("keyboard_vertical_offset") or 0.0)
|
|
2627
|
-
shift = max(0.0,
|
|
2666
|
+
shift = max(0.0, keyboard - offset) if keyboard > 0 else 0.0
|
|
2628
2667
|
style: Dict[str, Any] = dict(p.get("style") or {})
|
|
2629
|
-
|
|
2668
|
+
props: Dict[str, Any] = style
|
|
2669
|
+
if behavior == "height":
|
|
2670
|
+
|
|
2671
|
+
def _record_layout(frame: Dict[str, float]) -> None:
|
|
2672
|
+
if keyboard <= 0:
|
|
2673
|
+
measured = float(frame.get("height", 0.0))
|
|
2674
|
+
if measured > 0 and abs(measured - base_height) > 0.5:
|
|
2675
|
+
set_base_height(measured)
|
|
2676
|
+
|
|
2677
|
+
props = dict(style)
|
|
2678
|
+
props["on_layout"] = _record_layout
|
|
2679
|
+
if shift > 0 and base_height > 0:
|
|
2680
|
+
props["height"] = max(0.0, base_height - shift)
|
|
2681
|
+
elif shift > 0:
|
|
2630
2682
|
if behavior == "position":
|
|
2631
2683
|
transform = list(style.get("transform") or [])
|
|
2632
2684
|
transform.append({"translate_y": -shift})
|
|
@@ -2634,12 +2686,12 @@ def _KeyboardAvoidingContainer(**p: Any) -> Element:
|
|
|
2634
2686
|
else:
|
|
2635
2687
|
style["padding_bottom"] = _numeric_edge_padding(style, "bottom") + shift
|
|
2636
2688
|
children = p.get("children") or []
|
|
2637
|
-
return Element("KeyboardAvoidingView",
|
|
2689
|
+
return Element("KeyboardAvoidingView", props, list(children))
|
|
2638
2690
|
|
|
2639
2691
|
|
|
2640
2692
|
def KeyboardAvoidingView(
|
|
2641
2693
|
*children: Element,
|
|
2642
|
-
behavior: Literal["padding", "position"] = "padding",
|
|
2694
|
+
behavior: Literal["padding", "position", "height"] = "padding",
|
|
2643
2695
|
keyboard_vertical_offset: float = 0.0,
|
|
2644
2696
|
style: StyleProp = None,
|
|
2645
2697
|
key: Optional[str] = None,
|
|
@@ -2656,8 +2708,10 @@ def KeyboardAvoidingView(
|
|
|
2656
2708
|
Args:
|
|
2657
2709
|
*children: Children rendered inside the avoiding container.
|
|
2658
2710
|
behavior: ``"padding"`` (adds bottom padding, resizing the
|
|
2659
|
-
content)
|
|
2660
|
-
|
|
2711
|
+
content), ``"position"`` (translates the container upward
|
|
2712
|
+
without resizing), or ``"height"`` (shrinks the
|
|
2713
|
+
container's height by the keyboard overlap, matching
|
|
2714
|
+
React Native's ``"height"`` behavior).
|
|
2661
2715
|
keyboard_vertical_offset: Distance in layout units already
|
|
2662
2716
|
covered by other UI (e.g. a nav bar); subtracted from the
|
|
2663
2717
|
keyboard height before applying the shift.
|
pythonnative/diagnostics.py
CHANGED
|
@@ -37,6 +37,7 @@ __all__ = [
|
|
|
37
37
|
"is_dev",
|
|
38
38
|
"warn",
|
|
39
39
|
"warn_once",
|
|
40
|
+
"swallowed",
|
|
40
41
|
"get_warnings",
|
|
41
42
|
"clear_warnings",
|
|
42
43
|
"set_error_reporter",
|
|
@@ -143,6 +144,32 @@ def warn_once(message: str, key: Optional[str] = None) -> None:
|
|
|
143
144
|
warn(message)
|
|
144
145
|
|
|
145
146
|
|
|
147
|
+
def swallowed(context: str) -> None:
|
|
148
|
+
"""Surface a suppressed native-backend exception (dev mode only).
|
|
149
|
+
|
|
150
|
+
The native view/module backends intentionally degrade gracefully in
|
|
151
|
+
production: a failed style application or a missing OS API must not
|
|
152
|
+
crash the app. Call this from the ``except`` block instead of
|
|
153
|
+
``pass`` so that, in dev mode, each suppression is reported once per
|
|
154
|
+
call site instead of disappearing.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
context: Where the suppression happened, e.g.
|
|
158
|
+
``"ios.TextHandler._apply"``.
|
|
159
|
+
"""
|
|
160
|
+
if not is_dev():
|
|
161
|
+
return
|
|
162
|
+
exc = sys.exc_info()[1]
|
|
163
|
+
if exc is None:
|
|
164
|
+
return
|
|
165
|
+
frame = sys._getframe(1)
|
|
166
|
+
location = f"{os.path.basename(frame.f_code.co_filename)}:{frame.f_lineno}"
|
|
167
|
+
warn_once(
|
|
168
|
+
f"{context} suppressed {type(exc).__name__}: {exc} ({location})",
|
|
169
|
+
key=f"swallowed:{context}:{location}",
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
|
|
146
173
|
def get_warnings() -> List[str]:
|
|
147
174
|
"""Return a snapshot of the recorded warnings (oldest first)."""
|
|
148
175
|
with _warn_lock:
|