pawui 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.
- pawui-0.1.0/PKG-INFO +104 -0
- pawui-0.1.0/README.md +82 -0
- pawui-0.1.0/pawui/__init__.py +6 -0
- pawui-0.1.0/pawui/__main__.py +6 -0
- pawui-0.1.0/pawui/animate.py +99 -0
- pawui-0.1.0/pawui/cli.py +272 -0
- pawui-0.1.0/pawui/components.py +335 -0
- pawui-0.1.0/pawui/errors.py +52 -0
- pawui-0.1.0/pawui/nodes.py +60 -0
- pawui-0.1.0/pawui/parser.py +227 -0
- pawui-0.1.0/pawui/resolve.py +185 -0
- pawui-0.1.0/pawui/runtime.py +285 -0
- pawui-0.1.0/pawui/state.py +71 -0
- pawui-0.1.0/pawui/theme.py +106 -0
- pawui-0.1.0/pawui.egg-info/PKG-INFO +104 -0
- pawui-0.1.0/pawui.egg-info/SOURCES.txt +30 -0
- pawui-0.1.0/pawui.egg-info/dependency_links.txt +1 -0
- pawui-0.1.0/pawui.egg-info/entry_points.txt +2 -0
- pawui-0.1.0/pawui.egg-info/requires.txt +9 -0
- pawui-0.1.0/pawui.egg-info/top_level.txt +1 -0
- pawui-0.1.0/pyproject.toml +55 -0
- pawui-0.1.0/setup.cfg +4 -0
- pawui-0.1.0/tests/test_animate.py +49 -0
- pawui-0.1.0/tests/test_cli.py +129 -0
- pawui-0.1.0/tests/test_components.py +167 -0
- pawui-0.1.0/tests/test_logical.py +190 -0
- pawui-0.1.0/tests/test_nodes.py +60 -0
- pawui-0.1.0/tests/test_parser.py +105 -0
- pawui-0.1.0/tests/test_resolve.py +235 -0
- pawui-0.1.0/tests/test_runtime.py +240 -0
- pawui-0.1.0/tests/test_state.py +83 -0
- pawui-0.1.0/tests/test_theme.py +108 -0
pawui-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pawui
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: 轻量、直接运行的 Python 声明式 UI 层
|
|
5
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
6
|
+
Classifier: Intended Audience :: Developers
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
11
|
+
Classifier: Topic :: Software Development :: User Interfaces
|
|
12
|
+
Requires-Python: >=3.10
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
Requires-Dist: PySide6>=6.5
|
|
15
|
+
Provides-Extra: dev
|
|
16
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
17
|
+
Requires-Dist: pytest-qt>=4.2; extra == "dev"
|
|
18
|
+
Requires-Dist: ruff>=0.1.0; extra == "dev"
|
|
19
|
+
Requires-Dist: mypy>=1.0; extra == "dev"
|
|
20
|
+
Requires-Dist: build>=1.0; extra == "dev"
|
|
21
|
+
Requires-Dist: twine>=4.0; extra == "dev"
|
|
22
|
+
|
|
23
|
+
# PawUI
|
|
24
|
+
|
|
25
|
+
轻量、直接运行的 Python 声明式 UI 层。HTML 风格,Qt/PySide6 渲染(原生抗锯齿、QSS 圆角/悬停/聚焦、IME 组字正常),无构建。
|
|
26
|
+
|
|
27
|
+
## 运行
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pawui app.paw
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
也可以传统方式启动:
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
import pawui
|
|
37
|
+
pawui.run("app.paw")
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## 语法(HTML 风格)
|
|
41
|
+
|
|
42
|
+
```html
|
|
43
|
+
<Window title="你好" width="460" height="840" theme="dark">
|
|
44
|
+
<Column padding="24" spacing="12">
|
|
45
|
+
<Text size="24" bold>欢迎使用 PawUI 👋</Text>
|
|
46
|
+
<Text size="13" color="subtext">副标题</Text>
|
|
47
|
+
|
|
48
|
+
<Text>{$greeting}</Text>
|
|
49
|
+
|
|
50
|
+
<Row spacing="8">
|
|
51
|
+
<Button on_click="increment">点我 +1</Button>
|
|
52
|
+
<Button on_click="reset">清空</Button>
|
|
53
|
+
</Row>
|
|
54
|
+
|
|
55
|
+
<Input placeholder="输入你的名字..." on_change="on_name"/>
|
|
56
|
+
<Text>你好,{$name}</Text>
|
|
57
|
+
|
|
58
|
+
<Card label="点击量" value="{$count}"/>
|
|
59
|
+
</Column>
|
|
60
|
+
</Window>
|
|
61
|
+
|
|
62
|
+
<!-- 自定义组件 -->
|
|
63
|
+
<Component name="Card">
|
|
64
|
+
<Column padding="12" bg="surface">
|
|
65
|
+
<Text color="subtext">{$label}</Text>
|
|
66
|
+
<Text size="22" bold>{$value}</Text>
|
|
67
|
+
</Column>
|
|
68
|
+
</Component>
|
|
69
|
+
|
|
70
|
+
<script>
|
|
71
|
+
def increment():
|
|
72
|
+
state.count = int(state.get("count", 0)) + 1
|
|
73
|
+
</script>
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## 要点
|
|
77
|
+
|
|
78
|
+
- **状态绑定**:`{$count}` / `{count}` / `$count`,在 `<script>` 里改 `state.count = ...` 界面自动刷新。
|
|
79
|
+
- **事件**:字符串引用 `<script>` / `main.py` 里的函数:`on_click="函数名"`。
|
|
80
|
+
- **组件**:`<Component name="X">...</Component>` 定义,直接 `<X .../>` 使用。
|
|
81
|
+
- **主题**:`theme="dark" | "light"`,颜色可直接引用:`color="subtext"`、`bg="surface"`。
|
|
82
|
+
- **布局**:`Column` 纵排、`Row` 横排,`spacing` / `padding` 调节。
|
|
83
|
+
|
|
84
|
+
## 内置组件
|
|
85
|
+
|
|
86
|
+
`Window` `Column` `Row` `Text` `Button` `Input` `Checkbox`(开关) `Divider` `Spacer` `If`(条件渲染) `For`(列表循环)
|
|
87
|
+
|
|
88
|
+
```html
|
|
89
|
+
<For each="item" in="{$items}">
|
|
90
|
+
<If condition="{$item.done}">
|
|
91
|
+
<Text>{$item.name}</Text>
|
|
92
|
+
</If>
|
|
93
|
+
</For>
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## CLI
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
pawui run app.paw # 运行(或直接 pawui app.paw)
|
|
100
|
+
pawui check app.paw # 仅语法检查
|
|
101
|
+
pawui schema # 输出组件 JSON Schema
|
|
102
|
+
pawui render app.paw # 离屏渲染自检
|
|
103
|
+
pawui --version
|
|
104
|
+
```
|
pawui-0.1.0/README.md
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# PawUI
|
|
2
|
+
|
|
3
|
+
轻量、直接运行的 Python 声明式 UI 层。HTML 风格,Qt/PySide6 渲染(原生抗锯齿、QSS 圆角/悬停/聚焦、IME 组字正常),无构建。
|
|
4
|
+
|
|
5
|
+
## 运行
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pawui app.paw
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
也可以传统方式启动:
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
import pawui
|
|
15
|
+
pawui.run("app.paw")
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## 语法(HTML 风格)
|
|
19
|
+
|
|
20
|
+
```html
|
|
21
|
+
<Window title="你好" width="460" height="840" theme="dark">
|
|
22
|
+
<Column padding="24" spacing="12">
|
|
23
|
+
<Text size="24" bold>欢迎使用 PawUI 👋</Text>
|
|
24
|
+
<Text size="13" color="subtext">副标题</Text>
|
|
25
|
+
|
|
26
|
+
<Text>{$greeting}</Text>
|
|
27
|
+
|
|
28
|
+
<Row spacing="8">
|
|
29
|
+
<Button on_click="increment">点我 +1</Button>
|
|
30
|
+
<Button on_click="reset">清空</Button>
|
|
31
|
+
</Row>
|
|
32
|
+
|
|
33
|
+
<Input placeholder="输入你的名字..." on_change="on_name"/>
|
|
34
|
+
<Text>你好,{$name}</Text>
|
|
35
|
+
|
|
36
|
+
<Card label="点击量" value="{$count}"/>
|
|
37
|
+
</Column>
|
|
38
|
+
</Window>
|
|
39
|
+
|
|
40
|
+
<!-- 自定义组件 -->
|
|
41
|
+
<Component name="Card">
|
|
42
|
+
<Column padding="12" bg="surface">
|
|
43
|
+
<Text color="subtext">{$label}</Text>
|
|
44
|
+
<Text size="22" bold>{$value}</Text>
|
|
45
|
+
</Column>
|
|
46
|
+
</Component>
|
|
47
|
+
|
|
48
|
+
<script>
|
|
49
|
+
def increment():
|
|
50
|
+
state.count = int(state.get("count", 0)) + 1
|
|
51
|
+
</script>
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## 要点
|
|
55
|
+
|
|
56
|
+
- **状态绑定**:`{$count}` / `{count}` / `$count`,在 `<script>` 里改 `state.count = ...` 界面自动刷新。
|
|
57
|
+
- **事件**:字符串引用 `<script>` / `main.py` 里的函数:`on_click="函数名"`。
|
|
58
|
+
- **组件**:`<Component name="X">...</Component>` 定义,直接 `<X .../>` 使用。
|
|
59
|
+
- **主题**:`theme="dark" | "light"`,颜色可直接引用:`color="subtext"`、`bg="surface"`。
|
|
60
|
+
- **布局**:`Column` 纵排、`Row` 横排,`spacing` / `padding` 调节。
|
|
61
|
+
|
|
62
|
+
## 内置组件
|
|
63
|
+
|
|
64
|
+
`Window` `Column` `Row` `Text` `Button` `Input` `Checkbox`(开关) `Divider` `Spacer` `If`(条件渲染) `For`(列表循环)
|
|
65
|
+
|
|
66
|
+
```html
|
|
67
|
+
<For each="item" in="{$items}">
|
|
68
|
+
<If condition="{$item.done}">
|
|
69
|
+
<Text>{$item.name}</Text>
|
|
70
|
+
</If>
|
|
71
|
+
</For>
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## CLI
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
pawui run app.paw # 运行(或直接 pawui app.paw)
|
|
78
|
+
pawui check app.paw # 仅语法检查
|
|
79
|
+
pawui schema # 输出组件 JSON Schema
|
|
80
|
+
pawui render app.paw # 离屏渲染自检
|
|
81
|
+
pawui --version
|
|
82
|
+
```
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""PawUI 动画:声明式入场动画 + 缓动曲线。
|
|
2
|
+
|
|
3
|
+
用法(在 .paw 里):
|
|
4
|
+
<Card animate="slide-up" duration="320" delay="60" easing="out-back"/>
|
|
5
|
+
<Column stagger="60"> ... </Column> # 子元素逐个入场
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from PySide6.QtCore import (
|
|
11
|
+
QEasingCurve,
|
|
12
|
+
QParallelAnimationGroup,
|
|
13
|
+
QPoint,
|
|
14
|
+
QPropertyAnimation,
|
|
15
|
+
QTimer,
|
|
16
|
+
)
|
|
17
|
+
from PySide6.QtWidgets import QGraphicsOpacityEffect, QWidget
|
|
18
|
+
|
|
19
|
+
_EASINGS = {
|
|
20
|
+
"linear": QEasingCurve.Type.Linear,
|
|
21
|
+
"in-cubic": QEasingCurve.Type.InCubic,
|
|
22
|
+
"out-cubic": QEasingCurve.Type.OutCubic,
|
|
23
|
+
"in-out-cubic": QEasingCurve.Type.InOutCubic,
|
|
24
|
+
"out-quad": QEasingCurve.Type.OutQuad,
|
|
25
|
+
"out-quart": QEasingCurve.Type.OutQuart,
|
|
26
|
+
"out-back": QEasingCurve.Type.OutBack,
|
|
27
|
+
"out-elastic": QEasingCurve.Type.OutElastic,
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
_SLIDE = {
|
|
31
|
+
"slide-up": (0, 22),
|
|
32
|
+
"slide-down": (0, -22),
|
|
33
|
+
"slide-left": (22, 0),
|
|
34
|
+
"slide-right": (-22, 0),
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
_KINDS = {"fade", "reveal", "slide-up", "slide-down", "slide-left", "slide-right"}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def easing(name: str) -> QEasingCurve.Type:
|
|
41
|
+
return _EASINGS.get(name, QEasingCurve.Type.OutCubic)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def is_animation(kind: str) -> bool:
|
|
45
|
+
return kind in _KINDS
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def entrance(widget: QWidget, kind: str, duration: int = 260, delay: int = 0,
|
|
49
|
+
curve: str = "out-cubic") -> QParallelAnimationGroup:
|
|
50
|
+
"""给一个控件做入场动画:淡入 + 可选位移/高度展开。"""
|
|
51
|
+
group = QParallelAnimationGroup(widget)
|
|
52
|
+
|
|
53
|
+
effect = QGraphicsOpacityEffect(widget)
|
|
54
|
+
effect.setOpacity(0.0)
|
|
55
|
+
widget.setGraphicsEffect(effect)
|
|
56
|
+
fade = QPropertyAnimation(effect, b"opacity")
|
|
57
|
+
fade.setDuration(duration)
|
|
58
|
+
fade.setStartValue(0.0)
|
|
59
|
+
fade.setEndValue(1.0)
|
|
60
|
+
fade.setEasingCurve(easing(curve))
|
|
61
|
+
group.addAnimation(fade)
|
|
62
|
+
|
|
63
|
+
pos_anim = None
|
|
64
|
+
height_anim = None
|
|
65
|
+
if kind in _SLIDE:
|
|
66
|
+
pos_anim = QPropertyAnimation(widget, b"pos")
|
|
67
|
+
pos_anim.setDuration(duration)
|
|
68
|
+
pos_anim.setEasingCurve(easing(curve))
|
|
69
|
+
group.addAnimation(pos_anim)
|
|
70
|
+
elif kind == "reveal":
|
|
71
|
+
height_anim = QPropertyAnimation(widget, b"maximumHeight")
|
|
72
|
+
height_anim.setDuration(duration)
|
|
73
|
+
height_anim.setEasingCurve(easing(curve))
|
|
74
|
+
group.addAnimation(height_anim)
|
|
75
|
+
|
|
76
|
+
def _start() -> None:
|
|
77
|
+
if pos_anim is not None:
|
|
78
|
+
end = widget.pos()
|
|
79
|
+
dx, dy = _SLIDE[kind]
|
|
80
|
+
pos_anim.setStartValue(end + QPoint(dx, dy))
|
|
81
|
+
pos_anim.setEndValue(end)
|
|
82
|
+
if height_anim is not None:
|
|
83
|
+
height_anim.setStartValue(0)
|
|
84
|
+
height_anim.setEndValue(max(widget.sizeHint().height(), widget.height()))
|
|
85
|
+
group.start()
|
|
86
|
+
|
|
87
|
+
QTimer.singleShot(max(0, delay), _start)
|
|
88
|
+
return group
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def fade_window(window: QWidget, duration: int = 200) -> QPropertyAnimation:
|
|
92
|
+
"""主题切换时给窗口来一下淡入。"""
|
|
93
|
+
anim = QPropertyAnimation(window, b"windowOpacity")
|
|
94
|
+
anim.setDuration(duration)
|
|
95
|
+
anim.setStartValue(0.55)
|
|
96
|
+
anim.setEndValue(1.0)
|
|
97
|
+
anim.setEasingCurve(QEasingCurve.Type.OutCubic)
|
|
98
|
+
anim.start()
|
|
99
|
+
return anim
|
pawui-0.1.0/pawui/cli.py
ADDED
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
"""PawUI 命令行入口:``pawui app.paw`` 直接运行。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from .errors import PyxError
|
|
11
|
+
from .parser import parse
|
|
12
|
+
from .runtime import Runtime
|
|
13
|
+
|
|
14
|
+
USAGE = """\
|
|
15
|
+
PawUI - 轻量、直接运行的 Python 声明式 UI 层
|
|
16
|
+
|
|
17
|
+
用法:
|
|
18
|
+
pawui <file.paw> 直接运行一个 .paw 文件
|
|
19
|
+
pawui run <file.paw> 同上(显式子命令)
|
|
20
|
+
pawui check <file.paw> 语法检查,不运行
|
|
21
|
+
pawui schema 输出组件 Schema (JSON)
|
|
22
|
+
pawui render <file.paw> 离屏渲染并输出信息
|
|
23
|
+
pawui --version 打印版本
|
|
24
|
+
pawui --help 显示帮助
|
|
25
|
+
|
|
26
|
+
示例:
|
|
27
|
+
pawui app.paw
|
|
28
|
+
pawui check app.paw
|
|
29
|
+
pawui schema
|
|
30
|
+
pawui render app.paw
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
SCHEMA = {
|
|
35
|
+
"components": {
|
|
36
|
+
"Window": {
|
|
37
|
+
"props": {
|
|
38
|
+
"title": {"type": "string", "default": "PawUI"},
|
|
39
|
+
"width": {"type": "integer", "default": 480},
|
|
40
|
+
"height": {"type": "integer", "default": 640},
|
|
41
|
+
"theme": {"type": "string", "enum": ["dark", "light"], "default": "dark"},
|
|
42
|
+
"padding": {"type": "integer", "default": 0},
|
|
43
|
+
"spacing": {"type": "integer", "default": 8},
|
|
44
|
+
},
|
|
45
|
+
"description": "Root window (exactly one per file)",
|
|
46
|
+
},
|
|
47
|
+
"Column": {
|
|
48
|
+
"props": {
|
|
49
|
+
"padding": {"type": ["integer", "array"], "default": 12},
|
|
50
|
+
"spacing": {"type": "integer", "default": 8},
|
|
51
|
+
"bg": {"type": "string", "description": "Background color"},
|
|
52
|
+
"radius": {"type": "integer", "default": 24},
|
|
53
|
+
"expand": {"type": "boolean", "default": False},
|
|
54
|
+
"stagger": {"type": "integer", "default": 0},
|
|
55
|
+
},
|
|
56
|
+
"description": "Vertical container",
|
|
57
|
+
},
|
|
58
|
+
"Row": {
|
|
59
|
+
"props": {
|
|
60
|
+
"padding": {"type": ["integer", "array"], "default": 12},
|
|
61
|
+
"spacing": {"type": "integer", "default": 8},
|
|
62
|
+
"bg": {"type": "string", "description": "Background color"},
|
|
63
|
+
"radius": {"type": "integer", "default": 24},
|
|
64
|
+
"expand": {"type": "boolean", "default": False},
|
|
65
|
+
"stagger": {"type": "integer", "default": 0},
|
|
66
|
+
},
|
|
67
|
+
"description": "Horizontal container",
|
|
68
|
+
},
|
|
69
|
+
"Text": {
|
|
70
|
+
"props": {
|
|
71
|
+
"size": {"type": "integer", "default": 12},
|
|
72
|
+
"bold": {"type": "boolean", "default": False},
|
|
73
|
+
"italic": {"type": "boolean", "default": False},
|
|
74
|
+
"color": {"type": "string", "default": "text"},
|
|
75
|
+
"fg": {"type": "string", "default": "text"},
|
|
76
|
+
},
|
|
77
|
+
"description": "Text display (content between tags)",
|
|
78
|
+
},
|
|
79
|
+
"Button": {
|
|
80
|
+
"props": {
|
|
81
|
+
"on_click": {"type": "string", "description": "Handler function name"},
|
|
82
|
+
"bg": {"type": "string", "default": "accent"},
|
|
83
|
+
"fg": {"type": "string", "default": "background"},
|
|
84
|
+
"size": {"type": "integer", "default": 12},
|
|
85
|
+
"radius": {"type": "integer", "default": 17},
|
|
86
|
+
"disabled": {"type": "boolean", "default": False},
|
|
87
|
+
},
|
|
88
|
+
"description": "Clickable button (label between tags)",
|
|
89
|
+
},
|
|
90
|
+
"Input": {
|
|
91
|
+
"props": {
|
|
92
|
+
"placeholder": {"type": "string", "default": ""},
|
|
93
|
+
"value": {"type": "string", "default": ""},
|
|
94
|
+
"on_change": {"type": "string", "description": "Handler(text)"},
|
|
95
|
+
"on_enter": {"type": "string", "description": "Handler(text)"},
|
|
96
|
+
"size": {"type": "integer", "default": 14},
|
|
97
|
+
"show": {"type": "string", "enum": ["", "password"], "default": ""},
|
|
98
|
+
},
|
|
99
|
+
"description": "Text input (self-closing)",
|
|
100
|
+
},
|
|
101
|
+
"Checkbox": {
|
|
102
|
+
"props": {
|
|
103
|
+
"checked": {"type": "boolean", "default": False},
|
|
104
|
+
"on_change": {"type": "string", "description": "Handler(checked)"},
|
|
105
|
+
"size": {"type": "integer", "default": 12},
|
|
106
|
+
},
|
|
107
|
+
"description": "Toggle switch (label between tags)",
|
|
108
|
+
},
|
|
109
|
+
"Divider": {
|
|
110
|
+
"props": {
|
|
111
|
+
"color": {"type": "string", "default": "border"},
|
|
112
|
+
"thickness": {"type": "integer", "default": 2},
|
|
113
|
+
},
|
|
114
|
+
"description": "Horizontal divider (self-closing)",
|
|
115
|
+
},
|
|
116
|
+
"Spacer": {
|
|
117
|
+
"props": {
|
|
118
|
+
"width": {"type": "integer", "default": 1},
|
|
119
|
+
"height": {"type": "integer", "default": 1},
|
|
120
|
+
},
|
|
121
|
+
"description": "Empty space (self-closing)",
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
"animation_props": {
|
|
125
|
+
"animate": {
|
|
126
|
+
"type": "string",
|
|
127
|
+
"enum": ["fade", "reveal", "slide-up", "slide-down", "slide-left", "slide-right"],
|
|
128
|
+
"description": "Entrance animation type",
|
|
129
|
+
},
|
|
130
|
+
"duration": {"type": "integer", "default": 260, "description": "Animation duration (ms)"},
|
|
131
|
+
"delay": {"type": "integer", "default": 0, "description": "Initial delay (ms)"},
|
|
132
|
+
"easing": {
|
|
133
|
+
"type": "string",
|
|
134
|
+
"enum": ["linear", "in-cubic", "out-cubic", "in-out-cubic", "out-quad", "out-quart", "out-back", "out-elastic"],
|
|
135
|
+
"default": "out-cubic",
|
|
136
|
+
"description": "Easing curve",
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
"theme": {
|
|
140
|
+
"extends": {"type": "string", "enum": ["dark", "light"], "default": "dark"},
|
|
141
|
+
"colors": {
|
|
142
|
+
"background": {"type": "string", "default": "#1e1e2e"},
|
|
143
|
+
"surface": {"type": "string", "default": "#282a36"},
|
|
144
|
+
"text": {"type": "string", "default": "#f8f8f2"},
|
|
145
|
+
"subtext": {"type": "string", "default": "#a6adc8"},
|
|
146
|
+
"accent": {"type": "string", "default": "#7aa2f7"},
|
|
147
|
+
"border": {"type": "string", "default": "#44475a"},
|
|
148
|
+
"danger": {"type": "string", "default": "#f7768e"},
|
|
149
|
+
},
|
|
150
|
+
"custom_colors": {"type": "object", "description": "Additional named colors"},
|
|
151
|
+
},
|
|
152
|
+
"syntax": {
|
|
153
|
+
"interpolation": ["{$var}", "{var}", "$var"],
|
|
154
|
+
"events": "on_click=\"handler\" / on_change=\"handler\" / on_enter=\"handler\"",
|
|
155
|
+
"components": "<Component name=\"Name\">...<Component/>",
|
|
156
|
+
"script": "<script>def handler(): pass</script>",
|
|
157
|
+
},
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def run(path: str | Path, context: dict[str, Any] | None = None, theme: str = "dark") -> None:
|
|
162
|
+
"""读取并运行一个 .paw 文件(阻塞,直到窗口关闭)。"""
|
|
163
|
+
p = Path(path)
|
|
164
|
+
if not p.exists():
|
|
165
|
+
raise FileNotFoundError(f"PawUI: file not found: {p}")
|
|
166
|
+
source = p.read_text(encoding="utf-8")
|
|
167
|
+
rt = Runtime(source, str(p), context=context, theme=theme)
|
|
168
|
+
rt.run(block=True)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def check(path: str | Path) -> int:
|
|
172
|
+
"""语法检查 .paw 文件,不运行。"""
|
|
173
|
+
p = Path(path)
|
|
174
|
+
if not p.exists():
|
|
175
|
+
print(f"PawUI: file not found: {p}", file=sys.stderr)
|
|
176
|
+
return 1
|
|
177
|
+
source = p.read_text(encoding="utf-8")
|
|
178
|
+
try:
|
|
179
|
+
program = parse(source, str(p))
|
|
180
|
+
print(f"✓ {p}: syntax OK")
|
|
181
|
+
print(f" Elements: {len(program.elements)}")
|
|
182
|
+
print(f" Script: {'yes' if program.script else 'no'}")
|
|
183
|
+
for el in program.elements:
|
|
184
|
+
if el.tag == "component":
|
|
185
|
+
print(f" Component: {el.name}")
|
|
186
|
+
return 0
|
|
187
|
+
except PyxError as e:
|
|
188
|
+
print(e.formatted(), file=sys.stderr)
|
|
189
|
+
return 1
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def schema_cmd() -> int:
|
|
193
|
+
"""输出组件 Schema (JSON)。"""
|
|
194
|
+
print(json.dumps(SCHEMA, indent=2, ensure_ascii=False))
|
|
195
|
+
return 0
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def render(path: str | Path) -> int:
|
|
199
|
+
"""离屏渲染并输出信息。"""
|
|
200
|
+
import os
|
|
201
|
+
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
|
|
202
|
+
|
|
203
|
+
p = Path(path)
|
|
204
|
+
if not p.exists():
|
|
205
|
+
print(f"PawUI: file not found: {p}", file=sys.stderr)
|
|
206
|
+
return 1
|
|
207
|
+
|
|
208
|
+
source = p.read_text(encoding="utf-8")
|
|
209
|
+
try:
|
|
210
|
+
rt = Runtime(source, str(p))
|
|
211
|
+
rt.run(block=False)
|
|
212
|
+
print(f"✓ {p}: render OK")
|
|
213
|
+
if rt.root:
|
|
214
|
+
print(f" Window: {rt.root.windowTitle()}")
|
|
215
|
+
size = rt.root.size()
|
|
216
|
+
print(f" Size: {size.width()}x{size.height()}")
|
|
217
|
+
rt.root.close()
|
|
218
|
+
return 0
|
|
219
|
+
except PyxError as e:
|
|
220
|
+
print(e.formatted(), file=sys.stderr)
|
|
221
|
+
return 1
|
|
222
|
+
except Exception as e:
|
|
223
|
+
print(f"PawUI render error: {e}", file=sys.stderr)
|
|
224
|
+
return 1
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def main(argv: list[str] | None = None) -> int:
|
|
228
|
+
args = list(sys.argv[1:] if argv is None else argv)
|
|
229
|
+
if not args:
|
|
230
|
+
print(USAGE, file=sys.stderr)
|
|
231
|
+
return 2
|
|
232
|
+
|
|
233
|
+
cmd = args[0]
|
|
234
|
+
if cmd in ("-h", "--help", "help"):
|
|
235
|
+
print(USAGE)
|
|
236
|
+
return 0
|
|
237
|
+
if cmd in ("-v", "--version"):
|
|
238
|
+
from . import __version__
|
|
239
|
+
print(f"PawUI {__version__}")
|
|
240
|
+
return 0
|
|
241
|
+
|
|
242
|
+
if cmd == "check":
|
|
243
|
+
if len(args) < 2:
|
|
244
|
+
print("Usage: pawui check <file.paw>", file=sys.stderr)
|
|
245
|
+
return 2
|
|
246
|
+
return check(args[1])
|
|
247
|
+
|
|
248
|
+
if cmd == "schema":
|
|
249
|
+
return schema_cmd()
|
|
250
|
+
|
|
251
|
+
if cmd == "render":
|
|
252
|
+
if len(args) < 2:
|
|
253
|
+
print("Usage: pawui render <file.paw>", file=sys.stderr)
|
|
254
|
+
return 2
|
|
255
|
+
return render(args[1])
|
|
256
|
+
|
|
257
|
+
file = args[1] if cmd == "run" else cmd
|
|
258
|
+
if not file:
|
|
259
|
+
print(USAGE, file=sys.stderr)
|
|
260
|
+
return 2
|
|
261
|
+
|
|
262
|
+
try:
|
|
263
|
+
run(file)
|
|
264
|
+
except PyxError as e:
|
|
265
|
+
print(e.formatted(), file=sys.stderr)
|
|
266
|
+
return 1
|
|
267
|
+
except FileNotFoundError as e:
|
|
268
|
+
print(e, file=sys.stderr)
|
|
269
|
+
return 1
|
|
270
|
+
except KeyboardInterrupt:
|
|
271
|
+
return 130
|
|
272
|
+
return 0
|