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.
Files changed (53) hide show
  1. {lumiview-0.1.0.dev2 → lumiview-0.1.0.dev3}/Cargo.lock +1 -1
  2. {lumiview-0.1.0.dev2 → lumiview-0.1.0.dev3}/Cargo.toml +1 -1
  3. {lumiview-0.1.0.dev2 → lumiview-0.1.0.dev3}/PKG-INFO +15 -5
  4. {lumiview-0.1.0.dev2 → lumiview-0.1.0.dev3}/README.md +14 -4
  5. {lumiview-0.1.0.dev2 → lumiview-0.1.0.dev3}/README_zh.md +14 -4
  6. lumiview-0.1.0.dev3/docs/architecture.md +117 -0
  7. lumiview-0.1.0.dev3/docs/getting-started.md +157 -0
  8. lumiview-0.1.0.dev3/docs/integrations.md +120 -0
  9. lumiview-0.1.0.dev3/docs/plugins.md +153 -0
  10. lumiview-0.1.0.dev3/docs/serve.md +97 -0
  11. {lumiview-0.1.0.dev2 → lumiview-0.1.0.dev3}/examples/bridge_demo.py +3 -3
  12. {lumiview-0.1.0.dev2 → lumiview-0.1.0.dev3}/examples/custom_titlebar.py +3 -3
  13. {lumiview-0.1.0.dev2 → lumiview-0.1.0.dev3}/examples/events.py +16 -16
  14. {lumiview-0.1.0.dev2 → lumiview-0.1.0.dev3}/examples/fastapi_demo.py +3 -3
  15. {lumiview-0.1.0.dev2 → lumiview-0.1.0.dev3}/examples/hello_world.py +21 -4
  16. {lumiview-0.1.0.dev2 → lumiview-0.1.0.dev3}/examples/hello_world_sync.py +2 -2
  17. {lumiview-0.1.0.dev2 → lumiview-0.1.0.dev3}/examples/multi_window.py +5 -5
  18. {lumiview-0.1.0.dev2 → lumiview-0.1.0.dev3}/pyproject.toml +1 -1
  19. lumiview-0.1.0.dev3/python/lumiview/__init__.py +102 -0
  20. {lumiview-0.1.0.dev2 → lumiview-0.1.0.dev3}/python/lumiview/_binding.py +5 -3
  21. lumiview-0.1.0.dev3/python/lumiview/_core.pyi +934 -0
  22. lumiview-0.1.0.dev2/python/lumiview/_app.py → lumiview-0.1.0.dev3/python/lumiview/app.py +225 -60
  23. lumiview-0.1.0.dev2/python/lumiview/_bridge.py → lumiview-0.1.0.dev3/python/lumiview/bridge.py +45 -10
  24. lumiview-0.1.0.dev3/python/lumiview/events.py +526 -0
  25. {lumiview-0.1.0.dev2 → lumiview-0.1.0.dev3}/python/lumiview/plugins/window_controls.py +26 -7
  26. lumiview-0.1.0.dev2/python/lumiview/_scope.py → lumiview-0.1.0.dev3/python/lumiview/scope.py +88 -87
  27. {lumiview-0.1.0.dev2 → lumiview-0.1.0.dev3}/python/lumiview/serve/asgi.py +1 -1
  28. {lumiview-0.1.0.dev2 → lumiview-0.1.0.dev3}/python/lumiview/serve/base.py +6 -4
  29. lumiview-0.1.0.dev2/python/lumiview/_task.py → lumiview-0.1.0.dev3/python/lumiview/task.py +15 -8
  30. lumiview-0.1.0.dev3/python/lumiview/utils.py +76 -0
  31. lumiview-0.1.0.dev3/python/lumiview/window.py +1938 -0
  32. lumiview-0.1.0.dev3/src/event_loop.rs +318 -0
  33. lumiview-0.1.0.dev3/src/events.rs +1073 -0
  34. {lumiview-0.1.0.dev2 → lumiview-0.1.0.dev3}/src/lib.rs +38 -1
  35. lumiview-0.1.0.dev3/src/monitor.rs +92 -0
  36. {lumiview-0.1.0.dev2 → lumiview-0.1.0.dev3}/src/types.rs +247 -9
  37. lumiview-0.1.0.dev3/src/window.rs +894 -0
  38. lumiview-0.1.0.dev2/python/lumiview/__init__.py +0 -143
  39. lumiview-0.1.0.dev2/python/lumiview/_core.pyi +0 -501
  40. lumiview-0.1.0.dev2/python/lumiview/_events.py +0 -59
  41. lumiview-0.1.0.dev2/python/lumiview/_window.py +0 -1130
  42. lumiview-0.1.0.dev2/python/lumiview/utils.py +0 -43
  43. lumiview-0.1.0.dev2/src/event_loop.rs +0 -245
  44. lumiview-0.1.0.dev2/src/events.rs +0 -553
  45. lumiview-0.1.0.dev2/src/window.rs +0 -556
  46. {lumiview-0.1.0.dev2 → lumiview-0.1.0.dev3}/.github/workflows/publish-to-pypi.yml +0 -0
  47. {lumiview-0.1.0.dev2 → lumiview-0.1.0.dev3}/.gitignore +0 -0
  48. {lumiview-0.1.0.dev2 → lumiview-0.1.0.dev3}/CONTRIBUTING.md +0 -0
  49. {lumiview-0.1.0.dev2 → lumiview-0.1.0.dev3}/LICENSE +0 -0
  50. {lumiview-0.1.0.dev2 → lumiview-0.1.0.dev3}/python/lumiview/plugins/__init__.py +0 -0
  51. {lumiview-0.1.0.dev2 → lumiview-0.1.0.dev3}/python/lumiview/serve/__init__.py +0 -0
  52. {lumiview-0.1.0.dev2 → lumiview-0.1.0.dev3}/python/lumiview/serve/static.py +0 -0
  53. {lumiview-0.1.0.dev2 → lumiview-0.1.0.dev3}/python/lumiview/serve/wsgi.py +0 -0
@@ -804,7 +804,7 @@ checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
804
804
 
805
805
  [[package]]
806
806
  name = "lumiview"
807
- version = "0.1.0-dev2"
807
+ version = "0.1.0-dev3"
808
808
  dependencies = [
809
809
  "gtk",
810
810
  "pyo3",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "lumiview"
3
- version = "0.1.0-dev2"
3
+ version = "0.1.0-dev3"
4
4
  edition = "2021"
5
5
  description = "Python binding for tao — cross-platform window management."
6
6
  readme = "README.md"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lumiview
3
- Version: 0.1.0.dev2
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] Hook event system (AppHookEvent / WindowHookEvent)
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] Hook event system (AppHookEvent / WindowHookEvent)
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] 钩子事件系统 (AppHookEvent / WindowHookEvent)
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
+ ```