pythonnative 0.22.1__py3-none-any.whl → 0.24.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. pythonnative/__init__.py +43 -50
  2. pythonnative/animated.py +1 -1
  3. pythonnative/appearance.py +124 -0
  4. pythonnative/cli/pn.py +3 -3
  5. pythonnative/components.py +873 -466
  6. pythonnative/diagnostics.py +214 -0
  7. pythonnative/element.py +5 -2
  8. pythonnative/events.py +13 -8
  9. pythonnative/hooks.py +454 -49
  10. pythonnative/hot_reload.py +9 -1
  11. pythonnative/images.py +196 -0
  12. pythonnative/mutations.py +3 -12
  13. pythonnative/native_views/__init__.py +1 -7
  14. pythonnative/native_views/android.py +651 -46
  15. pythonnative/native_views/desktop.py +116 -20
  16. pythonnative/native_views/ios.py +974 -52
  17. pythonnative/navigation.py +41 -17
  18. pythonnative/preview.py +74 -7
  19. pythonnative/project/config.py +1 -9
  20. pythonnative/reconciler.py +863 -441
  21. pythonnative/screen.py +409 -27
  22. pythonnative/storage.py +3 -3
  23. pythonnative/style.py +148 -6
  24. pythonnative/templates/android_template/app/src/main/AndroidManifest.xml +2 -1
  25. pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/PNAccessibilityDelegate.kt +47 -0
  26. pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/PNBorderDrawable.kt +67 -0
  27. pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/PNVirtualListView.java +172 -0
  28. pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/ScreenFragment.kt +22 -0
  29. pythonnative/virtual_rows.py +137 -0
  30. {pythonnative-0.22.1.dist-info → pythonnative-0.24.0.dist-info}/METADATA +5 -4
  31. {pythonnative-0.22.1.dist-info → pythonnative-0.24.0.dist-info}/RECORD +35 -28
  32. {pythonnative-0.22.1.dist-info → pythonnative-0.24.0.dist-info}/WHEEL +1 -1
  33. {pythonnative-0.22.1.dist-info → pythonnative-0.24.0.dist-info}/entry_points.txt +0 -0
  34. {pythonnative-0.22.1.dist-info → pythonnative-0.24.0.dist-info}/licenses/LICENSE +0 -0
  35. {pythonnative-0.22.1.dist-info → pythonnative-0.24.0.dist-info}/top_level.txt +0 -0
@@ -376,10 +376,27 @@ def _apply_common(widget: Any, props: Dict[str, Any]) -> None:
376
376
  widget.configure(background=color)
377
377
  except Exception:
378
378
  pass
379
- if any(k in props for k in ("border_width", "border_color")):
379
+ side_width_keys = (
380
+ "border_left_width",
381
+ "border_top_width",
382
+ "border_right_width",
383
+ "border_bottom_width",
384
+ )
385
+ if any(k in props for k in ("border_width", "border_color", *side_width_keys)):
380
386
  try:
381
387
  width = props.get("border_width")
382
388
  color = _tk_color(props.get("border_color")) or "#3c3c43"
389
+ # Tk highlight borders are uniform, so per-side borders are
390
+ # approximated with the widest side's width and its color.
391
+ side_widths = [props.get(k) for k in side_width_keys]
392
+ if any(w is not None for w in side_widths):
393
+ width = max(float(w) for w in side_widths if w is not None)
394
+ for side, w in zip(("left", "top", "right", "bottom"), side_widths):
395
+ if w is not None and float(w) > 0:
396
+ side_color = _tk_color(props.get(f"border_{side}_color"))
397
+ if side_color is not None:
398
+ color = side_color
399
+ break
383
400
  if width:
384
401
  widget.configure(
385
402
  highlightthickness=int(round(_finite(width))),
@@ -390,6 +407,10 @@ def _apply_common(widget: Any, props: Dict[str, Any]) -> None:
390
407
  widget.configure(highlightthickness=0)
391
408
  except Exception:
392
409
  pass
410
+ if "test_id" in props and props["test_id"] is not None:
411
+ # Stamped for introspection so preview-level tooling and tests
412
+ # can locate widgets the same way Maestro does on device.
413
+ widget._pn_test_id = str(props["test_id"])
393
414
  if "transform" in props:
394
415
  _set_translate_from_transform(widget, props["transform"])
395
416
  _place(widget)
@@ -782,7 +803,7 @@ class TextHandler(DesktopViewHandler):
782
803
  class ButtonHandler(DesktopViewHandler):
783
804
  def build(self, props: Dict[str, Any]) -> Any:
784
805
  button = tk.Button(_master(), highlightthickness=0, takefocus=0)
785
- button.configure(command=lambda: _fire(button, "on_click"))
806
+ button.configure(command=lambda: _fire(button, "on_press"))
786
807
  return button
787
808
 
788
809
  def apply(self, button: Any, props: Dict[str, Any]) -> None:
@@ -941,10 +962,13 @@ class TextInputHandler(DesktopViewHandler):
941
962
  class ImageHandler(DesktopViewHandler):
942
963
  """Best-effort image preview.
943
964
 
944
- Tk's ``PhotoImage`` loads PNG/GIF/PPM from local paths; network URLs
945
- and JPEG aren't supported without Pillow, so those fall back to a
946
- labeled placeholder. The handler keeps a reference to the
947
- ``PhotoImage`` (Tk garbage-collects images that aren't referenced).
965
+ Tk's ``PhotoImage`` loads PNG/GIF/PPM from local paths; JPEG isn't
966
+ supported without Pillow, so undecodable formats fall back to a
967
+ labeled placeholder. Network URLs are fetched through the shared
968
+ image pipeline (`pythonnative.images`), so caching and ``on_load``
969
+ / ``on_error`` behave like the mobile backends. The handler keeps
970
+ a reference to the ``PhotoImage`` (Tk garbage-collects images that
971
+ aren't referenced).
948
972
  """
949
973
 
950
974
  def build(self, props: Dict[str, Any]) -> Any:
@@ -952,24 +976,59 @@ class ImageHandler(DesktopViewHandler):
952
976
 
953
977
  def apply(self, label: Any, props: Dict[str, Any]) -> None:
954
978
  merged = getattr(label, "_pn_props", props)
955
- if "source" in props:
956
- source = props.get("source")
957
- photo = None
958
- if source and "://" not in str(source):
979
+ if "placeholder_color" in props:
980
+ color = _tk_color(props.get("placeholder_color"))
981
+ if color is not None:
959
982
  try:
960
- photo = tk.PhotoImage(file=str(source))
983
+ label.configure(background=color)
961
984
  except Exception:
962
- photo = None
963
- label._pn_photo = photo # keep a reference alive
985
+ pass
986
+ if "source" in props:
987
+ source = props.get("source")
988
+ label._pn_pending_source = source
989
+ if source and "://" in str(source):
990
+ from ..images import fetch
991
+
992
+ def _on_ready(path: str, src: Any = source) -> None:
993
+ if getattr(label, "_pn_pending_source", None) == src:
994
+ self._show_file(label, path, src)
995
+
996
+ def _on_error(message: str, src: Any = source) -> None:
997
+ if getattr(label, "_pn_pending_source", None) == src:
998
+ self._show_fallback(label, src)
999
+ _fire(label, "on_error", message)
1000
+
1001
+ self._show_fallback(label, source)
1002
+ fetch(str(source), _on_ready, _on_error)
1003
+ else:
1004
+ self._show_file(label, str(source) if source else None, source)
1005
+ _apply_common(label, merged)
1006
+
1007
+ def _show_file(self, label: Any, path: Optional[str], source: Any) -> None:
1008
+ photo = None
1009
+ if path:
964
1010
  try:
965
- if photo is not None:
966
- label.configure(image=photo, text="")
967
- else:
968
- name = str(source).rsplit("/", 1)[-1] if source else "image"
969
- label.configure(image="", text=f"\U0001f5bc\n{name}", compound="center")
1011
+ photo = tk.PhotoImage(file=path)
970
1012
  except Exception:
971
- pass
972
- _apply_common(label, merged)
1013
+ photo = None
1014
+ label._pn_photo = photo # keep a reference alive
1015
+ try:
1016
+ if photo is not None:
1017
+ label.configure(image=photo, text="")
1018
+ _fire(label, "on_load")
1019
+ else:
1020
+ self._show_fallback(label, source)
1021
+ if path:
1022
+ _fire(label, "on_error", "decode failed")
1023
+ except Exception:
1024
+ pass
1025
+
1026
+ def _show_fallback(self, label: Any, source: Any) -> None:
1027
+ name = str(source).rsplit("/", 1)[-1] if source else "image"
1028
+ try:
1029
+ label.configure(image="", text=f"\U0001f5bc\n{name}", compound="center")
1030
+ except Exception:
1031
+ pass
973
1032
 
974
1033
  def measure_intrinsic(self, native_view: Any, max_width: float, max_height: float) -> Tuple[float, float]:
975
1034
  photo = getattr(native_view, "_pn_photo", None)
@@ -1269,6 +1328,42 @@ class ModalHandler(DesktopViewHandler):
1269
1328
  return
1270
1329
 
1271
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
+
1272
1367
  # ======================================================================
1273
1368
  # TabBar
1274
1369
  # ======================================================================
@@ -1521,6 +1616,7 @@ def register_handlers(registry: Any) -> None:
1521
1616
  registry.register("ScrollView", ScrollViewHandler())
1522
1617
  registry.register("SafeAreaView", SafeAreaViewHandler())
1523
1618
  registry.register("Modal", ModalHandler())
1619
+ registry.register("Portal", PortalHandler())
1524
1620
  registry.register("Slider", SliderHandler())
1525
1621
  registry.register("TabBar", TabBarHandler())
1526
1622
  registry.register("Pressable", PressableHandler())