lumiview 0.1.0.dev2__tar.gz → 0.1.0.dev3__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.
- {lumiview-0.1.0.dev2 → lumiview-0.1.0.dev3}/Cargo.lock +1 -1
- {lumiview-0.1.0.dev2 → lumiview-0.1.0.dev3}/Cargo.toml +1 -1
- {lumiview-0.1.0.dev2 → lumiview-0.1.0.dev3}/PKG-INFO +15 -5
- {lumiview-0.1.0.dev2 → lumiview-0.1.0.dev3}/README.md +14 -4
- {lumiview-0.1.0.dev2 → lumiview-0.1.0.dev3}/README_zh.md +14 -4
- lumiview-0.1.0.dev3/docs/architecture.md +117 -0
- lumiview-0.1.0.dev3/docs/getting-started.md +157 -0
- lumiview-0.1.0.dev3/docs/integrations.md +120 -0
- lumiview-0.1.0.dev3/docs/plugins.md +153 -0
- lumiview-0.1.0.dev3/docs/serve.md +97 -0
- {lumiview-0.1.0.dev2 → lumiview-0.1.0.dev3}/examples/bridge_demo.py +3 -3
- {lumiview-0.1.0.dev2 → lumiview-0.1.0.dev3}/examples/custom_titlebar.py +3 -3
- {lumiview-0.1.0.dev2 → lumiview-0.1.0.dev3}/examples/events.py +16 -16
- {lumiview-0.1.0.dev2 → lumiview-0.1.0.dev3}/examples/fastapi_demo.py +3 -3
- {lumiview-0.1.0.dev2 → lumiview-0.1.0.dev3}/examples/hello_world.py +21 -4
- {lumiview-0.1.0.dev2 → lumiview-0.1.0.dev3}/examples/hello_world_sync.py +2 -2
- {lumiview-0.1.0.dev2 → lumiview-0.1.0.dev3}/examples/multi_window.py +5 -5
- {lumiview-0.1.0.dev2 → lumiview-0.1.0.dev3}/pyproject.toml +1 -1
- lumiview-0.1.0.dev3/python/lumiview/__init__.py +102 -0
- {lumiview-0.1.0.dev2 → lumiview-0.1.0.dev3}/python/lumiview/_binding.py +5 -3
- lumiview-0.1.0.dev3/python/lumiview/_core.pyi +934 -0
- lumiview-0.1.0.dev2/python/lumiview/_app.py → lumiview-0.1.0.dev3/python/lumiview/app.py +225 -60
- lumiview-0.1.0.dev2/python/lumiview/_bridge.py → lumiview-0.1.0.dev3/python/lumiview/bridge.py +45 -10
- lumiview-0.1.0.dev3/python/lumiview/events.py +526 -0
- {lumiview-0.1.0.dev2 → lumiview-0.1.0.dev3}/python/lumiview/plugins/window_controls.py +26 -7
- lumiview-0.1.0.dev2/python/lumiview/_scope.py → lumiview-0.1.0.dev3/python/lumiview/scope.py +88 -87
- {lumiview-0.1.0.dev2 → lumiview-0.1.0.dev3}/python/lumiview/serve/asgi.py +1 -1
- {lumiview-0.1.0.dev2 → lumiview-0.1.0.dev3}/python/lumiview/serve/base.py +6 -4
- lumiview-0.1.0.dev2/python/lumiview/_task.py → lumiview-0.1.0.dev3/python/lumiview/task.py +15 -8
- lumiview-0.1.0.dev3/python/lumiview/utils.py +76 -0
- lumiview-0.1.0.dev3/python/lumiview/window.py +1938 -0
- lumiview-0.1.0.dev3/src/event_loop.rs +318 -0
- lumiview-0.1.0.dev3/src/events.rs +1073 -0
- {lumiview-0.1.0.dev2 → lumiview-0.1.0.dev3}/src/lib.rs +38 -1
- lumiview-0.1.0.dev3/src/monitor.rs +92 -0
- {lumiview-0.1.0.dev2 → lumiview-0.1.0.dev3}/src/types.rs +247 -9
- lumiview-0.1.0.dev3/src/window.rs +894 -0
- lumiview-0.1.0.dev2/python/lumiview/__init__.py +0 -143
- lumiview-0.1.0.dev2/python/lumiview/_core.pyi +0 -501
- lumiview-0.1.0.dev2/python/lumiview/_events.py +0 -59
- lumiview-0.1.0.dev2/python/lumiview/_window.py +0 -1130
- lumiview-0.1.0.dev2/python/lumiview/utils.py +0 -43
- lumiview-0.1.0.dev2/src/event_loop.rs +0 -245
- lumiview-0.1.0.dev2/src/events.rs +0 -553
- lumiview-0.1.0.dev2/src/window.rs +0 -556
- {lumiview-0.1.0.dev2 → lumiview-0.1.0.dev3}/.github/workflows/publish-to-pypi.yml +0 -0
- {lumiview-0.1.0.dev2 → lumiview-0.1.0.dev3}/.gitignore +0 -0
- {lumiview-0.1.0.dev2 → lumiview-0.1.0.dev3}/CONTRIBUTING.md +0 -0
- {lumiview-0.1.0.dev2 → lumiview-0.1.0.dev3}/LICENSE +0 -0
- {lumiview-0.1.0.dev2 → lumiview-0.1.0.dev3}/python/lumiview/plugins/__init__.py +0 -0
- {lumiview-0.1.0.dev2 → lumiview-0.1.0.dev3}/python/lumiview/serve/__init__.py +0 -0
- {lumiview-0.1.0.dev2 → lumiview-0.1.0.dev3}/python/lumiview/serve/static.py +0 -0
- {lumiview-0.1.0.dev2 → lumiview-0.1.0.dev3}/python/lumiview/serve/wsgi.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: lumiview
|
|
3
|
-
Version: 0.1.0.
|
|
3
|
+
Version: 0.1.0.dev3
|
|
4
4
|
Classifier: Development Status :: 2 - Pre-Alpha
|
|
5
5
|
Classifier: Programming Language :: Python :: 3
|
|
6
6
|
Classifier: Programming Language :: Python :: 3.10
|
|
@@ -82,6 +82,14 @@ flowchart TB
|
|
|
82
82
|
|
|
83
83
|
**Threading model:** Main thread (event loop + native ops) → Async thread (asyncio + user coroutines) → Thread pool (synchronous code).
|
|
84
84
|
|
|
85
|
+
## Documentation
|
|
86
|
+
|
|
87
|
+
- [Getting Started](docs/getting-started.md)
|
|
88
|
+
- [Architecture & Threading Model](docs/architecture.md)
|
|
89
|
+
- [Custom Plugins](docs/plugins.md)
|
|
90
|
+
- [Third-Party Integrations](docs/integrations.md)
|
|
91
|
+
- [Serve Protocol](docs/serve.md)
|
|
92
|
+
|
|
85
93
|
## Installation
|
|
86
94
|
|
|
87
95
|
```bash
|
|
@@ -91,17 +99,17 @@ pip install --pre lumiview
|
|
|
91
99
|
## Quick Start
|
|
92
100
|
|
|
93
101
|
```python
|
|
94
|
-
from lumiview import App, Window
|
|
102
|
+
from lumiview import App, Window, WindowOptions
|
|
95
103
|
|
|
96
104
|
app = App(name="HelloLumiView")
|
|
97
105
|
|
|
98
106
|
async def main():
|
|
99
|
-
win = await Window.create(
|
|
107
|
+
win = await Window.create(WindowOptions(
|
|
100
108
|
title="Hello LumiView!",
|
|
101
109
|
url="https://example.com",
|
|
102
110
|
width=900, height=640,
|
|
103
111
|
devtools=True,
|
|
104
|
-
)
|
|
112
|
+
))
|
|
105
113
|
title = await win.eval_js("document.title")
|
|
106
114
|
print(f"Page title: {title}")
|
|
107
115
|
|
|
@@ -135,7 +143,7 @@ For runnable code, see the [`examples/`](examples/) directory.
|
|
|
135
143
|
- [x] Window management (TaoWindow + Window)
|
|
136
144
|
- [x] WebView embedding (wryview)
|
|
137
145
|
- [x] JS ↔ Python Bridge (invoke/listen IPC)
|
|
138
|
-
- [x]
|
|
146
|
+
- [x] Event class system (WindowEvent / AppEvent)
|
|
139
147
|
- [x] Unified async/sync `Task` interface
|
|
140
148
|
- [x] Static file serving + WSGI proxy
|
|
141
149
|
- [x] ASGI adapter (FastAPI, Starlette, etc.)
|
|
@@ -148,6 +156,8 @@ For runnable code, see the [`examples/`](examples/) directory.
|
|
|
148
156
|
|
|
149
157
|
This is an early-stage project — **your feedback is crucial**.
|
|
150
158
|
|
|
159
|
+
Due to limited personal time, some features may not be completed promptly — PRs and other contributions are warmly welcomed.
|
|
160
|
+
|
|
151
161
|
- 🐛 [Report Bugs](https://github.com/xiaosuawa/LumiView/issues)
|
|
152
162
|
- 💡 [Share Ideas](https://github.com/xiaosuawa/LumiView/discussions)
|
|
153
163
|
- 🔧 [Contribution Guide](CONTRIBUTING.md)
|
|
@@ -50,6 +50,14 @@ flowchart TB
|
|
|
50
50
|
|
|
51
51
|
**Threading model:** Main thread (event loop + native ops) → Async thread (asyncio + user coroutines) → Thread pool (synchronous code).
|
|
52
52
|
|
|
53
|
+
## Documentation
|
|
54
|
+
|
|
55
|
+
- [Getting Started](docs/getting-started.md)
|
|
56
|
+
- [Architecture & Threading Model](docs/architecture.md)
|
|
57
|
+
- [Custom Plugins](docs/plugins.md)
|
|
58
|
+
- [Third-Party Integrations](docs/integrations.md)
|
|
59
|
+
- [Serve Protocol](docs/serve.md)
|
|
60
|
+
|
|
53
61
|
## Installation
|
|
54
62
|
|
|
55
63
|
```bash
|
|
@@ -59,17 +67,17 @@ pip install --pre lumiview
|
|
|
59
67
|
## Quick Start
|
|
60
68
|
|
|
61
69
|
```python
|
|
62
|
-
from lumiview import App, Window
|
|
70
|
+
from lumiview import App, Window, WindowOptions
|
|
63
71
|
|
|
64
72
|
app = App(name="HelloLumiView")
|
|
65
73
|
|
|
66
74
|
async def main():
|
|
67
|
-
win = await Window.create(
|
|
75
|
+
win = await Window.create(WindowOptions(
|
|
68
76
|
title="Hello LumiView!",
|
|
69
77
|
url="https://example.com",
|
|
70
78
|
width=900, height=640,
|
|
71
79
|
devtools=True,
|
|
72
|
-
)
|
|
80
|
+
))
|
|
73
81
|
title = await win.eval_js("document.title")
|
|
74
82
|
print(f"Page title: {title}")
|
|
75
83
|
|
|
@@ -103,7 +111,7 @@ For runnable code, see the [`examples/`](examples/) directory.
|
|
|
103
111
|
- [x] Window management (TaoWindow + Window)
|
|
104
112
|
- [x] WebView embedding (wryview)
|
|
105
113
|
- [x] JS ↔ Python Bridge (invoke/listen IPC)
|
|
106
|
-
- [x]
|
|
114
|
+
- [x] Event class system (WindowEvent / AppEvent)
|
|
107
115
|
- [x] Unified async/sync `Task` interface
|
|
108
116
|
- [x] Static file serving + WSGI proxy
|
|
109
117
|
- [x] ASGI adapter (FastAPI, Starlette, etc.)
|
|
@@ -116,6 +124,8 @@ For runnable code, see the [`examples/`](examples/) directory.
|
|
|
116
124
|
|
|
117
125
|
This is an early-stage project — **your feedback is crucial**.
|
|
118
126
|
|
|
127
|
+
Due to limited personal time, some features may not be completed promptly — PRs and other contributions are warmly welcomed.
|
|
128
|
+
|
|
119
129
|
- 🐛 [Report Bugs](https://github.com/xiaosuawa/LumiView/issues)
|
|
120
130
|
- 💡 [Share Ideas](https://github.com/xiaosuawa/LumiView/discussions)
|
|
121
131
|
- 🔧 [Contribution Guide](CONTRIBUTING.md)
|
|
@@ -50,6 +50,14 @@ flowchart TB
|
|
|
50
50
|
|
|
51
51
|
**线程模型:** 主线程(事件循环 + 原生操作)→ 异步线程(asyncio + 用户协程)→ 线程池(同步代码)。
|
|
52
52
|
|
|
53
|
+
## 文档
|
|
54
|
+
|
|
55
|
+
- [快速入门](docs/getting-started.md)
|
|
56
|
+
- [架构与线程模型](docs/architecture.md)
|
|
57
|
+
- [自定义插件](docs/plugins.md)
|
|
58
|
+
- [第三方集成](docs/integrations.md)
|
|
59
|
+
- [Serve 协议](docs/serve.md)
|
|
60
|
+
|
|
53
61
|
## 安装
|
|
54
62
|
|
|
55
63
|
```bash
|
|
@@ -59,17 +67,17 @@ pip install --pre lumiview
|
|
|
59
67
|
## 快速开始
|
|
60
68
|
|
|
61
69
|
```python
|
|
62
|
-
from lumiview import App, Window
|
|
70
|
+
from lumiview import App, Window, WindowOptions
|
|
63
71
|
|
|
64
72
|
app = App(name="HelloLumiView")
|
|
65
73
|
|
|
66
74
|
async def main():
|
|
67
|
-
win = await Window.create(
|
|
75
|
+
win = await Window.create(WindowOptions(
|
|
68
76
|
title="Hello LumiView!",
|
|
69
77
|
url="https://example.com",
|
|
70
78
|
width=900, height=640,
|
|
71
79
|
devtools=True,
|
|
72
|
-
)
|
|
80
|
+
))
|
|
73
81
|
title = await win.eval_js("document.title")
|
|
74
82
|
print(f"Page title: {title}")
|
|
75
83
|
|
|
@@ -103,7 +111,7 @@ app.run(main)
|
|
|
103
111
|
- [x] 窗口管理 (TaoWindow + Window)
|
|
104
112
|
- [x] WebView 嵌入 (wryview)
|
|
105
113
|
- [x] JS ↔ Python Bridge (invoke/listen IPC)
|
|
106
|
-
- [x]
|
|
114
|
+
- [x] 事件类体系 (WindowEvent / AppEvent)
|
|
107
115
|
- [x] 统一 async/sync `Task` 接口
|
|
108
116
|
- [x] 静态文件服务 + WSGI 代理
|
|
109
117
|
- [x] ASGI 适配 (FastAPI, Starlette 等)
|
|
@@ -116,6 +124,8 @@ app.run(main)
|
|
|
116
124
|
|
|
117
125
|
这是一个早期项目——**你的反馈至关重要**。
|
|
118
126
|
|
|
127
|
+
受个人时间所限,部分功能可能无法及时完成,欢迎各位通过 PR 等贡献方式一同实现。
|
|
128
|
+
|
|
119
129
|
- 🐛 [报告 Bug](https://github.com/xiaosuawa/LumiView/issues)
|
|
120
130
|
- 💡 [分享想法](https://github.com/xiaosuawa/LumiView/discussions)
|
|
121
131
|
- 🔧 [贡献指南](CONTRIBUTING.md)
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# 架构
|
|
2
|
+
|
|
3
|
+
## 分层
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
Rust 层 Python 层
|
|
7
|
+
────────────────────── ──────────────────────────────────
|
|
8
|
+
tao(窗口/事件循环) lumiview.app — App 单例、2+N 线程编排
|
|
9
|
+
│ lumiview.window — Window / WindowOptions
|
|
10
|
+
▼ lumiview.bridge — Bridge / BridgeError(IPC)
|
|
11
|
+
wryview(WebView 绑定) lumiview.scope — Scope / Plugin / 权限链
|
|
12
|
+
│ lumiview.task — Task / run_async
|
|
13
|
+
▼ lumiview.events — 事件类体系
|
|
14
|
+
lumiview._core(PyO3) lumiview.utils — match_pattern / navigation_policy
|
|
15
|
+
lumiview.serve — lumiview:// 自定义协议
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
- Rust 层只做薄绑定:`TaoEventLoop` / `TaoWindow` 等对象是 `#[pyclass(unsendable)]`,方法即原生调用。
|
|
19
|
+
- 低层绑定入口是 `lumiview._core`;日常开发用顶层 `lumiview` API。
|
|
20
|
+
- 所有跨线程/延迟操作统一返回 `Task[T]`:`await`、`.result()`、`.on_done()` 三用一体。
|
|
21
|
+
|
|
22
|
+
## 2+N 线程模型
|
|
23
|
+
|
|
24
|
+
| 线程 | 职责 |
|
|
25
|
+
|---|---|
|
|
26
|
+
| **主线程** | tao 事件循环 + 所有原生窗口/WebView 操作 |
|
|
27
|
+
| **asyncio 线程** | 运行用户协程(`app.run(entry)` 的 entry)与事件 handler |
|
|
28
|
+
| **线程池** | 同步 `task()` 调用、同步事件 handler(大小由 `App(max_workers=N)` 配置) |
|
|
29
|
+
|
|
30
|
+
关键推论:
|
|
31
|
+
|
|
32
|
+
- **事件 handler 从不运行在主线程**——它们跑在 asyncio 线程(同步 handler 经由线程池)。
|
|
33
|
+
- 主线程 GUI 库操作必须显式调度:`app.call_on_main(...)`。
|
|
34
|
+
|
|
35
|
+
## 事件流
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
tao 事件
|
|
39
|
+
→ Rust build_event() 构造 TaoEvent 子类(ResizedEvent 等)
|
|
40
|
+
→ App._on_tao_event(主线程)按 window_id 路由
|
|
41
|
+
→ Window._on_tao_event → 构造事件类实例(WindowEvent.ResizedEvent 等)
|
|
42
|
+
→ Window._emit(event):按 type(event) 找到 handler,投递到 asyncio 线程
|
|
43
|
+
→ handler(event) 收到事件对象(event.window 已填充)
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
可阻止事件(`CloseRequestedEvent` / `NavigationRequestedEvent` / `NewWindowRequestedEvent` / `DownloadStartedEvent`)走 `_dispatch_preventable`:先派发并**等待** handler 完成,再检查 `event.prevented` 决定是否执行默认行为(如关闭窗口、放行导航、允许下载)。
|
|
47
|
+
|
|
48
|
+
## 事件类体系
|
|
49
|
+
|
|
50
|
+
```text
|
|
51
|
+
Event # 所有事件的公共祖先
|
|
52
|
+
├── WindowBaseEvent # 窗口事件基类:数据 + prevent()/prevented
|
|
53
|
+
│ window: Window | None
|
|
54
|
+
└── AppBaseEvent # 应用事件基类(ReadyEvent / AppCloseEvent)
|
|
55
|
+
|
|
56
|
+
WindowEvent # 纯命名空间容器(非基类)
|
|
57
|
+
├── WindowEvent.TitleChangedEvent(WindowBaseEvent)
|
|
58
|
+
├── WindowEvent.CloseRequestedEvent(WindowBaseEvent) # 可 prevent()
|
|
59
|
+
├── WindowEvent.NavigationRequestedEvent # 可 prevent()
|
|
60
|
+
├── WindowEvent.DownloadStartedEvent # 可 save_to(path) / prevent()
|
|
61
|
+
└── ...
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
命名空间访问与 isinstance 检查同时成立:
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
from lumiview.events import WindowEvent, WindowBaseEvent
|
|
68
|
+
|
|
69
|
+
e = WindowEvent.TitleChangedEvent(title="x")
|
|
70
|
+
assert isinstance(e, WindowBaseEvent)
|
|
71
|
+
assert e is not None and e.title == "x"
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## 主线程调度机制
|
|
75
|
+
|
|
76
|
+
| API | 说明 |
|
|
77
|
+
|---|---|
|
|
78
|
+
| `app.call_on_main(fn, *args)` | 把 *fn* 排到主线程执行,返回 `Task[T]`;主线程上直接执行 |
|
|
79
|
+
| `@main_thread` | 装饰器版:同步方法变成返回 `Task[R]` 并在主线程跑方法体 |
|
|
80
|
+
| `Task` | `concurrent.futures.Future` 子类;`await` / `.result()` / `.on_done()` |
|
|
81
|
+
| `run_async(fn, pool=...)` | 轻量派发(无 Task 开销):async 直接 await,sync 走线程池 |
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
from lumiview import App
|
|
85
|
+
from lumiview.task import run_async
|
|
86
|
+
|
|
87
|
+
app = App.get()
|
|
88
|
+
|
|
89
|
+
# 异步代码:直接 await
|
|
90
|
+
result = await app.call_on_main(lambda: window_op())
|
|
91
|
+
|
|
92
|
+
# 同步代码:.result() 阻塞
|
|
93
|
+
result = app.call_on_main(lambda: window_op()).result()
|
|
94
|
+
|
|
95
|
+
# 混合回调:async 直接跑、sync 去线程池
|
|
96
|
+
await run_async(mixed_callback, pool=app._threadpool)
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
> 经验:任何「从别的线程操作原生窗口」的崩溃(尤其 macOS)基本都是没走 `call_on_main`。典型案例:TimeFlow 用 pystray 时,状态栏菜单回调线程直接操作 `NSWindow` 崩溃——改为主线程调度后稳定。见 [integrations.md](integrations.md)。
|
|
100
|
+
|
|
101
|
+
## 不可跨线程对象(unsendable)
|
|
102
|
+
|
|
103
|
+
| 对象 | 跨线程 | 说明 |
|
|
104
|
+
|---|---|---|
|
|
105
|
+
| `TaoEventLoop` | ❌ | tao `!Send`(Rust 侧标记) |
|
|
106
|
+
| `TaoEventLoopProxy` | ✅ | 跨线程唤醒通道(`send_event`) |
|
|
107
|
+
| `TaoWindow` | ❌ | 主线程操作(含 Windows 透明 surface 重绘) |
|
|
108
|
+
| `WebView`(wryview) | ❌ | 主线程 |
|
|
109
|
+
| `Window`(Python 层) | ✅ 方法走 `@main_thread` | 唯一的线程安全通道 |
|
|
110
|
+
|
|
111
|
+
`Window` 把不可跨线程的原生对象直接存在实例上(`win._window` / `win._webview`),所有访问经 `call_on_main` 派发到主线程——外部永远不直接触碰原生对象。
|
|
112
|
+
|
|
113
|
+
## 设计原则
|
|
114
|
+
|
|
115
|
+
1. **跨线程/延迟操作返回 `Task`** —— 类型和行为可预测,不做上下文相关的自动解包。
|
|
116
|
+
2. **事件 handler 不跑主线程** —— 主线程只属于 tao 循环与原生操作,事件处理在 asyncio 线程。
|
|
117
|
+
3. **配置即数据** —— `WindowOptions` 是(可变的)dataclass:可 `replace`、可子类做模板(子类需重新应用 `@dataclass`)、可跨线程传递。
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# 快速入门
|
|
2
|
+
|
|
3
|
+
## 安装
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
pip install --pre lumiview
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
当前为预览阶段(`0.1.0.dev*`),API 尚未冻结,升级时请留意变更。
|
|
10
|
+
|
|
11
|
+
## 最小应用
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
import asyncio
|
|
15
|
+
|
|
16
|
+
from lumiview import App, Window, WindowOptions
|
|
17
|
+
|
|
18
|
+
app = App(name="MyApp")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
async def main():
|
|
22
|
+
win = await Window.create(WindowOptions(
|
|
23
|
+
title="Hello",
|
|
24
|
+
html="<h1>Hello from LumiView!</h1>",
|
|
25
|
+
))
|
|
26
|
+
|
|
27
|
+
# 演示:5 秒后自动关闭。默认 exit_on_last_window=True,
|
|
28
|
+
# 最后一个窗口关闭时应用自动退出。
|
|
29
|
+
await asyncio.sleep(5)
|
|
30
|
+
await win.close()
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
app.run(main)
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
要点:
|
|
37
|
+
|
|
38
|
+
- `Window.create(options)` 是 `@main_thread` 方法,返回 `Task[Window]` —— 异步代码里 `await`,同步代码里 `.result()`。
|
|
39
|
+
- `app.run(main)` 阻塞主线程直到应用退出;`main` 可以是异步函数(跑在 asyncio 线程)或同步函数(跑在线程池)。
|
|
40
|
+
- `html=` 直接渲染字符串;加载 URL 用 `url=`;加载本地目录用 `source=`(见 [serve.md](serve.md))。
|
|
41
|
+
|
|
42
|
+
## 事件订阅
|
|
43
|
+
|
|
44
|
+
用 `win.on(事件类)` 装饰器注册 handler,handler 收到**事件对象**:
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
from lumiview import App, Window, WindowOptions
|
|
48
|
+
from lumiview.events import WindowEvent
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
async def main():
|
|
52
|
+
win = await Window.create(WindowOptions(
|
|
53
|
+
title="Events",
|
|
54
|
+
html="<p id='t'>hi</p>",
|
|
55
|
+
))
|
|
56
|
+
|
|
57
|
+
@win.on(WindowEvent.TitleChangedEvent)
|
|
58
|
+
def on_title(event):
|
|
59
|
+
print("新标题:", event.title)
|
|
60
|
+
|
|
61
|
+
@win.on(WindowEvent.NavigationRequestedEvent)
|
|
62
|
+
def on_navigation(event):
|
|
63
|
+
print("导航到:", event.url)
|
|
64
|
+
if event.url.startswith("https://evil.com"):
|
|
65
|
+
event.prevent() # 阻止这次导航
|
|
66
|
+
|
|
67
|
+
@win.on(WindowEvent.CloseRequestedEvent)
|
|
68
|
+
def on_close(event):
|
|
69
|
+
if not saved:
|
|
70
|
+
event.prevent() # 取消关闭
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
- 事件类集中在 `WindowEvent` 命名空间。
|
|
74
|
+
- 所有 handler 都运行在 **asyncio 线程**(不是主线程、不是 wryview 回调线程)——不要在里面直接操作原生 GUI。
|
|
75
|
+
- 可阻止事件(`CloseRequestedEvent` / `NavigationRequestedEvent` / `NewWindowRequestedEvent` / `DownloadStartedEvent`)调用 `event.prevent()` 后,默认行为会被取消。
|
|
76
|
+
- `event.window` 指向触发事件的窗口实例。
|
|
77
|
+
|
|
78
|
+
## 窗口配置:WindowOptions
|
|
79
|
+
|
|
80
|
+
所有配置集中在一个(可变的)dataclass 里,字段即 `create()` 的旧关键字参数:
|
|
81
|
+
|
|
82
|
+
| 字段 | 说明 |
|
|
83
|
+
|---|---|
|
|
84
|
+
| `title` / `width` / `height` | 标题与尺寸(逻辑像素) |
|
|
85
|
+
| `transparent` | 透明背景(配合 CSS `background: transparent`) |
|
|
86
|
+
| `decorations` | 原生标题栏与边框(自定义标题栏时设 `False`) |
|
|
87
|
+
| `devtools` | 启用浏览器开发者工具 |
|
|
88
|
+
| `visible` / `resizable` / `focused` | 可见 / 可缩放 / 聚焦 |
|
|
89
|
+
| `always_on_top` / `maximized` | 置顶 / 启动即最大化 |
|
|
90
|
+
| `close_behavior` | 关闭行为(见下文「关闭流程」) |
|
|
91
|
+
| `bridge` | `Bridge` 实例(JS ↔ Python IPC,见 [plugins.md](plugins.md)) |
|
|
92
|
+
| `untrusted` | 不注入任何初始化脚本(与 `bridge` 互斥) |
|
|
93
|
+
| `prepare` | 创建前钩子(见下文) |
|
|
94
|
+
|
|
95
|
+
局部修改用 `dataclasses.replace`;配置模板用**子类**:
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
from dataclasses import dataclass, replace
|
|
99
|
+
|
|
100
|
+
base = WindowOptions(title="A", width=800)
|
|
101
|
+
opts = replace(base, width=1024) # 新对象;base 不变
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@dataclass(frozen=True)
|
|
105
|
+
class MyAppOptions(WindowOptions):
|
|
106
|
+
title: str = "MyApp"
|
|
107
|
+
devtools: bool = True
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
opts2 = MyAppOptions() # title="MyApp", devtools=True
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
> **注意**:子类模板**必须重新应用 `@dataclass(...)`**(是否 `frozen` 自选)。dataclass 生成的 `__init__` 在生成时固化基类默认值——不加装饰器的普通子类继承到的仍是基类默认值(`"lumiview"`),类属性声明不会生效。
|
|
114
|
+
|
|
115
|
+
## `prepare` 钩子:注册早期事件
|
|
116
|
+
|
|
117
|
+
tao 窗口创建、webview 加载的那一刻事件流就开始了(`PageLoadStarted` 等可能立即触发),而 `win.on()` 通常只能在 `create()` 返回后调用。`prepare` 在 tao/webview 创建**之前**收到 Window 空壳实例(hooks 注册能力已就绪),可以提前订阅:
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
def prepare(win):
|
|
121
|
+
win.on(WindowEvent.PageLoadStartedEvent)(
|
|
122
|
+
lambda event: print("开始加载:", event.url)
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
win = await Window.create(WindowOptions(
|
|
126
|
+
html="<h1>hi</h1>",
|
|
127
|
+
prepare=prepare,
|
|
128
|
+
))
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## 多窗口
|
|
132
|
+
|
|
133
|
+
多次调用 `Window.create` 即可;每个窗口有独立 id(`win._id`),应用内部以 `app._windows`(`dict[window_id, Window]`)跟踪。默认 `exit_on_last_window=True`,最后一个窗口关闭时应用退出;托盘类应用可设 `App(name=..., exit_on_last_window=False)`。
|
|
134
|
+
|
|
135
|
+
## 关闭流程
|
|
136
|
+
|
|
137
|
+
`CloseBehavior` 控制收到关闭请求时的行为:
|
|
138
|
+
|
|
139
|
+
- `CloseBehavior.Close`(默认)——销毁窗口;
|
|
140
|
+
- `CloseBehavior.Hide`——隐藏而不是销毁;
|
|
141
|
+
- `CloseBehavior.Ignore`——什么都不做。
|
|
142
|
+
|
|
143
|
+
`CloseRequestedEvent` 可在任何行为之前 `prevent()`:
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
from lumiview import CloseBehavior
|
|
147
|
+
|
|
148
|
+
win = await Window.create(WindowOptions(
|
|
149
|
+
close_behavior=CloseBehavior.Hide, # 先隐藏
|
|
150
|
+
))
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
@win.on(WindowEvent.CloseRequestedEvent)
|
|
154
|
+
def on_close(event):
|
|
155
|
+
if not saved:
|
|
156
|
+
event.prevent() # 未保存 → 什么都不发生
|
|
157
|
+
```
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# 第三方集成
|
|
2
|
+
|
|
3
|
+
核心规则只有一条:**任何操作原生 GUI 的代码必须跑在主线程**(tao 事件循环线程)。事件 handler 运行在 asyncio 线程,第三方库的回调可能运行在任何线程——需要跨线程操作窗口时,统一走 `app.call_on_main(...)`。
|
|
4
|
+
|
|
5
|
+
```python
|
|
6
|
+
from lumiview import App
|
|
7
|
+
|
|
8
|
+
app = App.get()
|
|
9
|
+
|
|
10
|
+
handle = app.call_on_main(lambda: native_operation()) # → Task[T]
|
|
11
|
+
result = handle.result() # 同步等待
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## 1. pystray 系统托盘(macOS 案例)
|
|
15
|
+
|
|
16
|
+
macOS 上 `NSApplication` 是进程级单例,由 tao 创建。pystray 自己启动的 run 循环会与之冲突——**不要调用 `icon.run()` / `icon.run_detached()`**:状态栏 item 的呈现与事件由 tao 的主线程事件循环驱动,菜单回调里再调度回主线程。
|
|
17
|
+
|
|
18
|
+
```python
|
|
19
|
+
import pystray
|
|
20
|
+
from PIL import Image
|
|
21
|
+
|
|
22
|
+
from lumiview import App, Window, WindowOptions
|
|
23
|
+
|
|
24
|
+
app = App(name="TrayApp", exit_on_last_window=False)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def on_quit(icon, item):
|
|
28
|
+
app.exit()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def on_toggle(icon, item):
|
|
32
|
+
# 回调运行在状态栏事件线程 —— 原生操作调度回主线程
|
|
33
|
+
app.call_on_main(lambda: win.set_title("toggled"))
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
icon = pystray.Icon(
|
|
37
|
+
"tray",
|
|
38
|
+
Image.new("RGBA", (64, 64), (0, 120, 215, 255)),
|
|
39
|
+
menu=pystray.Menu(
|
|
40
|
+
pystray.MenuItem("切换标题", on_toggle),
|
|
41
|
+
pystray.MenuItem("退出", on_quit),
|
|
42
|
+
),
|
|
43
|
+
)
|
|
44
|
+
icon.visible = True # 不调用 icon.run() / icon.run_detached()
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
async def main():
|
|
48
|
+
global win
|
|
49
|
+
win = await Window.create(WindowOptions(title="Tray"))
|
|
50
|
+
# 关闭窗口不退出(exit_on_last_window=False)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
app.run(main)
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Windows / Linux 上 pystray 自带线程循环可用,但同样的主线程调度规则仍然适用:菜单回调里操作窗口一律 `app.call_on_main(...)`。
|
|
57
|
+
|
|
58
|
+
## 2. 原生对话框(tkinter / PySide6)
|
|
59
|
+
|
|
60
|
+
对话框属于 GUI 操作,必须在主线程创建;且阻塞式对话框会阻塞主线程事件循环,需要先关闭对话框再继续处理:
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
import tkinter as tk
|
|
64
|
+
|
|
65
|
+
from lumiview import App
|
|
66
|
+
|
|
67
|
+
app = App.get()
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def pick_file():
|
|
71
|
+
root = tk.Tk()
|
|
72
|
+
root.withdraw()
|
|
73
|
+
path = tk.filedialog.askopenfilename()
|
|
74
|
+
root.destroy()
|
|
75
|
+
return path
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
# 在主线程弹对话框;期间 tao 事件循环被阻塞(模态),结束后恢复
|
|
79
|
+
path = app.call_on_main(pick_file).result()
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## 3. AppKit / Cocoa 直接操作(macOS,pyobjc)
|
|
83
|
+
|
|
84
|
+
所有 AppKit 调用必须在主线程;在事件 handler 里直接操作会崩溃(NSWindow 非线程安全):
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
def bring_to_front():
|
|
88
|
+
from AppKit import NSApp
|
|
89
|
+
|
|
90
|
+
NSApp.activateIgnoringOtherApps_(True)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
app.call_on_main(bring_to_front)
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## 4. GTK 工具(Linux)
|
|
97
|
+
|
|
98
|
+
GTK 不是线程安全的:所有 GTK 调用(对话框、窗口操作、Clipboard 等)必须主线程。tao 的 GTK 主循环是唯一允许操作 GTK 对象的线程;从 handler 里调用前先 `app.call_on_main(...)`。
|
|
99
|
+
|
|
100
|
+
## 5. 无 UI 约束的系统 API
|
|
101
|
+
|
|
102
|
+
不触碰 GUI 的调用(截图、文件读写、网络请求、子进程等)没有线程约束——直接在事件 handler / 命令里跑即可(异步函数在 asyncio 线程,同步函数自动走线程池):
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
from lumiview import WindowEvent
|
|
106
|
+
|
|
107
|
+
@win.on(WindowEvent.TitleChangedEvent) # win 来自 Window.create(...)
|
|
108
|
+
def on_title(event):
|
|
109
|
+
# 后台直接做:写日志、截图、上报……
|
|
110
|
+
save_screenshot() # 同步函数 → 线程池执行
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
如果要在同步 handler 里显式使用线程池,用 `lumiview.task.run_async`:
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
from lumiview.task import run_async
|
|
117
|
+
|
|
118
|
+
# 在异步上下文里混合同步/异步回调
|
|
119
|
+
await run_async(sync_work, pool=app._threadpool)
|
|
120
|
+
```
|