- 重构模块使用新的代码风格 - 老代码 (src 目录下的代码) 继续使用之前的代码风格 通过在需要不同代码风格的目录下放对应 .clang-format 配置文件实现
- ZDWM 最小核心草案
- 目标
- 最小核心子系统
- ID 分配策略
- 固定集合约束
- 非核心但常见的服务
- 运行时装配与所有权
- 启动与初始化约定
- 显示器热插拔策略
- Workspace-Output 配置
- 策略配置
- 边框约定
- 交互态
- 核心外服务接入约定
- 配置集成约定
- 对象关系
- 核心不变量
- Generation 字段
- 主执行流程
- 文件说明
- 下一步实施顺序
ZDWM 最小核心草案
这个目录放的是一套头文件级别的最小核心 API 草案,目标是先把窗口管理器最核心的对象关系、子系统边界和主执行流程固定下来,再决定具体实现。
目标
- 保持核心技术栈简洁,不引入脚本运行时、序列化协议和多进程插件宿主。
- 先把最小核心收敛到可以管理窗口、计算布局、处理输入、提交后端副作用。
- 将渲染、状态栏、配置、插件都放在核心之外,作为后续服务挂接。
最小核心子系统
runtime
负责主循环、模块装配和固定执行顺序。
backend
负责平台交互。它把平台原始事件转成 wm_event_t ,执行 wm_effect_t ,
并在需要时根据固定的键盘/鼠标绑定表安装平台侧被动抓键 / 抓按钮。
像 X11 的 _NET_ACTIVE_WINDOW 这类 ClientMessage 也应在这里先归一化成
request event,而不是直接改 core 状态。
state
负责保存窗口、工作区、输出和全局堆叠顺序,是唯一真状态。
当前头文件级草案先直接展示结构体字段,便于讨论;
实现阶段再收敛为不透明结构体也不迟。
窗口元数据(title/app_id/class/instance)直接存储在 wm_window_t 中。
窗口 hint / capability(如 urgent / fixed_size / skip_taskbar=)也直接存储在
=wm_window_t 中。
工作区名称存储在 wm_workspace_t 中。
policy
负责把事件路由为命令,并把命令应用到状态。
WM_EVENT_KEY_PRESS / WM_EVENT_POINTER_BUTTON_PRESS 的路由都使用 bootstrap
固定下来的输入绑定表;像 WM_EVENT_POINTER_ENTER 这种不经绑定的输入事实,
则由 policy 配置决定是否转换为 =FOCUS_WINDOW=。
layout
负责为某个输出上当前显示的工作区中的平铺窗口计算外框几何。
ID 分配策略
最小核心采用"运行时固定,启动时配置"的 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)
/ 动态实体:线性查找(简单且足够) const wm_window_t *win = wm_state_window_get(state, window_id); / O(n) ```
**设计优势**:
- 灵活性:workspace 数量由配置决定,不硬编码
- 性能:半静态实体零开销访问,动态实体简单线性查找
- 简单性:无需复杂的映射表,代码更清晰
固定集合约束
以下集合一经 bootstrap 确定,运行时不再增删或重建:
outputsworkspaceslayout registry- 每个 workspace 的
available_layouts key binding tablepointer binding table
运行时允许变化的只是:
- 某个 output 当前显示的
current_workspace_id - 某个 workspace 当前选中的
layout_id - 窗口集合及窗口运行态
非核心但常见的服务
这些服务故意不放进最小核心:
render
状态栏、标题和其他装饰绘制。窗口边框不是独立服务,而是
WM_EFFECT_CONFIGURE_WINDOW 的一部分。
status
网速、内存、CPU、音量、时间等信息采集与聚合。
config
默认配置和用户覆盖。
plugin
动态扩展加载。
运行时装配与所有权
为了避免再次出现当前全局 wm 那样的大对象耦合,最小核心里的所有权再收紧一层:
runtime拥有state、=plan= 、=command_buffer= 和layout_registry。 其中 workspace/output/layout registry 集合在 init 完成后保持固定。runtime也拥有policy config、键盘/鼠标绑定表视图、全局边框配置、 交互态和服务注册表。backend只负责平台事件翻译和平台副作用,不拥有控制状态,不直接拼 bar。- 窗口元数据(title/app_id/class/instance)直接存储在
wm_window_t中,与控制状态一同管理。 - 工作区名称存储在
wm_workspace_t中,与控制状态一同管理。
启动与初始化约定
最小核心的初始化流程:
-
wm_runtime_init()接收一份 bootstrap 描述,包含:- 工作区数量和初始配置(由用户配置决定)
- 布局算法注册(由代码或配置决定)
- 策略配置(focus_raises 等)
- 键盘绑定表(规范化的
keysym + modifiers -> wm_command_t模板) - 鼠标按键绑定表(规范化的
button + modifiers + target -> wm_command_t模板) - 全局统一边框宽度
- 全局边框颜色集(普通/焦点)
- 初始命令(如启动时扫描到的窗口)
- 启动时扫描到的已有窗口,不直接塞进
wm_state_t,而是翻译成一批MANAGE_WINDOW命令进入 runtime。 - 若 backend 提供
set_keybindings()/set_pointer_bindings(),runtime 在 init 时把 bootstrap 里的绑定表同步给 backend,供平台侧建立被动抓键 / 抓按钮。 - 工作区在启动时根据配置分配 N 个,运行时不再增删。
- 输出在启动时扫描并分配对应数量的数组,运行时不再增删。
- layout registry 在启动时注册完成,运行时不再增删。
显示器热插拔策略
当检测到显示器配置变化(新增/删除/几何改变)时:
- 后端检测到 RandR/Output 变化,使
backend.next_event()返回WM_BACKEND_NEXT_RESTART_REQUIRED - Runtime 退出主循环,清理资源
- 由启动脚本(systemd/xinitrc)自动重启 WM
- WM 重启后重新扫描显示器配置,从干净状态启动
注意:
- core 不处理
output changed/removed这类增量事件 - core 内不存在运行时增删 output/workspace/layout 集合的命令
**优势**:
- ✅ 逻辑简单,无需复杂的运行时重配置
- ✅ 状态一致,避免增量更新的边界情况
- ✅ 实现可靠,从干净状态启动
**代价**:
- 窗口会短暂闪烁(~1-2 秒)
- 但相比复杂的热插拔逻辑,这是合理的工程权衡
Workspace-Output 配置
工作区配置可以在启动阶段根据输出数量生成;一旦 bootstrap 完成,归属关系保持固定:
配置方式 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 ```
**可见性规则**:
- 普通非
sticky窗口是否进入可见集,先看它的workspace是否正被归属 output 选中 - 多输出下,窗口最终是否出现在某个 output 上,还取决于
frame_rect是否与该 output 的几何相交 sticky窗口只放宽 workspace 可见性,不提供 output 复制语义;它同样按最终矩形与 output 的相交关系决定可见性- 切换 workspace 只改变归属 output 的
current_workspace_id,不改变 workspace/output 的固定绑定
策略配置
前一版草案里有几处“建议作为可选策略”的描述,现在收敛为显式 wm_policy_config_t :
focus_raises
焦点切换时是否隐式执行 raise。
pointer_enter_focuses_window
收到 WM_EVENT_POINTER_ENTER 时是否切换焦点(focus-follows-mouse)。
sticky_windows_participate_in_direction_focus
sticky 窗口是否参与方向焦点搜索。
manage_sets_focus
新受管窗口是否默认夺取所属 workspace 的焦点。
switch_workspace_restores_last_focus
切换工作区时是否恢复该 workspace 的最近焦点。
minimize_clears_focus
最小化当前焦点窗口时是否清空或重选该 workspace 焦点。
边框约定
当前最小核心只保留窗口边框,不支持标题栏和其他窗口装饰。
wm_window_t不存储border_width或border_color;边框样式不是窗口真状态。wm_runtime_bootstrap_t.border_width提供全局统一边框宽度。wm_runtime_bootstrap_t.border_palette提供全局边框颜色集(普通/焦点)。float_rect和frame_rect都表示“包含边框后的外框矩形”。-
runtime在提交WM_EFFECT_CONFIGURE_WINDOW时解析有效边框宽度:WM_GEOMETRY_FULLSCREEN: 边框宽度为 0- 其余模式 : 使用全局边框宽度
-
runtime在提交WM_EFFECT_CONFIGURE_WINDOW时解析有效边框颜色:- 当前 workspace 的 focused window :
border_palette.focused_rgba - 其余窗口 :
border_palette.normal_rgba - 若有效边框宽度为 0,则 backend 可忽略颜色字段
- 当前 workspace 的 focused window :
交互态
move/resize 这类拖拽交互不属于 wm_state_t 真状态,但也不能继续散落在平台分支里。runtime 应维护一个单一交互会话:
- 空闲
- 正在移动 floating 窗口
- 正在调整 floating 窗口大小
交互会话至少记录目标窗口、起始指针位置、起始窗口矩形和起始 output,供 policy.route_event() 生成连续的 move/resize 命令。
核心外服务接入约定
既然 render/status/bar 被明确放在核心外,就需要一条稳定的接入边界:
WM_EFFECT_RENDER_OUTPUT不是平台副作用,而是 runtime 发给服务层的失效通知。backend.apply_effect()只接收 map/unmap/configure/focus/restack 这类平台 effect。runtime在处理完 plan 后,把 render 和 metadata 相关事件广播给已注册服务。- 服务只读
state/meta/descriptor快照,不直接改wm_state_t。
配置集成约定
config 仍然在核心外,但它的输出必须改成 core-native 数据,而不是旧的 client_t/tag_t 钩子:
- 配置加载结果应产出 workspace 描述、policy 配置、layout 注册、规则表、
键盘/鼠标绑定到 command 的映射和服务设置。
以及全局统一边框宽度和边框颜色集。
这里的输入绑定应直接收敛为
wm_key_binding_table_t/wm_pointer_binding_table_t。 - 配置层不得直接持有
client_t、=tag_t= 或直接修改wm_state_t。 - 配置重载的边界是“重建 bootstrap 和服务配置,再显式应用到 runtime”,而不是任意时刻从外部写内存。
补充约定:
- 鼠标绑定在最小核心里只区分
root/window两类目标。 - 状态栏、标签栏、任务栏等点击区域属于核心外服务,不进入 core 的 pointer binding 表。
对象关系
这套草案里的关系固定为:
window -> workspace
每个窗口只属于一个工作区。
workspace -> output
每个工作区固定归属某个输出,运行时不再改变。
output -> current_workspace
每个输出同一时刻只显示一个当前工作区(从归属的工作区中选择)。
window visible on output
这是派生结果,不是独立存储的真相。
stack_order[]
这是全局 z-order 的唯一真相,从下到上排列。
window metadata
窗口标题、类名等字符串信息直接存储在 wm_window_t 中。
核心算法(layout/policy)不应依赖这些字段,仅用于匹配规则和服务层展示。
workspace name
工作区名称直接存储在 wm_workspace_t 中,用于状态栏显示。
核心算法不应依赖此字段。
workspace layouts
每个 workspace 维护一个可用布局列表(=available_layouts=),在启动时根据配置分配,之后固定不变。
workspace 的 layout_id 字段指向当前活动的布局(列表中的一个),运行时允许切换。
使用 CYCLE_LAYOUT / SET_LAYOUT 命令切换当前活动布局。
window rectangles
frame_rect 是窗口当前最终外框矩形;=float_rect= 是 floating 模式下记忆的外框矩形。
这两个矩形都包含边框。
核心不变量
- 每个窗口必须且只能属于一个工作区。
- 每个工作区必须且只能固定归属一个输出,运行时不再改变。
- 每个输出必须且只能显示一个当前工作区(从归属的工作区中选择)。
- 可见性由窗口归属、工作区归属、输出当前工作区和最小化状态共同推导。
geometry_mode只负责几何模式,不负责 z-order;其中WM_GEOMETRY_MINIMIZED统一覆盖 X11IconicState这类“最小化/图标化”状态。stack_order[]只负责 z-order,不负责几何模式。floating是二值状态:窗口要么由 layout 决定基础几何,要么由float_rect决定基础几何。sticky => floating;=sticky= 只放宽 workspace 可见性,不改变窗口归属工作区。- floating / sticky 窗口都可以跨多个输出显示,但归属工作区仍然只有一个。
- 多输出下 floating / sticky 窗口是否迁移工作区,由锚点和跨输出策略决定,不由覆盖面积决定。
- layout 的
symbol字段用于状态栏显示,应为 1-2 个字符的简短标识符(如 "T", "M");=name= 应保持稳定、可读(如 "tile", "monocle")。 - 窗口标题、类名等元数据存储在
wm_window_t中,与控制状态一同管理。 - 工作区名称存储在
wm_workspace_t中,核心算法不应依赖此字段。 - 每个 workspace 的可用布局列表在启动时分配,之后固定不变;运行时只允许通过
CYCLE_LAYOUT或SET_LAYOUT命令切换当前活动布局。 frame_rect和float_rect都表示包含边框后的外框矩形。border_width/border_color都不是wm_window_t真状态;它们由 runtime 从全局配置、几何模式和焦点关系推导。
Generation 字段
generation 是版本号,不是业务关系字段。
当前草案只建议保留:
wm_state_t.generation
每次成功提交状态变更后递增,用于缓存失效、增量重算和调试。
以后如果有必要,再给渲染缓存或状态服务增加更细粒度的版本号。
主执行流程
runtime 负责固定主循环顺序:
backend.next_event()- runtime 先处理窗口元数据更新这类辅助事件,并直接更新
wm_window_t中的元数据字段 policy.route_event()policy.apply_command()- 根据
dirty_flags决定是否重新运行layout、解析最终外框矩形,并补全configureeffect 的边框宽度和边框颜色 - 将平台 effect 发送给
backend - 将 render 失效通知发送给服务层
backend.flush()
若后端发现显示器配置变化,不进入 output 增量更新分支,而是让 backend.next_event()
返回 WM_BACKEND_NEXT_RESTART_REQUIRED ,由外层退出并整机重启。
文件说明
wm_types.h
通用标量类型、矩形、窗口几何模式和策略枚举。
wm_state.h
全局状态容器;当前草案同时保留公开字段和统一访问 API,便于先把语义定清楚。
wm_event.h
统一后的 runtime 输入事件;不承载 stop/restart 这类控制信号。
像 _NET_ACTIVE_WINDOW 这类平台协议请求,也应在这一层归一化为独立的
request event。
客户端 property / hint 变化(如 urgent / fixed_size / =skip_taskbar=)也应在
这一层归一化为辅助事件,而不是伪装成 configure request。
wm_binding.h
WM_EVENT_KEY_PRESS / WM_EVENT_POINTER_BUTTON_PRESS 使用的输入绑定表和匹配规则。
wm_command.h
强类型语义命令。
命令语义补充见 WM_COMMAND_RULES.org ;
sticky 多显示器与跨屏行为补充见 WM_STICKY_WINDOW_RULES.org 。
wm_plan.h
脏标记和后端副作用。
wm_layout.h
布局注册表、布局输入和布局输出。
wm_backend.h
后端子系统接口。
wm_policy_config.h
显式策略开关,替代散落在文档正文里的"可选策略"。
wm_policy.h
事件路由和命令应用接口。
wm_runtime.h
运行时上下文和生命周期接口。
wm_service.h
核心外服务的注册和事件订阅接口。
关键命令的前置条件、状态转移、可见性和副作用规则。
sticky 窗口在 workspace、taskbar、多显示器和跨 output 拖动下的补充规则。
wm_policy_apply_command() 的伪代码骨架和实现顺序建议。
Workspace-Output 配置示例和最佳实践。
下一步实施顺序
当前阶段不建议继续扩展抽象,应该开始把最小核心草案落成可运行骨架。推荐顺序如下:
- 在
src/core/下建立最小骨架,至少放wm_types/binding/state/plan/layout/policy/policy_config/runtime/service的头文件和空实现,先不要替换现有src/逻辑。 - 优先实现
state + query + plan这一层,把动态数组管理、=find_*= 查询函数和stack_order[]操作补齐。 - 再实现
wm_policy_apply_command()的一小批核心命令:MANAGE_WINDOWUNMANAGE_WINDOWSWITCH_WORKSPACESET_LAYOUTCYCLE_LAYOUTTOGGLE_FLOATINGSET_MAXIMIZEDMOVE_FLOATING_WINDOWBEGIN_MOVE_FLOATING_INTERACTIONBEGIN_RESIZE_FLOATING_INTERACTION - 给上述命令建立一个无 X11 依赖的测试入口,直接构造
wm_command_t,检查wm_state_t和wm_plan_t的变化是否符合文档规则。 - 等
event -> command -> state -> plan/effect这条链路跑通后,再编写一层很薄的 adapter,把现有事件翻译为wm_event_t,并把 effect 翻译回当前 X11 操作。 其中 fullscreen / maximize / minimize 这类平台请求,应先归一化为WM_EVENT_WINDOW_STATE_REQUEST,再由路由层翻译成SET_*命令。 status/render/bar暂时不要先动。它们属于核心外服务,应在最小核心链路稳定后再迁移。