完善最小核心架构设计和数据结构简化

主要变更:

- 数据结构简化
  - 合并窗口元数据到 wm_window_t(删除 wm_window_meta.h)
  - 合并工作区名称到 wm_workspace_t(删除 wm_workspace_desc.h)
  - 简化布局标识符,只保留 name 字段
  - 删除 window_map,采用直接索引+线性查找

- 架构设计完善
  - 确定 window→workspace→output 固定归属关系
  - 制定 ID 分配策略(半静态实体直接索引,动态实体线性查找)
  - 确定显示器热插拔策略(检测到变化时重启 WM)
  - 添加工作区配置示例和多种配置方式

- 新增模块
  - wm_policy_config.h:策略配置开关
  - wm_service.h:核心外服务注册和事件订阅
  - WM_EVENT_ROUTING.org:事件路由详细文档
  - WORKSPACE_CONFIG_EXAMPLES.org:工作区配置示例
  - MINIMAL_CORE_WITH_EXTENSIONS.org:设计对比文档
  - config_system.org:配置系统设计

- 文档完善
  - 大幅扩展 README.org,补充 ID 分配、热插拔、配置集成等说明
  - 更新 WM_COMMAND_RULES.org,补充命令规则细节
  - 更新 WM_POLICY_APPLY_COMMAND_SKELETON.org,完善实现骨架
This commit is contained in:
2026-03-11 05:28:39 +08:00
parent f106857cd8
commit 329943ab3e
18 changed files with 2335 additions and 169 deletions

View File

@@ -18,6 +18,9 @@
3. =state=
负责保存窗口、工作区、输出和全局堆叠顺序,是唯一真状态。
采用不透明结构体设计,内部实现完全封装。
窗口元数据title/app_id/class/instance直接存储在 =wm_window_t= 中。
工作区名称存储在 =wm_workspace_t= 中。
4. =policy=
负责把事件路由为命令,并把命令应用到状态。
@@ -25,11 +28,63 @@
5. =layout=
负责为某个输出上当前显示的工作区中的平铺窗口计算几何。
6. =window metadata=
负责窗口标题、类名、实例名、app id 这类展示和匹配信息。
* ID 分配策略
7. =workspace descriptor=
负责 workspace 在 bar 中显示时使用的名字和文本或图标符号。
最小核心采用"运行时固定,启动时配置"的 ID 策略:
**半静态实体(启动时分配,运行时固定)**
- =workspace= : 启动时根据配置分配 N 个(如 10 个ID = 数组索引0-N-1
- =output= : 启动时根据扫描到的显示器数量分配ID = 数组索引
- =layout= : 启动时根据注册的布局算法分配ID = 数组索引
这些实体在启动时一次性分配,运行时不再增删。
访问时直接用索引,零开销查找,无需映射表。
```c
// 半静态实体:直接访问(零开销)
wm_workspace_t *ws = &state->workspaces[workspace_id]; // O(1)
wm_output_t *output = &state->outputs[output_id]; // O(1)
```
**动态实体(运行时增删)**
- =window= : 动态数组ID 由后端生成(如 X11 的 xcb_window_t
查找时线性搜索 O(n),对于几十到几百个窗口性能足够。
(后续如成为性能热点,可改用 hashmap 优化到 O(1)
```c
// 半静态实体:直接访问(零开销)
wm_workspace_t *ws = &state->workspaces[workspace_id]; // O(1)
wm_output_t *output = &state->outputs[output_id]; // O(1)
// 动态实体:线性查找(简单且足够)
wm_window_t *win = wm_state_find_window(state, window_id); // O(n)
```
**设计优势**
- 灵活性workspace 数量由配置决定,不硬编码
- 性能:半静态实体零开销访问,动态实体简单线性查找
- 简单性:无需复杂的映射表,代码更清晰
* 热插拔策略
**显示器变化时自动重启 WM**
当检测到显示器配置变化(新增/删除/几何改变)时,最简单且可靠的方案是重启整个窗口管理器。
理由:
1. 逻辑简单:无需处理复杂的运行时重配置
2. 状态一致:避免重配置后的不一致状态
3. 实现成本低:不需要增量更新逻辑
4. 可靠性高:从干净状态启动,避免边界情况
实现方式:
- 后端检测到 RandR/Output 变化时,退出主循环
- 由启动脚本(如 systemd user service 或 .xinitrc自动重启 WM
- WM 重启前保存必要状态(如当前工作区),启动后恢复
对用户影响:
- 显示器拔插时窗口会短暂闪烁(~1-2 秒)
- 但比复杂的增量逻辑更可靠,符合"简单优先"原则
* 非核心但常见的服务
@@ -47,6 +102,145 @@
4. =plugin=
动态扩展加载。
* 运行时装配与所有权
为了避免再次出现当前全局 =wm= 那样的大对象耦合,最小核心里的所有权再收紧一层:
1. =runtime= 拥有 =state==plan==command_buffer==layout_registry=
2. =runtime= 也拥有 =policy config= 、交互态和服务注册表。
3. =backend= 只负责平台事件翻译和平台副作用,不拥有控制状态,不直接拼 bar。
4. 窗口元数据title/app_id/class/instance直接存储在 =wm_window_t= 中,与控制状态一同管理。
5. 工作区名称存储在 =wm_workspace_t= 中,与控制状态一同管理。
* 启动与初始化约定
最小核心的初始化流程:
1. =wm_runtime_init()= 接收一份 bootstrap 描述,包含:
- 工作区数量和初始配置(由用户配置决定)
- 布局算法注册(由代码或配置决定)
- 策略配置focus_raises 等)
- 初始命令(如启动时扫描到的窗口)
2. 启动时扫描到的已有窗口,不直接塞进 =wm_state_t= ,而是翻译成一批 =MANAGE_WINDOW= 命令进入 runtime。
3. 工作区在启动时根据配置分配 N 个,运行时不再增删。
4. 输出在启动时扫描并分配对应数量的数组,运行时不再增删。
* 显示器热插拔策略
当检测到显示器配置变化(新增/删除/几何改变)时:
1. 后端检测到 RandR/Output 变化,发送 =WM_EVENT_QUIT= 给 runtime
2. Runtime 退出主循环,清理资源
3. 由启动脚本systemd/xinitrc自动重启 WM
4. WM 重启后重新扫描显示器配置,从干净状态启动
**优势**
- ✅ 逻辑简单,无需复杂的运行时重配置
- ✅ 状态一致,避免增量更新的边界情况
- ✅ 实现可靠,从干净状态启动
**代价**
- 窗口会短暂闪烁(~1-2 秒)
- 但相比复杂的热插拔逻辑,这是合理的工程权衡
* Workspace-Output 配置
工作区在启动时根据输出数量动态配置,固定归属到特定输出:
**配置方式 1手动指定**
```lua
workspaces = {
{ id = 1, name = "web", output_id = 0 }, -- DP-1 的 workspace
{ id = 2, name = "code", output_id = 0 },
{ id = 3, name = "chat", output_id = 1 }, -- DP-2 的 workspace
{ id = 4, name = "media", output_id = 1 },
}
```
**配置方式 2规则式**
```lua
-- 每个 output 自动分配 N 个 workspace
workspace_distribution = {
{ output_id = 0, count = 5 }, -- DP-1: workspace 1-5
{ output_id = 1, count = 5 }, -- DP-2: workspace 6-10
}
```
**配置方式 3脚本式**
```lua
function configure_workspaces(outputs)
local workspaces = {}
local ws_id = 1
for _, output in ipairs(outputs) do
for i = 1, 3 do -- 每个 output 3 个 workspace
table.insert(workspaces, {
id = ws_id,
name = tostring(ws_id),
output_id = output.id
})
ws_id = ws_id + 1
end
end
return workspaces
end
```
**可见性规则**
- 窗口仅在归属的 output 上可见
- 窗口可见 iff窗口的 workspace 是该 output 的当前显示 workspace
- 切换 workspace 只影响归属的 output
* 策略配置
前一版草案里有几处“建议作为可选策略”的描述,现在收敛为显式 =wm_policy_config_t=
1. =focus_raises=
焦点切换时是否隐式执行 raise。
2. =sticky_windows_participate_in_direction_focus=
sticky 窗口是否参与方向焦点搜索。
3. =manage_sets_focus=
新受管窗口是否默认夺取所属 workspace 的焦点。
4. =switch_workspace_restores_last_focus=
切换工作区时是否恢复该 workspace 的最近焦点。
5. =minimize_clears_focus=
最小化当前焦点窗口时是否清空或重选该 workspace 焦点。
* 交互态
move/resize 这类拖拽交互不属于 =wm_state_t= 真状态但也不能继续散落在平台分支里。runtime 应维护一个单一交互会话:
1. 空闲
2. 正在移动 floating 窗口
3. 正在调整 floating 窗口大小
交互会话至少记录目标窗口、起始指针位置、起始窗口矩形和起始 output供 =policy.route_event()= 生成连续的 move/resize 命令。
* 核心外服务接入约定
既然 =render/status/bar= 被明确放在核心外,就需要一条稳定的接入边界:
1. =WM_EFFECT_RENDER_OUTPUT= 不是平台副作用,而是 runtime 发给服务层的失效通知。
2. =backend.apply_effect()= 只接收 map/unmap/configure/focus/restack 这类平台 effect。
3. =runtime= 在处理完 plan 后,把 render 和 metadata 相关事件广播给已注册服务。
4. 服务只读 =state/meta/descriptor= 快照,不直接改 =wm_state_t= 。
* 配置集成约定
=config= 仍然在核心外,但它的输出必须改成 core-native 数据,而不是旧的 =client_t/tag_t= 钩子:
1. 配置加载结果应产出 workspace 描述、policy 配置、layout 注册、规则表、快捷键到 command 的映射和服务设置。
2. 配置层不得直接持有 =client_t= 、=tag_t= 或直接修改 =wm_state_t= 。
3. 配置重载的边界是“重建 bootstrap 和服务配置,再显式应用到 runtime”而不是任意时刻从外部写内存。
* 对象关系
这套草案里的关系固定为:
@@ -54,35 +248,41 @@
1. =window -> workspace=
每个窗口只属于一个工作区。
2. =output -> current_workspace=
每个输出同一时刻只显示一个当前工作区。
2. =workspace -> output=
每个工作区固定归属某个输出,运行时不再改变
3. =window visible on output=
3. =output -> current_workspace=
每个输出同一时刻只显示一个当前工作区(从归属的工作区中选择)。
4. =window visible on output=
这是派生结果,不是独立存储的真相。
4. =stack_order[]=
5. =stack_order[]=
这是全局 z-order 的唯一真相,从下到上排列。
5. =window metadata=
窗口标题等字符串信息不放进最小控制状态,而是作为独立元数据表存在
6. =window metadata=
窗口标题、类名等字符串信息直接存储在 =wm_window_t= 中
核心算法layout/policy不应依赖这些字段仅用于匹配规则和服务层展示。
6. =workspace descriptor=
workspace 在 bar 中显示时使用的名字和符号不放进运行时控制状态,而是作为独立描述表存在
7. =workspace name=
工作区名称直接存储在 =wm_workspace_t= 中,用于状态栏显示
核心算法不应依赖此字段。
* 核心不变量
1. 每个窗口必须且只能属于一个工作区。
2. 每个输出必须且只能显示一个当前工作区
3. 可见性由窗口归属、输出当前工作区和最小化状态共同推导
4. =geometry_mode= 只负责几何模式,不负责 z-order
5. =stack_order[]=负责 z-order,不负责几何模式
6. =floating= 是二值状态:窗口要么由 layout 决定基础几何,要么由 =float_rect= 决定基础几何
7. =sticky= 只影响可见性,不改变窗口归属工作区
8. floating 窗口可以跨多个输出显示,但归属工作区仍然只有一个
9. 多输出下 floating 窗口是否迁移工作区,由锚点和跨输出策略决定,不由覆盖面积决定
10. layout 的名字和 bar 符号属于布局注册表描述信息,不属于工作区运行状态
11. 窗口标题和类名属于窗口元数据,不属于窗口控制状态
12. workspace 的显示名称和 bar 符号属于 workspace 描述信息,不属于工作区运行状态
2. 每个工作区必须且只能固定归属一个输出,运行时不再改变
3. 每个输出必须且只能显示一个当前工作区(从归属的工作区中选择)
4. 可见性由窗口归属、工作区归属、输出当前工作区和最小化状态共同推导
5. =geometry_mode= 只负责几何模式,不负责 z-order。
6. =stack_order[]= 只负责 z-order不负责几何模式
7. =floating= 是二值状态:窗口要么由 layout 决定基础几何,要么由 =float_rect= 决定基础几何
8. =sticky= 只影响可见性,不改变窗口归属工作区
9. floating 窗口可以跨多个输出显示,但归属工作区仍然只有一个
10. 多输出下 floating 窗口是否迁移工作区,由锚点和跨输出策略决定,不由覆盖面积决定
11. layout 的 name 字段同时用于状态栏显示,应为 1-2 个字符的简短标识符(如 "T", "M"
12. 窗口标题、类名等元数据存储在 =wm_window_t= 中,与控制状态一同管理
13. 工作区名称存储在 =wm_workspace_t= 中,核心算法不应依赖此字段。
* Generation 字段
@@ -100,11 +300,13 @@ workspace 在 bar 中显示时使用的名字和符号不放进运行时控制
=runtime= 负责固定主循环顺序:
1. =backend.next_event()=
2. =policy.route_event()=
3. =policy.apply_command()=
4. 根据 =dirty_flags= 决定是否重新运行 =layout=
5. 逐条执行 =wm_effect_t=
6. 可选的渲染刷新
2. runtime 先处理窗口元数据更新这类辅助事件,并直接更新 =wm_window_t= 中的元数据字段
3. =policy.route_event()=
4. =policy.apply_command()=
5. 根据 =dirty_flags= 决定是否重新运行 =layout= 并解析最终矩形
6. 将平台 effect 发送给 =backend=
7. 将 render 失效通知发送给服务层
8. =backend.flush()=
* 文件说明
@@ -112,7 +314,7 @@ workspace 在 bar 中显示时使用的名字和符号不放进运行时控制
通用标量类型、矩形、窗口几何模式和策略枚举。
2. =wm_state.h=
窗口、工作区、输出和全局状态结构,以及查询接口
全局状态容器,完全封装的内部实现,提供统一的访问 API
3. =wm_event.h=
统一后的运行时事件。
@@ -127,32 +329,35 @@ workspace 在 bar 中显示时使用的名字和符号不放进运行时控制
6. =wm_layout.h=
布局注册表、布局输入和布局输出。
7. =wm_window_meta.h=
窗口标题、类名、实例名和 app id 的元数据表。
8. =wm_workspace_desc.h=
workspace 的显示名称、文本符号和图标符号描述表。
9. =wm_backend.h=
7. =wm_backend.h=
后端子系统接口。
10. =wm_policy.h=
8. =wm_policy_config.h=
显式策略开关,替代散落在文档正文里的"可选策略"。
9. =wm_policy.h=
事件路由和命令应用接口。
11. =wm_runtime.h=
10. =wm_runtime.h=
运行时上下文和生命周期接口。
11. =wm_service.h=
核心外服务的注册和事件订阅接口。
12. [[file:WM_COMMAND_RULES.org][WM_COMMAND_RULES.org]]
关键命令的前置条件、状态转移、可见性和副作用规则。
13. [[file:WM_POLICY_APPLY_COMMAND_SKELETON.org][WM_POLICY_APPLY_COMMAND_SKELETON.org]]
=wm_policy_apply_command()= 的伪代码骨架和实现顺序建议。
14. [[file:WORKSPACE_CONFIG_EXAMPLES.org][WORKSPACE_CONFIG_EXAMPLES.org]]
Workspace-Output 配置示例和最佳实践。
* 下一步实施顺序
当前阶段不建议继续扩展抽象,应该开始把最小核心草案落成可运行骨架。推荐顺序如下:
1.=src/core/= 下建立最小骨架,=wm_types/state/plan/layout/policy/runtime= 的头文件和空实现,先不要替换现有 =src/= 逻辑。
1. 在 =src/core/= 下建立最小骨架,至少放 =wm_types/state/plan/layout/policy/policy_config/runtime/service= 的头文件和空实现,先不要替换现有 =src/= 逻辑。
2. 优先实现 =state + query + plan= 这一层,把动态数组管理、=find_*= 查询函数和 =stack_order[]= 操作补齐。