pythonnative 0.16.0__py3-none-any.whl → 0.17.1__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 +26 -2
- pythonnative/alerts.py +254 -68
- pythonnative/animated.py +224 -180
- pythonnative/hooks.py +271 -1
- pythonnative/layout.py +35 -1
- pythonnative/native_modules/camera.py +118 -121
- pythonnative/native_modules/file_system.py +3 -3
- pythonnative/native_modules/location.py +90 -109
- pythonnative/native_modules/notifications.py +148 -126
- pythonnative/native_views/android.py +187 -66
- pythonnative/native_views/ios.py +133 -42
- pythonnative/navigation.py +57 -2
- pythonnative/net.py +244 -0
- pythonnative/reconciler.py +38 -0
- pythonnative/runtime.py +487 -0
- pythonnative/screen.py +52 -3
- pythonnative/storage.py +409 -0
- {pythonnative-0.16.0.dist-info → pythonnative-0.17.1.dist-info}/METADATA +3 -1
- {pythonnative-0.16.0.dist-info → pythonnative-0.17.1.dist-info}/RECORD +23 -20
- {pythonnative-0.16.0.dist-info → pythonnative-0.17.1.dist-info}/WHEEL +0 -0
- {pythonnative-0.16.0.dist-info → pythonnative-0.17.1.dist-info}/entry_points.txt +0 -0
- {pythonnative-0.16.0.dist-info → pythonnative-0.17.1.dist-info}/licenses/LICENSE +0 -0
- {pythonnative-0.16.0.dist-info → pythonnative-0.17.1.dist-info}/top_level.txt +0 -0
pythonnative/animated.py
CHANGED
|
@@ -1,46 +1,42 @@
|
|
|
1
1
|
"""Animated values + animation drivers + animated component wrappers.
|
|
2
2
|
|
|
3
|
-
Modeled on React Native's
|
|
3
|
+
Modeled on React Native's ``Animated`` API but with an
|
|
4
|
+
``async``-aware completion contract. The core primitives are:
|
|
4
5
|
|
|
5
6
|
- [`AnimatedValue`][pythonnative.animated.AnimatedValue]: a numeric
|
|
6
7
|
cell with subscribers; animations mutate it over time.
|
|
7
8
|
- ``Animated.timing`` / ``Animated.spring`` / ``Animated.decay``:
|
|
8
|
-
animation factories.
|
|
9
|
+
animation factories. The objects they return implement
|
|
10
|
+
``__await__``, so you can write ``await Animated.timing(v, to=1.0)``
|
|
11
|
+
to suspend until the animation finishes.
|
|
9
12
|
- ``Animated.sequence`` / ``Animated.parallel`` / ``Animated.delay``:
|
|
10
|
-
composition.
|
|
13
|
+
composition; also awaitable.
|
|
11
14
|
- ``Animated.View`` / ``Animated.Text`` / ``Animated.Image``:
|
|
12
15
|
components whose ``style`` may contain ``AnimatedValue`` instances.
|
|
13
|
-
The component subscribes to the value during mount and forwards
|
|
14
|
-
changes directly to the underlying native handler's
|
|
15
|
-
``set_animated_property`` hook (bypassing the reconciler so
|
|
16
|
-
per-frame work doesn't go through full Python reconciliation).
|
|
17
16
|
|
|
18
17
|
Driver:
|
|
19
18
|
|
|
20
|
-
- A single background thread ticks at ~60 Hz, advancing every
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
-
|
|
25
|
-
|
|
26
|
-
``set_animated_property(view, prop, target, duration_ms, easing)``
|
|
27
|
-
call when the animation starts, so UIKit / Android can interpolate
|
|
28
|
-
at GPU 60 Hz without per-frame Python work. The Python ticker
|
|
29
|
-
then keeps the reactive ``AnimatedValue.value`` reading
|
|
30
|
-
approximately synchronized for any non-native consumers.
|
|
19
|
+
- A single background thread ticks at ~60 Hz, advancing every active
|
|
20
|
+
animation by ``dt``.
|
|
21
|
+
- Animations expose two APIs:
|
|
22
|
+
- ``handle.start()`` — fire-and-forget. Returns ``self``.
|
|
23
|
+
- ``await handle`` (or ``await handle.run()``) — wait for the
|
|
24
|
+
animation to complete; cancellation cancels the animation.
|
|
31
25
|
|
|
32
26
|
Example:
|
|
33
27
|
```python
|
|
34
28
|
import pythonnative as pn
|
|
35
29
|
|
|
30
|
+
|
|
36
31
|
@pn.component
|
|
37
32
|
def FadeIn():
|
|
38
|
-
opacity = pn.
|
|
33
|
+
opacity = pn.use_animated_value(0.0)
|
|
39
34
|
|
|
40
|
-
def fade_in():
|
|
41
|
-
pn.Animated.timing(opacity, to=1.0, duration=400)
|
|
35
|
+
async def fade_in():
|
|
36
|
+
await pn.Animated.timing(opacity, to=1.0, duration=400)
|
|
37
|
+
await pn.Animated.timing(opacity, to=0.5, duration=200)
|
|
42
38
|
|
|
43
|
-
pn.
|
|
39
|
+
pn.use_async_effect(fade_in, [])
|
|
44
40
|
|
|
45
41
|
return pn.Animated.View(
|
|
46
42
|
pn.Text("Hello!"),
|
|
@@ -51,6 +47,7 @@ Example:
|
|
|
51
47
|
|
|
52
48
|
from __future__ import annotations
|
|
53
49
|
|
|
50
|
+
import asyncio
|
|
54
51
|
import math
|
|
55
52
|
import threading
|
|
56
53
|
import time
|
|
@@ -58,14 +55,19 @@ from typing import Any, Callable, Dict, List, Optional, Tuple
|
|
|
58
55
|
|
|
59
56
|
from .element import Element
|
|
60
57
|
from .hooks import use_effect, use_ref
|
|
58
|
+
from .runtime import resolve_future
|
|
61
59
|
from .style import StyleProp, resolve_style
|
|
62
60
|
|
|
63
61
|
# Maximum frame rate at which the Python ticker drives animations.
|
|
64
|
-
# We aim for 60 Hz but back off when no animation is active.
|
|
65
62
|
_TARGET_FPS = 60.0
|
|
66
63
|
_FRAME_DT = 1.0 / _TARGET_FPS
|
|
67
64
|
|
|
68
|
-
#
|
|
65
|
+
# Upper bound on how much wall-clock time the animation loop will try to
|
|
66
|
+
# catch up on in a single iteration after thread starvation. At 60 fps
|
|
67
|
+
# this is ~333 ms of simulated motion; further drift is dropped to keep
|
|
68
|
+
# the loop responsive.
|
|
69
|
+
_MAX_CATCHUP_FRAMES = 20
|
|
70
|
+
|
|
69
71
|
_EASINGS: Dict[str, Callable[[float], float]] = {
|
|
70
72
|
"linear": lambda t: t,
|
|
71
73
|
"ease_in": lambda t: t * t,
|
|
@@ -104,13 +106,14 @@ def _resolve_easing(name: Any) -> Callable[[float], float]:
|
|
|
104
106
|
class AnimatedValue:
|
|
105
107
|
"""A subscribable numeric cell driven by animations.
|
|
106
108
|
|
|
107
|
-
Direct mutation via
|
|
108
|
-
|
|
109
|
+
Direct mutation via
|
|
110
|
+
[`set_value`][pythonnative.animated.AnimatedValue.set_value]
|
|
111
|
+
fires subscribers immediately; animations call ``set_value`` from
|
|
109
112
|
the ticker thread.
|
|
110
113
|
|
|
111
114
|
Subscribers are ``(prop_name, callback)`` tuples. Each animated
|
|
112
|
-
component (e.g.,
|
|
113
|
-
AnimatedValue prop in its style during mount.
|
|
115
|
+
component (e.g., ``Animated.View``) subscribes once per
|
|
116
|
+
``AnimatedValue`` prop in its style during mount.
|
|
114
117
|
"""
|
|
115
118
|
|
|
116
119
|
__slots__ = ("_value", "_subscribers", "_lock")
|
|
@@ -126,11 +129,7 @@ class AnimatedValue:
|
|
|
126
129
|
return self._value
|
|
127
130
|
|
|
128
131
|
def set_value(self, new_value: float) -> None:
|
|
129
|
-
"""Set the value immediately and fire all subscribers.
|
|
130
|
-
|
|
131
|
-
Used by user code for instant snaps; animations also call this
|
|
132
|
-
once per tick to update the value.
|
|
133
|
-
"""
|
|
132
|
+
"""Set the value immediately and fire all subscribers."""
|
|
134
133
|
new_value = float(new_value)
|
|
135
134
|
with self._lock:
|
|
136
135
|
self._value = new_value
|
|
@@ -146,8 +145,7 @@ class AnimatedValue:
|
|
|
146
145
|
|
|
147
146
|
Returns an unsubscribe callable. ``prop`` is metadata only —
|
|
148
147
|
it lets the subscriber differentiate this binding from others
|
|
149
|
-
on the same AnimatedValue
|
|
150
|
-
multiple props on multiple views).
|
|
148
|
+
on the same ``AnimatedValue``.
|
|
151
149
|
"""
|
|
152
150
|
with self._lock:
|
|
153
151
|
self._subscribers.append((prop, callback))
|
|
@@ -176,9 +174,9 @@ class AnimatedValue:
|
|
|
176
174
|
class _AnimationManager:
|
|
177
175
|
"""Single-threaded driver for all currently-running animations.
|
|
178
176
|
|
|
179
|
-
Holds a list of ``
|
|
180
|
-
|
|
181
|
-
|
|
177
|
+
Holds a list of ``_RunningAnimation`` instances and ticks them at
|
|
178
|
+
~60 Hz. The thread starts on first use and idles when nothing is
|
|
179
|
+
active.
|
|
182
180
|
"""
|
|
183
181
|
|
|
184
182
|
def __init__(self) -> None:
|
|
@@ -207,6 +205,20 @@ class _AnimationManager:
|
|
|
207
205
|
|
|
208
206
|
def _loop(self) -> None:
|
|
209
207
|
last = time.monotonic()
|
|
208
|
+
# Clamping the per-tick dt is important for numerical stability:
|
|
209
|
+
# an underdamped spring with a 0.3 s step explodes immediately,
|
|
210
|
+
# and on iOS/Android the animation thread can be starved for
|
|
211
|
+
# several frames during render bursts. We integrate physics on a
|
|
212
|
+
# clamped dt (max 2 target frames) and sub-step when wall-clock
|
|
213
|
+
# has advanced more than that, so the perceived motion still
|
|
214
|
+
# tracks real time at most a couple of frames behind. After an
|
|
215
|
+
# extreme starvation (e.g. the app was backgrounded for seconds)
|
|
216
|
+
# we cap the catch-up at ``_MAX_CATCHUP_FRAMES`` worth of
|
|
217
|
+
# physics; any further wall-clock drift is dropped on the floor,
|
|
218
|
+
# which keeps the loop responsive instead of spinning forward
|
|
219
|
+
# through hundreds of substeps.
|
|
220
|
+
max_step = _FRAME_DT * 2.0
|
|
221
|
+
max_catchup = _FRAME_DT * _MAX_CATCHUP_FRAMES
|
|
210
222
|
while not self._stopped:
|
|
211
223
|
now = time.monotonic()
|
|
212
224
|
dt = now - last
|
|
@@ -214,17 +226,22 @@ class _AnimationManager:
|
|
|
214
226
|
with self._lock:
|
|
215
227
|
active = list(self._animations)
|
|
216
228
|
if not active:
|
|
217
|
-
# Idle: sleep longer until something starts.
|
|
218
229
|
time.sleep(0.05)
|
|
219
230
|
last = time.monotonic()
|
|
220
231
|
continue
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
232
|
+
remaining = min(dt, max_catchup)
|
|
233
|
+
while remaining > 0.0:
|
|
234
|
+
step = remaining if remaining <= max_step else max_step
|
|
235
|
+
remaining -= step
|
|
236
|
+
for anim in active:
|
|
237
|
+
if getattr(anim, "_completed", False):
|
|
238
|
+
continue
|
|
239
|
+
try:
|
|
240
|
+
finished = anim.advance(step)
|
|
241
|
+
except Exception:
|
|
242
|
+
finished = True
|
|
243
|
+
if finished:
|
|
244
|
+
self.remove(anim)
|
|
228
245
|
time.sleep(_FRAME_DT)
|
|
229
246
|
|
|
230
247
|
|
|
@@ -237,21 +254,28 @@ _manager = _AnimationManager()
|
|
|
237
254
|
|
|
238
255
|
|
|
239
256
|
class _RunningAnimation:
|
|
240
|
-
"""Base class for in-flight animations; advance() returns True when done."""
|
|
257
|
+
"""Base class for in-flight animations; ``advance()`` returns True when done."""
|
|
241
258
|
|
|
242
|
-
def __init__(self, value: AnimatedValue
|
|
259
|
+
def __init__(self, value: AnimatedValue) -> None:
|
|
243
260
|
self.value = value
|
|
244
|
-
self.
|
|
261
|
+
self._completion_futures: List[asyncio.Future[None]] = []
|
|
262
|
+
self._completed = False
|
|
263
|
+
|
|
264
|
+
def add_completion_future(self, future: asyncio.Future[None]) -> None:
|
|
265
|
+
"""Register ``future`` to be resolved when the animation ends."""
|
|
266
|
+
self._completion_futures.append(future)
|
|
267
|
+
if self._completed:
|
|
268
|
+
resolve_future(future, None)
|
|
245
269
|
|
|
246
270
|
def advance(self, dt: float) -> bool:
|
|
247
271
|
raise NotImplementedError
|
|
248
272
|
|
|
249
273
|
def _finish(self) -> None:
|
|
250
|
-
if self.
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
274
|
+
if self._completed:
|
|
275
|
+
return
|
|
276
|
+
self._completed = True
|
|
277
|
+
for fut in self._completion_futures:
|
|
278
|
+
resolve_future(fut, None)
|
|
255
279
|
|
|
256
280
|
|
|
257
281
|
class _TimingAnimation(_RunningAnimation):
|
|
@@ -261,9 +285,8 @@ class _TimingAnimation(_RunningAnimation):
|
|
|
261
285
|
to: float,
|
|
262
286
|
duration: float,
|
|
263
287
|
easing: Callable[[float], float],
|
|
264
|
-
on_complete: Optional[Callable[[], None]],
|
|
265
288
|
) -> None:
|
|
266
|
-
super().__init__(value
|
|
289
|
+
super().__init__(value)
|
|
267
290
|
self._from = value.value
|
|
268
291
|
self._to = float(to)
|
|
269
292
|
self._duration = max(0.001, float(duration) / 1000.0)
|
|
@@ -292,9 +315,8 @@ class _SpringAnimation(_RunningAnimation):
|
|
|
292
315
|
stiffness: float,
|
|
293
316
|
damping: float,
|
|
294
317
|
mass: float,
|
|
295
|
-
on_complete: Optional[Callable[[], None]],
|
|
296
318
|
) -> None:
|
|
297
|
-
super().__init__(value
|
|
319
|
+
super().__init__(value)
|
|
298
320
|
self._to = float(to)
|
|
299
321
|
self._velocity = 0.0
|
|
300
322
|
self._stiffness = float(stiffness)
|
|
@@ -316,20 +338,13 @@ class _SpringAnimation(_RunningAnimation):
|
|
|
316
338
|
|
|
317
339
|
|
|
318
340
|
class _DecayAnimation(_RunningAnimation):
|
|
319
|
-
def __init__(
|
|
320
|
-
|
|
321
|
-
value: AnimatedValue,
|
|
322
|
-
velocity: float,
|
|
323
|
-
deceleration: float,
|
|
324
|
-
on_complete: Optional[Callable[[], None]],
|
|
325
|
-
) -> None:
|
|
326
|
-
super().__init__(value, on_complete)
|
|
341
|
+
def __init__(self, value: AnimatedValue, velocity: float, deceleration: float) -> None:
|
|
342
|
+
super().__init__(value)
|
|
327
343
|
self._velocity = float(velocity)
|
|
328
344
|
self._deceleration = float(deceleration)
|
|
329
345
|
self._rest_threshold = 0.001
|
|
330
346
|
|
|
331
347
|
def advance(self, dt: float) -> bool:
|
|
332
|
-
# Exponential decay of velocity.
|
|
333
348
|
self._velocity *= math.exp(-self._deceleration * dt * 1000.0)
|
|
334
349
|
new_x = self.value.value + self._velocity * dt
|
|
335
350
|
self.value.set_value(new_x)
|
|
@@ -339,94 +354,150 @@ class _DecayAnimation(_RunningAnimation):
|
|
|
339
354
|
return False
|
|
340
355
|
|
|
341
356
|
|
|
342
|
-
class
|
|
343
|
-
|
|
357
|
+
class _DelayAnimation(_RunningAnimation):
|
|
358
|
+
def __init__(self, duration_ms: float) -> None:
|
|
359
|
+
super().__init__(AnimatedValue(0.0))
|
|
360
|
+
self._elapsed = 0.0
|
|
361
|
+
self._duration = max(0.001, duration_ms / 1000.0)
|
|
344
362
|
|
|
345
|
-
def
|
|
346
|
-
self.
|
|
347
|
-
self.
|
|
363
|
+
def advance(self, dt: float) -> bool:
|
|
364
|
+
self._elapsed += dt
|
|
365
|
+
if self._elapsed >= self._duration:
|
|
366
|
+
self._finish()
|
|
367
|
+
return True
|
|
368
|
+
return False
|
|
348
369
|
|
|
349
|
-
def start(self, on_complete: Optional[Callable[[], None]] = None) -> None:
|
|
350
|
-
if self._mode == "parallel":
|
|
351
|
-
remaining = [len(self._items)]
|
|
352
|
-
lock = threading.Lock()
|
|
353
|
-
|
|
354
|
-
def _one_done() -> None:
|
|
355
|
-
with lock:
|
|
356
|
-
remaining[0] -= 1
|
|
357
|
-
if remaining[0] <= 0 and on_complete is not None:
|
|
358
|
-
try:
|
|
359
|
-
on_complete()
|
|
360
|
-
except Exception:
|
|
361
|
-
pass
|
|
362
|
-
|
|
363
|
-
for item in self._items:
|
|
364
|
-
if item is None:
|
|
365
|
-
_one_done()
|
|
366
|
-
continue
|
|
367
|
-
try:
|
|
368
|
-
item.start(_one_done)
|
|
369
|
-
except Exception:
|
|
370
|
-
_one_done()
|
|
371
|
-
return
|
|
372
370
|
|
|
373
|
-
|
|
374
|
-
|
|
371
|
+
# ======================================================================
|
|
372
|
+
# Public animation handles
|
|
373
|
+
# ======================================================================
|
|
375
374
|
|
|
376
|
-
def _next() -> None:
|
|
377
|
-
i = index[0]
|
|
378
|
-
if i >= len(self._items):
|
|
379
|
-
if on_complete is not None:
|
|
380
|
-
try:
|
|
381
|
-
on_complete()
|
|
382
|
-
except Exception:
|
|
383
|
-
pass
|
|
384
|
-
return
|
|
385
|
-
item = self._items[i]
|
|
386
|
-
index[0] += 1
|
|
387
|
-
if item is None:
|
|
388
|
-
_next()
|
|
389
|
-
return
|
|
390
|
-
try:
|
|
391
|
-
item.start(_next)
|
|
392
|
-
except Exception:
|
|
393
|
-
_next()
|
|
394
375
|
|
|
395
|
-
|
|
376
|
+
class _AwaitableAnimation:
|
|
377
|
+
"""Base for awaitable animation handles.
|
|
378
|
+
|
|
379
|
+
Subclasses implement :meth:`start` and :meth:`stop`. Awaiting the
|
|
380
|
+
handle (``await handle``) starts the animation if necessary and
|
|
381
|
+
suspends until it completes. Cancelling the awaiting task calls
|
|
382
|
+
:meth:`stop`.
|
|
383
|
+
|
|
384
|
+
Calling :meth:`start` returns ``self`` so handles can be chained
|
|
385
|
+
or stashed: ``handle = pn.Animated.timing(...).start()``.
|
|
386
|
+
"""
|
|
387
|
+
|
|
388
|
+
def start(self) -> "_AwaitableAnimation":
|
|
389
|
+
raise NotImplementedError
|
|
396
390
|
|
|
397
391
|
def stop(self) -> None:
|
|
398
|
-
|
|
392
|
+
raise NotImplementedError
|
|
393
|
+
|
|
394
|
+
def run(self) -> "_AwaitableAnimation":
|
|
395
|
+
"""Return ``self`` for explicit ``await handle.run()`` style.
|
|
396
|
+
|
|
397
|
+
Equivalent to ``await handle`` directly; provided because some
|
|
398
|
+
readers prefer the slightly more explicit form, particularly
|
|
399
|
+
when storing the awaitable before resolving it.
|
|
400
|
+
"""
|
|
401
|
+
return self
|
|
402
|
+
|
|
403
|
+
async def _drive(self) -> None:
|
|
404
|
+
raise NotImplementedError
|
|
405
|
+
|
|
406
|
+
def __await__(self) -> Any:
|
|
407
|
+
try:
|
|
408
|
+
asyncio.get_running_loop()
|
|
409
|
+
except RuntimeError as exc:
|
|
410
|
+
raise RuntimeError(
|
|
411
|
+
"Animations can only be awaited from inside an asyncio task; "
|
|
412
|
+
"use handle.start() to fire-and-forget instead."
|
|
413
|
+
) from exc
|
|
414
|
+
|
|
415
|
+
async def _runner() -> None:
|
|
399
416
|
try:
|
|
400
|
-
|
|
401
|
-
except
|
|
402
|
-
|
|
417
|
+
await self._drive()
|
|
418
|
+
except asyncio.CancelledError:
|
|
419
|
+
self.stop()
|
|
420
|
+
raise
|
|
421
|
+
|
|
422
|
+
return _runner().__await__()
|
|
403
423
|
|
|
404
424
|
|
|
405
|
-
class _AnimationHandle:
|
|
406
|
-
"""Public handle returned by
|
|
425
|
+
class _AnimationHandle(_AwaitableAnimation):
|
|
426
|
+
"""Public handle returned by ``Animated.timing`` / ``.spring`` / ``.decay``.
|
|
407
427
|
|
|
408
|
-
Wraps a
|
|
409
|
-
a fresh in-flight animation (matches
|
|
410
|
-
return value is reusable).
|
|
428
|
+
Wraps a ``_RunningAnimation`` factory so each ``.start()`` call
|
|
429
|
+
creates a fresh in-flight animation (matches React Native — the
|
|
430
|
+
``Animated.timing`` return value is reusable).
|
|
411
431
|
"""
|
|
412
432
|
|
|
413
|
-
def __init__(self, factory: Callable[[
|
|
433
|
+
def __init__(self, factory: Callable[[], _RunningAnimation]) -> None:
|
|
414
434
|
self._factory = factory
|
|
415
435
|
self._current: Optional[_RunningAnimation] = None
|
|
416
436
|
|
|
417
|
-
def start(self
|
|
418
|
-
"""Begin the animation
|
|
437
|
+
def start(self) -> "_AnimationHandle":
|
|
438
|
+
"""Begin the animation. Returns ``self`` for chaining."""
|
|
419
439
|
self.stop()
|
|
420
|
-
anim = self._factory(
|
|
440
|
+
anim = self._factory()
|
|
421
441
|
self._current = anim
|
|
422
442
|
_manager.add(anim)
|
|
443
|
+
return self
|
|
423
444
|
|
|
424
445
|
def stop(self) -> None:
|
|
425
446
|
"""Cancel the running instance (no-op if not running)."""
|
|
426
447
|
if self._current is not None:
|
|
448
|
+
self._current._finish()
|
|
427
449
|
_manager.remove(self._current)
|
|
428
450
|
self._current = None
|
|
429
451
|
|
|
452
|
+
async def _drive(self) -> None:
|
|
453
|
+
if self._current is None:
|
|
454
|
+
self.start()
|
|
455
|
+
loop = asyncio.get_running_loop()
|
|
456
|
+
future: asyncio.Future[None] = loop.create_future()
|
|
457
|
+
assert self._current is not None
|
|
458
|
+
self._current.add_completion_future(future)
|
|
459
|
+
await future
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
class _CompositeAnimation(_AwaitableAnimation):
|
|
463
|
+
"""Run a list of animations in sequence or in parallel."""
|
|
464
|
+
|
|
465
|
+
def __init__(self, items: List[Any], mode: str) -> None:
|
|
466
|
+
self._items = list(items)
|
|
467
|
+
self._mode = mode
|
|
468
|
+
|
|
469
|
+
def start(self) -> "_CompositeAnimation":
|
|
470
|
+
"""Schedule the composite on the framework runtime, fire-and-forget."""
|
|
471
|
+
from .runtime import run_async
|
|
472
|
+
|
|
473
|
+
run_async(self._drive())
|
|
474
|
+
return self
|
|
475
|
+
|
|
476
|
+
def stop(self) -> None:
|
|
477
|
+
for item in self._items:
|
|
478
|
+
try:
|
|
479
|
+
item.stop()
|
|
480
|
+
except Exception:
|
|
481
|
+
pass
|
|
482
|
+
|
|
483
|
+
async def _drive(self) -> None:
|
|
484
|
+
if self._mode == "parallel":
|
|
485
|
+
await asyncio.gather(*(self._await_item(item) for item in self._items))
|
|
486
|
+
return
|
|
487
|
+
for item in self._items:
|
|
488
|
+
await self._await_item(item)
|
|
489
|
+
|
|
490
|
+
@staticmethod
|
|
491
|
+
async def _await_item(item: Any) -> None:
|
|
492
|
+
if item is None:
|
|
493
|
+
return
|
|
494
|
+
if isinstance(item, _AwaitableAnimation):
|
|
495
|
+
await item
|
|
496
|
+
else:
|
|
497
|
+
# Plain awaitables and coroutines are supported too — lets
|
|
498
|
+
# users mix in ``asyncio.sleep`` or other awaitables.
|
|
499
|
+
await item
|
|
500
|
+
|
|
430
501
|
|
|
431
502
|
# ======================================================================
|
|
432
503
|
# Animated component wrappers
|
|
@@ -434,10 +505,10 @@ class _AnimationHandle:
|
|
|
434
505
|
|
|
435
506
|
|
|
436
507
|
def _resolve_style_with_values(style: StyleProp) -> Tuple[Dict[str, Any], Dict[str, AnimatedValue]]:
|
|
437
|
-
"""
|
|
508
|
+
"""Split ``style`` into a plain dict and animated bindings.
|
|
438
509
|
|
|
439
|
-
AnimatedValue entries in the style are replaced with their
|
|
440
|
-
|
|
510
|
+
AnimatedValue entries in the style are replaced with their current
|
|
511
|
+
numeric value in ``plain_style`` and recorded in
|
|
441
512
|
``animated_bindings`` so the wrapping component can subscribe
|
|
442
513
|
after mount.
|
|
443
514
|
"""
|
|
@@ -457,11 +528,7 @@ def _make_animated_factory(
|
|
|
457
528
|
element_type: str,
|
|
458
529
|
accept_children: bool,
|
|
459
530
|
) -> Callable[..., Element]:
|
|
460
|
-
"""Build an animated wrapper for ``element_type``.
|
|
461
|
-
|
|
462
|
-
The returned factory is used as the public
|
|
463
|
-
``Animated.View`` / ``Animated.Text`` / ``Animated.Image``.
|
|
464
|
-
"""
|
|
531
|
+
"""Build an animated wrapper for ``element_type``."""
|
|
465
532
|
from .hooks import component # local import to avoid cycle
|
|
466
533
|
|
|
467
534
|
@component
|
|
@@ -482,9 +549,9 @@ def _make_animated_factory(
|
|
|
482
549
|
return lambda: None
|
|
483
550
|
|
|
484
551
|
for prop, value in bindings.items():
|
|
485
|
-
|
|
552
|
+
|
|
486
553
|
def _on_change(new_val: float, _prop: str = prop, _view: Any = view) -> None:
|
|
487
|
-
handler = _get_handler_for(
|
|
554
|
+
handler = _get_handler_for(_view)
|
|
488
555
|
if handler is None:
|
|
489
556
|
return
|
|
490
557
|
setter = getattr(handler, "set_animated_property", None)
|
|
@@ -516,7 +583,6 @@ def _make_animated_factory(
|
|
|
516
583
|
if element_type == "Image":
|
|
517
584
|
source = args[0] if args else kwargs.pop("source", "")
|
|
518
585
|
return _Image(source, style=plain_style, ref=ref)
|
|
519
|
-
# View
|
|
520
586
|
children = list(args) if accept_children else []
|
|
521
587
|
return _View(*children, style=plain_style, ref=ref)
|
|
522
588
|
|
|
@@ -524,29 +590,19 @@ def _make_animated_factory(
|
|
|
524
590
|
|
|
525
591
|
|
|
526
592
|
def _animated_prop_name(prop: str) -> str:
|
|
527
|
-
"""Map a style key to the name expected by
|
|
593
|
+
"""Map a style key to the name expected by ``set_animated_property``."""
|
|
528
594
|
if prop == "opacity":
|
|
529
595
|
return "opacity"
|
|
530
596
|
if prop == "background_color":
|
|
531
597
|
return "background_color"
|
|
532
|
-
# Transform shorthand keys: ``translate_x``, ``translate_y``,
|
|
533
|
-
# ``scale``, ``scale_x``, ``scale_y``, ``rotate``.
|
|
534
598
|
if prop in ("translate_x", "translate_y", "scale", "scale_x", "scale_y", "rotate"):
|
|
535
599
|
return prop
|
|
536
600
|
return prop
|
|
537
601
|
|
|
538
602
|
|
|
539
603
|
def _get_handler_for(native_view: Any) -> Any:
|
|
540
|
-
"""Best-effort lookup of the registered handler for ``native_view``.
|
|
541
|
-
|
|
542
|
-
Animated bindings need a handler reference to call
|
|
543
|
-
`set_animated_property`. Since the registry is keyed by element
|
|
544
|
-
type and we only have the native view, we fall back to looking
|
|
545
|
-
up the most recently registered "View" handler — works in
|
|
546
|
-
practice because all animated targets are flex containers,
|
|
547
|
-
images, or text views, and every iOS/Android handler subclass
|
|
548
|
-
inherits the same `set_animated_property` from the base.
|
|
549
|
-
"""
|
|
604
|
+
"""Best-effort lookup of the registered handler for ``native_view``."""
|
|
605
|
+
del native_view
|
|
550
606
|
try:
|
|
551
607
|
from .native_views import get_registry
|
|
552
608
|
|
|
@@ -570,8 +626,8 @@ def _get_handler_for(native_view: Any) -> Any:
|
|
|
570
626
|
class _AnimatedNamespace:
|
|
571
627
|
"""Public ``Animated`` namespace.
|
|
572
628
|
|
|
573
|
-
Exposes the
|
|
574
|
-
component wrappers (
|
|
629
|
+
Exposes the ``Value`` type, animation factories, composers, and
|
|
630
|
+
component wrappers (``View``, ``Text``, ``Image``).
|
|
575
631
|
"""
|
|
576
632
|
|
|
577
633
|
Value = AnimatedValue
|
|
@@ -586,8 +642,8 @@ class _AnimatedNamespace:
|
|
|
586
642
|
) -> _AnimationHandle:
|
|
587
643
|
"""Linearly interpolate ``value`` to ``to`` over ``duration`` ms."""
|
|
588
644
|
|
|
589
|
-
def _factory(
|
|
590
|
-
return _TimingAnimation(value, to, duration, _resolve_easing(easing)
|
|
645
|
+
def _factory() -> _RunningAnimation:
|
|
646
|
+
return _TimingAnimation(value, to, duration, _resolve_easing(easing))
|
|
591
647
|
|
|
592
648
|
return _AnimationHandle(_factory)
|
|
593
649
|
|
|
@@ -602,8 +658,8 @@ class _AnimatedNamespace:
|
|
|
602
658
|
) -> _AnimationHandle:
|
|
603
659
|
"""Run a damped harmonic spring toward ``to``."""
|
|
604
660
|
|
|
605
|
-
def _factory(
|
|
606
|
-
return _SpringAnimation(value, to, stiffness, damping, mass
|
|
661
|
+
def _factory() -> _RunningAnimation:
|
|
662
|
+
return _SpringAnimation(value, to, stiffness, damping, mass)
|
|
607
663
|
|
|
608
664
|
return _AnimationHandle(_factory)
|
|
609
665
|
|
|
@@ -616,8 +672,8 @@ class _AnimatedNamespace:
|
|
|
616
672
|
) -> _AnimationHandle:
|
|
617
673
|
"""Decelerate ``value`` from its current velocity until it rests."""
|
|
618
674
|
|
|
619
|
-
def _factory(
|
|
620
|
-
return _DecayAnimation(value, velocity, deceleration
|
|
675
|
+
def _factory() -> _RunningAnimation:
|
|
676
|
+
return _DecayAnimation(value, velocity, deceleration)
|
|
621
677
|
|
|
622
678
|
return _AnimationHandle(_factory)
|
|
623
679
|
|
|
@@ -635,21 +691,8 @@ class _AnimatedNamespace:
|
|
|
635
691
|
def delay(duration: float) -> _AnimationHandle:
|
|
636
692
|
"""Wait ``duration`` ms before continuing in a sequence."""
|
|
637
693
|
|
|
638
|
-
def _factory(
|
|
639
|
-
|
|
640
|
-
def __init__(self, on_complete: Optional[Callable[[], None]]) -> None:
|
|
641
|
-
super().__init__(AnimatedValue(0.0), on_complete)
|
|
642
|
-
self._elapsed = 0.0
|
|
643
|
-
self._duration = max(0.001, duration / 1000.0)
|
|
644
|
-
|
|
645
|
-
def advance(self, dt: float) -> bool:
|
|
646
|
-
self._elapsed += dt
|
|
647
|
-
if self._elapsed >= self._duration:
|
|
648
|
-
self._finish()
|
|
649
|
-
return True
|
|
650
|
-
return False
|
|
651
|
-
|
|
652
|
-
return _Delay(on_complete)
|
|
694
|
+
def _factory() -> _RunningAnimation:
|
|
695
|
+
return _DelayAnimation(duration)
|
|
653
696
|
|
|
654
697
|
return _AnimationHandle(_factory)
|
|
655
698
|
|
|
@@ -662,7 +705,7 @@ Animated = _AnimatedNamespace()
|
|
|
662
705
|
|
|
663
706
|
|
|
664
707
|
def use_animated_value(initial: float = 0.0) -> AnimatedValue:
|
|
665
|
-
"""Return an [`AnimatedValue`][pythonnative.AnimatedValue]
|
|
708
|
+
"""Return an [`AnimatedValue`][pythonnative.AnimatedValue] that is stable across renders.
|
|
666
709
|
|
|
667
710
|
Convenience wrapper for the common pattern
|
|
668
711
|
``pn.use_memo(lambda: AnimatedValue(initial), [])``. The same
|
|
@@ -679,14 +722,15 @@ def use_animated_value(initial: float = 0.0) -> AnimatedValue:
|
|
|
679
722
|
```python
|
|
680
723
|
import pythonnative as pn
|
|
681
724
|
|
|
725
|
+
|
|
682
726
|
@pn.component
|
|
683
727
|
def FadeIn():
|
|
684
728
|
opacity = pn.use_animated_value(0.0)
|
|
685
729
|
|
|
686
|
-
def fade_in():
|
|
687
|
-
pn.Animated.timing(opacity, to=1.0, duration=300)
|
|
730
|
+
async def fade_in():
|
|
731
|
+
await pn.Animated.timing(opacity, to=1.0, duration=300)
|
|
688
732
|
|
|
689
|
-
pn.
|
|
733
|
+
pn.use_async_effect(fade_in, [])
|
|
690
734
|
return pn.Animated.View(
|
|
691
735
|
pn.Text("Hello"),
|
|
692
736
|
style=pn.style(opacity=opacity),
|