pythonnative 0.22.0__py3-none-any.whl → 0.23.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 (53) hide show
  1. pythonnative/__init__.py +16 -2
  2. pythonnative/animated.py +6 -6
  3. pythonnative/appearance.py +124 -0
  4. pythonnative/cli/pn.py +1 -1
  5. pythonnative/components.py +718 -48
  6. pythonnative/events.py +5 -5
  7. pythonnative/gestures.py +3 -3
  8. pythonnative/hooks.py +44 -3
  9. pythonnative/hot_reload.py +4 -4
  10. pythonnative/images.py +196 -0
  11. pythonnative/layout.py +3 -3
  12. pythonnative/mutations.py +4 -13
  13. pythonnative/native_modules/camera.py +1 -1
  14. pythonnative/native_modules/haptics.py +2 -2
  15. pythonnative/native_modules/location.py +1 -1
  16. pythonnative/native_modules/permissions.py +2 -2
  17. pythonnative/native_modules/secure_store.py +1 -1
  18. pythonnative/native_views/__init__.py +1 -7
  19. pythonnative/native_views/android.py +592 -64
  20. pythonnative/native_views/base.py +3 -3
  21. pythonnative/native_views/desktop.py +85 -26
  22. pythonnative/native_views/ios.py +762 -74
  23. pythonnative/navigation.py +4 -4
  24. pythonnative/net.py +3 -3
  25. pythonnative/platform.py +1 -1
  26. pythonnative/platform_metrics.py +5 -5
  27. pythonnative/preview.py +34 -3
  28. pythonnative/project/builder.py +1 -1
  29. pythonnative/project/config.py +4 -12
  30. pythonnative/project/doctor.py +2 -2
  31. pythonnative/project/icons.py +1 -1
  32. pythonnative/project/ios.py +1 -1
  33. pythonnative/project/permissions.py +2 -2
  34. pythonnative/project/runtime_assets.py +3 -3
  35. pythonnative/reconciler.py +9 -9
  36. pythonnative/runtime.py +8 -8
  37. pythonnative/screen.py +90 -5
  38. pythonnative/sdk/_components.py +1 -1
  39. pythonnative/storage.py +3 -3
  40. pythonnative/style.py +66 -7
  41. pythonnative/templates/android_template/app/src/main/AndroidManifest.xml +2 -1
  42. pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/PNAccessibilityDelegate.kt +47 -0
  43. pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/PNBorderDrawable.kt +67 -0
  44. pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/PNVirtualListView.java +172 -0
  45. pythonnative/templates/ios_template/ios_template.xcodeproj/project.pbxproj +2 -2
  46. pythonnative/templates/ios_template/ios_templateUITests/ios_templateUITests.swift +1 -1
  47. pythonnative/virtual_rows.py +137 -0
  48. {pythonnative-0.22.0.dist-info → pythonnative-0.23.0.dist-info}/METADATA +13 -13
  49. {pythonnative-0.22.0.dist-info → pythonnative-0.23.0.dist-info}/RECORD +53 -47
  50. {pythonnative-0.22.0.dist-info → pythonnative-0.23.0.dist-info}/WHEEL +1 -1
  51. {pythonnative-0.22.0.dist-info → pythonnative-0.23.0.dist-info}/entry_points.txt +0 -0
  52. {pythonnative-0.22.0.dist-info → pythonnative-0.23.0.dist-info}/licenses/LICENSE +0 -0
  53. {pythonnative-0.22.0.dist-info → pythonnative-0.23.0.dist-info}/top_level.txt +0 -0
@@ -23,7 +23,7 @@ class ViewHandler:
23
23
 
24
24
  A `ViewHandler` knows how to create, update, re-parent, and destroy
25
25
  native views of one element type. The reconciler never calls a
26
- handler directly it emits a batch of mutation ops
26
+ handler directly; it emits a batch of mutation ops
27
27
  (`pythonnative.mutations`) that the
28
28
  [`NativeViewRegistry`][pythonnative.native_views.NativeViewRegistry]
29
29
  applies by dispatching to handlers. Handlers never need to know
@@ -132,7 +132,7 @@ class ViewHandler:
132
132
 
133
133
  The default implementation returns ``(0, 0)``; override for
134
134
  leaves whose size depends on their content. Container handlers
135
- leave this alone the engine sizes containers by laying out
135
+ leave this alone; the engine sizes containers by laying out
136
136
  their children.
137
137
 
138
138
  Args:
@@ -149,7 +149,7 @@ class ViewHandler:
149
149
  """Execute an imperative command (e.g. ``"scroll_to_offset"``).
150
150
 
151
151
  Commands are the escape hatch for one-shot imperative actions
152
- that don't fit declarative props scrolling, focusing,
152
+ that don't fit declarative props: scrolling, focusing,
153
153
  flashing indicators. Unknown commands should be ignored.
154
154
 
155
155
  Args:
@@ -74,7 +74,7 @@ from .base import ViewHandler
74
74
  # ``pn preview`` installs it before mounting the app; the placement
75
75
  # logic (``_place``) positions widgets *inside* their logical parent via
76
76
  # Tk's ``-in`` option, which only works when both windows share a
77
- # top-level guaranteed by the single-stage design.
77
+ # top-level, guaranteed by the single-stage design.
78
78
 
79
79
  _ROOT_CONTAINER: Any = None
80
80
  _DEFAULT_FONT_SIZE = 15
@@ -125,7 +125,7 @@ def _tk_color(value: Any) -> Optional[str]:
125
125
  """Convert a PythonNative color into a Tk color string.
126
126
 
127
127
  Accepts ``#rgb`` / ``#rrggbb`` / ``#aarrggbb`` hex (alpha is
128
- dropped Tk has no per-color alpha), ``rgb()`` / ``rgba()``
128
+ dropped; Tk has no per-color alpha), ``rgb()`` / ``rgba()``
129
129
  functional notation, ``(r, g, b)`` tuples, packed integers, and
130
130
  named colors (passed through for Tk to resolve). Returns ``None``
131
131
  for ``transparent`` / unparseable values so callers can leave the
@@ -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)
@@ -686,8 +707,8 @@ class ScrollViewHandler(FlexContainerHandler):
686
707
 
687
708
  The layout engine lets the content grow past the viewport on the
688
709
  scroll axis; this handler offsets its children's placement by the
689
- current scroll offset (overflow outside the frame is *not* clipped
690
- a documented preview limitation of the single-stage design).
710
+ current scroll offset (overflow outside the frame is *not* clipped,
711
+ a documented preview limitation of the single-stage design).
691
712
 
692
713
  Commands:
693
714
  ``scroll_to_offset(x=…, y=…)``: jump to an offset.
@@ -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)
@@ -1232,7 +1291,7 @@ class PressableHandler(DesktopViewHandler):
1232
1291
 
1233
1292
 
1234
1293
  class ModalHandler(DesktopViewHandler):
1235
- """Overlay modal a frame that fills the stage when ``visible``.
1294
+ """Overlay modal, a frame that fills the stage when ``visible``.
1236
1295
 
1237
1296
  The reconciler lays the modal's content out against the full
1238
1297
  viewport (see ``Reconciler._layout_visible_modals``) and applies
@@ -1275,7 +1334,7 @@ class ModalHandler(DesktopViewHandler):
1275
1334
 
1276
1335
 
1277
1336
  class TabBarHandler(DesktopViewHandler):
1278
- """Bottom tab bar a row of buttons laid out across its width."""
1337
+ """Bottom tab bar, a row of buttons laid out across its width."""
1279
1338
 
1280
1339
  def build(self, props: Dict[str, Any]) -> Any:
1281
1340
  frame = tk.Frame(_master(), highlightthickness=1, bd=0, background="#f2f2f7")
@@ -1454,7 +1513,7 @@ class SegmentedControlHandler(DesktopViewHandler):
1454
1513
 
1455
1514
 
1456
1515
  class DatePickerHandler(DesktopViewHandler):
1457
- """Preview DatePicker a text entry for the ISO date/time string."""
1516
+ """Preview DatePicker, a text entry for the ISO date/time string."""
1458
1517
 
1459
1518
  def build(self, props: Dict[str, Any]) -> Any:
1460
1519
  entry = tk.Entry(_master(), highlightthickness=1, bd=0)