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.
- pythonnative/__init__.py +16 -2
- pythonnative/animated.py +6 -6
- pythonnative/appearance.py +124 -0
- pythonnative/cli/pn.py +1 -1
- pythonnative/components.py +718 -48
- pythonnative/events.py +5 -5
- pythonnative/gestures.py +3 -3
- pythonnative/hooks.py +44 -3
- pythonnative/hot_reload.py +4 -4
- pythonnative/images.py +196 -0
- pythonnative/layout.py +3 -3
- pythonnative/mutations.py +4 -13
- pythonnative/native_modules/camera.py +1 -1
- pythonnative/native_modules/haptics.py +2 -2
- pythonnative/native_modules/location.py +1 -1
- pythonnative/native_modules/permissions.py +2 -2
- pythonnative/native_modules/secure_store.py +1 -1
- pythonnative/native_views/__init__.py +1 -7
- pythonnative/native_views/android.py +592 -64
- pythonnative/native_views/base.py +3 -3
- pythonnative/native_views/desktop.py +85 -26
- pythonnative/native_views/ios.py +762 -74
- pythonnative/navigation.py +4 -4
- pythonnative/net.py +3 -3
- pythonnative/platform.py +1 -1
- pythonnative/platform_metrics.py +5 -5
- pythonnative/preview.py +34 -3
- pythonnative/project/builder.py +1 -1
- pythonnative/project/config.py +4 -12
- pythonnative/project/doctor.py +2 -2
- pythonnative/project/icons.py +1 -1
- pythonnative/project/ios.py +1 -1
- pythonnative/project/permissions.py +2 -2
- pythonnative/project/runtime_assets.py +3 -3
- pythonnative/reconciler.py +9 -9
- pythonnative/runtime.py +8 -8
- pythonnative/screen.py +90 -5
- pythonnative/sdk/_components.py +1 -1
- pythonnative/storage.py +3 -3
- pythonnative/style.py +66 -7
- pythonnative/templates/android_template/app/src/main/AndroidManifest.xml +2 -1
- pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/PNAccessibilityDelegate.kt +47 -0
- pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/PNBorderDrawable.kt +67 -0
- pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/PNVirtualListView.java +172 -0
- pythonnative/templates/ios_template/ios_template.xcodeproj/project.pbxproj +2 -2
- pythonnative/templates/ios_template/ios_templateUITests/ios_templateUITests.swift +1 -1
- pythonnative/virtual_rows.py +137 -0
- {pythonnative-0.22.0.dist-info → pythonnative-0.23.0.dist-info}/METADATA +13 -13
- {pythonnative-0.22.0.dist-info → pythonnative-0.23.0.dist-info}/RECORD +53 -47
- {pythonnative-0.22.0.dist-info → pythonnative-0.23.0.dist-info}/WHEEL +1 -1
- {pythonnative-0.22.0.dist-info → pythonnative-0.23.0.dist-info}/entry_points.txt +0 -0
- {pythonnative-0.22.0.dist-info → pythonnative-0.23.0.dist-info}/licenses/LICENSE +0 -0
- {pythonnative-0.22.0.dist-info → pythonnative-0.23.0.dist-info}/top_level.txt +0 -0
pythonnative/events.py
CHANGED
|
@@ -10,7 +10,7 @@ the bridge. This module replaces that with a single dispatch channel:
|
|
|
10
10
|
- Handlers wire their platform listener **once** at view creation; the
|
|
11
11
|
listener calls [`dispatch_event`][pythonnative.events.dispatch_event]
|
|
12
12
|
with the view's tag and the event name.
|
|
13
|
-
- Re-renders only mutate this Python-side registry
|
|
13
|
+
- Re-renders only mutate this Python-side registry; no native call is
|
|
14
14
|
made when just a callback identity changes.
|
|
15
15
|
|
|
16
16
|
The set of event names present on an element is forwarded to handlers
|
|
@@ -78,7 +78,7 @@ class EventRegistry:
|
|
|
78
78
|
|
|
79
79
|
Returns:
|
|
80
80
|
``True`` when a callback existed and was invoked (even if
|
|
81
|
-
it raised
|
|
81
|
+
it raised: exceptions are swallowed so a buggy app
|
|
82
82
|
callback can't crash the platform's UI thread), ``False``
|
|
83
83
|
when nothing is registered.
|
|
84
84
|
"""
|
|
@@ -115,7 +115,7 @@ def dispatch_event(tag: int, name: str, *args: Any) -> bool:
|
|
|
115
115
|
|
|
116
116
|
Args:
|
|
117
117
|
tag: The view's reconciler-assigned tag.
|
|
118
|
-
name: Event name
|
|
118
|
+
name: Event name, the original prop name (``"on_click"``,
|
|
119
119
|
``"on_change"``, …) or a gesture channel (``"gesture:0"``).
|
|
120
120
|
*args: Positional arguments forwarded to the user callback,
|
|
121
121
|
preserving each prop's documented signature.
|
|
@@ -144,8 +144,8 @@ def extract_events(props: Dict[str, Any]) -> Tuple[Dict[str, Any], Dict[str, Cal
|
|
|
144
144
|
- ``gestures`` lists of gesture descriptors are serialized to plain
|
|
145
145
|
dicts (handlers wire recognizers from them) while their callbacks
|
|
146
146
|
are folded into per-gesture ``"gesture:<i>"`` routers.
|
|
147
|
-
- The resulting payload carries ``_pn_events``
|
|
148
|
-
event names present
|
|
147
|
+
- The resulting payload carries ``_pn_events`` (a frozenset of the
|
|
148
|
+
event names present), so handlers can wire listeners
|
|
149
149
|
conditionally and the prop differ can detect listener
|
|
150
150
|
addition/removal without comparing closures.
|
|
151
151
|
|
pythonnative/gestures.py
CHANGED
|
@@ -351,7 +351,7 @@ def serialize_gestures(
|
|
|
351
351
|
# guarantees identical semantics on both backends.
|
|
352
352
|
|
|
353
353
|
EmitFn = Callable[[int, Dict[str, Any]], None]
|
|
354
|
-
"""``emit(gesture_index, payload)
|
|
354
|
+
"""``emit(gesture_index, payload)``: the arbiter's output channel."""
|
|
355
355
|
|
|
356
356
|
|
|
357
357
|
class _VelocityTracker:
|
|
@@ -400,7 +400,7 @@ class _Recognizer:
|
|
|
400
400
|
def kind(self) -> str:
|
|
401
401
|
return str(self.config.get("kind", ""))
|
|
402
402
|
|
|
403
|
-
# Event hooks
|
|
403
|
+
# Event hooks: ``pointers`` maps pointer id -> (x, y).
|
|
404
404
|
def down(self, pointers: Dict[int, Tuple[float, float]], t: float) -> None:
|
|
405
405
|
pass
|
|
406
406
|
|
|
@@ -805,7 +805,7 @@ class GestureArbiter:
|
|
|
805
805
|
|
|
806
806
|
One arbiter serves one view. The host backend feeds it normalized
|
|
807
807
|
pointer events (positions in the view's coordinate space, times in
|
|
808
|
-
seconds
|
|
808
|
+
seconds, any monotonic clock) and provides an ``emit`` callback
|
|
809
809
|
that forwards ``(gesture_index, payload)`` pairs to
|
|
810
810
|
[`dispatch_event`][pythonnative.events.dispatch_event].
|
|
811
811
|
|
pythonnative/hooks.py
CHANGED
|
@@ -613,7 +613,7 @@ def use_query(
|
|
|
613
613
|
Args:
|
|
614
614
|
fetcher: Zero-arg ``async`` callable that resolves to the
|
|
615
615
|
current data.
|
|
616
|
-
deps: Dependency list
|
|
616
|
+
deps: Dependency list. Refetches whenever any entry changes.
|
|
617
617
|
initial: Optional starting value for ``data`` before the
|
|
618
618
|
first fetch completes.
|
|
619
619
|
|
|
@@ -698,7 +698,7 @@ class MutationCall(Generic[T]):
|
|
|
698
698
|
Returned by the second element of the
|
|
699
699
|
[`use_mutation`][pythonnative.use_mutation] tuple. Awaiting the
|
|
700
700
|
handle resolves to the mutator's return value (or re-raises its
|
|
701
|
-
exception); discarding the handle is safe
|
|
701
|
+
exception); discarding the handle is safe. Python won't warn
|
|
702
702
|
about an unawaited coroutine because this is a plain object.
|
|
703
703
|
|
|
704
704
|
Example:
|
|
@@ -899,6 +899,47 @@ def use_keyboard_height() -> float:
|
|
|
899
899
|
return platform_metrics.get_keyboard_height()
|
|
900
900
|
|
|
901
901
|
|
|
902
|
+
def use_color_scheme() -> str:
|
|
903
|
+
"""Return the effective color scheme and re-render when it changes.
|
|
904
|
+
|
|
905
|
+
Equivalent to React Native's ``useColorScheme``. The system value
|
|
906
|
+
is published by the screen host; an app-level override set through
|
|
907
|
+
[`appearance.set_color_scheme`][pythonnative.appearance.set_color_scheme]
|
|
908
|
+
takes precedence.
|
|
909
|
+
|
|
910
|
+
Returns:
|
|
911
|
+
``"light"`` or ``"dark"``.
|
|
912
|
+
|
|
913
|
+
Raises:
|
|
914
|
+
RuntimeError: If called outside a `@component` function.
|
|
915
|
+
|
|
916
|
+
Example:
|
|
917
|
+
```python
|
|
918
|
+
import pythonnative as pn
|
|
919
|
+
|
|
920
|
+
@pn.component
|
|
921
|
+
def Banner():
|
|
922
|
+
scheme = pn.use_color_scheme()
|
|
923
|
+
bg = "#000000" if scheme == "dark" else "#FFFFFF"
|
|
924
|
+
return pn.View(style=pn.style(background_color=bg))
|
|
925
|
+
```
|
|
926
|
+
"""
|
|
927
|
+
from . import appearance
|
|
928
|
+
|
|
929
|
+
ctx = _get_hook_state()
|
|
930
|
+
if ctx is None:
|
|
931
|
+
raise RuntimeError("use_color_scheme must be called inside a @component function")
|
|
932
|
+
|
|
933
|
+
_, set_tick = use_state(0)
|
|
934
|
+
|
|
935
|
+
def subscribe() -> Callable[[], None]:
|
|
936
|
+
return appearance.subscribe(lambda: set_tick(lambda n: n + 1))
|
|
937
|
+
|
|
938
|
+
use_effect(subscribe, [])
|
|
939
|
+
|
|
940
|
+
return appearance.get_color_scheme()
|
|
941
|
+
|
|
942
|
+
|
|
902
943
|
# ======================================================================
|
|
903
944
|
# Context
|
|
904
945
|
# ======================================================================
|
|
@@ -1068,7 +1109,7 @@ class NavigationHandle:
|
|
|
1068
1109
|
Wraps the host's push/pop primitives so screens can navigate
|
|
1069
1110
|
without knowing the underlying native navigation stack. The
|
|
1070
1111
|
typical user-facing surface is the declarative handle returned by
|
|
1071
|
-
a [`Stack`][pythonnative.create_stack_navigator]
|
|
1112
|
+
a [`Stack`][pythonnative.create_stack_navigator]; this class is
|
|
1072
1113
|
the lower-level fallback used when no navigator is rendered (and
|
|
1073
1114
|
as the bridge that declarative navigators delegate to when they
|
|
1074
1115
|
need to push real native screens).
|
pythonnative/hot_reload.py
CHANGED
|
@@ -17,7 +17,7 @@ Two strategies share the device-side surface:
|
|
|
17
17
|
the reconciler tree is walked and every component function whose
|
|
18
18
|
module was reloaded is swapped in place. Hook state, navigation
|
|
19
19
|
state, and even scroll positions survive because the underlying
|
|
20
|
-
``VNode`` objects are reused
|
|
20
|
+
``VNode`` objects are reused; the next render simply calls the
|
|
21
21
|
new function bodies through the old slots.
|
|
22
22
|
- **Full remount**: when the in-place swap fails (e.g. the new
|
|
23
23
|
module raised at import time, or a render exception bubbled out
|
|
@@ -197,8 +197,8 @@ class ModuleReloader:
|
|
|
197
197
|
|
|
198
198
|
Designed to be invoked from device-side glue when a hot-reload
|
|
199
199
|
push completes. All public methods are static; the class holds a
|
|
200
|
-
single piece of process-wide state
|
|
201
|
-
has most recently been applied to ``sys.modules``
|
|
200
|
+
single piece of process-wide state (the manifest version that
|
|
201
|
+
has most recently been applied to ``sys.modules``) so that
|
|
202
202
|
multiple screen hosts polling the same manifest do not each
|
|
203
203
|
re-execute the user-app modules. The first host to see a new
|
|
204
204
|
version pays the ``reload_modules`` cost; subsequent hosts on the
|
|
@@ -302,7 +302,7 @@ class ModuleReloader:
|
|
|
302
302
|
|
|
303
303
|
Returns:
|
|
304
304
|
The list of module names that are currently fresh in
|
|
305
|
-
``sys.modules
|
|
305
|
+
``sys.modules``, either freshly reloaded by this call, or
|
|
306
306
|
already reloaded by an earlier host for the same version.
|
|
307
307
|
"""
|
|
308
308
|
with ModuleReloader._reload_lock:
|
pythonnative/images.py
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
"""Shared image pipeline: async fetch with memory and disk caching.
|
|
2
|
+
|
|
3
|
+
Every platform's ``Image`` handler routes remote sources through this
|
|
4
|
+
module. The pipeline downloads on a daemon thread (never blocking the
|
|
5
|
+
UI), stores the raw bytes in a platform-appropriate disk cache keyed
|
|
6
|
+
by URL hash, deduplicates concurrent requests for the same URL, and
|
|
7
|
+
keeps a small in-memory LRU of recently fetched byte payloads so
|
|
8
|
+
scrolling back to an image doesn't touch the filesystem again.
|
|
9
|
+
|
|
10
|
+
Decoding stays platform-native (``BitmapFactory`` / ``UIImage`` /
|
|
11
|
+
``PhotoImage``): callbacks receive a *local file path*, which each
|
|
12
|
+
handler decodes with its platform's downsampling facilities.
|
|
13
|
+
|
|
14
|
+
Callbacks are delivered on the platform main thread via
|
|
15
|
+
[`call_on_main_thread`][pythonnative.runtime.call_on_main_thread], so
|
|
16
|
+
handlers can touch native views directly.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import hashlib
|
|
22
|
+
import os
|
|
23
|
+
import tempfile
|
|
24
|
+
import threading
|
|
25
|
+
import urllib.request
|
|
26
|
+
from collections import OrderedDict
|
|
27
|
+
from typing import Callable, Dict, List, Optional, Tuple
|
|
28
|
+
|
|
29
|
+
_DOWNLOAD_TIMEOUT_S = 30.0
|
|
30
|
+
_MEMORY_CACHE_MAX_BYTES = 16 * 1024 * 1024
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _default_cache_dir() -> str:
|
|
34
|
+
"""Resolve the on-disk cache directory for the current platform.
|
|
35
|
+
|
|
36
|
+
Android: the app's ``Context.getCacheDir()`` (purged by the OS
|
|
37
|
+
under storage pressure). iOS: ``~/Library/Caches`` (excluded from
|
|
38
|
+
backups). Desktop: a per-user directory under the system temp dir.
|
|
39
|
+
"""
|
|
40
|
+
try:
|
|
41
|
+
from .utils import IS_ANDROID
|
|
42
|
+
|
|
43
|
+
if IS_ANDROID:
|
|
44
|
+
from .utils import get_android_context
|
|
45
|
+
|
|
46
|
+
base = str(get_android_context().getCacheDir().getAbsolutePath())
|
|
47
|
+
return os.path.join(base, "pn_images")
|
|
48
|
+
except Exception:
|
|
49
|
+
pass
|
|
50
|
+
home = os.path.expanduser("~")
|
|
51
|
+
caches = os.path.join(home, "Library", "Caches")
|
|
52
|
+
if os.path.isdir(caches): # iOS / macOS
|
|
53
|
+
return os.path.join(caches, "pn_images")
|
|
54
|
+
return os.path.join(tempfile.gettempdir(), "pn_images")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class _ByteLru:
|
|
58
|
+
"""Tiny thread-safe LRU for raw image bytes."""
|
|
59
|
+
|
|
60
|
+
def __init__(self, max_bytes: int) -> None:
|
|
61
|
+
self._max = max_bytes
|
|
62
|
+
self._size = 0
|
|
63
|
+
self._items: "OrderedDict[str, bytes]" = OrderedDict()
|
|
64
|
+
self._lock = threading.Lock()
|
|
65
|
+
|
|
66
|
+
def get(self, key: str) -> Optional[bytes]:
|
|
67
|
+
with self._lock:
|
|
68
|
+
data = self._items.get(key)
|
|
69
|
+
if data is not None:
|
|
70
|
+
self._items.move_to_end(key)
|
|
71
|
+
return data
|
|
72
|
+
|
|
73
|
+
def put(self, key: str, data: bytes) -> None:
|
|
74
|
+
if len(data) > self._max:
|
|
75
|
+
return
|
|
76
|
+
with self._lock:
|
|
77
|
+
old = self._items.pop(key, None)
|
|
78
|
+
if old is not None:
|
|
79
|
+
self._size -= len(old)
|
|
80
|
+
self._items[key] = data
|
|
81
|
+
self._size += len(data)
|
|
82
|
+
while self._size > self._max and self._items:
|
|
83
|
+
_, evicted = self._items.popitem(last=False)
|
|
84
|
+
self._size -= len(evicted)
|
|
85
|
+
|
|
86
|
+
def clear(self) -> None:
|
|
87
|
+
with self._lock:
|
|
88
|
+
self._items.clear()
|
|
89
|
+
self._size = 0
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
_memory_cache = _ByteLru(_MEMORY_CACHE_MAX_BYTES)
|
|
93
|
+
_cache_dir: Optional[str] = None
|
|
94
|
+
# URL -> callbacks waiting on an in-flight download.
|
|
95
|
+
_in_flight: Dict[str, List[Tuple[Callable[[str], None], Callable[[str], None]]]] = {}
|
|
96
|
+
_in_flight_lock = threading.Lock()
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _get_cache_dir() -> str:
|
|
100
|
+
global _cache_dir
|
|
101
|
+
if _cache_dir is None:
|
|
102
|
+
_cache_dir = _default_cache_dir()
|
|
103
|
+
os.makedirs(_cache_dir, exist_ok=True)
|
|
104
|
+
return _cache_dir
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _cache_path(url: str) -> str:
|
|
108
|
+
digest = hashlib.sha256(url.encode("utf-8")).hexdigest()
|
|
109
|
+
ext = os.path.splitext(url.split("?", 1)[0])[1]
|
|
110
|
+
if len(ext) > 8 or "/" in ext:
|
|
111
|
+
ext = ""
|
|
112
|
+
return os.path.join(_get_cache_dir(), digest + ext)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _dispatch_main(fn: Callable[[], None]) -> None:
|
|
116
|
+
try:
|
|
117
|
+
from .runtime import call_on_main_thread
|
|
118
|
+
|
|
119
|
+
call_on_main_thread(fn)
|
|
120
|
+
except Exception:
|
|
121
|
+
try:
|
|
122
|
+
fn()
|
|
123
|
+
except Exception:
|
|
124
|
+
pass
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def fetch(
|
|
128
|
+
url: str,
|
|
129
|
+
on_ready: Callable[[str], None],
|
|
130
|
+
on_error: Optional[Callable[[str], None]] = None,
|
|
131
|
+
) -> None:
|
|
132
|
+
"""Fetch ``url`` into the cache and deliver a local file path.
|
|
133
|
+
|
|
134
|
+
Cache hits (memory or disk) still deliver asynchronously-consistent
|
|
135
|
+
behavior but resolve without a network round trip. Concurrent
|
|
136
|
+
requests for the same URL share one download. Callbacks run on the
|
|
137
|
+
platform main thread.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
url: The ``http(s)`` image URL.
|
|
141
|
+
on_ready: Called with the local file path once available.
|
|
142
|
+
on_error: Called with an error message if the download fails.
|
|
143
|
+
"""
|
|
144
|
+
path = _cache_path(url)
|
|
145
|
+
if _memory_cache.get(url) is not None or os.path.isfile(path):
|
|
146
|
+
_dispatch_main(lambda: on_ready(path))
|
|
147
|
+
return
|
|
148
|
+
|
|
149
|
+
err = on_error or (lambda _msg: None)
|
|
150
|
+
with _in_flight_lock:
|
|
151
|
+
waiters = _in_flight.get(url)
|
|
152
|
+
if waiters is not None:
|
|
153
|
+
waiters.append((on_ready, err))
|
|
154
|
+
return
|
|
155
|
+
_in_flight[url] = [(on_ready, err)]
|
|
156
|
+
|
|
157
|
+
def _worker() -> None:
|
|
158
|
+
error_msg: Optional[str] = None
|
|
159
|
+
try:
|
|
160
|
+
req = urllib.request.Request(url, headers={"User-Agent": "PythonNative"})
|
|
161
|
+
with urllib.request.urlopen(req, timeout=_DOWNLOAD_TIMEOUT_S) as resp:
|
|
162
|
+
data = resp.read()
|
|
163
|
+
_memory_cache.put(url, data)
|
|
164
|
+
tmp = path + ".part"
|
|
165
|
+
with open(tmp, "wb") as fh:
|
|
166
|
+
fh.write(data)
|
|
167
|
+
os.replace(tmp, path)
|
|
168
|
+
except Exception as exc:
|
|
169
|
+
error_msg = str(exc)
|
|
170
|
+
with _in_flight_lock:
|
|
171
|
+
callbacks = _in_flight.pop(url, [])
|
|
172
|
+
|
|
173
|
+
def _notify(ready_cb: Callable[[str], None], error_cb: Callable[[str], None], msg: Optional[str]) -> None:
|
|
174
|
+
if msg is None:
|
|
175
|
+
_dispatch_main(lambda: ready_cb(path))
|
|
176
|
+
else:
|
|
177
|
+
_dispatch_main(lambda: error_cb(msg))
|
|
178
|
+
|
|
179
|
+
for ready_cb, error_cb in callbacks:
|
|
180
|
+
_notify(ready_cb, error_cb, error_msg)
|
|
181
|
+
|
|
182
|
+
threading.Thread(target=_worker, name=f"pn-image-{path[-12:]}", daemon=True).start()
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def clear_cache() -> None:
|
|
186
|
+
"""Empty the memory cache and delete all cached image files."""
|
|
187
|
+
_memory_cache.clear()
|
|
188
|
+
try:
|
|
189
|
+
directory = _get_cache_dir()
|
|
190
|
+
for name in os.listdir(directory):
|
|
191
|
+
try:
|
|
192
|
+
os.remove(os.path.join(directory, name))
|
|
193
|
+
except OSError:
|
|
194
|
+
pass
|
|
195
|
+
except OSError:
|
|
196
|
+
pass
|
pythonnative/layout.py
CHANGED
|
@@ -9,7 +9,7 @@ The engine is invoked by the reconciler after each commit pass:
|
|
|
9
9
|
|
|
10
10
|
1. The reconciler maintains a parallel
|
|
11
11
|
[`LayoutNode`][pythonnative.layout.LayoutNode] tree (cached across
|
|
12
|
-
passes
|
|
12
|
+
passes: clean subtrees keep their nodes, dirty ones are rebuilt).
|
|
13
13
|
2. [`calculate_layout`][pythonnative.layout.calculate_layout] is called
|
|
14
14
|
with the viewport size; it recursively determines each node's
|
|
15
15
|
``(x, y, width, height)`` relative to its parent's coordinate space.
|
|
@@ -618,7 +618,7 @@ def _measure_node(
|
|
|
618
618
|
resolved_direction = _resolve_direction(style, direction)
|
|
619
619
|
|
|
620
620
|
# Incremental-layout memo: a clean node measured under identical
|
|
621
|
-
# inputs reuses its previous result without recursing
|
|
621
|
+
# inputs reuses its previous result without recursing; its whole
|
|
622
622
|
# subtree keeps the sizes from the prior pass.
|
|
623
623
|
memo = node._measure_memo
|
|
624
624
|
if (
|
|
@@ -863,7 +863,7 @@ def _layout_flex_children(
|
|
|
863
863
|
their line's cross size. The computed line structure is stored on
|
|
864
864
|
``parent._lines`` for the positioning pass.
|
|
865
865
|
|
|
866
|
-
Returns ``(used_main, used_cross)
|
|
866
|
+
Returns ``(used_main, used_cross)``, the total content size used
|
|
867
867
|
by the in-flow children, including inter-child gaps but excluding
|
|
868
868
|
the parent's own padding. The caller adds padding back in for the
|
|
869
869
|
container's outer size.
|
pythonnative/mutations.py
CHANGED
|
@@ -22,8 +22,8 @@ Op ordering rules (the reconciler guarantees these):
|
|
|
22
22
|
|
|
23
23
|
1. A `CreateOp` for a tag precedes any other op referencing that tag.
|
|
24
24
|
2. `InsertOp` ops appear after both the parent and child exist.
|
|
25
|
-
3. `DestroyOp` ops
|
|
26
|
-
|
|
25
|
+
3. `DestroyOp` ops are emitted children-first; handlers detach the view
|
|
26
|
+
from its parent as part of destruction.
|
|
27
27
|
4. `SetFrameOp` ops are only emitted for frames that actually changed
|
|
28
28
|
since the last layout pass (frame diffing).
|
|
29
29
|
"""
|
|
@@ -35,7 +35,6 @@ __all__ = [
|
|
|
35
35
|
"CreateOp",
|
|
36
36
|
"UpdateOp",
|
|
37
37
|
"InsertOp",
|
|
38
|
-
"RemoveOp",
|
|
39
38
|
"DestroyOp",
|
|
40
39
|
"SetFrameOp",
|
|
41
40
|
"Mutation",
|
|
@@ -49,7 +48,7 @@ class CreateOp:
|
|
|
49
48
|
Attributes:
|
|
50
49
|
tag: Unique integer identity assigned by the reconciler.
|
|
51
50
|
type_name: Element type name (e.g. ``"Text"``).
|
|
52
|
-
props: Initial *clean* props
|
|
51
|
+
props: Initial *clean* props; callables have already been
|
|
53
52
|
routed to the [`EventRegistry`][pythonnative.events.EventRegistry]
|
|
54
53
|
and replaced by the ``_pn_events`` name set.
|
|
55
54
|
"""
|
|
@@ -86,14 +85,6 @@ class InsertOp:
|
|
|
86
85
|
index: int
|
|
87
86
|
|
|
88
87
|
|
|
89
|
-
@dataclass(frozen=True)
|
|
90
|
-
class RemoveOp:
|
|
91
|
-
"""Detach the child view from the parent view (without destroying it)."""
|
|
92
|
-
|
|
93
|
-
parent_tag: int
|
|
94
|
-
child_tag: int
|
|
95
|
-
|
|
96
|
-
|
|
97
88
|
@dataclass(frozen=True)
|
|
98
89
|
class DestroyOp:
|
|
99
90
|
"""Release the native view registered under ``tag``.
|
|
@@ -126,5 +117,5 @@ class SetFrameOp:
|
|
|
126
117
|
return (self.x, self.y, self.width, self.height)
|
|
127
118
|
|
|
128
119
|
|
|
129
|
-
Mutation = Union[CreateOp, UpdateOp, InsertOp,
|
|
120
|
+
Mutation = Union[CreateOp, UpdateOp, InsertOp, DestroyOp, SetFrameOp]
|
|
130
121
|
"""Union of every op type carried by a commit transaction."""
|
|
@@ -152,7 +152,7 @@ def _ios_launch_picker(on_result: Callable[[Optional[str]], None], source: str)
|
|
|
152
152
|
_pending_delegates.pop(id(delegate), None)
|
|
153
153
|
on_result(None)
|
|
154
154
|
|
|
155
|
-
# Reference SEL/objc_method so the lint pass keeps the import
|
|
155
|
+
# Reference SEL/objc_method so the lint pass keeps the import;
|
|
156
156
|
# they're needed for the delegate class above.
|
|
157
157
|
_ = (SEL, objc_method)
|
|
158
158
|
except Exception:
|
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
Two interfaces live here:
|
|
4
4
|
|
|
5
|
-
- [`Haptics`][pythonnative.Haptics]
|
|
5
|
+
- [`Haptics`][pythonnative.Haptics]: semantic, iOS-style feedback
|
|
6
6
|
(impact / notification / selection) backed by
|
|
7
7
|
``UIFeedbackGenerator`` on iOS and ``VibrationEffect`` patterns on
|
|
8
8
|
Android.
|
|
9
|
-
- [`Vibration`][pythonnative.Vibration]
|
|
9
|
+
- [`Vibration`][pythonnative.Vibration]: a blunt "buzz for N
|
|
10
10
|
milliseconds" interface for cases where you want an explicit
|
|
11
11
|
duration.
|
|
12
12
|
|
|
@@ -167,7 +167,7 @@ def _android_get(on_result: Callable[[Optional[Coords]], None]) -> None:
|
|
|
167
167
|
Context = jclass("android.content.Context")
|
|
168
168
|
lm = ctx.getSystemService(Context.LOCATION_SERVICE)
|
|
169
169
|
|
|
170
|
-
# Try the most recent known fix first
|
|
170
|
+
# Try the most recent known fix first; it's instant and avoids
|
|
171
171
|
# the GPS warm-up delay.
|
|
172
172
|
try:
|
|
173
173
|
for provider in ("gps", "network", "passive"):
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
[`Permissions`][pythonnative.Permissions] normalizes the very different
|
|
4
4
|
iOS and Android permission models behind two calls:
|
|
5
5
|
|
|
6
|
-
- ``check(permission)
|
|
6
|
+
- ``check(permission)``: synchronous, returns a status string without
|
|
7
7
|
prompting.
|
|
8
|
-
- ``request(permission)
|
|
8
|
+
- ``request(permission)``: a coroutine that shows the system prompt
|
|
9
9
|
(if needed) and resolves to the resulting status.
|
|
10
10
|
|
|
11
11
|
Statuses are ``"granted"``, ``"denied"``, ``"blocked"`` (denied with
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""Encrypted key/value storage for secrets (tokens, credentials).
|
|
2
2
|
|
|
3
3
|
[`SecureStore`][pythonnative.SecureStore] persists small string values
|
|
4
|
-
in the iOS Keychain and Android ``EncryptedSharedPreferences
|
|
4
|
+
in the iOS Keychain and Android ``EncryptedSharedPreferences``, the
|
|
5
5
|
right place for auth tokens and other secrets that
|
|
6
6
|
[`AsyncStorage`][pythonnative.AsyncStorage] (plain, unencrypted) should
|
|
7
7
|
never hold.
|
|
@@ -37,7 +37,7 @@ import threading
|
|
|
37
37
|
import time
|
|
38
38
|
from typing import Any, Dict, Optional, Sequence, Tuple
|
|
39
39
|
|
|
40
|
-
from ..mutations import CreateOp, DestroyOp, InsertOp, Mutation,
|
|
40
|
+
from ..mutations import CreateOp, DestroyOp, InsertOp, Mutation, SetFrameOp, UpdateOp
|
|
41
41
|
from .base import ViewHandler
|
|
42
42
|
|
|
43
43
|
# ======================================================================
|
|
@@ -192,12 +192,6 @@ class NativeViewRegistry:
|
|
|
192
192
|
if parent is not None and child is not None:
|
|
193
193
|
parent.handler.insert_child(parent.view, child.view, op.index)
|
|
194
194
|
return
|
|
195
|
-
if isinstance(op, RemoveOp):
|
|
196
|
-
parent = self._records.get(op.parent_tag)
|
|
197
|
-
child = self._records.get(op.child_tag)
|
|
198
|
-
if parent is not None and child is not None:
|
|
199
|
-
parent.handler.remove_child(parent.view, child.view)
|
|
200
|
-
return
|
|
201
195
|
if isinstance(op, DestroyOp):
|
|
202
196
|
record = self._records.pop(op.tag, None)
|
|
203
197
|
if record is not None:
|