qt-css-engine 0.1.0__tar.gz
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-0.1.0/.github/CODE_OF_CONDUCT.md +58 -0
- qt_css_engine-0.1.0/.github/CONTRIBUTING.md +34 -0
- qt_css_engine-0.1.0/.github/workflows/publish.yml +36 -0
- qt_css_engine-0.1.0/.github/workflows/test-publish.yml +33 -0
- qt_css_engine-0.1.0/.gitignore +14 -0
- qt_css_engine-0.1.0/LICENSE.md +9 -0
- qt_css_engine-0.1.0/PKG-INFO +187 -0
- qt_css_engine-0.1.0/README.md +162 -0
- qt_css_engine-0.1.0/demo/main.py +378 -0
- qt_css_engine-0.1.0/demo/styles.css +278 -0
- qt_css_engine-0.1.0/pyproject.toml +95 -0
- qt_css_engine-0.1.0/qt_css_engine/__init__.py +23 -0
- qt_css_engine-0.1.0/qt_css_engine/constants.py +190 -0
- qt_css_engine-0.1.0/qt_css_engine/css_parser.py +397 -0
- qt_css_engine-0.1.0/qt_css_engine/engine.py +1035 -0
- qt_css_engine-0.1.0/qt_css_engine/gradients.py +321 -0
- qt_css_engine-0.1.0/qt_css_engine/handlers.py +402 -0
- qt_css_engine-0.1.0/qt_css_engine/py.typed +0 -0
- qt_css_engine-0.1.0/qt_css_engine/qt_compat/QtCore.py +15 -0
- qt_css_engine-0.1.0/qt_css_engine/qt_compat/QtGui.py +10 -0
- qt_css_engine-0.1.0/qt_css_engine/qt_compat/QtWidgets.py +10 -0
- qt_css_engine-0.1.0/qt_css_engine/qt_compat/__init__.py +22 -0
- qt_css_engine-0.1.0/qt_css_engine/qt_compat/_api.py +15 -0
- qt_css_engine-0.1.0/qt_css_engine/types.py +77 -0
- qt_css_engine-0.1.0/qt_css_engine/utils.py +447 -0
- qt_css_engine-0.1.0/tests/conftest.py +14 -0
- qt_css_engine-0.1.0/tests/test_anim.py +2374 -0
- qt_css_engine-0.1.0/tests/test_css_parser.py +1285 -0
- qt_css_engine-0.1.0/tests/test_gradients.py +371 -0
- qt_css_engine-0.1.0/tests/test_interactions.py +41 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Contributor Covenant Code of Conduct
|
|
2
|
+
|
|
3
|
+
## Our Pledge
|
|
4
|
+
|
|
5
|
+
We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation.
|
|
6
|
+
|
|
7
|
+
We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community.
|
|
8
|
+
|
|
9
|
+
## Our Standards
|
|
10
|
+
|
|
11
|
+
Examples of behavior that contributes to a positive environment for our community include:
|
|
12
|
+
|
|
13
|
+
* Demonstrating empathy and kindness toward other people
|
|
14
|
+
* Being respectful of differing opinions, viewpoints, and experiences
|
|
15
|
+
* Giving and gracefully accepting constructive feedback
|
|
16
|
+
* Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience
|
|
17
|
+
* Focusing on what is best not just for us as individuals, but for the overall community
|
|
18
|
+
|
|
19
|
+
Examples of unacceptable behavior include:
|
|
20
|
+
|
|
21
|
+
* The use of sexualized language or imagery, and sexual attention or advances of any kind
|
|
22
|
+
* Trolling, insulting or derogatory comments, and personal or political attacks
|
|
23
|
+
* Public or private harassment
|
|
24
|
+
* Publishing others’ private information, such as a physical or email address, without their explicit permission
|
|
25
|
+
* Other conduct which could reasonably be considered inappropriate in a professional setting
|
|
26
|
+
|
|
27
|
+
## Enforcement Responsibilities
|
|
28
|
+
|
|
29
|
+
Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful.
|
|
30
|
+
|
|
31
|
+
Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate.
|
|
32
|
+
|
|
33
|
+
## Scope
|
|
34
|
+
|
|
35
|
+
This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces.
|
|
36
|
+
|
|
37
|
+
## Enforcement
|
|
38
|
+
|
|
39
|
+
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement. All complaints will be reviewed and investigated promptly and fairly.
|
|
40
|
+
|
|
41
|
+
All community leaders are obligated to respect the privacy and security of the reporter of any incident.
|
|
42
|
+
|
|
43
|
+
## Enforcement Guidelines
|
|
44
|
+
|
|
45
|
+
Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct:
|
|
46
|
+
|
|
47
|
+
1. **Correction**
|
|
48
|
+
2. **Warning**
|
|
49
|
+
3. **Temporary Ban**
|
|
50
|
+
4. **Permanent Ban**
|
|
51
|
+
|
|
52
|
+
(See [Contributor Covenant Community Impact Guidelines](https://www.contributor-covenant.org/version/2/1/code_of_conduct/#enforcement-guidelines) for details.)
|
|
53
|
+
|
|
54
|
+
## Attribution
|
|
55
|
+
|
|
56
|
+
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.1, available at [https://www.contributor-covenant.org/version/2/1/code_of_conduct/](https://www.contributor-covenant.org/version/2/1/code_of_conduct/).
|
|
57
|
+
|
|
58
|
+
[homepage]: https://www.contributor-covenant.org
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
## Contributing
|
|
2
|
+
|
|
3
|
+
Contributions are welcome. Make sure to run the tests and check the demo app and all widgets before submitting a pull request.
|
|
4
|
+
Tests do not cover all the cases so visual confirmation is required.
|
|
5
|
+
Keep in mind that there might be big changes to the codebase so prepare to rebase your branch if needed.
|
|
6
|
+
Messy commit histories will be squashed before merging.
|
|
7
|
+
|
|
8
|
+
PR names and commit messages are in the format: `type(scope): short description`
|
|
9
|
+
|
|
10
|
+
Extra details must be added to the commit message after a newline so:
|
|
11
|
+
|
|
12
|
+
```text
|
|
13
|
+
fix(css_parser): fix parsing of :hover pseudo-class
|
|
14
|
+
|
|
15
|
+
- :hover pseudo-class was not being parsed correctly in some cases
|
|
16
|
+
- added tests to cover that case
|
|
17
|
+
- more details here
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Dev commands
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
# Install dependencies and run the demo
|
|
24
|
+
uv sync --all-extras
|
|
25
|
+
uv run ./demo/main.py
|
|
26
|
+
|
|
27
|
+
# Run the test suite
|
|
28
|
+
uv run pytest
|
|
29
|
+
|
|
30
|
+
# Check and format the code
|
|
31
|
+
uvx ruff check --fix
|
|
32
|
+
uvx ruff format
|
|
33
|
+
uvx basedpyright
|
|
34
|
+
```
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- "v*"
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
build-and-publish:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
environment: pypi
|
|
12
|
+
if: github.event.base_ref == 'refs/heads/main'
|
|
13
|
+
permissions:
|
|
14
|
+
id-token: write
|
|
15
|
+
contents: read
|
|
16
|
+
|
|
17
|
+
steps:
|
|
18
|
+
- uses: actions/checkout@v6
|
|
19
|
+
with:
|
|
20
|
+
fetch-depth: 0
|
|
21
|
+
|
|
22
|
+
- uses: astral-sh/setup-uv@v8.0.0
|
|
23
|
+
with:
|
|
24
|
+
python-version: "3.14"
|
|
25
|
+
|
|
26
|
+
- name: Build
|
|
27
|
+
run: uv build
|
|
28
|
+
|
|
29
|
+
- name: Smoke test wheel
|
|
30
|
+
run: uv run --isolated --no-project --with dist/*.whl -- python -c "import qt_css_engine"
|
|
31
|
+
|
|
32
|
+
- name: Smoke test sdist
|
|
33
|
+
run: uv run --isolated --no-project --with dist/*.tar.gz -- python -c "import qt_css_engine"
|
|
34
|
+
|
|
35
|
+
- name: Publish
|
|
36
|
+
run: uv publish
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
name: Publish to TestPyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
workflow_dispatch:
|
|
5
|
+
|
|
6
|
+
jobs:
|
|
7
|
+
build-and-publish:
|
|
8
|
+
runs-on: ubuntu-latest
|
|
9
|
+
environment: testpypi
|
|
10
|
+
permissions:
|
|
11
|
+
id-token: write
|
|
12
|
+
contents: read
|
|
13
|
+
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v6
|
|
16
|
+
with:
|
|
17
|
+
fetch-depth: 0
|
|
18
|
+
|
|
19
|
+
- uses: astral-sh/setup-uv@v8.0.0
|
|
20
|
+
with:
|
|
21
|
+
python-version: "3.14"
|
|
22
|
+
|
|
23
|
+
- name: Build
|
|
24
|
+
run: uv build
|
|
25
|
+
|
|
26
|
+
- name: Smoke test wheel
|
|
27
|
+
run: uv run --isolated --no-project --with dist/*.whl -- python -c "import qt_css_engine"
|
|
28
|
+
|
|
29
|
+
- name: Smoke test sdist
|
|
30
|
+
run: uv run --isolated --no-project --with dist/*.tar.gz -- python -c "import qt_css_engine"
|
|
31
|
+
|
|
32
|
+
- name: Publish
|
|
33
|
+
run: uv publish --index testpypi
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 George Sladkovsky
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
6
|
+
|
|
7
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: qt-css-engine
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: CSS transition animations for PyQt6/PySide6 — extends static QSS with transition and other properties
|
|
5
|
+
Project-URL: Homepage, https://github.com/Video-Nomad/qt-css-engine
|
|
6
|
+
Project-URL: Repository, https://github.com/Video-Nomad/qt-css-engine
|
|
7
|
+
Project-URL: Issues, https://github.com/Video-Nomad/qt-css-engine/issues
|
|
8
|
+
Author-email: George Sladkovsky <george.sladkovsky@gmail.com>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE.md
|
|
11
|
+
Keywords: animation,css,pyqt6,qss,qt,stylesheet,transition
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Environment :: X11 Applications :: Qt
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
21
|
+
Classifier: Topic :: Software Development :: User Interfaces
|
|
22
|
+
Requires-Python: >=3.14
|
|
23
|
+
Requires-Dist: tinycss2>=1.5.1
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
# Qt CSS Engine
|
|
27
|
+
|
|
28
|
+
A CSS animation engine for PyQt6/PySide6 that extends Qt's static stylesheet system (QSS) with dynamic CSS transitions and extra properties like `box-shadow`, `opacity` and CSS gradients. Qt's stylesheet engine has no concept of time or interpolation — this project implements an out-of-band animation system that intercepts `transition:` declarations from a stylesheet, tracks widget pseudo-states (hover, pressed, focus), and drives smooth property animations via Qt's animation framework. Dynamic class change is also supported so `.btn` -> `.btn.active` -> `.btn.other-state` will animate based on the `transition` property.
|
|
29
|
+
|
|
30
|
+
All that is required is to install `TransitionEngine` as an event filter and it will take care of the rest.
|
|
31
|
+
|
|
32
|
+
CSS Hot reload is supported via `TransitionEngine.reload_rules(new_rules)`
|
|
33
|
+
|
|
34
|
+
Subcontrols (e.g `::item`, `::handle`) are not supported because they are not real QWidgets. They will be just passed through as classic QSS stylesheet blocks.
|
|
35
|
+
|
|
36
|
+
This engine was made primarily for use in [YASB](https://github.com/amnweb/yasb) project and this will be the main focus for now, but it can be integrated into any Qt application.
|
|
37
|
+
|
|
38
|
+
**WARNING: This project is still in early development, very experimental and is not ready for production use. There will be bugs and breaking changes.**
|
|
39
|
+
|
|
40
|
+
## Python and Qt version
|
|
41
|
+
|
|
42
|
+
Python 3.14+
|
|
43
|
+
|
|
44
|
+
PySide6/PyQt6 6.10+
|
|
45
|
+
|
|
46
|
+
## Installation
|
|
47
|
+
|
|
48
|
+
```
|
|
49
|
+
uv add qt-css-engine
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Usage
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
from qt_css_engine import TransitionEngine, extract_rules
|
|
56
|
+
|
|
57
|
+
app = QApplication([])
|
|
58
|
+
cleaned_qss, rules = extract_rules(stylesheet)
|
|
59
|
+
app.setStyleSheet(cleaned_qss)
|
|
60
|
+
engine = TransitionEngine(rules)
|
|
61
|
+
app.installEventFilter(engine)
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Simple transition examples
|
|
65
|
+
|
|
66
|
+
```css
|
|
67
|
+
/* A simple widget with a hover transition */
|
|
68
|
+
.btn {
|
|
69
|
+
background-color: steelblue;
|
|
70
|
+
color: white;
|
|
71
|
+
border-radius: 4px;
|
|
72
|
+
transition: background-color 300ms ease;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.btn:hover {
|
|
76
|
+
background-color: royalblue;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/* A widget with a box-shadow transition */
|
|
80
|
+
#btn {
|
|
81
|
+
background-color: steelblue;
|
|
82
|
+
color: white;
|
|
83
|
+
border-radius: 4px;
|
|
84
|
+
box-shadow: 0px 0px 0px black;
|
|
85
|
+
transition: box-shadow 300ms ease;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
#btn:hover {
|
|
89
|
+
box-shadow: 4px 4px 4px black;
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## More complex examples
|
|
94
|
+
|
|
95
|
+
Check the `./demo/main.py` for hot reloading, dynamic class change and more.
|
|
96
|
+
|
|
97
|
+
## Transition syntax and supported values
|
|
98
|
+
|
|
99
|
+
### Shorthand
|
|
100
|
+
|
|
101
|
+
```css
|
|
102
|
+
transition: <property> <duration> [<easing>] [<delay>];
|
|
103
|
+
transition: <property> <duration> [<delay>] [<easing>];
|
|
104
|
+
|
|
105
|
+
/* multiple */
|
|
106
|
+
transition: background-color 300ms ease, border-radius 200ms linear 50ms;
|
|
107
|
+
|
|
108
|
+
/* all animatable properties */
|
|
109
|
+
transition: all 300ms ease-in-out;
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Longhands
|
|
113
|
+
|
|
114
|
+
```css
|
|
115
|
+
transition-property: background-color, border-radius;
|
|
116
|
+
transition-duration: 300ms, 200ms;
|
|
117
|
+
transition-timing-function: ease, linear;
|
|
118
|
+
transition-delay: 0ms, 50ms;
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Longhands override the shorthand when both are declared in the same block. Values cycle per the CSS spec when list lengths differ.
|
|
122
|
+
|
|
123
|
+
### Time units
|
|
124
|
+
|
|
125
|
+
| Unit | Example |
|
|
126
|
+
| --- | --- |
|
|
127
|
+
| Milliseconds | `300ms` |
|
|
128
|
+
| Seconds | `0.3s` |
|
|
129
|
+
|
|
130
|
+
### Easing curves
|
|
131
|
+
|
|
132
|
+
| Value | Description |
|
|
133
|
+
| --- | --- |
|
|
134
|
+
| `linear` | Constant speed |
|
|
135
|
+
| `ease` | Slow in, slow out (default) |
|
|
136
|
+
| `ease-in` | Slow start |
|
|
137
|
+
| `ease-out` | Slow end |
|
|
138
|
+
| `ease-in-out` | Slow start and end |
|
|
139
|
+
| `cubic-bezier(x1, y1, x2, y2)` | Custom curve — values outside `[0, 1]` produce overshoot |
|
|
140
|
+
| `steps(n)` / `steps(n, jump-end\|jump-start\|jump-none\|jump-both)` | Discrete stepped animation |
|
|
141
|
+
| `step-start` / `step-end` | Aliases for `steps(1, jump-start)` / `steps(1, jump-end)` |
|
|
142
|
+
|
|
143
|
+
### Delay
|
|
144
|
+
|
|
145
|
+
Positive delay: animation starts after the delay elapses. The property is frozen at its current rendered value during the delay period.
|
|
146
|
+
|
|
147
|
+
Negative delay: animation starts immediately but offset `|delay|` ms into the timeline, as if it had already been running that long.
|
|
148
|
+
|
|
149
|
+
```css
|
|
150
|
+
transition: background 400ms ease 100ms; /* 100ms positive delay */
|
|
151
|
+
transition: background 400ms ease -100ms; /* starts 100ms in */
|
|
152
|
+
/* OR */
|
|
153
|
+
transition: background 400ms 100ms ease;
|
|
154
|
+
transition: background 400ms -100ms ease;
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## Supported properties
|
|
158
|
+
|
|
159
|
+
Color values accepted everywhere a color is listed: named (`red`, `steelblue`, …), `#rrggbb`, `#rrggbbaa`, `rgb()`, `rgba()`, `hsl()`, `hsla()`.
|
|
160
|
+
|
|
161
|
+
Numeric values accepted everywhere a length is listed: `<n>px`, `<n>pt`, `<n>em`.
|
|
162
|
+
|
|
163
|
+
| Property | Description | Supported values | Transition | Static |
|
|
164
|
+
| --- | --- | --- | --- | --- |
|
|
165
|
+
| `background-color` / `background` | Background color | color values; `linear-gradient()`, `radial-gradient()`, `conic-gradient()` (static only) | ✅ solid colors | ✅ |
|
|
166
|
+
| `color` | Text color | color values | ✅ | ✅ |
|
|
167
|
+
| `border-color` | Border color shorthand (→ 4 sides) | color values | ✅ | ✅ |
|
|
168
|
+
| `border-top-color`, `border-right-color`, `border-bottom-color`, `border-left-color` | Per-side border color | color values | ✅ | ✅ |
|
|
169
|
+
| `border-width` | Border width shorthand (→ 4 sides) | length values | ✅ | ✅ |
|
|
170
|
+
| `border-top-width`, `border-right-width`, `border-bottom-width`, `border-left-width` | Per-side border width | length values | ✅ | ✅ |
|
|
171
|
+
| `border-radius` | Border radius shorthand (→ 4 corners) | length values | ✅ | ✅ |
|
|
172
|
+
| `border-top-left-radius`, `border-top-right-radius`, `border-bottom-right-radius`, `border-bottom-left-radius` | Per-corner border radius | length values | ✅ | ✅ |
|
|
173
|
+
| `padding` | Padding shorthand (→ 4 sides) | length values | ✅ | ✅ |
|
|
174
|
+
| `padding-top`, `padding-right`, `padding-bottom`, `padding-left` | Per-side padding | length values | ✅ | ✅ |
|
|
175
|
+
| `margin` | Margin shorthand (→ 4 sides) | length values | ✅ | ✅ |
|
|
176
|
+
| `margin-top`, `margin-right`, `margin-bottom`, `margin-left` | Per-side margin | length values | ✅ | ✅ |
|
|
177
|
+
| `width`, `height` | Widget size | length values | ✅ | ✅ |
|
|
178
|
+
| `min-width`, `max-width`, `min-height`, `max-height` | Size constraints | length values | ✅ | ✅ |
|
|
179
|
+
| `font-size` | Font size | length values | ✅ | ✅ |
|
|
180
|
+
| `font-weight` | Font weight | `100`–`900` | ✅ | ✅ |
|
|
181
|
+
| `letter-spacing` | Letter spacing | length values | ✅ | ✅ |
|
|
182
|
+
| `word-spacing` | Word spacing | length values | ✅ | ✅ |
|
|
183
|
+
| `spacing` | Qt widget item spacing | length values | ✅ | ✅ |
|
|
184
|
+
| `opacity` | Widget opacity — not native QSS, applied via `QGraphicsOpacityEffect` | `0.0`–`1.0` | ✅ | ✅ |
|
|
185
|
+
| `box-shadow` | Drop shadow — not native QSS, applied via `QGraphicsDropShadowEffect`. No `inset`, `spread` is ignored. First shadow wins when multiple are declared. | `<x> <y> [blur] [spread] <color>` | ✅ | ✅ |
|
|
186
|
+
| `cursor` | Mouse cursor — Qt QSS ignores `cursor`, applied via `setCursor()` | `default`, `pointer`, `text`, `crosshair`, `wait`, `progress`, `help`, `move`, `grab`, `grabbing`, `copy`, `alias`, `not-allowed`, `no-drop`, `cell`, `all-scroll`, `n-resize`, `s-resize`, `e-resize`, `w-resize`, `ne-resize`, `nw-resize`, `se-resize`, `sw-resize`, `ns-resize`, `ew-resize`, `nesw-resize`, `nwse-resize`, `row-resize`, `col-resize`, `none` | ❌ | ✅ |
|
|
187
|
+
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# Qt CSS Engine
|
|
2
|
+
|
|
3
|
+
A CSS animation engine for PyQt6/PySide6 that extends Qt's static stylesheet system (QSS) with dynamic CSS transitions and extra properties like `box-shadow`, `opacity` and CSS gradients. Qt's stylesheet engine has no concept of time or interpolation — this project implements an out-of-band animation system that intercepts `transition:` declarations from a stylesheet, tracks widget pseudo-states (hover, pressed, focus), and drives smooth property animations via Qt's animation framework. Dynamic class change is also supported so `.btn` -> `.btn.active` -> `.btn.other-state` will animate based on the `transition` property.
|
|
4
|
+
|
|
5
|
+
All that is required is to install `TransitionEngine` as an event filter and it will take care of the rest.
|
|
6
|
+
|
|
7
|
+
CSS Hot reload is supported via `TransitionEngine.reload_rules(new_rules)`
|
|
8
|
+
|
|
9
|
+
Subcontrols (e.g `::item`, `::handle`) are not supported because they are not real QWidgets. They will be just passed through as classic QSS stylesheet blocks.
|
|
10
|
+
|
|
11
|
+
This engine was made primarily for use in [YASB](https://github.com/amnweb/yasb) project and this will be the main focus for now, but it can be integrated into any Qt application.
|
|
12
|
+
|
|
13
|
+
**WARNING: This project is still in early development, very experimental and is not ready for production use. There will be bugs and breaking changes.**
|
|
14
|
+
|
|
15
|
+
## Python and Qt version
|
|
16
|
+
|
|
17
|
+
Python 3.14+
|
|
18
|
+
|
|
19
|
+
PySide6/PyQt6 6.10+
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
uv add qt-css-engine
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Usage
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
from qt_css_engine import TransitionEngine, extract_rules
|
|
31
|
+
|
|
32
|
+
app = QApplication([])
|
|
33
|
+
cleaned_qss, rules = extract_rules(stylesheet)
|
|
34
|
+
app.setStyleSheet(cleaned_qss)
|
|
35
|
+
engine = TransitionEngine(rules)
|
|
36
|
+
app.installEventFilter(engine)
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Simple transition examples
|
|
40
|
+
|
|
41
|
+
```css
|
|
42
|
+
/* A simple widget with a hover transition */
|
|
43
|
+
.btn {
|
|
44
|
+
background-color: steelblue;
|
|
45
|
+
color: white;
|
|
46
|
+
border-radius: 4px;
|
|
47
|
+
transition: background-color 300ms ease;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.btn:hover {
|
|
51
|
+
background-color: royalblue;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/* A widget with a box-shadow transition */
|
|
55
|
+
#btn {
|
|
56
|
+
background-color: steelblue;
|
|
57
|
+
color: white;
|
|
58
|
+
border-radius: 4px;
|
|
59
|
+
box-shadow: 0px 0px 0px black;
|
|
60
|
+
transition: box-shadow 300ms ease;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
#btn:hover {
|
|
64
|
+
box-shadow: 4px 4px 4px black;
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## More complex examples
|
|
69
|
+
|
|
70
|
+
Check the `./demo/main.py` for hot reloading, dynamic class change and more.
|
|
71
|
+
|
|
72
|
+
## Transition syntax and supported values
|
|
73
|
+
|
|
74
|
+
### Shorthand
|
|
75
|
+
|
|
76
|
+
```css
|
|
77
|
+
transition: <property> <duration> [<easing>] [<delay>];
|
|
78
|
+
transition: <property> <duration> [<delay>] [<easing>];
|
|
79
|
+
|
|
80
|
+
/* multiple */
|
|
81
|
+
transition: background-color 300ms ease, border-radius 200ms linear 50ms;
|
|
82
|
+
|
|
83
|
+
/* all animatable properties */
|
|
84
|
+
transition: all 300ms ease-in-out;
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Longhands
|
|
88
|
+
|
|
89
|
+
```css
|
|
90
|
+
transition-property: background-color, border-radius;
|
|
91
|
+
transition-duration: 300ms, 200ms;
|
|
92
|
+
transition-timing-function: ease, linear;
|
|
93
|
+
transition-delay: 0ms, 50ms;
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Longhands override the shorthand when both are declared in the same block. Values cycle per the CSS spec when list lengths differ.
|
|
97
|
+
|
|
98
|
+
### Time units
|
|
99
|
+
|
|
100
|
+
| Unit | Example |
|
|
101
|
+
| --- | --- |
|
|
102
|
+
| Milliseconds | `300ms` |
|
|
103
|
+
| Seconds | `0.3s` |
|
|
104
|
+
|
|
105
|
+
### Easing curves
|
|
106
|
+
|
|
107
|
+
| Value | Description |
|
|
108
|
+
| --- | --- |
|
|
109
|
+
| `linear` | Constant speed |
|
|
110
|
+
| `ease` | Slow in, slow out (default) |
|
|
111
|
+
| `ease-in` | Slow start |
|
|
112
|
+
| `ease-out` | Slow end |
|
|
113
|
+
| `ease-in-out` | Slow start and end |
|
|
114
|
+
| `cubic-bezier(x1, y1, x2, y2)` | Custom curve — values outside `[0, 1]` produce overshoot |
|
|
115
|
+
| `steps(n)` / `steps(n, jump-end\|jump-start\|jump-none\|jump-both)` | Discrete stepped animation |
|
|
116
|
+
| `step-start` / `step-end` | Aliases for `steps(1, jump-start)` / `steps(1, jump-end)` |
|
|
117
|
+
|
|
118
|
+
### Delay
|
|
119
|
+
|
|
120
|
+
Positive delay: animation starts after the delay elapses. The property is frozen at its current rendered value during the delay period.
|
|
121
|
+
|
|
122
|
+
Negative delay: animation starts immediately but offset `|delay|` ms into the timeline, as if it had already been running that long.
|
|
123
|
+
|
|
124
|
+
```css
|
|
125
|
+
transition: background 400ms ease 100ms; /* 100ms positive delay */
|
|
126
|
+
transition: background 400ms ease -100ms; /* starts 100ms in */
|
|
127
|
+
/* OR */
|
|
128
|
+
transition: background 400ms 100ms ease;
|
|
129
|
+
transition: background 400ms -100ms ease;
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Supported properties
|
|
133
|
+
|
|
134
|
+
Color values accepted everywhere a color is listed: named (`red`, `steelblue`, …), `#rrggbb`, `#rrggbbaa`, `rgb()`, `rgba()`, `hsl()`, `hsla()`.
|
|
135
|
+
|
|
136
|
+
Numeric values accepted everywhere a length is listed: `<n>px`, `<n>pt`, `<n>em`.
|
|
137
|
+
|
|
138
|
+
| Property | Description | Supported values | Transition | Static |
|
|
139
|
+
| --- | --- | --- | --- | --- |
|
|
140
|
+
| `background-color` / `background` | Background color | color values; `linear-gradient()`, `radial-gradient()`, `conic-gradient()` (static only) | ✅ solid colors | ✅ |
|
|
141
|
+
| `color` | Text color | color values | ✅ | ✅ |
|
|
142
|
+
| `border-color` | Border color shorthand (→ 4 sides) | color values | ✅ | ✅ |
|
|
143
|
+
| `border-top-color`, `border-right-color`, `border-bottom-color`, `border-left-color` | Per-side border color | color values | ✅ | ✅ |
|
|
144
|
+
| `border-width` | Border width shorthand (→ 4 sides) | length values | ✅ | ✅ |
|
|
145
|
+
| `border-top-width`, `border-right-width`, `border-bottom-width`, `border-left-width` | Per-side border width | length values | ✅ | ✅ |
|
|
146
|
+
| `border-radius` | Border radius shorthand (→ 4 corners) | length values | ✅ | ✅ |
|
|
147
|
+
| `border-top-left-radius`, `border-top-right-radius`, `border-bottom-right-radius`, `border-bottom-left-radius` | Per-corner border radius | length values | ✅ | ✅ |
|
|
148
|
+
| `padding` | Padding shorthand (→ 4 sides) | length values | ✅ | ✅ |
|
|
149
|
+
| `padding-top`, `padding-right`, `padding-bottom`, `padding-left` | Per-side padding | length values | ✅ | ✅ |
|
|
150
|
+
| `margin` | Margin shorthand (→ 4 sides) | length values | ✅ | ✅ |
|
|
151
|
+
| `margin-top`, `margin-right`, `margin-bottom`, `margin-left` | Per-side margin | length values | ✅ | ✅ |
|
|
152
|
+
| `width`, `height` | Widget size | length values | ✅ | ✅ |
|
|
153
|
+
| `min-width`, `max-width`, `min-height`, `max-height` | Size constraints | length values | ✅ | ✅ |
|
|
154
|
+
| `font-size` | Font size | length values | ✅ | ✅ |
|
|
155
|
+
| `font-weight` | Font weight | `100`–`900` | ✅ | ✅ |
|
|
156
|
+
| `letter-spacing` | Letter spacing | length values | ✅ | ✅ |
|
|
157
|
+
| `word-spacing` | Word spacing | length values | ✅ | ✅ |
|
|
158
|
+
| `spacing` | Qt widget item spacing | length values | ✅ | ✅ |
|
|
159
|
+
| `opacity` | Widget opacity — not native QSS, applied via `QGraphicsOpacityEffect` | `0.0`–`1.0` | ✅ | ✅ |
|
|
160
|
+
| `box-shadow` | Drop shadow — not native QSS, applied via `QGraphicsDropShadowEffect`. No `inset`, `spread` is ignored. First shadow wins when multiple are declared. | `<x> <y> [blur] [spread] <color>` | ✅ | ✅ |
|
|
161
|
+
| `cursor` | Mouse cursor — Qt QSS ignores `cursor`, applied via `setCursor()` | `default`, `pointer`, `text`, `crosshair`, `wait`, `progress`, `help`, `move`, `grab`, `grabbing`, `copy`, `alias`, `not-allowed`, `no-drop`, `cell`, `all-scroll`, `n-resize`, `s-resize`, `e-resize`, `w-resize`, `ne-resize`, `nw-resize`, `se-resize`, `sw-resize`, `ns-resize`, `ew-resize`, `nesw-resize`, `nwse-resize`, `row-resize`, `col-resize`, `none` | ❌ | ✅ |
|
|
162
|
+
|