Files
zdwm/docs/min_core_draft/README.org

19 KiB
Raw Blame History

ZDWM 最小核心草案

这个目录放的是一套头文件级别的最小核心 API 草案,目标是先把窗口管理器最核心的对象关系、子系统边界和主执行流程固定下来,再决定具体实现。

目标

  1. 保持核心技术栈简洁,不引入脚本运行时、序列化协议和多进程插件宿主。
  2. 先把最小核心收敛到可以管理窗口、计算布局、处理输入、提交后端副作用。
  3. 将渲染、状态栏、配置、插件都放在核心之外,作为后续服务挂接。

最小核心子系统

  1. runtime

负责主循环、模块装配和固定执行顺序。

  1. backend

负责平台交互。它把平台原始事件转成 wm_event_t ,执行 wm_effect_t 并在需要时根据固定的键盘/鼠标绑定表安装平台侧被动抓键 / 抓按钮。 像 X11 的 _NET_ACTIVE_WINDOW 这类 ClientMessage 也应在这里先归一化成 request event而不是直接改 core 状态。

  1. state

负责保存窗口、工作区、输出和全局堆叠顺序,是唯一真状态。 当前头文件级草案先直接展示结构体字段,便于讨论; 实现阶段再收敛为不透明结构体也不迟。 窗口元数据title/app_id/class/instance直接存储在 wm_window_t 中。 窗口 hint / capabilityurgent / fixed_size / skip_taskbar=)也直接存储在 =wm_window_t 中。 工作区名称存储在 wm_workspace_t 中。

  1. policy

负责把事件路由为命令,并把命令应用到状态。 WM_EVENT_KEY_PRESS / WM_EVENT_POINTER_BUTTON_PRESS 的路由都使用 bootstrap 固定下来的输入绑定表。

  1. 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 确定,运行时不再增删或重建:

  1. outputs
  2. workspaces
  3. layout registry
  4. 每个 workspace 的 available_layouts
  5. key binding table
  6. pointer binding table

运行时允许变化的只是:

  1. 某个 output 当前显示的 current_workspace_id
  2. 某个 workspace 当前选中的 layout_id
  3. 窗口集合及窗口运行态

非核心但常见的服务

这些服务故意不放进最小核心:

  1. render

状态栏、标题和其他装饰绘制。窗口边框不是独立服务,而是 WM_EFFECT_CONFIGURE_WINDOW 的一部分。

  1. status

网速、内存、CPU、音量、时间等信息采集与聚合。

  1. config

默认配置和用户覆盖。

  1. plugin

动态扩展加载。

运行时装配与所有权

为了避免再次出现当前全局 wm 那样的大对象耦合,最小核心里的所有权再收紧一层:

  1. runtime 拥有 state 、=plan= 、=command_buffer= 和 layout_registry 。 其中 workspace/output/layout registry 集合在 init 完成后保持固定。
  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 等)
    • 键盘绑定表(规范化的 keysym + modifiers -> wm_command_t 模板)
    • 鼠标按键绑定表(规范化的 button + modifiers + target -> wm_command_t 模板)
    • 全局统一边框宽度
    • 全局边框颜色集(普通/焦点)
    • 初始命令(如启动时扫描到的窗口)
  2. 启动时扫描到的已有窗口,不直接塞进 wm_state_t ,而是翻译成一批 MANAGE_WINDOW 命令进入 runtime。
  3. 若 backend 提供 set_keybindings() / set_pointer_bindings() runtime 在 init 时把 bootstrap 里的绑定表同步给 backend供平台侧建立被动抓键 / 抓按钮。
  4. 工作区在启动时根据配置分配 N 个,运行时不再增删。
  5. 输出在启动时扫描并分配对应数量的数组,运行时不再增删。
  6. layout registry 在启动时注册完成,运行时不再增删。

显示器热插拔策略

当检测到显示器配置变化(新增/删除/几何改变)时:

  1. 后端检测到 RandR/Output 变化,使 backend.next_event() 返回 WM_BACKEND_NEXT_RESTART_REQUIRED
  2. Runtime 退出主循环,清理资源
  3. 由启动脚本systemd/xinitrc自动重启 WM
  4. 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

  1. focus_raises

焦点切换时是否隐式执行 raise。

  1. sticky_windows_participate_in_direction_focus

sticky 窗口是否参与方向焦点搜索。

  1. manage_sets_focus

新受管窗口是否默认夺取所属 workspace 的焦点。

  1. switch_workspace_restores_last_focus

切换工作区时是否恢复该 workspace 的最近焦点。

  1. minimize_clears_focus

最小化当前焦点窗口时是否清空或重选该 workspace 焦点。

边框约定

当前最小核心只保留窗口边框,不支持标题栏和其他窗口装饰。

  1. wm_window_t 不存储 border_widthborder_color ;边框样式不是窗口真状态。
  2. wm_runtime_bootstrap_t.border_width 提供全局统一边框宽度。
  3. wm_runtime_bootstrap_t.border_palette 提供全局边框颜色集(普通/焦点)。
  4. float_rectframe_rect 都表示“包含边框后的外框矩形”。
  5. runtime 在提交 WM_EFFECT_CONFIGURE_WINDOW 时解析有效边框宽度:

    • WM_GEOMETRY_FULLSCREEN : 边框宽度为 0
    • 其余模式 : 使用全局边框宽度
  6. runtime 在提交 WM_EFFECT_CONFIGURE_WINDOW 时解析有效边框颜色:

    • 当前 workspace 的 focused window : border_palette.focused_rgba
    • 其余窗口 : border_palette.normal_rgba
    • 若有效边框宽度为 0则 backend 可忽略颜色字段

交互态

move/resize 这类拖拽交互不属于 wm_state_t 真状态但也不能继续散落在平台分支里。runtime 应维护一个单一交互会话:

  1. 空闲
  2. 正在移动 floating 窗口
  3. 正在调整 floating 窗口大小

交互会话至少记录目标窗口、起始指针位置、起始窗口矩形和起始 outputpolicy.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 的映射和服务设置。 以及全局统一边框宽度和边框颜色集。 这里的输入绑定应直接收敛为 wm_key_binding_table_t / wm_pointer_binding_table_t
  2. 配置层不得直接持有 client_t 、=tag_t= 或直接修改 wm_state_t
  3. 配置重载的边界是“重建 bootstrap 和服务配置,再显式应用到 runtime”而不是任意时刻从外部写内存。

补充约定:

  1. 鼠标绑定在最小核心里只区分 root / window 两类目标。
  2. 状态栏、标签栏、任务栏等点击区域属于核心外服务,不进入 core 的 pointer binding 表。

对象关系

这套草案里的关系固定为:

  1. window -> workspace

每个窗口只属于一个工作区。

  1. workspace -> output

每个工作区固定归属某个输出,运行时不再改变。

  1. output -> current_workspace

每个输出同一时刻只显示一个当前工作区(从归属的工作区中选择)。

  1. window visible on output

这是派生结果,不是独立存储的真相。

  1. stack_order[]

这是全局 z-order 的唯一真相,从下到上排列。

  1. window metadata

窗口标题、类名等字符串信息直接存储在 wm_window_t 中。 核心算法layout/policy不应依赖这些字段仅用于匹配规则和服务层展示。

  1. workspace name

工作区名称直接存储在 wm_workspace_t 中,用于状态栏显示。 核心算法不应依赖此字段。

  1. workspace layouts

每个 workspace 维护一个可用布局列表(=available_layouts=),在启动时根据配置分配,之后固定不变。 workspace 的 layout_id 字段指向当前活动的布局(列表中的一个),运行时允许切换。 使用 CYCLE_LAYOUT / SET_LAYOUT 命令切换当前活动布局。

  1. window rectangles

frame_rect 是窗口当前最终外框矩形;=float_rect= 是 floating 模式下记忆的外框矩形。 这两个矩形都包含边框。

核心不变量

  1. 每个窗口必须且只能属于一个工作区。
  2. 每个工作区必须且只能固定归属一个输出,运行时不再改变。
  3. 每个输出必须且只能显示一个当前工作区(从归属的工作区中选择)。
  4. 可见性由窗口归属、工作区归属、输出当前工作区和最小化状态共同推导。
  5. geometry_mode 只负责几何模式,不负责 z-order其中 WM_GEOMETRY_MINIMIZED 统一覆盖 X11 IconicState 这类“最小化/图标化”状态。
  6. stack_order[] 只负责 z-order不负责几何模式。
  7. floating 是二值状态:窗口要么由 layout 决定基础几何,要么由 float_rect 决定基础几何。
  8. sticky => floating =sticky= 只放宽 workspace 可见性,不改变窗口归属工作区。
  9. floating / sticky 窗口都可以跨多个输出显示,但归属工作区仍然只有一个。
  10. 多输出下 floating / sticky 窗口是否迁移工作区,由锚点和跨输出策略决定,不由覆盖面积决定。
  11. layout 的 symbol 字段用于状态栏显示,应为 1-2 个字符的简短标识符(如 "T", "M"=name= 应保持稳定、可读(如 "tile", "monocle")。
  12. 窗口标题、类名等元数据存储在 wm_window_t 中,与控制状态一同管理。
  13. 工作区名称存储在 wm_workspace_t 中,核心算法不应依赖此字段。
  14. 每个 workspace 的可用布局列表在启动时分配,之后固定不变;运行时只允许通过 CYCLE_LAYOUTSET_LAYOUT 命令切换当前活动布局。
  15. frame_rectfloat_rect 都表示包含边框后的外框矩形。
  16. border_width / border_color 都不是 wm_window_t 真状态;它们由 runtime 从全局配置、几何模式和焦点关系推导。

Generation 字段

generation 是版本号,不是业务关系字段。

当前草案只建议保留:

  1. wm_state_t.generation

每次成功提交状态变更后递增,用于缓存失效、增量重算和调试。

以后如果有必要,再给渲染缓存或状态服务增加更细粒度的版本号。

主执行流程

runtime 负责固定主循环顺序:

  1. backend.next_event()
  2. runtime 先处理窗口元数据更新这类辅助事件,并直接更新 wm_window_t 中的元数据字段
  3. policy.route_event()
  4. policy.apply_command()
  5. 根据 dirty_flags 决定是否重新运行 layout 、解析最终外框矩形,并补全 configure effect 的边框宽度和边框颜色
  6. 将平台 effect 发送给 backend
  7. 将 render 失效通知发送给服务层
  8. backend.flush()

若后端发现显示器配置变化,不进入 output 增量更新分支,而是让 backend.next_event() 返回 WM_BACKEND_NEXT_RESTART_REQUIRED ,由外层退出并整机重启。

文件说明

  1. wm_types.h

通用标量类型、矩形、窗口几何模式和策略枚举。

  1. wm_state.h

全局状态容器;当前草案同时保留公开字段和统一访问 API便于先把语义定清楚。

  1. wm_event.h

统一后的 runtime 输入事件;不承载 stop/restart 这类控制信号。 像 _NET_ACTIVE_WINDOW 这类平台协议请求,也应在这一层归一化为独立的 request event。 客户端 property / hint 变化(如 urgent / fixed_size / =skip_taskbar=)也应在 这一层归一化为辅助事件,而不是伪装成 configure request。

  1. wm_binding.h

WM_EVENT_KEY_PRESS / WM_EVENT_POINTER_BUTTON_PRESS 使用的输入绑定表和匹配规则。

  1. wm_command.h

强类型语义命令。 命令语义补充见 WM_COMMAND_RULES.org sticky 多显示器与跨屏行为补充见 WM_STICKY_WINDOW_RULES.org

  1. wm_plan.h

脏标记和后端副作用。

  1. wm_layout.h

布局注册表、布局输入和布局输出。

  1. wm_backend.h

后端子系统接口。

  1. wm_policy_config.h

显式策略开关,替代散落在文档正文里的"可选策略"。

  1. wm_policy.h

事件路由和命令应用接口。

  1. wm_runtime.h

运行时上下文和生命周期接口。

  1. wm_service.h

核心外服务的注册和事件订阅接口。

  1. WM_COMMAND_RULES.org

关键命令的前置条件、状态转移、可见性和副作用规则。

  1. WM_STICKY_WINDOW_RULES.org

sticky 窗口在 workspace、taskbar、多显示器和跨 output 拖动下的补充规则。

  1. WM_POLICY_APPLY_COMMAND_SKELETON.org

wm_policy_apply_command() 的伪代码骨架和实现顺序建议。

  1. WORKSPACE_CONFIG_EXAMPLES.org

Workspace-Output 配置示例和最佳实践。

下一步实施顺序

当前阶段不建议继续扩展抽象,应该开始把最小核心草案落成可运行骨架。推荐顺序如下:

  1. src/core/ 下建立最小骨架,至少放 wm_types/binding/state/plan/layout/policy/policy_config/runtime/service 的头文件和空实现,先不要替换现有 src/ 逻辑。
  2. 优先实现 state + query + plan 这一层,把动态数组管理、=find_*= 查询函数和 stack_order[] 操作补齐。
  3. 再实现 wm_policy_apply_command() 的一小批核心命令: MANAGE_WINDOW UNMANAGE_WINDOW SWITCH_WORKSPACE SET_LAYOUT CYCLE_LAYOUT TOGGLE_FLOATING SET_MAXIMIZED MOVE_FLOATING_WINDOW BEGIN_MOVE_FLOATING_INTERACTION BEGIN_RESIZE_FLOATING_INTERACTION
  4. 给上述命令建立一个无 X11 依赖的测试入口,直接构造 wm_command_t ,检查 wm_state_twm_plan_t 的变化是否符合文档规则。
  5. event -> command -> state -> plan/effect 这条链路跑通后,再编写一层很薄的 adapter把现有事件翻译为 wm_event_t ,并把 effect 翻译回当前 X11 操作。 其中 fullscreen / maximize / minimize 这类平台请求,应先归一化为 WM_EVENT_WINDOW_STATE_REQUEST ,再由路由层翻译成 SET_* 命令。
  6. status/render/bar 暂时不要先动。它们属于核心外服务,应在最小核心链路稳定后再迁移。