pythonnative 0.32.0__py3-none-any.whl → 0.34.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 +27 -15
- pythonnative/animated.py +7 -13
- pythonnative/cli/pn.py +1 -2
- pythonnative/component.py +255 -0
- pythonnative/components/__init__.py +106 -0
- pythonnative/components/_base.py +132 -0
- pythonnative/components/controls.py +515 -0
- pythonnative/components/layout.py +459 -0
- pythonnative/components/lists.py +809 -0
- pythonnative/components/media.py +204 -0
- pythonnative/components/overlays.py +107 -0
- pythonnative/components/pressable.py +252 -0
- pythonnative/components/structural.py +167 -0
- pythonnative/components/text.py +291 -0
- pythonnative/diagnostics.py +1 -1
- pythonnative/element.py +108 -29
- pythonnative/gestures.py +1 -1
- pythonnative/hooks.py +366 -671
- pythonnative/hosts/__init__.py +85 -0
- pythonnative/hosts/android.py +269 -0
- pythonnative/hosts/base.py +665 -0
- pythonnative/hosts/desktop.py +107 -0
- pythonnative/hosts/ios.py +404 -0
- pythonnative/hot_reload.py +16 -24
- pythonnative/layout.py +372 -66
- pythonnative/native_modules/__init__.py +23 -0
- pythonnative/native_modules/net_info.py +5 -1
- pythonnative/native_views/__init__.py +1 -1
- pythonnative/native_views/android.py +50 -6
- pythonnative/native_views/base.py +64 -1
- pythonnative/native_views/desktop.py +11 -3
- pythonnative/native_views/ios.py +72 -12
- pythonnative/navigation/__init__.py +102 -0
- pythonnative/navigation/container.py +154 -0
- pythonnative/navigation/handle.py +573 -0
- pythonnative/navigation/hooks.py +94 -0
- pythonnative/navigation/host.py +58 -0
- pythonnative/navigation/linking.py +200 -0
- pythonnative/navigation/navigators.py +637 -0
- pythonnative/navigation/screen.py +149 -0
- pythonnative/navigation/state.py +248 -0
- pythonnative/net.py +4 -0
- pythonnative/platform_metrics.py +1 -1
- pythonnative/preview.py +21 -19
- pythonnative/project/android.py +7 -1
- pythonnative/project/doctor.py +1 -1
- pythonnative/project/ios.py +1 -0
- pythonnative/project/runtime_assets.py +1 -1
- pythonnative/reconciler/__init__.py +29 -0
- pythonnative/reconciler/boundaries.py +365 -0
- pythonnative/reconciler/children.py +88 -0
- pythonnative/reconciler/core.py +1153 -0
- pythonnative/reconciler/layout_pass.py +359 -0
- pythonnative/reconciler/vnode.py +266 -0
- pythonnative/scheduler.py +159 -0
- pythonnative/sdk/_components.py +2 -4
- pythonnative/style.py +95 -13
- pythonnative/suspense.py +8 -12
- pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/MainActivity.kt +85 -1
- pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/ScreenFragment.kt +6 -7
- pythonnative/templates/android_template/app/src/main/res/navigation/nav_graph.xml +1 -1
- pythonnative/templates/android_template/app/src/main/res/values/strings.xml +3 -1
- pythonnative/templates/ios_template/ios_template/AppDelegate.swift +33 -0
- pythonnative/templates/ios_template/ios_template/PythonRuntime.swift +1 -1
- pythonnative/templates/ios_template/ios_template/ViewController.swift +12 -10
- pythonnative/testing/__init__.py +53 -0
- pythonnative/testing/backend.py +276 -0
- pythonnative/testing/harness.py +391 -0
- {pythonnative-0.32.0.dist-info → pythonnative-0.34.0.dist-info}/METADATA +2 -21
- {pythonnative-0.32.0.dist-info → pythonnative-0.34.0.dist-info}/RECORD +74 -43
- pythonnative/components.py +0 -2856
- pythonnative/navigation.py +0 -1031
- pythonnative/reconciler.py +0 -2298
- pythonnative/screen.py +0 -2068
- {pythonnative-0.32.0.dist-info → pythonnative-0.34.0.dist-info}/WHEEL +0 -0
- {pythonnative-0.32.0.dist-info → pythonnative-0.34.0.dist-info}/entry_points.txt +0 -0
- {pythonnative-0.32.0.dist-info → pythonnative-0.34.0.dist-info}/licenses/LICENSE +0 -0
- {pythonnative-0.32.0.dist-info → pythonnative-0.34.0.dist-info}/top_level.txt +0 -0
|
@@ -43,6 +43,8 @@ Reactive state (with hooks):
|
|
|
43
43
|
[`use_net_info`][pythonnative.use_net_info]: connectivity.
|
|
44
44
|
"""
|
|
45
45
|
|
|
46
|
+
from typing import Any, Sequence
|
|
47
|
+
|
|
46
48
|
from .app_state import AppState, use_app_state
|
|
47
49
|
from .battery import Battery
|
|
48
50
|
from .biometrics import Biometrics
|
|
@@ -58,6 +60,25 @@ from .permissions import Permissions
|
|
|
58
60
|
from .secure_store import SecureStore
|
|
59
61
|
from .share import Share
|
|
60
62
|
|
|
63
|
+
|
|
64
|
+
def dispatch_activity_result(request_code: int, result_code: int, data: Any) -> bool:
|
|
65
|
+
"""Route an Android ``onActivityResult`` to whichever module is waiting on it.
|
|
66
|
+
|
|
67
|
+
Called by the screen host. Returns ``True`` when a pending request
|
|
68
|
+
consumed the result.
|
|
69
|
+
"""
|
|
70
|
+
from .camera import deliver_android_activity_result
|
|
71
|
+
|
|
72
|
+
return deliver_android_activity_result(request_code, result_code, data)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def dispatch_permissions_result(request_code: int, permissions: Sequence[str], grant_results: Sequence[int]) -> bool:
|
|
76
|
+
"""Route an Android ``onRequestPermissionsResult`` to the pending ``Permissions.request``."""
|
|
77
|
+
from .permissions import deliver_android_permission_result
|
|
78
|
+
|
|
79
|
+
return deliver_android_permission_result(request_code, permissions, grant_results)
|
|
80
|
+
|
|
81
|
+
|
|
61
82
|
__all__ = [
|
|
62
83
|
"AppState",
|
|
63
84
|
"Battery",
|
|
@@ -74,6 +95,8 @@ __all__ = [
|
|
|
74
95
|
"SecureStore",
|
|
75
96
|
"Share",
|
|
76
97
|
"Vibration",
|
|
98
|
+
"dispatch_activity_result",
|
|
99
|
+
"dispatch_permissions_result",
|
|
77
100
|
"use_app_state",
|
|
78
101
|
"use_net_info",
|
|
79
102
|
]
|
|
@@ -151,7 +151,11 @@ def use_net_info() -> NetInfoState:
|
|
|
151
151
|
|
|
152
152
|
def _subscribe() -> Callable[[], None]:
|
|
153
153
|
set_state(NetInfo.fetch())
|
|
154
|
-
|
|
154
|
+
|
|
155
|
+
def on_change(snapshot: NetInfoState) -> None:
|
|
156
|
+
set_state(snapshot)
|
|
157
|
+
|
|
158
|
+
return NetInfo.add_listener(on_change)
|
|
155
159
|
|
|
156
160
|
use_effect(_subscribe, [])
|
|
157
161
|
return state
|
|
@@ -220,7 +220,7 @@ class NativeViewRegistry:
|
|
|
220
220
|
if not finite:
|
|
221
221
|
_tripwire_log(
|
|
222
222
|
"set_frame:nan",
|
|
223
|
-
f"[set_frame:nan] type={record.type_name!r}
|
|
223
|
+
f"[set_frame:nan] type={record.type_name!r} x={op.x!r} y={op.y!r} w={op.width!r} h={op.height!r}",
|
|
224
224
|
)
|
|
225
225
|
record.handler.set_frame(record.view, op.x, op.y, op.width, op.height)
|
|
226
226
|
|
|
@@ -60,7 +60,15 @@ from .. import diagnostics
|
|
|
60
60
|
from ..events import dispatch_event, event_names
|
|
61
61
|
from ..gestures import make_arbiter
|
|
62
62
|
from ..utils import get_android_context
|
|
63
|
-
from .base import
|
|
63
|
+
from .base import (
|
|
64
|
+
TEXT_SHADOW_STYLE_KEYS,
|
|
65
|
+
ViewHandler,
|
|
66
|
+
_safe_max,
|
|
67
|
+
parse_color_int,
|
|
68
|
+
shadow_offset_xy,
|
|
69
|
+
transform_spans,
|
|
70
|
+
transform_text,
|
|
71
|
+
)
|
|
64
72
|
|
|
65
73
|
_SIDE_BORDER_WIDTH_KEYS = (
|
|
66
74
|
"border_left_width",
|
|
@@ -676,6 +684,15 @@ def _apply_common_visual(view: Any, props: Dict[str, Any]) -> None:
|
|
|
676
684
|
view.setClipToPadding(clip)
|
|
677
685
|
except Exception:
|
|
678
686
|
diagnostics.swallowed("android._apply_common_visual")
|
|
687
|
+
if "display" in props:
|
|
688
|
+
# Layout already zeroes the subtree; ``INVISIBLE`` (not ``GONE``,
|
|
689
|
+
# since Python owns layout) also drops it from touch delivery and
|
|
690
|
+
# the accessibility tree (inactive tabs, covered stack screens).
|
|
691
|
+
try:
|
|
692
|
+
View = jclass("android.view.View")
|
|
693
|
+
view.setVisibility(View.INVISIBLE if props["display"] == "none" else View.VISIBLE)
|
|
694
|
+
except Exception:
|
|
695
|
+
diagnostics.swallowed("android._apply_common_visual")
|
|
679
696
|
if "opacity" in props and props["opacity"] is not None:
|
|
680
697
|
try:
|
|
681
698
|
view.setAlpha(float(props["opacity"]))
|
|
@@ -1305,21 +1322,21 @@ class TextHandler(AndroidViewHandler):
|
|
|
1305
1322
|
return jclass("android.widget.TextView")(_ctx())
|
|
1306
1323
|
|
|
1307
1324
|
def _apply(self, tv: Any, props: Dict[str, Any], initial: bool) -> None:
|
|
1308
|
-
if "spans" in props or "text" in props:
|
|
1325
|
+
if "spans" in props or "text" in props or "text_transform" in props:
|
|
1309
1326
|
# ``update()`` merges changed props into the view state
|
|
1310
1327
|
# before calling ``_apply``, so the merged dict always has
|
|
1311
1328
|
# the latest text/spans pair (covering rich -> plain
|
|
1312
1329
|
# transitions where only ``spans`` changed).
|
|
1313
1330
|
merged = _state_of(tv).get("props") or props
|
|
1331
|
+
transform = merged.get("text_transform")
|
|
1314
1332
|
spans = merged.get("spans")
|
|
1315
1333
|
if spans:
|
|
1316
1334
|
try:
|
|
1317
|
-
tv.setText(_build_spannable(spans))
|
|
1335
|
+
tv.setText(_build_spannable(transform_spans(spans, transform)))
|
|
1318
1336
|
except Exception:
|
|
1319
|
-
tv.setText(
|
|
1337
|
+
tv.setText(transform_text(merged.get("text"), transform))
|
|
1320
1338
|
else:
|
|
1321
|
-
|
|
1322
|
-
tv.setText(str(text) if text is not None else "")
|
|
1339
|
+
tv.setText(transform_text(merged.get("text"), transform))
|
|
1323
1340
|
if "font_size" in props and props["font_size"] is not None:
|
|
1324
1341
|
tv.setTextSize(float(props["font_size"]))
|
|
1325
1342
|
if "color" in props and props["color"] is not None:
|
|
@@ -1372,8 +1389,35 @@ class TextHandler(AndroidViewHandler):
|
|
|
1372
1389
|
tv.setPaintFlags(flags)
|
|
1373
1390
|
except Exception:
|
|
1374
1391
|
diagnostics.swallowed("android.TextHandler._apply")
|
|
1392
|
+
if any(k in props for k in TEXT_SHADOW_STYLE_KEYS):
|
|
1393
|
+
self._apply_text_shadow(tv, _state_of(tv).get("props") or props)
|
|
1375
1394
|
_apply_common_visual(tv, props)
|
|
1376
1395
|
|
|
1396
|
+
@staticmethod
|
|
1397
|
+
def _apply_text_shadow(tv: Any, merged: Dict[str, Any]) -> None:
|
|
1398
|
+
"""Map the ``text_shadow_*`` keys onto ``TextView.setShadowLayer``.
|
|
1399
|
+
|
|
1400
|
+
The offset and radius are in dp (converted to pixels like every
|
|
1401
|
+
other length); the platform disables the shadow when every
|
|
1402
|
+
parameter is zero, which is also what clearing the keys does.
|
|
1403
|
+
"""
|
|
1404
|
+
try:
|
|
1405
|
+
if not any(merged.get(k) is not None for k in TEXT_SHADOW_STYLE_KEYS):
|
|
1406
|
+
tv.setShadowLayer(0.0, 0.0, 0.0, 0)
|
|
1407
|
+
return
|
|
1408
|
+
color = merged.get("text_shadow_color")
|
|
1409
|
+
argb = parse_color_int(color if color is not None else "#000000")
|
|
1410
|
+
dx, dy = shadow_offset_xy(merged.get("text_shadow_offset"))
|
|
1411
|
+
radius = merged.get("text_shadow_radius")
|
|
1412
|
+
radius_px = float(_dp(float(radius))) if radius is not None else 0.0
|
|
1413
|
+
# A zero radius with a non-zero offset would hide the shadow
|
|
1414
|
+
# on some renderers; use a hairline blur so it still draws.
|
|
1415
|
+
if radius_px <= 0.0 and (dx or dy):
|
|
1416
|
+
radius_px = 0.01
|
|
1417
|
+
tv.setShadowLayer(radius_px, float(_dp(dx)), float(_dp(dy)), argb)
|
|
1418
|
+
except Exception:
|
|
1419
|
+
diagnostics.swallowed("android.TextHandler._apply_text_shadow")
|
|
1420
|
+
|
|
1377
1421
|
|
|
1378
1422
|
class ButtonHandler(AndroidViewHandler):
|
|
1379
1423
|
def _build(self, props: Dict[str, Any]) -> Any:
|
|
@@ -15,7 +15,8 @@ for content-sized leaves (text, buttons, images).
|
|
|
15
15
|
"""
|
|
16
16
|
|
|
17
17
|
import math
|
|
18
|
-
|
|
18
|
+
import re
|
|
19
|
+
from typing import Any, Dict, Optional, Tuple, Union
|
|
19
20
|
|
|
20
21
|
|
|
21
22
|
class ViewHandler:
|
|
@@ -241,6 +242,68 @@ def parse_color_int(color: Union[str, int]) -> int:
|
|
|
241
242
|
return val
|
|
242
243
|
|
|
243
244
|
|
|
245
|
+
# ======================================================================
|
|
246
|
+
# Text helpers shared by every backend's Text handler
|
|
247
|
+
# ======================================================================
|
|
248
|
+
|
|
249
|
+
_WORD_START = re.compile(r"(?<!\S)(\S)")
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def transform_text(text: Any, mode: Optional[str]) -> str:
|
|
253
|
+
"""Apply a ``text_transform`` style value to ``text``.
|
|
254
|
+
|
|
255
|
+
Runs in Python before the string is handed to the native label, so
|
|
256
|
+
the displayed text and the intrinsic measurement agree on every
|
|
257
|
+
backend. ``"capitalize"`` upper-cases the first character of each
|
|
258
|
+
whitespace-separated word and leaves the rest untouched (matching
|
|
259
|
+
CSS / React Native rather than ``str.title()``, which would
|
|
260
|
+
lower-case the remainder). Unknown modes and ``"none"`` return the
|
|
261
|
+
text unchanged.
|
|
262
|
+
"""
|
|
263
|
+
s = "" if text is None else str(text)
|
|
264
|
+
if mode == "uppercase":
|
|
265
|
+
return s.upper()
|
|
266
|
+
if mode == "lowercase":
|
|
267
|
+
return s.lower()
|
|
268
|
+
if mode == "capitalize":
|
|
269
|
+
return _WORD_START.sub(lambda m: m.group(1).upper(), s)
|
|
270
|
+
return s
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def transform_spans(spans: Any, mode: Optional[str]) -> Any:
|
|
274
|
+
"""Return ``spans`` with `transform_text` applied to each span's ``text``.
|
|
275
|
+
|
|
276
|
+
Rich text is flattened into one native string, so a run-by-run
|
|
277
|
+
transform matches what a single-string transform would produce for
|
|
278
|
+
``uppercase`` / ``lowercase``. ``capitalize`` is evaluated per span,
|
|
279
|
+
which is also how CSS treats inline boxes.
|
|
280
|
+
"""
|
|
281
|
+
if not mode or mode == "none" or not isinstance(spans, list):
|
|
282
|
+
return spans
|
|
283
|
+
out = []
|
|
284
|
+
for span in spans:
|
|
285
|
+
if isinstance(span, dict) and "text" in span:
|
|
286
|
+
copy = dict(span)
|
|
287
|
+
copy["text"] = transform_text(span.get("text"), mode)
|
|
288
|
+
out.append(copy)
|
|
289
|
+
else:
|
|
290
|
+
out.append(span)
|
|
291
|
+
return out
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def shadow_offset_xy(value: Any) -> Tuple[float, float]:
|
|
295
|
+
"""Coerce a ``shadow_offset`` / ``text_shadow_offset`` value to ``(dx, dy)``."""
|
|
296
|
+
if isinstance(value, dict):
|
|
297
|
+
return (float(value.get("width", 0) or 0), float(value.get("height", 0) or 0))
|
|
298
|
+
if isinstance(value, (tuple, list)) and len(value) >= 2:
|
|
299
|
+
return (float(value[0]), float(value[1]))
|
|
300
|
+
return (0.0, 0.0)
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
TEXT_SHADOW_STYLE_KEYS = ("text_shadow_color", "text_shadow_offset", "text_shadow_radius")
|
|
304
|
+
"""Style keys that together describe a text shadow."""
|
|
305
|
+
|
|
306
|
+
|
|
244
307
|
# ======================================================================
|
|
245
308
|
# Helpers shared by Android and iOS measure callbacks
|
|
246
309
|
# ======================================================================
|
|
@@ -96,7 +96,7 @@ from typing import Any, Dict, List, Optional, Tuple
|
|
|
96
96
|
from .. import diagnostics
|
|
97
97
|
from ..events import dispatch_event, event_names
|
|
98
98
|
from ..gestures import make_arbiter
|
|
99
|
-
from .base import ViewHandler
|
|
99
|
+
from .base import ViewHandler, transform_text
|
|
100
100
|
|
|
101
101
|
# ======================================================================
|
|
102
102
|
# Stage / root container
|
|
@@ -1452,13 +1452,21 @@ _JUSTIFY_FOR_ALIGN = {"left": "left", "center": "center", "right": "right"}
|
|
|
1452
1452
|
|
|
1453
1453
|
|
|
1454
1454
|
class TextHandler(DesktopViewHandler):
|
|
1455
|
+
"""Tk ``Label`` text leaf.
|
|
1456
|
+
|
|
1457
|
+
``text_transform`` is applied to the string before it is stored, so
|
|
1458
|
+
``measure_intrinsic`` and the rendered label agree. The
|
|
1459
|
+
``text_shadow_*`` keys are accepted but ignored: Tk has no text
|
|
1460
|
+
shadow primitive, and the preview stays faithful to layout rather
|
|
1461
|
+
than approximating one with a second label.
|
|
1462
|
+
"""
|
|
1463
|
+
|
|
1455
1464
|
def build(self, props: Dict[str, Any]) -> Any:
|
|
1456
1465
|
return tk.Label(_master(), highlightthickness=0, bd=0, padx=0, pady=0)
|
|
1457
1466
|
|
|
1458
1467
|
def apply(self, label: Any, props: Dict[str, Any]) -> None:
|
|
1459
1468
|
merged = getattr(label, "_pn_props", props)
|
|
1460
|
-
|
|
1461
|
-
label._pn_text = "" if text is None else str(text)
|
|
1469
|
+
label._pn_text = transform_text(merged.get("text"), merged.get("text_transform"))
|
|
1462
1470
|
font = _make_font(merged)
|
|
1463
1471
|
label._pn_font = font
|
|
1464
1472
|
opts: Dict[str, Any] = {"text": label._pn_text, "font": font}
|
pythonnative/native_views/ios.py
CHANGED
|
@@ -63,7 +63,15 @@ from rubicon.objc import SEL, ObjCClass, ObjCInstance, objc_method
|
|
|
63
63
|
from .. import diagnostics
|
|
64
64
|
from ..events import dispatch_event, event_names
|
|
65
65
|
from . import _tripwire_log
|
|
66
|
-
from .base import
|
|
66
|
+
from .base import (
|
|
67
|
+
TEXT_SHADOW_STYLE_KEYS,
|
|
68
|
+
ViewHandler,
|
|
69
|
+
_safe_max,
|
|
70
|
+
parse_color_int,
|
|
71
|
+
shadow_offset_xy,
|
|
72
|
+
transform_spans,
|
|
73
|
+
transform_text,
|
|
74
|
+
)
|
|
67
75
|
|
|
68
76
|
|
|
69
77
|
def _safe_finite(value: Any, default: float = 0.0) -> float:
|
|
@@ -624,6 +632,30 @@ def _apply_shadow(view: Any, props: Dict[str, Any]) -> None:
|
|
|
624
632
|
diagnostics.swallowed("ios._apply_shadow")
|
|
625
633
|
|
|
626
634
|
|
|
635
|
+
def _text_shadow(props: Dict[str, Any]) -> Any:
|
|
636
|
+
"""Build an ``NSShadow`` from the ``text_shadow_*`` style keys, or ``None``.
|
|
637
|
+
|
|
638
|
+
Text shadows are attached to the attributed string
|
|
639
|
+
(``NSShadowAttributeName``) rather than the label's layer so they
|
|
640
|
+
follow the glyphs instead of the label's frame and don't interfere
|
|
641
|
+
with the view-level ``shadow_*`` props.
|
|
642
|
+
"""
|
|
643
|
+
if not any(props.get(k) is not None for k in TEXT_SHADOW_STYLE_KEYS):
|
|
644
|
+
return None
|
|
645
|
+
try:
|
|
646
|
+
shadow = ObjCClass("NSShadow").alloc().init()
|
|
647
|
+
color = props.get("text_shadow_color")
|
|
648
|
+
shadow.setShadowColor_(_uicolor(color if color is not None else "#000000"))
|
|
649
|
+
dx, dy = shadow_offset_xy(props.get("text_shadow_offset"))
|
|
650
|
+
shadow.setShadowOffset_((dx, dy))
|
|
651
|
+
radius = props.get("text_shadow_radius")
|
|
652
|
+
shadow.setShadowBlurRadius_(float(radius) if radius is not None else 0.0)
|
|
653
|
+
return shadow
|
|
654
|
+
except Exception:
|
|
655
|
+
diagnostics.swallowed("ios._text_shadow")
|
|
656
|
+
return None
|
|
657
|
+
|
|
658
|
+
|
|
627
659
|
def _make_transform(spec: Any) -> Any:
|
|
628
660
|
"""Build a `CGAffineTransform` from a list of transform dicts.
|
|
629
661
|
|
|
@@ -821,6 +853,14 @@ def _apply_common_visual(view: Any, props: Dict[str, Any]) -> None:
|
|
|
821
853
|
diagnostics.swallowed("ios._apply_common_visual")
|
|
822
854
|
if "overflow" in props:
|
|
823
855
|
view.setClipsToBounds_(props["overflow"] == "hidden")
|
|
856
|
+
if "display" in props:
|
|
857
|
+
# Layout already zeroes the subtree; hiding also removes it from
|
|
858
|
+
# hit testing and the accessibility tree (inactive tabs, covered
|
|
859
|
+
# stack screens).
|
|
860
|
+
try:
|
|
861
|
+
view.setHidden_(props["display"] == "none")
|
|
862
|
+
except Exception:
|
|
863
|
+
diagnostics.swallowed("ios._apply_common_visual")
|
|
824
864
|
if "opacity" in props and props["opacity"] is not None:
|
|
825
865
|
try:
|
|
826
866
|
view.setAlpha_(float(props["opacity"]))
|
|
@@ -1969,25 +2009,32 @@ class TextHandler(IOSViewHandler):
|
|
|
1969
2009
|
diagnostics.swallowed("ios.TextHandler._font_for")
|
|
1970
2010
|
return font
|
|
1971
2011
|
|
|
2012
|
+
# Keys that need the plain string re-rendered as an attributed
|
|
2013
|
+
# string (plain ``setText_`` can't express them).
|
|
2014
|
+
_ATTRIBUTED_KEYS = (
|
|
2015
|
+
"letter_spacing",
|
|
2016
|
+
"line_height",
|
|
2017
|
+
"text_decoration",
|
|
2018
|
+
) + TEXT_SHADOW_STYLE_KEYS
|
|
2019
|
+
|
|
1972
2020
|
_SPAN_REBUILD_KEYS = (
|
|
1973
2021
|
"spans",
|
|
1974
2022
|
"text",
|
|
2023
|
+
"text_transform",
|
|
1975
2024
|
"font_size",
|
|
1976
2025
|
"font_weight",
|
|
1977
2026
|
"font_family",
|
|
1978
2027
|
"italic",
|
|
1979
2028
|
"bold",
|
|
1980
2029
|
"color",
|
|
1981
|
-
|
|
1982
|
-
"line_height",
|
|
1983
|
-
"text_decoration",
|
|
1984
|
-
)
|
|
2030
|
+
) + _ATTRIBUTED_KEYS
|
|
1985
2031
|
|
|
1986
2032
|
def _apply(self, label: Any, props: Dict[str, Any], initial: bool) -> None:
|
|
1987
2033
|
merged_state = _state_of(label).get("props") or props
|
|
1988
2034
|
has_spans = bool(merged_state.get("spans"))
|
|
1989
|
-
|
|
1990
|
-
|
|
2035
|
+
text_changed = "text" in props or "text_transform" in props
|
|
2036
|
+
if text_changed and not has_spans:
|
|
2037
|
+
label.setText_(transform_text(merged_state.get("text"), merged_state.get("text_transform")))
|
|
1991
2038
|
# Font requires combining size + weight + family + italic + bold.
|
|
1992
2039
|
font_keys_present = any(k in props for k in ("font_size", "font_weight", "font_family", "italic", "bold"))
|
|
1993
2040
|
if font_keys_present:
|
|
@@ -2018,8 +2065,13 @@ class TextHandler(IOSViewHandler):
|
|
|
2018
2065
|
self._apply_spans(label, merged_state)
|
|
2019
2066
|
elif "spans" in props:
|
|
2020
2067
|
# Rich -> plain transition: restore the plain string.
|
|
2021
|
-
label.setText_(
|
|
2022
|
-
elif
|
|
2068
|
+
label.setText_(transform_text(merged_state.get("text"), merged_state.get("text_transform")))
|
|
2069
|
+
elif any(k in props for k in self._ATTRIBUTED_KEYS) or (
|
|
2070
|
+
# ``setText_`` drops attributes, so a text change re-renders
|
|
2071
|
+
# the attributed string when any attributed key is active.
|
|
2072
|
+
text_changed
|
|
2073
|
+
and any(merged_state.get(k) is not None for k in self._ATTRIBUTED_KEYS)
|
|
2074
|
+
):
|
|
2023
2075
|
self._apply_attributed(label, merged_state)
|
|
2024
2076
|
_apply_view_border(label, props)
|
|
2025
2077
|
_apply_shadow(label, props)
|
|
@@ -2041,6 +2093,7 @@ class TextHandler(IOSViewHandler):
|
|
|
2041
2093
|
"""
|
|
2042
2094
|
try:
|
|
2043
2095
|
spans = [s for s in (merged.get("spans") or []) if isinstance(s, dict)]
|
|
2096
|
+
spans = transform_spans(spans, merged.get("text_transform"))
|
|
2044
2097
|
full_text = "".join(str(s.get("text", "")) for s in spans)
|
|
2045
2098
|
NSMutableAttributedString = ObjCClass("NSMutableAttributedString")
|
|
2046
2099
|
NSMutableParagraphStyle = ObjCClass("NSMutableParagraphStyle")
|
|
@@ -2062,6 +2115,9 @@ class TextHandler(IOSViewHandler):
|
|
|
2062
2115
|
attr.addAttribute_value_range_("NSUnderline", 1, full_range)
|
|
2063
2116
|
elif merged.get("text_decoration") == "line_through":
|
|
2064
2117
|
attr.addAttribute_value_range_("NSStrikethrough", 1, full_range)
|
|
2118
|
+
shadow = _text_shadow(merged)
|
|
2119
|
+
if shadow is not None:
|
|
2120
|
+
attr.addAttribute_value_range_("NSShadow", shadow, full_range)
|
|
2065
2121
|
|
|
2066
2122
|
pos = 0
|
|
2067
2123
|
for span in spans:
|
|
@@ -2095,7 +2151,7 @@ class TextHandler(IOSViewHandler):
|
|
|
2095
2151
|
label.setAttributedText_(attr)
|
|
2096
2152
|
except Exception:
|
|
2097
2153
|
try:
|
|
2098
|
-
label.setText_(
|
|
2154
|
+
label.setText_(transform_text(merged.get("text"), merged.get("text_transform")))
|
|
2099
2155
|
except Exception:
|
|
2100
2156
|
diagnostics.swallowed("ios.TextHandler._apply_spans")
|
|
2101
2157
|
|
|
@@ -2103,8 +2159,9 @@ class TextHandler(IOSViewHandler):
|
|
|
2103
2159
|
"""Re-render the label's text as an NSAttributedString.
|
|
2104
2160
|
|
|
2105
2161
|
Needed for ``letter_spacing`` (NSKernAttributeName),
|
|
2106
|
-
``line_height`` (paragraph style),
|
|
2107
|
-
(underline/strikethrough)
|
|
2162
|
+
``line_height`` (paragraph style), ``text_decoration``
|
|
2163
|
+
(underline/strikethrough), and the ``text_shadow_*`` keys
|
|
2164
|
+
(NSShadowAttributeName). Plain text setters do not support
|
|
2108
2165
|
these attributes.
|
|
2109
2166
|
"""
|
|
2110
2167
|
try:
|
|
@@ -2140,6 +2197,9 @@ class TextHandler(IOSViewHandler):
|
|
|
2140
2197
|
attr.addAttribute_value_range_("NSUnderline", 1, full_range)
|
|
2141
2198
|
elif decoration == "line_through":
|
|
2142
2199
|
attr.addAttribute_value_range_("NSStrikethrough", 1, full_range)
|
|
2200
|
+
shadow = _text_shadow(props)
|
|
2201
|
+
if shadow is not None:
|
|
2202
|
+
attr.addAttribute_value_range_("NSShadow", shadow, full_range)
|
|
2143
2203
|
label.setAttributedText_(attr)
|
|
2144
2204
|
except Exception:
|
|
2145
2205
|
diagnostics.swallowed("ios.TextHandler._apply_attributed")
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""Navigation: stack, tab, and drawer navigators with a React Navigation-style API.
|
|
2
|
+
|
|
3
|
+
```python
|
|
4
|
+
import pythonnative as pn
|
|
5
|
+
|
|
6
|
+
Stack = pn.create_stack_navigator()
|
|
7
|
+
|
|
8
|
+
@pn.component
|
|
9
|
+
def HomeScreen():
|
|
10
|
+
nav = pn.use_navigation()
|
|
11
|
+
return pn.Button("Open 42", on_press=lambda: nav.navigate("Detail", id=42))
|
|
12
|
+
|
|
13
|
+
@pn.component
|
|
14
|
+
def DetailScreen():
|
|
15
|
+
route = pn.use_route()
|
|
16
|
+
return pn.Text(f"Item {route.params['id']}")
|
|
17
|
+
|
|
18
|
+
@pn.component
|
|
19
|
+
def App():
|
|
20
|
+
return pn.NavigationContainer(
|
|
21
|
+
Stack.Navigator(
|
|
22
|
+
Stack.Screen("Home", HomeScreen, title="Home"),
|
|
23
|
+
Stack.Screen("Detail", DetailScreen, options=lambda route: {"title": f"Item {route.params['id']}"}),
|
|
24
|
+
)
|
|
25
|
+
)
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Public surface (all re-exported from ``pythonnative``):
|
|
29
|
+
|
|
30
|
+
- Factories: [`create_stack_navigator`][pythonnative.create_stack_navigator],
|
|
31
|
+
[`create_tab_navigator`][pythonnative.create_tab_navigator],
|
|
32
|
+
[`create_drawer_navigator`][pythonnative.create_drawer_navigator],
|
|
33
|
+
[`NavigationContainer`][pythonnative.NavigationContainer].
|
|
34
|
+
- Hooks: [`use_navigation`][pythonnative.use_navigation],
|
|
35
|
+
[`use_route`][pythonnative.use_route],
|
|
36
|
+
[`use_is_focused`][pythonnative.use_is_focused],
|
|
37
|
+
[`use_focus_effect`][pythonnative.use_focus_effect].
|
|
38
|
+
- Types: [`Navigation`][pythonnative.Navigation],
|
|
39
|
+
[`Route`][pythonnative.navigation.Route],
|
|
40
|
+
[`NavigationState`][pythonnative.navigation.NavigationState],
|
|
41
|
+
[`ScreenOptions`][pythonnative.ScreenOptions],
|
|
42
|
+
[`LinkingConfig`][pythonnative.LinkingConfig].
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
from .container import ContainerContext, NavigationContainer
|
|
46
|
+
from .handle import (
|
|
47
|
+
DrawerNavigation,
|
|
48
|
+
EventName,
|
|
49
|
+
FocusContext,
|
|
50
|
+
HostNavigator,
|
|
51
|
+
Navigation,
|
|
52
|
+
NavigationContext,
|
|
53
|
+
NavigationEvent,
|
|
54
|
+
NavigatorCore,
|
|
55
|
+
TabNavigation,
|
|
56
|
+
)
|
|
57
|
+
from .hooks import use_focus_effect, use_is_focused, use_navigation, use_route
|
|
58
|
+
from .host import NAV_STATE_ARG, HostContext, HostRoot
|
|
59
|
+
from .linking import LinkingConfig
|
|
60
|
+
from .navigators import (
|
|
61
|
+
DrawerNavigator,
|
|
62
|
+
StackNavigator,
|
|
63
|
+
TabNavigator,
|
|
64
|
+
create_drawer_navigator,
|
|
65
|
+
create_stack_navigator,
|
|
66
|
+
create_tab_navigator,
|
|
67
|
+
)
|
|
68
|
+
from .screen import HeaderSlot, ScreenDef, ScreenOptions
|
|
69
|
+
from .state import NavigationState, Route
|
|
70
|
+
|
|
71
|
+
__all__ = [
|
|
72
|
+
"NAV_STATE_ARG",
|
|
73
|
+
"ContainerContext",
|
|
74
|
+
"DrawerNavigation",
|
|
75
|
+
"DrawerNavigator",
|
|
76
|
+
"EventName",
|
|
77
|
+
"FocusContext",
|
|
78
|
+
"HeaderSlot",
|
|
79
|
+
"HostContext",
|
|
80
|
+
"HostNavigator",
|
|
81
|
+
"HostRoot",
|
|
82
|
+
"LinkingConfig",
|
|
83
|
+
"Navigation",
|
|
84
|
+
"NavigationContainer",
|
|
85
|
+
"NavigationContext",
|
|
86
|
+
"NavigationEvent",
|
|
87
|
+
"NavigationState",
|
|
88
|
+
"NavigatorCore",
|
|
89
|
+
"Route",
|
|
90
|
+
"ScreenDef",
|
|
91
|
+
"ScreenOptions",
|
|
92
|
+
"StackNavigator",
|
|
93
|
+
"TabNavigation",
|
|
94
|
+
"TabNavigator",
|
|
95
|
+
"create_drawer_navigator",
|
|
96
|
+
"create_stack_navigator",
|
|
97
|
+
"create_tab_navigator",
|
|
98
|
+
"use_focus_effect",
|
|
99
|
+
"use_is_focused",
|
|
100
|
+
"use_navigation",
|
|
101
|
+
"use_route",
|
|
102
|
+
]
|