tempest-core 0.1.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.
- tempest_core/__init__.py +86 -0
- tempest_core/animation.py +425 -0
- tempest_core/components/__init__.py +119 -0
- tempest_core/components/bars.py +266 -0
- tempest_core/components/base.py +54 -0
- tempest_core/components/brforms.py +461 -0
- tempest_core/components/cards.py +200 -0
- tempest_core/components/dates.py +221 -0
- tempest_core/components/disclosure.py +83 -0
- tempest_core/components/feedback.py +194 -0
- tempest_core/components/fields.py +204 -0
- tempest_core/components/layout.py +179 -0
- tempest_core/components/mediainputs.py +254 -0
- tempest_core/components/menu.py +111 -0
- tempest_core/components/navigation.py +215 -0
- tempest_core/components/selection.py +269 -0
- tempest_core/components/table.py +235 -0
- tempest_core/core/__init__.py +45 -0
- tempest_core/core/introspection.py +271 -0
- tempest_core/core/ir.py +159 -0
- tempest_core/core/reconciler.py +302 -0
- tempest_core/core/state.py +656 -0
- tempest_core/devices.py +144 -0
- tempest_core/i18n.py +90 -0
- tempest_core/icons.py +394 -0
- tempest_core/navigation.py +114 -0
- tempest_core/py.typed +0 -0
- tempest_core/style.py +722 -0
- tempest_core/theme.py +126 -0
- tempest_core/validators.py +168 -0
- tempest_core/widgets/__init__.py +352 -0
- tempest_core/widgets/animated.py +283 -0
- tempest_core/widgets/base.py +386 -0
- tempest_core/widgets/button.py +31 -0
- tempest_core/widgets/events.py +760 -0
- tempest_core/widgets/forms.py +241 -0
- tempest_core/widgets/gestures.py +426 -0
- tempest_core/widgets/indicators.py +53 -0
- tempest_core/widgets/inputs.py +553 -0
- tempest_core/widgets/layout.py +370 -0
- tempest_core/widgets/lists.py +518 -0
- tempest_core/widgets/media.py +613 -0
- tempest_core/widgets/navigation_widgets.py +182 -0
- tempest_core/widgets/overlays.py +270 -0
- tempest_core/widgets/text.py +19 -0
- tempest_core-0.1.0.dist-info/METADATA +66 -0
- tempest_core-0.1.0.dist-info/RECORD +48 -0
- tempest_core-0.1.0.dist-info/WHEEL +4 -0
tempest_core/__init__.py
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""tempest-core — the renderer-agnostic core shared across the tempest stack.
|
|
2
|
+
|
|
3
|
+
The engine behind both tempestroid (native renderers) and tempestweb (DOM): the
|
|
4
|
+
IR, reconciler, state model, style model, widgets, components and the
|
|
5
|
+
cross-cutting helpers (animation, i18n, navigation, theme, validators). It carries
|
|
6
|
+
no platform-coupled code (no Qt, no JNI, no Android, no DOM) so it imports cleanly
|
|
7
|
+
under CPython, Pyodide and a headless server.
|
|
8
|
+
|
|
9
|
+
This is the single source of truth — consumers depend on the published package and
|
|
10
|
+
import from here (``from tempest_core import App, Column, build, diff``) rather than
|
|
11
|
+
vendoring a copy.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from tempest_core.animation import AnimationController
|
|
15
|
+
from tempest_core.core import (
|
|
16
|
+
App,
|
|
17
|
+
Insert,
|
|
18
|
+
Node,
|
|
19
|
+
OverlayEntry,
|
|
20
|
+
Patch,
|
|
21
|
+
Path,
|
|
22
|
+
Remove,
|
|
23
|
+
Reorder,
|
|
24
|
+
Replace,
|
|
25
|
+
Scene,
|
|
26
|
+
Update,
|
|
27
|
+
build,
|
|
28
|
+
build_scene,
|
|
29
|
+
diff,
|
|
30
|
+
diff_scene,
|
|
31
|
+
event_catalog,
|
|
32
|
+
introspect,
|
|
33
|
+
widget_catalog,
|
|
34
|
+
)
|
|
35
|
+
from tempest_core.i18n import Locale, t, translate
|
|
36
|
+
from tempest_core.navigation import NavStack, Route, routes_from_path
|
|
37
|
+
from tempest_core.style import Style
|
|
38
|
+
from tempest_core.theme import MediaQueryData, Theme, ThemeMode
|
|
39
|
+
from tempest_core.widgets import (
|
|
40
|
+
Button,
|
|
41
|
+
Column,
|
|
42
|
+
Component,
|
|
43
|
+
Container,
|
|
44
|
+
Row,
|
|
45
|
+
Text,
|
|
46
|
+
Widget,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
__all__ = [
|
|
50
|
+
"AnimationController",
|
|
51
|
+
"App",
|
|
52
|
+
"Button",
|
|
53
|
+
"Column",
|
|
54
|
+
"Component",
|
|
55
|
+
"Container",
|
|
56
|
+
"Insert",
|
|
57
|
+
"Locale",
|
|
58
|
+
"MediaQueryData",
|
|
59
|
+
"NavStack",
|
|
60
|
+
"Node",
|
|
61
|
+
"OverlayEntry",
|
|
62
|
+
"Patch",
|
|
63
|
+
"Path",
|
|
64
|
+
"Remove",
|
|
65
|
+
"Reorder",
|
|
66
|
+
"Replace",
|
|
67
|
+
"Route",
|
|
68
|
+
"Row",
|
|
69
|
+
"Scene",
|
|
70
|
+
"Style",
|
|
71
|
+
"Text",
|
|
72
|
+
"Theme",
|
|
73
|
+
"ThemeMode",
|
|
74
|
+
"Update",
|
|
75
|
+
"Widget",
|
|
76
|
+
"build",
|
|
77
|
+
"build_scene",
|
|
78
|
+
"diff",
|
|
79
|
+
"diff_scene",
|
|
80
|
+
"event_catalog",
|
|
81
|
+
"introspect",
|
|
82
|
+
"routes_from_path",
|
|
83
|
+
"t",
|
|
84
|
+
"translate",
|
|
85
|
+
"widget_catalog",
|
|
86
|
+
]
|
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
"""Explicit, core-driven animation framework.
|
|
2
|
+
|
|
3
|
+
This module holds the *driver* half of tempestroid's animation system, which
|
|
4
|
+
lives entirely in the renderer-agnostic core so the reconciler stays pure: an
|
|
5
|
+
:class:`AnimationController` advances a normalized ``value`` (0.0..1.0) on the
|
|
6
|
+
app's frame clock, a :class:`Tween` interpolates a typed value (``float``,
|
|
7
|
+
:class:`~tempestroid.style.Color`, :class:`~tempestroid.style.Edge` or a numeric
|
|
8
|
+
``tuple``) from that ``value``, and the user's ``view`` reads the interpolated
|
|
9
|
+
result to build a tree whose styles are already at their per-frame target.
|
|
10
|
+
|
|
11
|
+
Because the interpolation happens here — not in either leaf renderer — both the
|
|
12
|
+
Qt and Compose backends only ever see *final* props for the current frame, so
|
|
13
|
+
the divergence between "interpolate in the core" (Qt) and "drive the native
|
|
14
|
+
animation engine" (Compose) is confined to the leaf renderers (documented in the
|
|
15
|
+
conformance suite).
|
|
16
|
+
|
|
17
|
+
The :class:`AnimationController` is wired to an :class:`~tempestroid.core.state.App`
|
|
18
|
+
lazily — :meth:`AnimationController.forward`/:meth:`AnimationController.reverse`
|
|
19
|
+
bind the controller to whatever app registered it — so this module never imports
|
|
20
|
+
``App`` and there is no import cycle (the binding is duck-typed through a small
|
|
21
|
+
:class:`_AppClock` protocol).
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import math
|
|
27
|
+
from collections.abc import Callable
|
|
28
|
+
from typing import Generic, Protocol, TypeVar, cast, runtime_checkable
|
|
29
|
+
|
|
30
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
31
|
+
|
|
32
|
+
from tempest_core.style import Color, Curve, Edge
|
|
33
|
+
|
|
34
|
+
__all__ = ["Spring", "AnimationController", "Tween"]
|
|
35
|
+
|
|
36
|
+
T = TypeVar("T")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@runtime_checkable
|
|
40
|
+
class _AppClock(Protocol):
|
|
41
|
+
"""The slice of :class:`~tempestroid.core.state.App` an animation drives.
|
|
42
|
+
|
|
43
|
+
Declared structurally so :class:`AnimationController` binds to an app via
|
|
44
|
+
duck typing, keeping ``animation.py`` free of any ``App`` import (no cycle).
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
def register_animation(self, ctrl: AnimationController) -> None:
|
|
48
|
+
"""Register an active controller on the app's frame clock."""
|
|
49
|
+
...
|
|
50
|
+
|
|
51
|
+
def unregister_animation(self, ctrl: AnimationController) -> None:
|
|
52
|
+
"""Remove a finished/stopped controller from the app's frame clock."""
|
|
53
|
+
...
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class Spring(BaseModel):
|
|
57
|
+
"""A spring's physical parameters, used instead of a fixed duration.
|
|
58
|
+
|
|
59
|
+
When a :class:`AnimationController` is given a :class:`Spring`, it advances
|
|
60
|
+
its ``value`` by integrating a damped harmonic oscillator toward the target
|
|
61
|
+
(1.0 on ``forward``, 0.0 on ``reverse``) rather than easing over a fixed
|
|
62
|
+
``duration_s``. Frozen so it can be compared/diffed by value.
|
|
63
|
+
|
|
64
|
+
Attributes:
|
|
65
|
+
stiffness: The spring constant ``k`` (higher snaps faster).
|
|
66
|
+
damping: The damping coefficient ``c`` (higher settles with less bounce).
|
|
67
|
+
mass: The attached mass ``m`` (higher is more sluggish).
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
model_config = ConfigDict(frozen=True)
|
|
71
|
+
|
|
72
|
+
stiffness: float = Field(default=300.0, gt=0.0)
|
|
73
|
+
damping: float = Field(default=30.0, ge=0.0)
|
|
74
|
+
mass: float = Field(default=1.0, gt=0.0)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _apply_curve(curve: Curve, t: float) -> float:
|
|
78
|
+
"""Map a linear progress ``t`` (0..1) through an easing curve.
|
|
79
|
+
|
|
80
|
+
These are pure, dependency-free approximations of the named curves so the
|
|
81
|
+
core can interpolate without a renderer. The leaf renderers may apply their
|
|
82
|
+
own native curve to the *same* ``Curve`` value; the core's job is only to
|
|
83
|
+
produce a smooth per-frame value for the simulator/test clock.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
curve: The easing curve to apply.
|
|
87
|
+
t: Linear progress, clamped to ``[0.0, 1.0]``.
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
The eased progress, in ``[0.0, 1.0]`` for the monotone curves and
|
|
91
|
+
possibly slightly outside for the overshoot curves (``ELASTIC``).
|
|
92
|
+
"""
|
|
93
|
+
t = 0.0 if t < 0.0 else 1.0 if t > 1.0 else t
|
|
94
|
+
if curve is Curve.LINEAR:
|
|
95
|
+
return t
|
|
96
|
+
if curve is Curve.EASE_IN:
|
|
97
|
+
return t * t
|
|
98
|
+
if curve is Curve.EASE_OUT:
|
|
99
|
+
return 1.0 - (1.0 - t) * (1.0 - t)
|
|
100
|
+
if curve in (Curve.EASE_IN_OUT, Curve.EASE):
|
|
101
|
+
# Smooth cubic ease-in-out (matches CSS ``ease``/``ease-in-out`` closely).
|
|
102
|
+
if t < 0.5:
|
|
103
|
+
return 4.0 * t * t * t
|
|
104
|
+
f = 2.0 * t - 2.0
|
|
105
|
+
return 1.0 + f * f * f / 2.0
|
|
106
|
+
if curve is Curve.BOUNCE:
|
|
107
|
+
return _bounce_out(t)
|
|
108
|
+
if curve is Curve.ELASTIC:
|
|
109
|
+
if t in (0.0, 1.0):
|
|
110
|
+
return t
|
|
111
|
+
c4 = (2.0 * math.pi) / 3.0
|
|
112
|
+
return float(-(2.0 ** (10.0 * t - 10.0)) * math.sin((t * 10.0 - 10.75) * c4))
|
|
113
|
+
return t
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _bounce_out(t: float) -> float:
|
|
117
|
+
"""Compute the ``ease-out`` bounce curve (decelerating with rebounds).
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
t: Linear progress, in ``[0.0, 1.0]``.
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
The bounced progress, in ``[0.0, 1.0]``.
|
|
124
|
+
"""
|
|
125
|
+
n1 = 7.5625
|
|
126
|
+
d1 = 2.75
|
|
127
|
+
if t < 1.0 / d1:
|
|
128
|
+
return n1 * t * t
|
|
129
|
+
if t < 2.0 / d1:
|
|
130
|
+
t -= 1.5 / d1
|
|
131
|
+
return n1 * t * t + 0.75
|
|
132
|
+
if t < 2.5 / d1:
|
|
133
|
+
t -= 2.25 / d1
|
|
134
|
+
return n1 * t * t + 0.9375
|
|
135
|
+
t -= 2.625 / d1
|
|
136
|
+
return n1 * t * t + 0.984375
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
class AnimationController:
|
|
140
|
+
"""Drives a normalized ``value`` on the app's frame clock.
|
|
141
|
+
|
|
142
|
+
A controller is renderer-agnostic: it owns only its progress (``value``,
|
|
143
|
+
0.0..1.0), the direction it is moving (``forward`` toward 1.0, ``reverse``
|
|
144
|
+
toward 0.0), and how to advance — either an eased ramp over ``duration_s`` or
|
|
145
|
+
a :class:`Spring` integration. The app's clock calls :meth:`_advance` once
|
|
146
|
+
per frame with the elapsed ``dt`` and removes the controller when it reports
|
|
147
|
+
completion.
|
|
148
|
+
|
|
149
|
+
The controller binds to an app lazily: it stores no ``App`` reference until
|
|
150
|
+
:meth:`forward`/:meth:`reverse` is called *after* the app has registered it
|
|
151
|
+
(via :meth:`bind`), so a controller can be constructed in a ``view`` without
|
|
152
|
+
a circular import.
|
|
153
|
+
|
|
154
|
+
Attributes:
|
|
155
|
+
value: The current progress, 0.0..1.0 — read by the ``view``.
|
|
156
|
+
|
|
157
|
+
Methods:
|
|
158
|
+
bind: Attach the controller to an app's frame clock.
|
|
159
|
+
forward: Animate ``value`` toward 1.0 and (re)register on the app clock.
|
|
160
|
+
reverse: Animate ``value`` toward 0.0 and (re)register on the app clock.
|
|
161
|
+
stop: Halt the animation and unregister from the app clock.
|
|
162
|
+
"""
|
|
163
|
+
|
|
164
|
+
def __init__(
|
|
165
|
+
self,
|
|
166
|
+
duration_s: float,
|
|
167
|
+
curve: Curve = Curve.EASE_IN_OUT,
|
|
168
|
+
spring: Spring | None = None,
|
|
169
|
+
*,
|
|
170
|
+
time_source: Callable[[], float] | None = None,
|
|
171
|
+
) -> None:
|
|
172
|
+
"""Initialize the controller.
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
duration_s: The ramp duration in seconds (ignored when ``spring`` is
|
|
176
|
+
given). Must be positive for a fixed-duration ramp.
|
|
177
|
+
curve: The easing curve applied to the linear ramp.
|
|
178
|
+
spring: Optional spring parameters; when set, the controller
|
|
179
|
+
integrates a damped oscillator instead of easing over a fixed
|
|
180
|
+
duration.
|
|
181
|
+
time_source: Optional injectable monotonic clock (seconds). Tests
|
|
182
|
+
pass a deterministic source; in production the app supplies its
|
|
183
|
+
own loop clock, so this is normally left unset.
|
|
184
|
+
"""
|
|
185
|
+
self.duration_s: float = duration_s
|
|
186
|
+
self.curve: Curve = curve
|
|
187
|
+
self.spring: Spring | None = spring
|
|
188
|
+
self.value: float = 0.0
|
|
189
|
+
self._time_source: Callable[[], float] | None = time_source
|
|
190
|
+
self._dir: int = 0
|
|
191
|
+
self._elapsed: float = 0.0
|
|
192
|
+
# Spring integration state (velocity), only used when ``spring`` is set.
|
|
193
|
+
self._velocity: float = 0.0
|
|
194
|
+
self._app: _AppClock | None = None
|
|
195
|
+
|
|
196
|
+
def bind(self, app: _AppClock) -> None:
|
|
197
|
+
"""Attach the controller to an app's frame clock.
|
|
198
|
+
|
|
199
|
+
Called by :meth:`~tempestroid.core.state.App.register_animation` so a
|
|
200
|
+
later :meth:`stop` can unregister, and so :meth:`forward`/:meth:`reverse`
|
|
201
|
+
can (re)register even if invoked after construction.
|
|
202
|
+
|
|
203
|
+
Args:
|
|
204
|
+
app: The app driving this controller's frames.
|
|
205
|
+
"""
|
|
206
|
+
self._app = app
|
|
207
|
+
|
|
208
|
+
def forward(self) -> None:
|
|
209
|
+
"""Animate ``value`` toward 1.0 and (re)register on the app clock."""
|
|
210
|
+
self._dir = 1
|
|
211
|
+
if self._app is not None:
|
|
212
|
+
self._app.register_animation(self)
|
|
213
|
+
|
|
214
|
+
def reverse(self) -> None:
|
|
215
|
+
"""Animate ``value`` toward 0.0 and (re)register on the app clock."""
|
|
216
|
+
self._dir = -1
|
|
217
|
+
if self._app is not None:
|
|
218
|
+
self._app.register_animation(self)
|
|
219
|
+
|
|
220
|
+
def stop(self) -> None:
|
|
221
|
+
"""Halt the animation and unregister from the app clock."""
|
|
222
|
+
self._dir = 0
|
|
223
|
+
self._velocity = 0.0
|
|
224
|
+
if self._app is not None:
|
|
225
|
+
self._app.unregister_animation(self)
|
|
226
|
+
|
|
227
|
+
def _advance(self, dt: float) -> bool:
|
|
228
|
+
"""Advance the controller by ``dt`` seconds toward its target.
|
|
229
|
+
|
|
230
|
+
Args:
|
|
231
|
+
dt: Elapsed wall-clock time since the previous frame, in seconds.
|
|
232
|
+
|
|
233
|
+
Returns:
|
|
234
|
+
``True`` when the controller has reached its target (0.0 on reverse,
|
|
235
|
+
1.0 on forward) and should be unregistered; ``False`` otherwise.
|
|
236
|
+
"""
|
|
237
|
+
if self._dir == 0:
|
|
238
|
+
return True
|
|
239
|
+
if dt < 0.0:
|
|
240
|
+
dt = 0.0
|
|
241
|
+
if self.spring is not None:
|
|
242
|
+
return self._advance_spring(dt)
|
|
243
|
+
return self._advance_ramp(dt)
|
|
244
|
+
|
|
245
|
+
def _advance_ramp(self, dt: float) -> bool:
|
|
246
|
+
"""Advance an eased fixed-duration ramp.
|
|
247
|
+
|
|
248
|
+
Args:
|
|
249
|
+
dt: Elapsed time since the previous frame, in seconds.
|
|
250
|
+
|
|
251
|
+
Returns:
|
|
252
|
+
``True`` when the ramp has reached its target end, else ``False``.
|
|
253
|
+
"""
|
|
254
|
+
target = 1.0 if self._dir > 0 else 0.0
|
|
255
|
+
if self.duration_s <= 0.0:
|
|
256
|
+
self.value = target
|
|
257
|
+
self._dir = 0
|
|
258
|
+
return True
|
|
259
|
+
self._elapsed += dt
|
|
260
|
+
progress = self._elapsed / self.duration_s
|
|
261
|
+
if progress >= 1.0:
|
|
262
|
+
self.value = target
|
|
263
|
+
self._elapsed = 0.0
|
|
264
|
+
self._dir = 0
|
|
265
|
+
return True
|
|
266
|
+
eased = _apply_curve(self.curve, progress)
|
|
267
|
+
# On reverse, walk the eased curve back from 1.0 toward 0.0.
|
|
268
|
+
self.value = eased if self._dir > 0 else 1.0 - eased
|
|
269
|
+
return False
|
|
270
|
+
|
|
271
|
+
def _advance_spring(self, dt: float) -> bool:
|
|
272
|
+
"""Integrate the damped harmonic oscillator one frame toward the target.
|
|
273
|
+
|
|
274
|
+
Args:
|
|
275
|
+
dt: Elapsed time since the previous frame, in seconds.
|
|
276
|
+
|
|
277
|
+
Returns:
|
|
278
|
+
``True`` when the spring has settled at its target, else ``False``.
|
|
279
|
+
"""
|
|
280
|
+
spring = self.spring
|
|
281
|
+
assert spring is not None # narrowed by the caller
|
|
282
|
+
target = 1.0 if self._dir > 0 else 0.0
|
|
283
|
+
displacement = self.value - target
|
|
284
|
+
force = -spring.stiffness * displacement - spring.damping * self._velocity
|
|
285
|
+
acceleration = force / spring.mass
|
|
286
|
+
self._velocity += acceleration * dt
|
|
287
|
+
self.value += self._velocity * dt
|
|
288
|
+
# Settle once both the displacement and the velocity are negligible.
|
|
289
|
+
if abs(self.value - target) < 0.001 and abs(self._velocity) < 0.001:
|
|
290
|
+
self.value = target
|
|
291
|
+
self._velocity = 0.0
|
|
292
|
+
self._dir = 0
|
|
293
|
+
return True
|
|
294
|
+
return False
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
class Tween(BaseModel, Generic[T]):
|
|
298
|
+
"""A linear interpolator between two typed endpoints.
|
|
299
|
+
|
|
300
|
+
Supports ``float``, :class:`~tempestroid.style.Color` (per-channel),
|
|
301
|
+
:class:`~tempestroid.style.Edge` (per-side) and numeric ``tuple`` endpoints.
|
|
302
|
+
The ``view`` reads :meth:`at` with an :class:`AnimationController`'s ``value``
|
|
303
|
+
to get the per-frame interpolated value, which it then feeds into a
|
|
304
|
+
:class:`~tempestroid.style.Style` — so the interpolation stays in the core.
|
|
305
|
+
|
|
306
|
+
Type Args:
|
|
307
|
+
T: The endpoint type being interpolated.
|
|
308
|
+
|
|
309
|
+
Attributes:
|
|
310
|
+
begin: The value at ``t == 0.0``.
|
|
311
|
+
end: The value at ``t == 1.0``.
|
|
312
|
+
|
|
313
|
+
Methods:
|
|
314
|
+
at: Interpolate between ``begin`` and ``end`` at fraction ``t``.
|
|
315
|
+
"""
|
|
316
|
+
|
|
317
|
+
model_config = ConfigDict(frozen=True)
|
|
318
|
+
|
|
319
|
+
begin: T
|
|
320
|
+
end: T
|
|
321
|
+
|
|
322
|
+
def at(self, t: float) -> T:
|
|
323
|
+
"""Interpolate between :attr:`begin` and :attr:`end` at fraction ``t``.
|
|
324
|
+
|
|
325
|
+
Args:
|
|
326
|
+
t: The interpolation fraction, typically an
|
|
327
|
+
:class:`AnimationController`'s ``value`` (0.0..1.0). Values
|
|
328
|
+
outside ``[0, 1]`` extrapolate linearly.
|
|
329
|
+
|
|
330
|
+
Returns:
|
|
331
|
+
The interpolated value, of the same type as the endpoints.
|
|
332
|
+
|
|
333
|
+
Raises:
|
|
334
|
+
TypeError: If the endpoint type is not a supported interpolatable
|
|
335
|
+
type.
|
|
336
|
+
"""
|
|
337
|
+
begin: object = self.begin
|
|
338
|
+
end: object = self.end
|
|
339
|
+
if isinstance(begin, bool) or isinstance(end, bool):
|
|
340
|
+
raise TypeError("Tween does not interpolate bool endpoints")
|
|
341
|
+
if isinstance(begin, (int, float)) and isinstance(end, (int, float)):
|
|
342
|
+
return _lerp_float(float(begin), float(end), t) # type: ignore[return-value]
|
|
343
|
+
if isinstance(begin, Color) and isinstance(end, Color):
|
|
344
|
+
return _lerp_color(begin, end, t) # type: ignore[return-value]
|
|
345
|
+
if isinstance(begin, Edge) and isinstance(end, Edge):
|
|
346
|
+
return _lerp_edge(begin, end, t) # type: ignore[return-value]
|
|
347
|
+
if isinstance(begin, tuple) and isinstance(end, tuple):
|
|
348
|
+
a = cast("tuple[float, ...]", begin)
|
|
349
|
+
b = cast("tuple[float, ...]", end)
|
|
350
|
+
return _lerp_tuple(a, b, t) # type: ignore[return-value]
|
|
351
|
+
type_name: str = type(begin).__name__
|
|
352
|
+
raise TypeError(f"Tween cannot interpolate endpoints of type {type_name}")
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def _lerp_float(a: float, b: float, t: float) -> float:
|
|
356
|
+
"""Linearly interpolate two floats.
|
|
357
|
+
|
|
358
|
+
Args:
|
|
359
|
+
a: The value at ``t == 0.0``.
|
|
360
|
+
b: The value at ``t == 1.0``.
|
|
361
|
+
t: The interpolation fraction.
|
|
362
|
+
|
|
363
|
+
Returns:
|
|
364
|
+
``a + (b - a) * t``.
|
|
365
|
+
"""
|
|
366
|
+
return a + (b - a) * t
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def _lerp_color(a: Color, b: Color, t: float) -> Color:
|
|
370
|
+
"""Interpolate two colors per channel (r, g, b rounded; alpha as float).
|
|
371
|
+
|
|
372
|
+
Args:
|
|
373
|
+
a: The color at ``t == 0.0``.
|
|
374
|
+
b: The color at ``t == 1.0``.
|
|
375
|
+
t: The interpolation fraction.
|
|
376
|
+
|
|
377
|
+
Returns:
|
|
378
|
+
The interpolated color.
|
|
379
|
+
"""
|
|
380
|
+
return Color(
|
|
381
|
+
r=round(_lerp_float(a.r, b.r, t)),
|
|
382
|
+
g=round(_lerp_float(a.g, b.g, t)),
|
|
383
|
+
b=round(_lerp_float(a.b, b.b, t)),
|
|
384
|
+
a=_lerp_float(a.a, b.a, t),
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def _lerp_edge(a: Edge, b: Edge, t: float) -> Edge:
|
|
389
|
+
"""Interpolate two edges per side.
|
|
390
|
+
|
|
391
|
+
Args:
|
|
392
|
+
a: The edge at ``t == 0.0``.
|
|
393
|
+
b: The edge at ``t == 1.0``.
|
|
394
|
+
t: The interpolation fraction.
|
|
395
|
+
|
|
396
|
+
Returns:
|
|
397
|
+
The interpolated edge.
|
|
398
|
+
"""
|
|
399
|
+
return Edge(
|
|
400
|
+
top=_lerp_float(a.top, b.top, t),
|
|
401
|
+
right=_lerp_float(a.right, b.right, t),
|
|
402
|
+
bottom=_lerp_float(a.bottom, b.bottom, t),
|
|
403
|
+
left=_lerp_float(a.left, b.left, t),
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
def _lerp_tuple(
|
|
408
|
+
a: tuple[float, ...], b: tuple[float, ...], t: float
|
|
409
|
+
) -> tuple[float, ...]:
|
|
410
|
+
"""Interpolate two equal-length numeric tuples element-wise.
|
|
411
|
+
|
|
412
|
+
Args:
|
|
413
|
+
a: The tuple at ``t == 0.0``.
|
|
414
|
+
b: The tuple at ``t == 1.0``.
|
|
415
|
+
t: The interpolation fraction.
|
|
416
|
+
|
|
417
|
+
Returns:
|
|
418
|
+
The interpolated tuple.
|
|
419
|
+
|
|
420
|
+
Raises:
|
|
421
|
+
ValueError: If the tuples differ in length.
|
|
422
|
+
"""
|
|
423
|
+
if len(a) != len(b):
|
|
424
|
+
raise ValueError("Tween tuple endpoints must have the same length")
|
|
425
|
+
return tuple(_lerp_float(x, y, t) for x, y in zip(a, b, strict=True))
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""Composite, higher-level UI components built from primitive widgets.
|
|
2
|
+
|
|
3
|
+
Each component is a :class:`tempestroid.widgets.Component` that lowers to a
|
|
4
|
+
primitive ``Text`` / ``Row`` / ``Column`` / ``Container`` tree via its ``render``
|
|
5
|
+
method, so it works in both renderers (Qt and Compose) with no renderer changes
|
|
6
|
+
and is fully device-ready. The package collects reusable page-structure and
|
|
7
|
+
navigation building blocks:
|
|
8
|
+
|
|
9
|
+
* :class:`AppBar` — top bar with leading widget, title and trailing actions.
|
|
10
|
+
* :class:`Header` / :class:`Footer` — page header band and bottom bar.
|
|
11
|
+
* :class:`Sidebar` — fixed-width lateral column.
|
|
12
|
+
* :class:`Scaffold` — page frame stacking app bar, body and bottom bar.
|
|
13
|
+
* :class:`NavBar` — selectable navigation/tab bar with an active index.
|
|
14
|
+
* Brazilian form inputs — :class:`EmailInput`, :class:`PasswordInput`,
|
|
15
|
+
:class:`PhoneInput`, :class:`CPFInput`, :class:`CNPJInput` and the grouped
|
|
16
|
+
:class:`AddressInput` (pair them with :mod:`tempestroid.validators`).
|
|
17
|
+
* Media pickers — :class:`ImagePicker`, :class:`DocumentPicker` and the circular
|
|
18
|
+
:class:`ImagePicture` profile-photo picker.
|
|
19
|
+
|
|
20
|
+
The default theme tokens and :func:`merge_style` (used to overlay a caller's
|
|
21
|
+
``style`` onto a component default) are re-exported for building custom
|
|
22
|
+
components in the same idiom.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
from tempest_core.components.bars import (
|
|
28
|
+
AppBar,
|
|
29
|
+
CollapsingAppBar,
|
|
30
|
+
Footer,
|
|
31
|
+
Header,
|
|
32
|
+
)
|
|
33
|
+
from tempest_core.components.base import (
|
|
34
|
+
ACCENT,
|
|
35
|
+
BACKGROUND,
|
|
36
|
+
MUTED,
|
|
37
|
+
ON_MUTED,
|
|
38
|
+
ON_SURFACE,
|
|
39
|
+
SURFACE,
|
|
40
|
+
merge_style,
|
|
41
|
+
)
|
|
42
|
+
from tempest_core.components.brforms import (
|
|
43
|
+
AddressInput,
|
|
44
|
+
CNPJInput,
|
|
45
|
+
CPFInput,
|
|
46
|
+
EmailInput,
|
|
47
|
+
PasswordInput,
|
|
48
|
+
PhoneInput,
|
|
49
|
+
)
|
|
50
|
+
from tempest_core.components.cards import Avatar, Card, Divider, ListTile
|
|
51
|
+
from tempest_core.components.dates import Calendar, Clock
|
|
52
|
+
from tempest_core.components.disclosure import Accordion
|
|
53
|
+
from tempest_core.components.feedback import Badge, Banner, EmptyState
|
|
54
|
+
from tempest_core.components.fields import SearchBar, Stepper
|
|
55
|
+
from tempest_core.components.layout import Grid, Scaffold, Sidebar
|
|
56
|
+
from tempest_core.components.mediainputs import (
|
|
57
|
+
DocumentPicker,
|
|
58
|
+
ImagePicker,
|
|
59
|
+
ImagePicture,
|
|
60
|
+
)
|
|
61
|
+
from tempest_core.components.menu import Burger, Drawer
|
|
62
|
+
from tempest_core.components.navigation import Breadcrumb, NavBar
|
|
63
|
+
from tempest_core.components.selection import (
|
|
64
|
+
Chip,
|
|
65
|
+
RadioGroup,
|
|
66
|
+
Rating,
|
|
67
|
+
SegmentedControl,
|
|
68
|
+
)
|
|
69
|
+
from tempest_core.components.table import DataTable, Table, TableCell, TableRow
|
|
70
|
+
|
|
71
|
+
__all__ = [
|
|
72
|
+
"AppBar",
|
|
73
|
+
"CollapsingAppBar",
|
|
74
|
+
"Header",
|
|
75
|
+
"Footer",
|
|
76
|
+
"Sidebar",
|
|
77
|
+
"Scaffold",
|
|
78
|
+
"Grid",
|
|
79
|
+
"NavBar",
|
|
80
|
+
"Breadcrumb",
|
|
81
|
+
"Burger",
|
|
82
|
+
"Drawer",
|
|
83
|
+
"Calendar",
|
|
84
|
+
"Clock",
|
|
85
|
+
"Card",
|
|
86
|
+
"ListTile",
|
|
87
|
+
"Avatar",
|
|
88
|
+
"Divider",
|
|
89
|
+
"SegmentedControl",
|
|
90
|
+
"RadioGroup",
|
|
91
|
+
"Chip",
|
|
92
|
+
"Rating",
|
|
93
|
+
"Stepper",
|
|
94
|
+
"SearchBar",
|
|
95
|
+
"EmailInput",
|
|
96
|
+
"PasswordInput",
|
|
97
|
+
"PhoneInput",
|
|
98
|
+
"CPFInput",
|
|
99
|
+
"CNPJInput",
|
|
100
|
+
"AddressInput",
|
|
101
|
+
"ImagePicker",
|
|
102
|
+
"DocumentPicker",
|
|
103
|
+
"ImagePicture",
|
|
104
|
+
"Accordion",
|
|
105
|
+
"Banner",
|
|
106
|
+
"EmptyState",
|
|
107
|
+
"Badge",
|
|
108
|
+
"Table",
|
|
109
|
+
"DataTable",
|
|
110
|
+
"TableCell",
|
|
111
|
+
"TableRow",
|
|
112
|
+
"merge_style",
|
|
113
|
+
"BACKGROUND",
|
|
114
|
+
"SURFACE",
|
|
115
|
+
"ACCENT",
|
|
116
|
+
"MUTED",
|
|
117
|
+
"ON_SURFACE",
|
|
118
|
+
"ON_MUTED",
|
|
119
|
+
]
|