|
| 1 | +__all__ = ("smooth_attr", ) |
| 2 | +import typing as T |
| 3 | +from functools import partial |
| 4 | +import math |
| 5 | + |
| 6 | +from kivy.metrics import dp |
| 7 | +from kivy.clock import Clock |
| 8 | +from kivy.event import EventDispatcher |
| 9 | +from kivy import properties as P |
| 10 | +NUMERIC_TYPES = (P.NumericProperty, P.BoundedNumericProperty, ) |
| 11 | +SEQUENCE_TYPES = (P.ColorProperty, P.ReferenceListProperty, P.ListProperty, ) |
| 12 | + |
| 13 | + |
| 14 | +class smooth_attr: |
| 15 | + ''' |
| 16 | + Makes an attribute smoothly follow another. |
| 17 | +
|
| 18 | + .. code-block:: |
| 19 | +
|
| 20 | + import types |
| 21 | +
|
| 22 | + widget = Widget(x=0) |
| 23 | + obj = types.SimpleNamespace(xx=100) |
| 24 | +
|
| 25 | + # 'obj.xx' will smoothly follow 'widget.x'. |
| 26 | + smooth_attr(target=(widget, 'x'), follower=(obj, 'xx')) |
| 27 | +
|
| 28 | + To make its effect temporary, use it with a with-statement: |
| 29 | +
|
| 30 | + .. code-block:: |
| 31 | +
|
| 32 | + # The effect lasts only within the with-block. |
| 33 | + with smooth_attr(...): |
| 34 | + ... |
| 35 | +
|
| 36 | + A key feature of this API is that if the target value changes while being followed, |
| 37 | + the follower automatically adjusts to the new value. |
| 38 | +
|
| 39 | + :param target: Must be a numeric or numeric sequence type property, that is, one of the following: |
| 40 | +
|
| 41 | + * :class:`~kivy.properties.NumericProperty` |
| 42 | + * :class:`~kivy.properties.BoundedNumericProperty` |
| 43 | + * :class:`~kivy.properties.ReferenceListProperty` |
| 44 | + * :class:`~kivy.properties.ListProperty` |
| 45 | + * :class:`~kivy.properties.ColorProperty` |
| 46 | +
|
| 47 | + :param speed: The speed coefficient for following. A larger value results in faster following. |
| 48 | + :param min_diff: If the difference between the target and the follower is less than this value, |
| 49 | + the follower will instantly jump to the target's value. When the target is a ``ColorProperty``, |
| 50 | + you most likely want to set this to a very small value, such as ``0.01``. Defaults to ``dp(2)``. |
| 51 | +
|
| 52 | + .. versionadded:: 0.8.0 |
| 53 | + ''' |
| 54 | + __slots__ = ("__exit__", ) |
| 55 | + |
| 56 | + def __init__(self, target: tuple[EventDispatcher, str], follower: tuple[T.Any, str], |
| 57 | + *, speed=10.0, min_diff=dp(2)): |
| 58 | + target_obj, target_attr = target |
| 59 | + target_desc = target_obj.property(target_attr) |
| 60 | + if isinstance(target_desc, NUMERIC_TYPES): |
| 61 | + update = self._update_follower |
| 62 | + elif isinstance(target_desc, SEQUENCE_TYPES): |
| 63 | + update = self._update_follower_ver_seq |
| 64 | + else: |
| 65 | + raise ValueError(f"Unsupported target type: {target_desc}") |
| 66 | + trigger = Clock.schedule_interval( |
| 67 | + partial(update, *target, *follower, -speed, -min_diff, min_diff), 0 |
| 68 | + ) |
| 69 | + bind_uid = target_obj.fbind(target_attr, trigger) |
| 70 | + self.__exit__ = partial(self._cleanup, trigger, target_obj, target_attr, bind_uid) |
| 71 | + |
| 72 | + @staticmethod |
| 73 | + def _cleanup(trigger, target_obj, target_attr, bind_uid, *__): |
| 74 | + trigger.cancel() |
| 75 | + target_obj.unbind_uid(target_attr, bind_uid) |
| 76 | + |
| 77 | + def __enter__(self): |
| 78 | + pass |
| 79 | + |
| 80 | + def _update_follower(getattr, setattr, math_exp, target_obj, target_attr, follower_obj, follower_attr, |
| 81 | + negative_speed, min, max, dt): |
| 82 | + t_value = getattr(target_obj, target_attr) |
| 83 | + f_value = getattr(follower_obj, follower_attr) |
| 84 | + diff = f_value - t_value |
| 85 | + |
| 86 | + if min < diff < max: |
| 87 | + setattr(follower_obj, follower_attr, t_value) |
| 88 | + return False |
| 89 | + |
| 90 | + new_value = t_value + math_exp(negative_speed * dt) * diff |
| 91 | + setattr(follower_obj, follower_attr, new_value) |
| 92 | + |
| 93 | + _update_follower = partial(_update_follower, getattr, setattr, math.exp) |
| 94 | + |
| 95 | + def _update_follower_ver_seq(getattr, setattr, math_exp, seq_cls, zip, target_obj, target_attr, |
| 96 | + follower_obj, follower_attr, negative_speed, min, max, dt): |
| 97 | + t_value = getattr(target_obj, target_attr) |
| 98 | + f_value = getattr(follower_obj, follower_attr) |
| 99 | + p = math_exp(negative_speed * dt) |
| 100 | + still_going = False |
| 101 | + new_value = seq_cls( |
| 102 | + (t_elem + p * diff) if ( |
| 103 | + diff := f_elem - t_elem, |
| 104 | + _still_going := (diff <= min or max <= diff), |
| 105 | + still_going := (still_going or _still_going), |
| 106 | + ) and _still_going else t_elem |
| 107 | + for t_elem, f_elem in zip(t_value, f_value) |
| 108 | + ) |
| 109 | + setattr(follower_obj, follower_attr, new_value) |
| 110 | + return still_going |
| 111 | + |
| 112 | + _update_follower_ver_seq = partial(_update_follower_ver_seq, getattr, setattr, math.exp, tuple, zip) |
0 commit comments