qt-css-engine 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.
- qt_css_engine/__init__.py +23 -0
- qt_css_engine/constants.py +190 -0
- qt_css_engine/css_parser.py +397 -0
- qt_css_engine/engine.py +1035 -0
- qt_css_engine/gradients.py +321 -0
- qt_css_engine/handlers.py +402 -0
- qt_css_engine/py.typed +0 -0
- qt_css_engine/qt_compat/QtCore.py +15 -0
- qt_css_engine/qt_compat/QtGui.py +10 -0
- qt_css_engine/qt_compat/QtWidgets.py +10 -0
- qt_css_engine/qt_compat/__init__.py +22 -0
- qt_css_engine/qt_compat/_api.py +15 -0
- qt_css_engine/types.py +77 -0
- qt_css_engine/utils.py +447 -0
- qt_css_engine-0.1.0.dist-info/METADATA +187 -0
- qt_css_engine-0.1.0.dist-info/RECORD +18 -0
- qt_css_engine-0.1.0.dist-info/WHEEL +4 -0
- qt_css_engine-0.1.0.dist-info/licenses/LICENSE.md +9 -0
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
from qt_css_engine.constants import NON_NEGATIVE_PROPS
|
|
2
|
+
|
|
3
|
+
from .qt_compat.QtCore import QEasingCurve, QObject, QVariantAnimation
|
|
4
|
+
from .qt_compat.QtGui import QColor
|
|
5
|
+
from .qt_compat.QtWidgets import QWidget
|
|
6
|
+
from .types import ShadowParams, WidgetContext
|
|
7
|
+
from .utils import (
|
|
8
|
+
apply_opacity_to_widget,
|
|
9
|
+
apply_shadow_to_widget,
|
|
10
|
+
interpolate_oklab,
|
|
11
|
+
lerp_shadow,
|
|
12
|
+
parse_box_shadow,
|
|
13
|
+
parse_color,
|
|
14
|
+
parse_css_numeric,
|
|
15
|
+
parse_css_val,
|
|
16
|
+
scoped_anim_style,
|
|
17
|
+
shadow_as_transparent,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class BoxShadowHandle(QObject):
|
|
22
|
+
"""
|
|
23
|
+
Animates box-shadow via a QGraphicsDropShadowEffect on the widget.
|
|
24
|
+
|
|
25
|
+
Stored in active_animations like other animation types so orphan cleanup (class
|
|
26
|
+
removal, rule changes) correctly clears the shadow. Transitions between shadow
|
|
27
|
+
states — including None (no shadow) — interpolate all fields and alpha.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
widget: QWidget,
|
|
33
|
+
initial_raw: str,
|
|
34
|
+
duration_ms: int,
|
|
35
|
+
easing_curve: QEasingCurve | QEasingCurve.Type,
|
|
36
|
+
parent: QObject | None = None,
|
|
37
|
+
effect_priority: str = "opacity",
|
|
38
|
+
) -> None:
|
|
39
|
+
super().__init__(parent)
|
|
40
|
+
self.widget = widget
|
|
41
|
+
self.effect_priority = effect_priority
|
|
42
|
+
self._current: ShadowParams | None = parse_box_shadow(initial_raw)
|
|
43
|
+
self._start: ShadowParams | None = None
|
|
44
|
+
self._end: ShadowParams | None = None
|
|
45
|
+
self._anim_origin: ShadowParams | None = self._current
|
|
46
|
+
apply_shadow_to_widget(widget, self._current, self.effect_priority)
|
|
47
|
+
|
|
48
|
+
self.anim = QVariantAnimation(self)
|
|
49
|
+
self.anim.setDuration(duration_ms)
|
|
50
|
+
self.anim.setEasingCurve(easing_curve)
|
|
51
|
+
self.anim.setStartValue(0.0)
|
|
52
|
+
self.anim.setEndValue(1.0)
|
|
53
|
+
self.anim.valueChanged.connect(self._on_tick)
|
|
54
|
+
self.anim.finished.connect(self._on_finished)
|
|
55
|
+
|
|
56
|
+
def _on_tick(self, t: float) -> None:
|
|
57
|
+
"""Interpolate the shadow between _start and _end at progress t and apply it."""
|
|
58
|
+
# Resolve None as a transparent copy of the other end for fade in/out.
|
|
59
|
+
if self._start is None and self._end is None:
|
|
60
|
+
return
|
|
61
|
+
start = (
|
|
62
|
+
self._start if self._start is not None else shadow_as_transparent(self._end) # type: ignore
|
|
63
|
+
)
|
|
64
|
+
end = (
|
|
65
|
+
self._end if self._end is not None else shadow_as_transparent(self._start) # type: ignore
|
|
66
|
+
)
|
|
67
|
+
self._current = lerp_shadow(start, end, t)
|
|
68
|
+
apply_shadow_to_widget(self.widget, self._current, self.effect_priority)
|
|
69
|
+
|
|
70
|
+
def _on_finished(self) -> None:
|
|
71
|
+
"""Snap to exact target on completion to clear floating-point residuals."""
|
|
72
|
+
self._current = self._end
|
|
73
|
+
self._start = None
|
|
74
|
+
try:
|
|
75
|
+
apply_shadow_to_widget(self.widget, self._current, self.effect_priority)
|
|
76
|
+
except RuntimeError:
|
|
77
|
+
pass
|
|
78
|
+
|
|
79
|
+
def update_spec(self, duration_ms: int, easing_curve: QEasingCurve) -> None:
|
|
80
|
+
"""Update duration and easing curve without restarting the animation."""
|
|
81
|
+
self.anim.setDuration(duration_ms)
|
|
82
|
+
self.anim.setEasingCurve(easing_curve)
|
|
83
|
+
|
|
84
|
+
def snap_to(self, value_raw: str) -> None:
|
|
85
|
+
"""Instantly apply a CSS box-shadow value without animation."""
|
|
86
|
+
self.anim.stop()
|
|
87
|
+
self._current = parse_box_shadow(value_raw)
|
|
88
|
+
self._start = None
|
|
89
|
+
self._end = None
|
|
90
|
+
self._anim_origin = self._current
|
|
91
|
+
apply_shadow_to_widget(self.widget, self._current, self.effect_priority)
|
|
92
|
+
|
|
93
|
+
def set_target(self, target_raw: str) -> None:
|
|
94
|
+
"""Start or re-target the animation toward a new CSS box-shadow value."""
|
|
95
|
+
target = parse_box_shadow(target_raw)
|
|
96
|
+
|
|
97
|
+
is_running = self.anim.state() == self.anim.State.Running
|
|
98
|
+
if is_running and target == self._end:
|
|
99
|
+
return
|
|
100
|
+
if not is_running and target == self._current:
|
|
101
|
+
return
|
|
102
|
+
|
|
103
|
+
if is_running and self._anim_origin is not None and target == self._anim_origin and self._end is not None:
|
|
104
|
+
# Reversing to origin — swap start/end and seek so steps() retraces original path.
|
|
105
|
+
dur = max(1, self.anim.duration())
|
|
106
|
+
raw_p = min(self.anim.currentTime(), dur) / dur
|
|
107
|
+
seek_ms = int((1.0 - raw_p) * dur)
|
|
108
|
+
old_end = self._end
|
|
109
|
+
self._anim_origin = old_end
|
|
110
|
+
self._start = old_end
|
|
111
|
+
self._end = target
|
|
112
|
+
self.anim.stop()
|
|
113
|
+
self.anim.setStartValue(0.0)
|
|
114
|
+
self.anim.setEndValue(1.0)
|
|
115
|
+
self.anim.start()
|
|
116
|
+
self.anim.setCurrentTime(seek_ms)
|
|
117
|
+
# Force tick: step-start always returns 1.0 so Qt suppresses valueChanged.
|
|
118
|
+
self._on_tick(self.anim.easingCurve().valueForProgress(min(seek_ms, dur) / dur))
|
|
119
|
+
return
|
|
120
|
+
|
|
121
|
+
self._anim_origin = self._current
|
|
122
|
+
self._start = self._current
|
|
123
|
+
self._end = target
|
|
124
|
+
self.anim.stop()
|
|
125
|
+
self.anim.setStartValue(0.0)
|
|
126
|
+
self.anim.setEndValue(1.0)
|
|
127
|
+
self.anim.start()
|
|
128
|
+
self._on_tick(self.anim.easingCurve().valueForProgress(0.0))
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class ColorAnimation(QObject):
|
|
132
|
+
"""
|
|
133
|
+
Animates QSS color properties (background-color, color, border-color, etc.).
|
|
134
|
+
Uses OKLab interpolation for perceptually uniform color transitions.
|
|
135
|
+
"""
|
|
136
|
+
|
|
137
|
+
def __init__(
|
|
138
|
+
self,
|
|
139
|
+
widget: QWidget,
|
|
140
|
+
prop: str,
|
|
141
|
+
initial_raw: str | QColor,
|
|
142
|
+
duration_ms: int,
|
|
143
|
+
easing_curve: QEasingCurve | QEasingCurve.Type,
|
|
144
|
+
parent: QObject | None = None,
|
|
145
|
+
ctx: WidgetContext | None = None,
|
|
146
|
+
) -> None:
|
|
147
|
+
super().__init__(parent)
|
|
148
|
+
self.widget = widget
|
|
149
|
+
self.prop = prop
|
|
150
|
+
self._ctx = ctx
|
|
151
|
+
self.current_color = parse_color(initial_raw) if isinstance(initial_raw, str) else QColor(initial_raw)
|
|
152
|
+
self.start_color = self.current_color
|
|
153
|
+
self.end_color = self.current_color
|
|
154
|
+
self._anim_origin_color: QColor | None = QColor(self.current_color)
|
|
155
|
+
|
|
156
|
+
self.anim = QVariantAnimation(self)
|
|
157
|
+
self.anim.setDuration(duration_ms)
|
|
158
|
+
self.anim.setEasingCurve(easing_curve)
|
|
159
|
+
self.anim.valueChanged.connect(self._on_tick)
|
|
160
|
+
|
|
161
|
+
@property
|
|
162
|
+
def _props(self) -> dict[str, str]:
|
|
163
|
+
"""Live css_anim_props dict from context, or widget fallback for standalone use."""
|
|
164
|
+
if self._ctx is not None:
|
|
165
|
+
return self._ctx.css_anim_props
|
|
166
|
+
# Fallback for standalone usage (tests without engine context).
|
|
167
|
+
props: dict[str, str] = getattr(self.widget, "_css_anim_props", {})
|
|
168
|
+
return props
|
|
169
|
+
|
|
170
|
+
def _on_tick(self, t: float) -> None:
|
|
171
|
+
"""Write interpolated color to css_anim_props and refresh the widget stylesheet."""
|
|
172
|
+
self.current_color = interpolate_oklab(self.start_color, self.end_color, t)
|
|
173
|
+
props = self._props
|
|
174
|
+
props[self.prop] = self.current_color.name(QColor.NameFormat.HexArgb)
|
|
175
|
+
self.widget.setStyleSheet(scoped_anim_style(self.widget, props))
|
|
176
|
+
|
|
177
|
+
def update_spec(self, duration_ms: int, easing_curve: QEasingCurve) -> None:
|
|
178
|
+
"""Update duration and easing curve without restarting the animation."""
|
|
179
|
+
self.anim.setDuration(duration_ms)
|
|
180
|
+
self.anim.setEasingCurve(easing_curve)
|
|
181
|
+
|
|
182
|
+
def snap_to(self, value_raw: str) -> None:
|
|
183
|
+
"""Instantly apply a CSS color value without animation."""
|
|
184
|
+
self.anim.stop()
|
|
185
|
+
self.current_color = parse_color(value_raw)
|
|
186
|
+
self.start_color = self.current_color
|
|
187
|
+
self.end_color = self.current_color
|
|
188
|
+
self._anim_origin_color = QColor(self.current_color)
|
|
189
|
+
self._props[self.prop] = self.current_color.name(QColor.NameFormat.HexArgb)
|
|
190
|
+
|
|
191
|
+
def set_target(self, target_raw: str) -> None:
|
|
192
|
+
"""Start or re-target the animation toward a new CSS color value."""
|
|
193
|
+
target_color = parse_color(target_raw)
|
|
194
|
+
is_running = self.anim.state() == self.anim.State.Running
|
|
195
|
+
if is_running and target_color == self.end_color:
|
|
196
|
+
return
|
|
197
|
+
|
|
198
|
+
if is_running and self._anim_origin_color is not None and target_color == self._anim_origin_color:
|
|
199
|
+
# Reversing to origin — swap start/end and seek so steps() retraces original path.
|
|
200
|
+
dur = max(1, self.anim.duration())
|
|
201
|
+
raw_p = min(self.anim.currentTime(), dur) / dur
|
|
202
|
+
seek_ms = int((1.0 - raw_p) * dur)
|
|
203
|
+
old_end = QColor(self.end_color)
|
|
204
|
+
self._anim_origin_color = old_end
|
|
205
|
+
self.start_color = old_end
|
|
206
|
+
self.end_color = target_color
|
|
207
|
+
self.anim.stop()
|
|
208
|
+
self.anim.setStartValue(0.0)
|
|
209
|
+
self.anim.setEndValue(1.0)
|
|
210
|
+
self.anim.start()
|
|
211
|
+
self.anim.setCurrentTime(seek_ms)
|
|
212
|
+
# Force tick: step-start always returns 1.0 so Qt suppresses valueChanged
|
|
213
|
+
# (new value == previous value from forward trip) — manually ensure the color updates.
|
|
214
|
+
self._on_tick(self.anim.easingCurve().valueForProgress(min(seek_ms, dur) / dur))
|
|
215
|
+
return
|
|
216
|
+
|
|
217
|
+
self._anim_origin_color = QColor(self.current_color)
|
|
218
|
+
self.start_color = self.current_color
|
|
219
|
+
self.end_color = target_color
|
|
220
|
+
self.anim.stop()
|
|
221
|
+
self.anim.setStartValue(0.0)
|
|
222
|
+
self.anim.setEndValue(1.0)
|
|
223
|
+
self.anim.start()
|
|
224
|
+
# Force first frame: Qt suppresses valueChanged when eased value at t=0 equals the
|
|
225
|
+
# previous currentValue. step-start (n=1) always returns 1.0, so forward and return
|
|
226
|
+
# trips both produce 1.0 — the signal is silenced on the return and the color never
|
|
227
|
+
# updates. Explicit tick ensures current_color is applied regardless.
|
|
228
|
+
self._on_tick(self.anim.easingCurve().valueForProgress(0.0))
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
class GenericPropertyAnimation(QObject):
|
|
232
|
+
"""
|
|
233
|
+
Animates numeric QSS properties (widths, heights, margins, padding, etc.).
|
|
234
|
+
Writes interpolated values directly into the widget's scoped inline stylesheet.
|
|
235
|
+
"""
|
|
236
|
+
|
|
237
|
+
def __init__(
|
|
238
|
+
self,
|
|
239
|
+
widget: QWidget,
|
|
240
|
+
prop: str,
|
|
241
|
+
initial_val: int | float,
|
|
242
|
+
duration_ms: int,
|
|
243
|
+
easing_curve: QEasingCurve | QEasingCurve.Type,
|
|
244
|
+
parent: QObject | None = None,
|
|
245
|
+
unit: str = "px",
|
|
246
|
+
ctx: WidgetContext | None = None,
|
|
247
|
+
) -> None:
|
|
248
|
+
super().__init__(parent)
|
|
249
|
+
self.widget = widget
|
|
250
|
+
self.prop = prop
|
|
251
|
+
self.unit = unit
|
|
252
|
+
self._ctx = ctx
|
|
253
|
+
self.current_val = float(initial_val)
|
|
254
|
+
# Captured at creation time (before any inline constraint is applied).
|
|
255
|
+
# Used as the animation target when returning to natural/unconstrained state,
|
|
256
|
+
# so we never call sizeHint() while min-width/max-width are still active.
|
|
257
|
+
self.natural_val: float = float(initial_val)
|
|
258
|
+
self._clean_on_finish = False
|
|
259
|
+
self._anim_origin_val: float | None = float(initial_val)
|
|
260
|
+
|
|
261
|
+
self.anim = QVariantAnimation(self)
|
|
262
|
+
self.anim.setDuration(duration_ms)
|
|
263
|
+
self.anim.setEasingCurve(easing_curve)
|
|
264
|
+
self.anim.valueChanged.connect(self._on_tick)
|
|
265
|
+
self.anim.finished.connect(self._on_finished)
|
|
266
|
+
|
|
267
|
+
@property
|
|
268
|
+
def _props(self) -> dict[str, str]:
|
|
269
|
+
"""Live css_anim_props dict from context, or widget fallback for standalone use."""
|
|
270
|
+
if self._ctx is not None:
|
|
271
|
+
return self._ctx.css_anim_props
|
|
272
|
+
props: dict[str, str] = getattr(self.widget, "_css_anim_props", {})
|
|
273
|
+
return props
|
|
274
|
+
|
|
275
|
+
def _on_tick(self, val: int | float) -> None:
|
|
276
|
+
"""Write interpolated numeric value to css_anim_props and refresh the widget stylesheet."""
|
|
277
|
+
self.current_val = float(val)
|
|
278
|
+
written = max(0.0, self.current_val) if self.prop in NON_NEGATIVE_PROPS else self.current_val
|
|
279
|
+
props = self._props
|
|
280
|
+
props[self.prop] = f"{written:.3f}{self.unit}"
|
|
281
|
+
self.widget.setStyleSheet(scoped_anim_style(self.widget, props))
|
|
282
|
+
|
|
283
|
+
def _on_finished(self) -> None:
|
|
284
|
+
"""Remove the inline size constraint when targeting the natural layout size."""
|
|
285
|
+
if not self._clean_on_finish:
|
|
286
|
+
return
|
|
287
|
+
self._clean_on_finish = False
|
|
288
|
+
props = self._props
|
|
289
|
+
if self.prop in props:
|
|
290
|
+
del props[self.prop]
|
|
291
|
+
try:
|
|
292
|
+
self.widget.setStyleSheet(scoped_anim_style(self.widget, props))
|
|
293
|
+
except RuntimeError:
|
|
294
|
+
pass
|
|
295
|
+
|
|
296
|
+
def update_spec(self, duration_ms: int, easing_curve: QEasingCurve) -> None:
|
|
297
|
+
"""Update duration and easing curve without restarting the animation."""
|
|
298
|
+
self.anim.setDuration(duration_ms)
|
|
299
|
+
self.anim.setEasingCurve(easing_curve)
|
|
300
|
+
|
|
301
|
+
def snap_to(self, value_raw: str) -> None:
|
|
302
|
+
"""Instantly apply a CSS numeric value without animation."""
|
|
303
|
+
self.anim.stop()
|
|
304
|
+
self._clean_on_finish = False
|
|
305
|
+
parsed = parse_css_numeric(value_raw)
|
|
306
|
+
if parsed is not None:
|
|
307
|
+
self.current_val = parsed[0]
|
|
308
|
+
self._anim_origin_val = self.current_val
|
|
309
|
+
self._props[self.prop] = f"{self.current_val:.3f}{self.unit}"
|
|
310
|
+
|
|
311
|
+
def snap_to_natural(self) -> None:
|
|
312
|
+
"""Stop animation and remove this prop from css_anim_props (returns widget to natural layout)."""
|
|
313
|
+
self.anim.stop()
|
|
314
|
+
self._clean_on_finish = False
|
|
315
|
+
props = self._props
|
|
316
|
+
if self.prop in props:
|
|
317
|
+
del props[self.prop]
|
|
318
|
+
|
|
319
|
+
def set_target(self, target_raw: str, clean_on_finish: bool = False) -> None:
|
|
320
|
+
"""Start or re-target the animation toward a new CSS numeric value."""
|
|
321
|
+
parsed = parse_css_numeric(target_raw)
|
|
322
|
+
if parsed is None:
|
|
323
|
+
return
|
|
324
|
+
t_val = float(parsed[0])
|
|
325
|
+
is_running = self.anim.state() == self.anim.State.Running
|
|
326
|
+
if is_running and t_val == self.anim.endValue():
|
|
327
|
+
return
|
|
328
|
+
|
|
329
|
+
if is_running and self._anim_origin_val is not None and abs(t_val - self._anim_origin_val) < 1e-6:
|
|
330
|
+
# Reversing to origin — swap start/end and seek so steps() retraces original path.
|
|
331
|
+
dur = max(1, self.anim.duration())
|
|
332
|
+
raw_p = min(self.anim.currentTime(), dur) / dur
|
|
333
|
+
seek_ms = int((1.0 - raw_p) * dur)
|
|
334
|
+
old_end = float(self.anim.endValue())
|
|
335
|
+
self._anim_origin_val = old_end
|
|
336
|
+
self._clean_on_finish = clean_on_finish
|
|
337
|
+
self.anim.stop()
|
|
338
|
+
self.anim.setStartValue(old_end)
|
|
339
|
+
self.anim.setEndValue(t_val)
|
|
340
|
+
self.anim.start()
|
|
341
|
+
self.anim.setCurrentTime(seek_ms)
|
|
342
|
+
return
|
|
343
|
+
|
|
344
|
+
self._anim_origin_val = self.current_val
|
|
345
|
+
self._clean_on_finish = clean_on_finish
|
|
346
|
+
self.anim.stop()
|
|
347
|
+
self.anim.setStartValue(self.current_val)
|
|
348
|
+
self.anim.setEndValue(t_val)
|
|
349
|
+
self.anim.start()
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
class OpacityAnimation(QObject):
|
|
353
|
+
"""Animates widget opacity via a QGraphicsOpacityEffect."""
|
|
354
|
+
|
|
355
|
+
def __init__(
|
|
356
|
+
self,
|
|
357
|
+
widget: QWidget,
|
|
358
|
+
initial_val: float | int | str,
|
|
359
|
+
duration_ms: int,
|
|
360
|
+
easing_curve: QEasingCurve | QEasingCurve.Type,
|
|
361
|
+
parent: QObject | None = None,
|
|
362
|
+
effect_priority: str = "opacity",
|
|
363
|
+
) -> None:
|
|
364
|
+
super().__init__(parent)
|
|
365
|
+
self.widget = widget
|
|
366
|
+
self.effect_priority = effect_priority
|
|
367
|
+
self._current_val = float(initial_val)
|
|
368
|
+
apply_opacity_to_widget(widget, self._current_val, self.effect_priority)
|
|
369
|
+
|
|
370
|
+
self.anim = QVariantAnimation(self)
|
|
371
|
+
self.anim.setDuration(duration_ms)
|
|
372
|
+
self.anim.setEasingCurve(easing_curve)
|
|
373
|
+
self.anim.valueChanged.connect(self._on_tick)
|
|
374
|
+
|
|
375
|
+
def _on_tick(self, val: float) -> None:
|
|
376
|
+
"""Apply interpolated opacity to the widget's QGraphicsOpacityEffect."""
|
|
377
|
+
self._current_val = val
|
|
378
|
+
apply_opacity_to_widget(self.widget, val, self.effect_priority)
|
|
379
|
+
|
|
380
|
+
def update_spec(self, duration_ms: int, easing_curve: QEasingCurve) -> None:
|
|
381
|
+
"""Update duration and easing curve without restarting the animation."""
|
|
382
|
+
self.anim.setDuration(duration_ms)
|
|
383
|
+
self.anim.setEasingCurve(easing_curve)
|
|
384
|
+
|
|
385
|
+
def snap_to(self, value_raw: str) -> None:
|
|
386
|
+
"""Instantly apply a CSS opacity value without animation."""
|
|
387
|
+
self.anim.stop()
|
|
388
|
+
t_val = parse_css_val(value_raw)
|
|
389
|
+
if isinstance(t_val, (int, float)):
|
|
390
|
+
self._current_val = float(t_val)
|
|
391
|
+
apply_opacity_to_widget(self.widget, self._current_val, self.effect_priority)
|
|
392
|
+
|
|
393
|
+
def set_target(self, target_raw: str) -> None:
|
|
394
|
+
"""Start or re-target the animation toward a new opacity value."""
|
|
395
|
+
t_val = parse_css_val(target_raw)
|
|
396
|
+
if not isinstance(t_val, (int, float)):
|
|
397
|
+
return
|
|
398
|
+
|
|
399
|
+
self.anim.stop()
|
|
400
|
+
self.anim.setStartValue(self._current_val)
|
|
401
|
+
self.anim.setEndValue(float(t_val))
|
|
402
|
+
self.anim.start()
|
qt_css_engine/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# pyright: reportMissingImports = false
|
|
2
|
+
# pyright: reportUnknownVariableType = false
|
|
3
|
+
# pyright: reportUnusedImport = false
|
|
4
|
+
# pyright: reportWildcardImportFromLibrary = false
|
|
5
|
+
# ty: ignore[unresolved-import]
|
|
6
|
+
|
|
7
|
+
from ._api import USE_PYSIDE6
|
|
8
|
+
|
|
9
|
+
if USE_PYSIDE6:
|
|
10
|
+
from PySide6.QtCore import *
|
|
11
|
+
from PySide6.QtCore import Signal, Slot # noqa: F401
|
|
12
|
+
else:
|
|
13
|
+
from PyQt6.QtCore import *
|
|
14
|
+
from PyQt6.QtCore import pyqtSignal as Signal # noqa: F401
|
|
15
|
+
from PyQt6.QtCore import pyqtSlot as Slot # noqa: F401
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# pyright: reportMissingImports = false
|
|
2
|
+
# pyright: reportWildcardImportFromLibrary = false
|
|
3
|
+
# pyright: reportUnknownVariableType = false
|
|
4
|
+
# pyright: reportMissingTypeStubs = false
|
|
5
|
+
# ty: ignore[unresolved-import]
|
|
6
|
+
# ty: ignore[unused-ignore-comment]
|
|
7
|
+
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from ._api import USE_PYSIDE6
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def qt_delete(obj: Any) -> None:
|
|
14
|
+
"""Synchronously delete a Qt C++ object, equivalent to C++ delete."""
|
|
15
|
+
if USE_PYSIDE6:
|
|
16
|
+
from shiboken6 import Shiboken
|
|
17
|
+
|
|
18
|
+
Shiboken.delete(obj) # type: ignore
|
|
19
|
+
else:
|
|
20
|
+
from PyQt6 import sip
|
|
21
|
+
|
|
22
|
+
sip.delete(obj) # type: ignore
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Qt binding detection. Import USE_PYSIDE6 to branch on the active binding."""
|
|
2
|
+
|
|
3
|
+
import importlib.util
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
_qt_api = os.environ.get("QT_API", "").lower()
|
|
8
|
+
_pyside6_loaded = "PySide6" in sys.modules
|
|
9
|
+
_pyqt6_loaded = "PyQt6" in sys.modules
|
|
10
|
+
|
|
11
|
+
USE_PYSIDE6: bool = (
|
|
12
|
+
_qt_api == "pyside6"
|
|
13
|
+
or (not _qt_api and _pyside6_loaded and not _pyqt6_loaded)
|
|
14
|
+
or (not _qt_api and not _pyside6_loaded and not _pyqt6_loaded and bool(importlib.util.find_spec("PySide6")))
|
|
15
|
+
)
|
qt_css_engine/types.py
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
from collections.abc import Callable
|
|
2
|
+
from dataclasses import dataclass, field
|
|
3
|
+
from enum import Enum, auto
|
|
4
|
+
from typing import TYPE_CHECKING, Union
|
|
5
|
+
|
|
6
|
+
from .qt_compat.QtCore import QTimer
|
|
7
|
+
from .qt_compat.QtGui import QColor
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from .handlers import BoxShadowHandle, ColorAnimation, GenericPropertyAnimation, OpacityAnimation
|
|
11
|
+
|
|
12
|
+
Animation = Union["ColorAnimation", "OpacityAnimation", "GenericPropertyAnimation", "BoxShadowHandle"]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class EvaluationCause(Enum):
|
|
16
|
+
"""Why the engine is evaluating a widget's CSS state right now."""
|
|
17
|
+
|
|
18
|
+
DIRECT = auto()
|
|
19
|
+
POLISH = auto()
|
|
20
|
+
PSEUDO_STATE = auto()
|
|
21
|
+
CLASS_CHANGE = auto()
|
|
22
|
+
CLASS_ANIMATION_FINISH = auto()
|
|
23
|
+
WINDOW_DEACTIVATE = auto()
|
|
24
|
+
RULE_RELOAD = auto()
|
|
25
|
+
DELAY_FIRE = auto()
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def snaps_transitions(self) -> bool:
|
|
29
|
+
"""True when animations should snap to target immediately rather than run."""
|
|
30
|
+
return self is EvaluationCause.POLISH
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def is_class_driven(self) -> bool:
|
|
34
|
+
"""True when the evaluation was triggered by a class property change."""
|
|
35
|
+
return self is EvaluationCause.CLASS_CHANGE
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class InternalWriteReason(Enum):
|
|
39
|
+
"""Why the engine is temporarily suppressing event evaluation during internal mutations."""
|
|
40
|
+
|
|
41
|
+
CLASS_CHANGE = auto()
|
|
42
|
+
MEASURE = auto()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass
|
|
46
|
+
class WidgetContext:
|
|
47
|
+
"""Per-widget state tracked by the transition engine."""
|
|
48
|
+
|
|
49
|
+
active_pseudos: set[str] = field(default_factory=lambda: set[str]())
|
|
50
|
+
css_anim_props: dict[str, str] = field(default_factory=lambda: dict[str, str]())
|
|
51
|
+
active_animations: dict[str, Animation] = field(default_factory=lambda: dict[str, Animation]())
|
|
52
|
+
internal_write_depth: int = 0
|
|
53
|
+
internal_write_reason: InternalWriteReason | None = None
|
|
54
|
+
pre_polish_size: tuple[int, int] | None = None
|
|
55
|
+
# Properties with in-flight class-change-initiated animations.
|
|
56
|
+
# Pseudo-state changes (hover/focus) defer to these until they finish.
|
|
57
|
+
class_anim_props: set[str] = field(default_factory=lambda: set[str]())
|
|
58
|
+
class_anim_gen: int = 0
|
|
59
|
+
# Per-property delay timers scheduled by transition-delay declarations.
|
|
60
|
+
# Cancelled whenever the widget state changes before the timer fires.
|
|
61
|
+
pending_delays: dict[str, QTimer] = field(default_factory=lambda: dict[str, QTimer]())
|
|
62
|
+
# Last cursor value applied via setCursor() — None means unsetCursor() (Qt default).
|
|
63
|
+
applied_cursor: str | None = None
|
|
64
|
+
# Per-property class-anim finished callbacks stored so they can be disconnected before
|
|
65
|
+
# reconnecting — prevents accumulation of stale closures on rapid class changes.
|
|
66
|
+
class_anim_callbacks: dict[str, Callable[[], None]] = field(default_factory=lambda: dict[str, Callable[[], None]]())
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@dataclass
|
|
70
|
+
class ShadowParams:
|
|
71
|
+
"""Decomposed CSS box-shadow parameters used for interpolation."""
|
|
72
|
+
|
|
73
|
+
offset_x: float = 0.0
|
|
74
|
+
offset_y: float = 4.0
|
|
75
|
+
blur: float = 8.0
|
|
76
|
+
spread: float = 0.0
|
|
77
|
+
color: QColor = field(default_factory=lambda: QColor(0, 0, 0, 80))
|