working condition

This commit is contained in:
Yura 2024-09-15 20:57:02 +03:00
parent 417e54da96
commit 511e0b0379
517 changed files with 29187 additions and 32696 deletions

View file

@ -0,0 +1,42 @@
__all__ = (
'MotionEventAlreadyEndedError',
'anim_attrs',
'anim_attrs_abbr',
'anim_with_dt',
'anim_with_dt_et',
'anim_with_dt_et_ratio',
'anim_with_et',
'anim_with_ratio',
'animate',
'create_texture_from_text',
'event',
'fade_transition',
'interpolate',
'move_on_after',
'n_frames',
'repeat_sleeping',
'rest_of_touch_events',
'run_in_executor',
'run_in_thread',
'sleep',
'sleep_free',
'suppress_event',
'sync_attr',
'sync_attrs',
'touch_up_event',
'transform',
'watch_touch',
)
from asyncgui import *
from ._exceptions import MotionEventAlreadyEndedError
from ._sleep import sleep, sleep_free, repeat_sleeping, move_on_after
from ._event import event
from ._anim_with_xxx import anim_with_dt, anim_with_et, anim_with_ratio, anim_with_dt_et, anim_with_dt_et_ratio
from ._animation import animate
from ._anim_attrs import anim_attrs, anim_attrs_abbr
from ._interpolate import interpolate, fade_transition
from ._touch import watch_touch, rest_of_touch_events, rest_of_touch_moves, touch_up_event
from ._threading import run_in_executor, run_in_thread
from ._n_frames import n_frames
from ._utils import transform, suppress_event, create_texture_from_text, sync_attr, sync_attrs

View file

@ -0,0 +1,114 @@
__all__ = ('anim_attrs', 'anim_attrs_abbr', )
import typing as T
import types
from functools import partial
import kivy.clock
from kivy.animation import AnimationTransition
import asyncgui
def _update(setattr, zip, min, obj, duration, transition, output_seq_type, anim_params, task, p_time, dt):
time = p_time[0] + dt
p_time[0] = time
# calculate progression
progress = min(1., time / duration)
t = transition(progress)
# apply progression on obj
for attr_name, org_value, slope, is_seq in anim_params:
if is_seq:
new_value = output_seq_type(
slope_elem * t + org_elem
for org_elem, slope_elem in zip(org_value, slope)
)
setattr(obj, attr_name, new_value)
else:
setattr(obj, attr_name, slope * t + org_value)
# time to stop ?
if progress >= 1.:
task._step()
return False
_update = partial(_update, setattr, zip, min)
@types.coroutine
def _anim_attrs(
obj, duration, step, transition, output_seq_type, animated_properties,
getattr=getattr, isinstance=isinstance, tuple=tuple, str=str, partial=partial, native_seq_types=(tuple, list),
zip=zip, Clock=kivy.clock.Clock, AnimationTransition=AnimationTransition,
_update=_update, _current_task=asyncgui._current_task, _sleep_forever=asyncgui._sleep_forever, /):
if isinstance(transition, str):
transition = getattr(AnimationTransition, transition)
# get current values & calculate slopes
anim_params = tuple(
(
org_value := getattr(obj, attr_name),
is_seq := isinstance(org_value, native_seq_types),
(
org_value := tuple(org_value),
slope := tuple(goal_elem - org_elem for goal_elem, org_elem in zip(goal_value, org_value)),
) if is_seq else (slope := goal_value - org_value),
) and (attr_name, org_value, slope, is_seq, )
for attr_name, goal_value in animated_properties.items()
)
try:
clock_event = Clock.schedule_interval(
partial(_update, obj, duration, transition, output_seq_type, anim_params, (yield _current_task)[0][0],
[0., ]),
step,
)
yield _sleep_forever
finally:
clock_event.cancel()
def anim_attrs(obj, *, duration=1.0, step=0, transition=AnimationTransition.linear, output_seq_type=tuple,
**animated_properties) -> T.Awaitable:
'''
Animates attibutes of any object.
.. code-block::
import types
obj = types.SimpleNamespace(x=0, size=(200, 300))
await anim_attrs(obj, x=100, size=(400, 400))
The ``output_seq_type`` parameter:
.. code-block::
obj = types.SimpleNamespace(size=(200, 300))
await anim_attrs(obj, size=(400, 400), output_seq_type=list)
assert type(obj.size) is list
.. warning::
Unlike :class:`kivy.animation.Animation`, this one does not support dictionary-type and nested-sequence.
.. code-block::
await anim_attrs(obj, pos_hint={'x': 1.}) # not supported
await anim_attrs(obj, nested_sequence=[[10, 20, ]]) # not supported
await anim_attrs(obj, color=(1, 0, 0, 1), pos=(100, 200)) # OK
.. versionadded:: 0.6.1
'''
return _anim_attrs(obj, duration, step, transition, output_seq_type, animated_properties)
def anim_attrs_abbr(obj, *, d=1.0, s=0, t=AnimationTransition.linear, output_seq_type=tuple,
**animated_properties) -> T.Awaitable:
'''
:func:`anim_attrs` cannot animate attributes named ``step``, ``duration`` and ``transition`` but this one can.
.. versionadded:: 0.6.1
'''
return _anim_attrs(obj, d, s, t, output_seq_type, animated_properties)

View file

@ -0,0 +1,146 @@
__all__ = (
'anim_with_dt', 'anim_with_et', 'anim_with_ratio', 'anim_with_dt_et', 'anim_with_dt_et_ratio',
)
from ._sleep import repeat_sleeping
async def anim_with_dt(*, step=0):
'''
An async form of :meth:`kivy.clock.Clock.schedule_interval`. The following callback-style code:
.. code-block::
def callback(dt):
print(dt)
if some_condition:
return False
Clock.schedule_interval(callback, 0.1)
is equivalent to the following async-style code:
.. code-block::
async for dt in anim_with_dt(step=0.1):
print(dt)
if some_condition:
break
.. versionadded:: 0.6.1
'''
async with repeat_sleeping(step=step) as sleep:
while True:
yield await sleep()
async def anim_with_et(*, step=0):
'''
Same as :func:`anim_with_dt` except this one generates the total elapsed time of the loop instead of the elapsed
time between frames.
.. code-block::
timeout = 3.0
async for et in anim_with_et(...):
...
if et > timeout:
break
You can calculate ``et`` by yourself if you want to:
.. code-block::
et = 0.
timeout = 3.0
async for dt in anim_with_dt(...):
et += dt
...
if et > timeout:
break
which should be as performant as the former.
.. versionadded:: 0.6.1
'''
et = 0.
async with repeat_sleeping(step=step) as sleep:
while True:
et += await sleep()
yield et
async def anim_with_dt_et(*, step=0):
'''
:func:`anim_with_dt` and :func:`anim_with_et` combined.
.. code-block::
async for dt, et in anim_with_dt_et(...):
...
.. versionadded:: 0.6.1
'''
et = 0.
async with repeat_sleeping(step=step) as sleep:
while True:
dt = await sleep()
et += dt
yield dt, et
async def anim_with_ratio(*, duration=1., step=0):
'''
Same as :func:`anim_with_et` except this one generates the total progression ratio of the loop.
.. code-block::
async for p in anim_with_ratio(duration=3.0):
print(p * 100, "%")
If you want to progress at a non-consistant rate, :class:`kivy.animation.AnimationTransition` may be helpful.
.. code-block::
from kivy.animation import AnimationTransition
in_cubic = AnimationTransition.in_cubic
async for p in anim_with_ratio(duration=3.0):
p = in_cubic(p)
print(p * 100, "%")
.. versionadded:: 0.6.1
'''
async with repeat_sleeping(step=step) as sleep:
if not duration:
await sleep()
yield 1.0
return
et = 0.
while et < duration:
et += await sleep()
yield et / duration
async def anim_with_dt_et_ratio(*, duration=1., step=0):
'''
:func:`anim_with_dt`, :func:`anim_with_et` and :func:`anim_with_ratio` combined.
.. code-block::
async for dt, et, p in anim_with_dt_et_ratio(...):
...
.. versionadded:: 0.6.1
'''
async with repeat_sleeping(step=step) as sleep:
if not duration:
dt = await sleep()
yield dt, dt, 1.0
return
et = 0.
while et < duration:
dt = await sleep()
et += dt
yield dt, et, et / duration

View file

@ -0,0 +1,115 @@
__all__ = ('animate', )
import typing as T
import types
from functools import partial
from kivy.clock import Clock
from kivy.animation import AnimationTransition
from asyncgui import _sleep_forever, _current_task
@types.coroutine
def animate(obj, *, duration=1.0, step=0, transition=AnimationTransition.linear, **animated_properties) -> T.Awaitable:
'''
Animates attibutes of any object. This is basically an async form of :class:`kivy.animation.Animation`.
.. code-block::
import types
obj = types.SimpleNamespace(x=0, size=(200, 300, ))
await animate(obj, x=100, size=(400, 400))
Kivy has two compound animations, :class:`kivy.animation.Sequence` and :class:`kivy.animation.Parallel`.
You can achieve the same functionality as them in asynckivy as follows:
.. code-block::
import asynckivy as ak
async def sequential_animation(widget):
await ak.animate(widget, x=100)
await ak.animate(widget, x=0)
async def parallel_animation(widget):
await ak.wait_all(
ak.animate(widget, x=100),
ak.animate(widget, y=100, duration=2),
)
.. deprecated:: 0.6.1
This will be removed before version 1.0.0.
Use :func:`asynckivy.anim_attrs` or :func:`asynckivy.anim_attrs_abbr` instead.
'''
if not duration:
for key, value in animated_properties.items():
setattr(obj, key, value)
return
if isinstance(transition, str):
transition = getattr(AnimationTransition, transition)
# get current values
properties = {}
for key, value in animated_properties.items():
original_value = getattr(obj, key)
if isinstance(original_value, (tuple, list)):
original_value = original_value[:]
elif isinstance(original_value, dict):
original_value = original_value.copy()
properties[key] = (original_value, value)
try:
clock_event = Clock.schedule_interval(
partial(_update, obj, duration, transition, properties, (yield _current_task)[0][0], [0., ]),
step,
)
yield _sleep_forever
finally:
clock_event.cancel()
def _calculate(isinstance, list, tuple, dict, range, len, a, b, t):
'''The logic of this function is identical to 'kivy.animation.Animation._calculate()'
'''
if isinstance(a, list) or isinstance(a, tuple):
if isinstance(a, list):
tp = list
else:
tp = tuple
return tp([_calculate(a[x], b[x], t) for x in range(len(a))])
elif isinstance(a, dict):
d = {}
for x in a:
if x not in b:
# User requested to animate only part of the dict.
# Copy the rest
d[x] = a[x]
else:
d[x] = _calculate(a[x], b[x], t)
return d
else:
return (a * (1. - t)) + (b * t)
def _update(setattr, _calculate, obj, duration, transition, properties, task, p_time, dt):
time = p_time[0] + dt
p_time[0] = time
# calculate progression
progress = min(1., time / duration)
t = transition(progress)
# apply progression on obj
for key, values in properties.items():
a, b = values
value = _calculate(a, b, t)
setattr(obj, key, value)
# time to stop ?
if progress >= 1.:
task._step()
return False
_calculate = partial(_calculate, isinstance, list, tuple, dict, range, len)
_update = partial(_update, setattr, _calculate)

View file

@ -0,0 +1,57 @@
__all__ = ('event', )
import typing as T
import types
from functools import partial
from asyncgui import _current_task, _sleep_forever
@types.coroutine
def event(event_dispatcher, event_name, *, filter=None, stop_dispatching=False) -> T.Awaitable[tuple]:
'''
Returns an awaitable that can be used to wait for:
* a Kivy event to occur.
* a Kivy property's value to change.
.. code-block::
# Wait for a button to be pressed.
await event(button, 'on_press')
# Wait for an 'on_touch_down' event to occur.
__, touch = await event(widget, 'on_touch_down')
# Wait for 'widget.x' to change.
__, x = await ak.event(widget, 'x')
The ``filter`` parameter:
.. code-block::
# Wait for an 'on_touch_down' event to occur inside a widget.
__, touch = await event(widget, 'on_touch_down', filter=lambda w, t: w.collide_point(*t.opos))
# Wait for 'widget.x' to become greater than 100.
if widget.x <= 100:
await event(widget, 'x', filter=lambda __, x: x > 100)
The ``stop_dispatching`` parameter:
It only works for events not for properties.
See :ref:`kivys-event-system` for details.
'''
task = (yield _current_task)[0][0]
bind_id = event_dispatcher.fbind(event_name, partial(_callback, filter, task, stop_dispatching))
assert bind_id # check if binding succeeded
try:
return (yield _sleep_forever)[0]
finally:
event_dispatcher.unbind_uid(event_name, bind_id)
def _callback(filter, task, stop_dispatching, *args, **kwargs):
if (filter is None) or filter(*args, **kwargs):
task._step(*args)
return stop_dispatching

View file

@ -0,0 +1,27 @@
__all__ = (
'MotionEventAlreadyEndedError',
)
class MotionEventAlreadyEndedError(Exception):
'''
This error occurs when an already-ended touch is passed to an asynckivy API that expects an ongoing touch.
For instance:
.. code-block::
:emphasize-lines: 4
import asynckivy as ak
class MyWidget(Widget):
def on_touch_up(self, touch): # not 'on_touch_down', oops!
ak.start(self.handle_touch(touch))
return True
async def handle_touch(self, touch):
try:
async for __ in ak.rest_of_touch_events(widget, touch):
...
except ak.MotionEventAlreadyEndedError:
...
'''

View file

@ -0,0 +1,75 @@
__all__ = ('interpolate', 'fade_transition', )
import typing as T
from contextlib import asynccontextmanager
from kivy.animation import AnimationTransition
from ._anim_with_xxx import anim_with_ratio
linear = AnimationTransition.linear
async def interpolate(start, end, *, duration=1.0, step=0, transition=linear) -> T.AsyncIterator:
'''
Interpolates between the values ``start`` and ``end`` in an async-manner.
Inspired by wasabi2d's interpolate_.
.. code-block::
async for v in interpolate(0, 100, duration=1.0, step=.3):
print(int(v))
============ ======
elapsed time output
============ ======
0 sec 0
0.3 sec 30
0.6 sec 60
0.9 sec 90
**1.2 sec** 100
============ ======
.. _interpolate: https://wasabi2d.readthedocs.io/en/stable/coros.html#clock.coro.interpolate
'''
if isinstance(transition, str):
transition = getattr(AnimationTransition, transition)
slope = end - start
yield transition(0.) * slope + start
async for p in anim_with_ratio(step=step, duration=duration):
if p >= 1.0:
break
yield transition(p) * slope + start
yield transition(1.) * slope + start
@asynccontextmanager
async def fade_transition(*widgets, duration=1.0, step=0) -> T.AsyncContextManager:
'''
Returns an async context manager that:
* fades-out the given widgets on ``__aenter__``.
* fades-in the given widgets on ``__aexit__``.
.. code-block::
async with fade_transition(widget1, widget2):
...
The ``widgets`` don't have to be actual Kivy widgets.
Anything that has an attribute named ``opacity`` would work.
'''
half_duration = duration / 2.
org_opas = tuple(w.opacity for w in widgets)
try:
async for p in anim_with_ratio(duration=half_duration, step=step):
p = 1.0 - p
for w, o in zip(widgets, org_opas):
w.opacity = p * o
yield
async for p in anim_with_ratio(duration=half_duration, step=step):
for w, o in zip(widgets, org_opas):
w.opacity = p * o
finally:
for w, o in zip(widgets, org_opas):
w.opacity = o

View file

@ -0,0 +1,43 @@
__all__ = ('n_frames', )
import typing as T
import types
from kivy.clock import Clock
from asyncgui import _current_task, _sleep_forever
@types.coroutine
def n_frames(n: int) -> T.Awaitable:
'''
Waits for a specified number of frames.
.. code-block::
await n_frames(2)
If you want to wait for one frame, :func:`asynckivy.sleep` is preferable for a performance reason.
.. code-block::
await sleep(0)
'''
if n < 0:
raise ValueError(f"Waiting for {n} frames doesn't make sense.")
if not n:
return
task = (yield _current_task)[0][0]
def callback(dt):
nonlocal n
n -= 1
if not n:
task._step()
return False
clock_event = Clock.schedule_interval(callback, 0)
try:
yield _sleep_forever
finally:
clock_event.cancel()

View file

@ -0,0 +1,125 @@
__all__ = ('sleep', 'sleep_free', 'repeat_sleeping', 'move_on_after', )
import typing as T
import types
from kivy.clock import Clock
from asyncgui import _current_task, _sleep_forever, move_on_when, Task, Cancelled
@types.coroutine
def sleep(duration) -> T.Awaitable[float]:
'''
An async form of :meth:`kivy.clock.Clock.schedule_once`.
.. code-block::
dt = await sleep(5) # wait for 5 seconds
'''
task = (yield _current_task)[0][0]
clock_event = Clock.create_trigger(task._step, duration, False, False)
clock_event()
try:
return (yield _sleep_forever)[0][0]
except Cancelled:
clock_event.cancel()
raise
@types.coroutine
def sleep_free(duration) -> T.Awaitable[float]:
'''
An async form of :meth:`kivy.clock.Clock.schedule_once_free`.
.. code-block::
dt = await sleep_free(5) # wait for 5 seconds
'''
task = (yield _current_task)[0][0]
clock_event = Clock.create_trigger_free(task._step, duration, False, False)
clock_event()
try:
return (yield _sleep_forever)[0][0]
except Cancelled:
clock_event.cancel()
raise
class repeat_sleeping:
'''
Returns an async context manager that provides an efficient way to repeat sleeping.
When there is a piece of code like this:
.. code-block::
while True:
await sleep(0)
...
it can be translated to:
.. code-block::
async with repeat_sleeping(step=0) as sleep:
while True:
await sleep()
...
The latter is more suitable for situations requiring frequent sleeps, such as moving an object in every frame.
**Restriction**
You are not allowed to perform any kind of async operations inside the with-block except you can
``await`` the return value of the function that is bound to the identifier of the as-clause.
.. code-block::
async with repeat_sleeping(step=0) as sleep:
await sleep() # OK
await something_else # NOT ALLOWED
async with async_context_manager: # NOT ALLOWED
...
async for __ in async_iterator: # NOT ALLOWED
...
'''
__slots__ = ('_step', '_trigger', )
@types.coroutine
def _sleep(_f=_sleep_forever):
return (yield _f)[0][0]
def __init__(self, *, step=0):
self._step = step
@types.coroutine
def __aenter__(self, _sleep=_sleep) -> T.Awaitable[T.Callable[[], T.Awaitable[float]]]:
task = (yield _current_task)[0][0]
self._trigger = Clock.create_trigger(task._step, self._step, True, False)
self._trigger()
return _sleep
async def __aexit__(self, exc_type, exc_val, exc_tb):
self._trigger.cancel()
def move_on_after(seconds: float) -> T.AsyncContextManager[Task]:
'''
Returns an async context manager that applies a time limit to its code block,
like :func:`trio.move_on_after` does.
.. code-block::
async with move_on_after(seconds) as bg_task:
...
if bg_task.finished:
print("The code block was interrupted due to a timeout")
else:
print("The code block exited gracefully.")
.. versionadded:: 0.6.1
'''
return move_on_when(sleep(seconds))

View file

@ -0,0 +1,64 @@
__all__ = ('run_in_thread', 'run_in_executor', )
import typing as T
from threading import Thread
from concurrent.futures import ThreadPoolExecutor
from kivy.clock import Clock
import asyncgui
def _wrapper(func, ev):
ret = None
exc = None
try:
ret = func()
except Exception as e:
exc = e
finally:
Clock.schedule_once(lambda __: ev.fire(ret, exc))
async def run_in_thread(func, *, daemon=None) -> T.Awaitable:
'''
Creates a new thread, runs a function within it, then waits for the completion of that function.
.. code-block::
return_value = await run_in_thread(func)
See :ref:`io-in-asynckivy` for details.
'''
ev = asyncgui.AsyncEvent()
Thread(
name='asynckivy.run_in_thread',
target=_wrapper, daemon=daemon, args=(func, ev, ),
).start()
ret, exc = (await ev.wait())[0]
if exc is not None:
raise exc
return ret
async def run_in_executor(executor: ThreadPoolExecutor, func) -> T.Awaitable:
'''
Runs a function within a :class:`concurrent.futures.ThreadPoolExecutor`, and waits for the completion of the
function.
.. code-block::
executor = ThreadPoolExecutor()
...
return_value = await run_in_executor(executor, func)
See :ref:`io-in-asynckivy` for details.
'''
ev = asyncgui.AsyncEvent()
future = executor.submit(_wrapper, func, ev)
try:
ret, exc = (await ev.wait())[0]
except asyncgui.Cancelled:
future.cancel()
raise
assert future.done()
if exc is not None:
raise exc
return ret

View file

@ -0,0 +1,196 @@
__all__ = ('watch_touch', 'rest_of_touch_moves', 'rest_of_touch_events', 'touch_up_event', )
import typing as T
import types
from functools import partial
from asyncgui import wait_any, current_task
from ._exceptions import MotionEventAlreadyEndedError
from ._sleep import sleep
from ._event import event
class watch_touch:
'''
Returns an async context manager that provides an easy way to handle touch events.
.. code-block::
async with watch_touch(widget, touch) as in_progress:
while await in_progress():
print('on_touch_move')
print('on_touch_up')
The ``await in_progress()`` waits for either an ``on_touch_move`` event or an ``on_touch_up`` event to occur, and
returns True or False respectively when they occurred.
**Caution**
* You are not allowed to perform any kind of async operations inside the with-block except ``await in_progress()``.
.. code-block::
async with watch_touch(widget, touch) as in_progress:
await in_progress() # OK
await something_else # NOT ALLOWED
async with async_context_manager: # NOT ALLOWED
...
async for __ in async_iterator: # NOT ALLOWED
...
* If the ``widget`` is the type of widget that grabs touches by itself, such as :class:`kivy.uix.button.Button`,
you probably want to set the ``stop_dispatching`` parameter to True in most cases.
* There are widgets/behaviors that can simulate a touch (e.g. :class:`kivy.uix.scrollview.ScrollView`,
:class:`kivy.uix.behaviors.DragBehavior` and ``kivy_garden.draggable.KXDraggableBehavior``).
If many such widgets are in the parent stack of the ``widget``, this API might mistakenly raise a
:exc:`asynckivy.MotionEventAlreadyEndedError`. If that happens, increase the ``timeout`` parameter.
'''
__slots__ = ('_widget', '_touch', '_stop_dispatching', '_timeout', '_uid_up', '_uid_move', '_no_cleanup', )
def __init__(self, widget, touch, *, stop_dispatching=False, timeout=1.):
self._widget = widget
self._touch = touch
self._stop_dispatching = stop_dispatching
self._timeout = timeout
self._no_cleanup = False
def _on_touch_up_sd(step, touch, w, t):
if t is touch:
if t.grab_current is w:
t.ungrab(w)
step(False)
return True
def _on_touch_move_sd(step, touch, w, t):
if t is touch:
if t.grab_current is w:
step(True)
return True
def _on_touch_up(step, touch, w, t):
if t.grab_current is w and t is touch:
t.ungrab(w)
step(False)
return True
def _on_touch_move(step, touch, w, t):
if t.grab_current is w and t is touch:
step(True)
return True
_callbacks = ((_on_touch_up_sd, _on_touch_move_sd, ), (_on_touch_up, _on_touch_move, ), )
del _on_touch_up, _on_touch_move, _on_touch_up_sd, _on_touch_move_sd
@staticmethod
@types.coroutine
def _true_if_touch_move_false_if_touch_up() -> bool:
return (yield lambda step_coro: None)[0][0]
@staticmethod
@types.coroutine
def _always_false() -> bool:
return False
yield # just to make this function a generator function
async def __aenter__(self) -> T.Awaitable[T.Callable[[], T.Awaitable[bool]]]:
touch = self._touch
widget = self._widget
if touch.time_end != -1:
# `on_touch_up` might have been already fired so we need to find out it actually was or not.
tasks = await wait_any(
sleep(self._timeout),
event(widget, 'on_touch_up', filter=lambda w, t: t is touch),
)
if tasks[0].finished:
raise MotionEventAlreadyEndedError(f"MotionEvent(uid={touch.uid}) has already ended")
self._no_cleanup = True
return self._always_false
step = (await current_task())._step
on_touch_up, on_touch_move = self._callbacks[not self._stop_dispatching]
touch.grab(widget)
self._uid_up = widget.fbind('on_touch_up', partial(on_touch_up, step, touch))
self._uid_move = widget.fbind('on_touch_move', partial(on_touch_move, step, touch))
assert self._uid_up
assert self._uid_move
return self._true_if_touch_move_false_if_touch_up
async def __aexit__(self, *args):
if self._no_cleanup:
return
w = self._widget
self._touch.ungrab(w)
w.unbind_uid('on_touch_up', self._uid_up)
w.unbind_uid('on_touch_move', self._uid_move)
async def touch_up_event(widget, touch, *, stop_dispatching=False, timeout=1.) -> T.Awaitable:
'''
*(experimental state)*
Returns an awaitable that can be used to wait for the ``on_touch_up`` event of the given ``touch`` to occur.
.. code-block::
__, touch = await event(widget, 'on_touch_down')
...
await touch_up_event(widget, touch)
You might wonder what the differences are compared to the code below.
.. code-block::
:emphasize-lines: 3
__, touch = await event(widget, 'on_touch_down')
...
await event(widget, 'on_touch_up', filter=lambda w, t: t is touch)
The latter has two problems:
If the ``on_touch_up`` event of the ``touch`` occurred before the highlighted line,
the execution will halt indefinitely at that point.
Even if the event didn't occur before that line, the execution still might halt because
`Kivy does not guarantee`_ that all touch events are delivered to all widgets.
This API takes care of both problems in the same way as :func:`watch_touch`.
If the ``on_touch_up`` event has already occurred, it raises a :exc:`MotionEventAlreadyEndedError` exception.
And it grabs/ungrabs the ``touch`` so that it won't miss any touch events.
Needless to say, if you want to wait for both ``on_touch_move`` and ``on_touch_up`` events at the same time,
use :func:`watch_touch` or :func:`rest_of_touch_events` instead.
.. _Kivy does not guarantee: https://kivy.org/doc/stable/guide/inputs.html#grabbing-touch-events
'''
touch.grab(widget)
try:
awaitable = event(
widget, 'on_touch_up', stop_dispatching=stop_dispatching,
filter=lambda w, t: t.grab_current is w and t is touch,
)
if touch.time_end == -1:
await awaitable
else:
tasks = await wait_any(sleep(timeout), awaitable)
if tasks[0].finished:
raise MotionEventAlreadyEndedError(f"MotionEvent(uid={touch.uid}) has already ended")
finally:
touch.ungrab(widget)
async def rest_of_touch_events(widget, touch, *, stop_dispatching=False, timeout=1.) -> T.AsyncIterator[None]:
'''
Returns an async iterator that iterates the number of times ``on_touch_move`` occurs,
and ends the iteration when ``on_touch_up`` occurs.
.. code-block::
async for __ in rest_of_touch_events(widget, touch):
print('on_touch_move')
print('on_touch_up')
This is a wrapper for :class:`watch_touch`. Although this one, I believe, is more intuitive than
:class:`watch_touch`, it has a couple of disadvantages - see :ref:`the-problem-with-async-generators`.
'''
async with watch_touch(widget, touch, stop_dispatching=stop_dispatching, timeout=timeout) as in_progress:
while await in_progress():
yield
rest_of_touch_moves = rest_of_touch_events

View file

@ -0,0 +1,256 @@
__all__ = ('transform', 'suppress_event', 'create_texture_from_text', 'sync_attr', 'sync_attrs', )
import typing as T
from contextlib import contextmanager
from functools import partial
from kivy.event import EventDispatcher
from kivy.graphics import PushMatrix, PopMatrix, InstructionGroup
from kivy.graphics.texture import Texture
from kivy.core.text import Label as CoreLabel
from kivy.core.text.markup import MarkupLabel as CoreMarkupLabel
@contextmanager
def transform(widget, *, use_outer_canvas=False) -> T.ContextManager[InstructionGroup]:
'''
Returns a context manager that sandwiches the ``widget``'s existing canvas instructions between
a :class:`kivy.graphics.PushMatrix` and a :class:`kivy.graphics.PopMatrix`, and inserts an
:class:`kivy.graphics.InstructionGroup` right next to the ``PushMatrix``. Those three instructions will be removed
when the context manager exits.
This may be useful when you want to animate a widget.
**Usage**
.. code-block::
from kivy.graphics import Rotate
async def rotate_widget(widget, *, angle=360.):
with transform(widget) as ig: # <- InstructionGroup
ig.add(rotate := Rotate(origin=widget.center))
await anim_attrs(rotate, angle=angle)
If the position or size of the ``widget`` changes during the animation, you might need :class:`sync_attr`.
**The use_outer_canvas parameter**
While the context manager is active, the content of the widget's canvas would be:
.. code-block:: yaml
# ... represents existing instructions
Widget:
canvas.before:
...
canvas:
PushMatrix
InstructionGroup
...
PopMatrix
canvas.after:
...
but if ``use_outer_canvas`` is True, it would be:
.. code-block:: yaml
Widget:
canvas.before:
PushMatrix
InstructionGroup
...
canvas:
...
canvas.after:
...
PopMatrix
'''
c = widget.canvas
if use_outer_canvas:
before = c.before
after = c.after
push_mat_idx = 0
ig_idx = 1
else:
c.before # ensure 'canvas.before' exists
# Index starts from 1 because 'canvas.before' is sitting at index 0 and we usually want it to remain first.
# See https://github.com/kivy/kivy/issues/7945 for details.
push_mat_idx = 1
ig_idx = 2
before = after = c
push_mat = PushMatrix()
ig = InstructionGroup()
pop_mat = PopMatrix()
before.insert(push_mat_idx, push_mat)
before.insert(ig_idx, ig)
after.add(pop_mat)
try:
yield ig
finally:
after.remove(pop_mat)
before.remove(ig)
before.remove(push_mat)
class suppress_event:
'''
Returns a context manager that prevents the callback functions (including the default handler) bound to an event
from being called.
.. code-block::
:emphasize-lines: 4
from kivy.uix.button import Button
btn = Button()
btn.bind(on_press=lambda __: print("pressed"))
with suppress_event(btn, 'on_press'):
btn.dispatch('on_press')
The above code prints nothing because the callback function won't be called.
Strictly speaking, this context manager doesn't prevent all callback functions from being called.
It only prevents the callback functions that were bound to an event before the context manager enters.
Thus, the following code prints ``pressed``.
.. code-block::
:emphasize-lines: 5
from kivy.uix.button import Button
btn = Button()
with suppress_event(btn, 'on_press'):
btn.bind(on_press=lambda __: print("pressed"))
btn.dispatch('on_press')
.. warning::
You need to be careful when you suppress an ``on_touch_xxx`` event.
See :ref:`kivys-event-system` for details.
'''
__slots__ = ('_dispatcher', '_name', '_bind_uid', '_filter', )
def __init__(self, event_dispatcher, event_name, *, filter=lambda *args, **kwargs: True):
self._dispatcher = event_dispatcher
self._name = event_name
self._filter = filter
def __enter__(self):
self._bind_uid = self._dispatcher.fbind(self._name, self._filter)
def __exit__(self, *args):
self._dispatcher.unbind_uid(self._name, self._bind_uid)
def create_texture_from_text(*, markup=False, **label_kwargs) -> Texture:
'''
.. code-block::
from kivy.metrics import sp
texture = create_texture_from_text(
text='Hello',
font_size=sp(50),
font_name='Roboto',
color=(1, 0, 0, 1),
)
The keyword arguments are similar to :external:kivy:doc:`api-kivy.uix.label` 's.
'''
core_cls = CoreMarkupLabel if markup else CoreLabel
core = core_cls(**label_kwargs)
core.refresh()
return core.texture
class sync_attr:
'''
Returns a context manager that creates one-directional binding between attributes.
.. code-block::
import types
widget = Widget()
obj = types.SimpleNamespace()
with sync_attr(from_=(widget, 'x'), to_=(obj, 'xx')):
widget.x = 10
assert obj.xx == 10 # synchronized
obj.xx = 20
assert widget.x == 10 # but not the other way around
This can be particularly useful when combined with :func:`transform`.
.. code-block::
from kivy.graphics import Rotate
async def rotate_widget(widget, *, angle=360.):
with transform(widget) as ig:
ig.add(rotate := Rotate(origin=widget.center))
with sync_attr(from_=(widget, 'center'), to_=(rotate, 'origin')):
await anim_attrs(rotate, angle=angle)
.. versionadded:: 0.6.1
'''
__slots__ = ('_from', '_to', '_bind_uid', )
def __init__(self, from_: T.Tuple[EventDispatcher, str], to_: T.Tuple[T.Any, str]):
self._from = from_
self._to = to_
def _sync(setattr, obj, attr_name, event_dispatcher, new_value):
setattr(obj, attr_name, new_value)
def __enter__(self, partial=partial, sync=partial(_sync, setattr)):
self._bind_uid = self._from[0].fbind(self._from[1], partial(sync, *self._to))
def __exit__(self, *args):
self._from[0].unbind_uid(self._from[1], self._bind_uid)
del _sync
class sync_attrs:
'''
When multiple :class:`sync_attr` calls take the same ``from_`` argument, they can be merged into a single
:class:`sync_attrs` call. For instance, the following code:
.. code-block::
with sync_attr((widget, 'x'), (obj1, 'x')), sync_attr((widget, 'x'), (obj2, 'xx')):
...
can be replaced with the following one:
.. code-block::
with sync_attrs((widget, 'x'), (obj1, 'x'), (obj2, 'xx')):
...
.. versionadded:: 0.6.1
'''
__slots__ = ('_from', '_to', '_bind_uid', )
def __init__(self, from_: T.Tuple[EventDispatcher, str], *to_):
self._from = from_
self._to = to_
def _sync(setattr, to_, event_dispatcher, new_value):
for obj, attr_name in to_:
setattr(obj, attr_name, new_value)
def __enter__(self, partial=partial, sync=partial(_sync, setattr)):
self._bind_uid = self._from[0].fbind(self._from[1], partial(sync, self._to))
def __exit__(self, *args):
self._from[0].unbind_uid(self._from[1], self._bind_uid)
del _sync

View file

@ -0,0 +1,20 @@
__all__ = (
'dt', 'delta_time',
'et', 'elapsed_time',
'dt_et', 'delta_time_elapsed_time',
'progress',
'dt_et_progress', 'delta_time_elapsed_time_progress',
)
import warnings
from . import _anim_with_xxx
warnings.warn("The 'vanim' module is deprecated. Use 'asynckivy.anim_with_xxx' instead.")
delta_time = dt = _anim_with_xxx.anim_with_dt
elapsed_time = et = _anim_with_xxx.anim_with_et
progress = _anim_with_xxx.anim_with_ratio
delta_time_elapsed_time = dt_et = _anim_with_xxx.anim_with_dt_et
delta_time_elapsed_time_progress = dt_et_progress = _anim_with_xxx.anim_with_dt_et_ratio