diff --git a/docs/min_core_draft/README.org b/docs/min_core_draft/README.org index f6142b6..1b76eec 100644 --- a/docs/min_core_draft/README.org +++ b/docs/min_core_draft/README.org @@ -14,17 +14,24 @@ 负责主循环、模块装配和固定执行顺序。 2. =backend= -负责平台交互。它把平台原始事件转成 =wm_event_t= ,并执行 =wm_effect_t= 。 +负责平台交互。它把平台原始事件转成 =wm_event_t= ,执行 =wm_effect_t= , +并在需要时根据固定的键盘/鼠标绑定表安装平台侧被动抓键 / 抓按钮。 +像 X11 的 =_NET_ACTIVE_WINDOW= 这类 =ClientMessage= 也应在这里先归一化成 +request event,而不是直接改 core 状态。 3. =state= 负责保存窗口、工作区、输出和全局堆叠顺序,是唯一真状态。 当前头文件级草案先直接展示结构体字段,便于讨论; 实现阶段再收敛为不透明结构体也不迟。 窗口元数据(title/app_id/class/instance)直接存储在 =wm_window_t= 中。 +窗口 hint / capability(如 =urgent= / =fixed_size= / =skip_taskbar=)也直接存储在 +=wm_window_t= 中。 工作区名称存储在 =wm_workspace_t= 中。 4. =policy= 负责把事件路由为命令,并把命令应用到状态。 +=WM_EVENT_KEY_PRESS= / =WM_EVENT_POINTER_BUTTON_PRESS= 的路由都使用 bootstrap +固定下来的输入绑定表。 5. =layout= 负责为某个输出上当前显示的工作区中的平铺窗口计算外框几何。 @@ -74,6 +81,8 @@ const wm_window_t *win = wm_state_window_get(state, window_id); // O(n) 2. =workspaces= 3. =layout registry= 4. 每个 workspace 的 =available_layouts= +5. =key binding table= +6. =pointer binding table= 运行时允许变化的只是: @@ -104,7 +113,8 @@ const wm_window_t *win = wm_state_window_get(state, window_id); // O(n) 1. =runtime= 拥有 =state= 、=plan= 、=command_buffer= 和 =layout_registry= 。 其中 workspace/output/layout registry 集合在 init 完成后保持固定。 -2. =runtime= 也拥有 =policy config= 、全局边框配置、交互态和服务注册表。 +2. =runtime= 也拥有 =policy config= 、键盘/鼠标绑定表视图、全局边框配置、 + 交互态和服务注册表。 3. =backend= 只负责平台事件翻译和平台副作用,不拥有控制状态,不直接拼 bar。 4. 窗口元数据(title/app_id/class/instance)直接存储在 =wm_window_t= 中,与控制状态一同管理。 5. 工作区名称存储在 =wm_workspace_t= 中,与控制状态一同管理。 @@ -117,16 +127,23 @@ const wm_window_t *win = wm_state_window_get(state, window_id); // O(n) - 工作区数量和初始配置(由用户配置决定) - 布局算法注册(由代码或配置决定) - 策略配置(focus_raises 等) + - 键盘绑定表(规范化的 =keysym + modifiers -> wm_command_t= 模板) + - 鼠标按键绑定表(规范化的 =button + modifiers + target -> wm_command_t= 模板) - 全局统一边框宽度 + - 全局边框颜色集(普通/焦点) - 初始命令(如启动时扫描到的窗口) 2. 启动时扫描到的已有窗口,不直接塞进 =wm_state_t= ,而是翻译成一批 =MANAGE_WINDOW= 命令进入 runtime。 -3. 工作区在启动时根据配置分配 N 个,运行时不再增删。 +3. 若 backend 提供 =set_keybindings()= / =set_pointer_bindings()= ,runtime + 在 init 时把 bootstrap 里的绑定表同步给 backend,供平台侧建立被动抓键 / + 抓按钮。 -4. 输出在启动时扫描并分配对应数量的数组,运行时不再增删。 +4. 工作区在启动时根据配置分配 N 个,运行时不再增删。 -5. layout registry 在启动时注册完成,运行时不再增删。 +5. 输出在启动时扫描并分配对应数量的数组,运行时不再增删。 + +6. layout registry 在启动时注册完成,运行时不再增删。 * 显示器热插拔策略 @@ -223,12 +240,17 @@ sticky 窗口是否参与方向焦点搜索。 当前最小核心只保留窗口边框,不支持标题栏和其他窗口装饰。 -1. =wm_window_t= 不存储 =border_width= ;边框宽度不是窗口真状态。 +1. =wm_window_t= 不存储 =border_width= 或 =border_color= ;边框样式不是窗口真状态。 2. =wm_runtime_bootstrap_t.border_width= 提供全局统一边框宽度。 -3. =float_rect= 和 =frame_rect= 都表示“包含边框后的外框矩形”。 -4. =runtime= 在提交 =WM_EFFECT_CONFIGURE_WINDOW= 时解析有效边框宽度: +3. =wm_runtime_bootstrap_t.border_palette= 提供全局边框颜色集(普通/焦点)。 +4. =float_rect= 和 =frame_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 可忽略颜色字段 * 交互态 @@ -253,11 +275,19 @@ move/resize 这类拖拽交互不属于 =wm_state_t= 真状态,但也不能继 =config= 仍然在核心外,但它的输出必须改成 core-native 数据,而不是旧的 =client_t/tag_t= 钩子: -1. 配置加载结果应产出 workspace 描述、policy 配置、layout 注册、规则表、快捷键到 command 的映射和服务设置。 - 以及全局统一边框宽度。 +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 表。 + * 对象关系 这套草案里的关系固定为: @@ -300,7 +330,8 @@ workspace 的 =layout_id= 字段指向当前活动的布局(列表中的一个 2. 每个工作区必须且只能固定归属一个输出,运行时不再改变。 3. 每个输出必须且只能显示一个当前工作区(从归属的工作区中选择)。 4. 可见性由窗口归属、工作区归属、输出当前工作区和最小化状态共同推导。 -5. =geometry_mode= 只负责几何模式,不负责 z-order。 +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 可见性,不改变窗口归属工作区。 @@ -311,7 +342,8 @@ workspace 的 =layout_id= 字段指向当前活动的布局(列表中的一个 13. 工作区名称存储在 =wm_workspace_t= 中,核心算法不应依赖此字段。 14. 每个 workspace 的可用布局列表在启动时分配,之后固定不变;运行时只允许通过 =CYCLE_LAYOUT= 或 =SET_LAYOUT= 命令切换当前活动布局。 15. =frame_rect= 和 =float_rect= 都表示包含边框后的外框矩形。 -16. =border_width= 不是 =wm_window_t= 真状态;它由 runtime 从全局配置和几何模式推导。 +16. =border_width= / =border_color= 都不是 =wm_window_t= 真状态;它们由 runtime + 从全局配置、几何模式和焦点关系推导。 * Generation 字段 @@ -332,7 +364,8 @@ workspace 的 =layout_id= 字段指向当前活动的布局(列表中的一个 2. runtime 先处理窗口元数据更新这类辅助事件,并直接更新 =wm_window_t= 中的元数据字段 3. =policy.route_event()= 4. =policy.apply_command()= -5. 根据 =dirty_flags= 决定是否重新运行 =layout= 、解析最终外框矩形,并补全 =configure= effect 的边框宽度 +5. 根据 =dirty_flags= 决定是否重新运行 =layout= 、解析最终外框矩形,并补全 + =configure= effect 的边框宽度和边框颜色 6. 将平台 effect 发送给 =backend= 7. 将 render 失效通知发送给服务层 8. =backend.flush()= @@ -350,50 +383,57 @@ workspace 的 =layout_id= 字段指向当前活动的布局(列表中的一个 3. =wm_event.h= 统一后的 runtime 输入事件;不承载 stop/restart 这类控制信号。 +像 =_NET_ACTIVE_WINDOW= 这类平台协议请求,也应在这一层归一化为独立的 +request event。 +客户端 property / hint 变化(如 =urgent= / =fixed_size= / =skip_taskbar=)也应在 +这一层归一化为辅助事件,而不是伪装成 configure request。 -4. =wm_command.h= +4. =wm_binding.h= +=WM_EVENT_KEY_PRESS= / =WM_EVENT_POINTER_BUTTON_PRESS= 使用的输入绑定表和匹配规则。 + +5. =wm_command.h= 强类型语义命令。 命令语义补充见 [[file:WM_COMMAND_RULES.org][WM_COMMAND_RULES.org]] ; =sticky= 多显示器与跨屏行为补充见 [[file:WM_STICKY_WINDOW_RULES.org][WM_STICKY_WINDOW_RULES.org]] 。 -5. =wm_plan.h= +6. =wm_plan.h= 脏标记和后端副作用。 -6. =wm_layout.h= +7. =wm_layout.h= 布局注册表、布局输入和布局输出。 -7. =wm_backend.h= +8. =wm_backend.h= 后端子系统接口。 -8. =wm_policy_config.h= +9. =wm_policy_config.h= 显式策略开关,替代散落在文档正文里的"可选策略"。 -9. =wm_policy.h= +10. =wm_policy.h= 事件路由和命令应用接口。 -10. =wm_runtime.h= +11. =wm_runtime.h= 运行时上下文和生命周期接口。 -11. =wm_service.h= +12. =wm_service.h= 核心外服务的注册和事件订阅接口。 -12. [[file:WM_COMMAND_RULES.org][WM_COMMAND_RULES.org]] +13. [[file:WM_COMMAND_RULES.org][WM_COMMAND_RULES.org]] 关键命令的前置条件、状态转移、可见性和副作用规则。 -13. [[file:WM_STICKY_WINDOW_RULES.org][WM_STICKY_WINDOW_RULES.org]] +14. [[file:WM_STICKY_WINDOW_RULES.org][WM_STICKY_WINDOW_RULES.org]] =sticky= 窗口在 workspace、taskbar、多显示器和跨 output 拖动下的补充规则。 -14. [[file:WM_POLICY_APPLY_COMMAND_SKELETON.org][WM_POLICY_APPLY_COMMAND_SKELETON.org]] +15. [[file:WM_POLICY_APPLY_COMMAND_SKELETON.org][WM_POLICY_APPLY_COMMAND_SKELETON.org]] =wm_policy_apply_command()= 的伪代码骨架和实现顺序建议。 -15. [[file:WORKSPACE_CONFIG_EXAMPLES.org][WORKSPACE_CONFIG_EXAMPLES.org]] +16. [[file:WORKSPACE_CONFIG_EXAMPLES.org][WORKSPACE_CONFIG_EXAMPLES.org]] Workspace-Output 配置示例和最佳实践。 * 下一步实施顺序 当前阶段不建议继续扩展抽象,应该开始把最小核心草案落成可运行骨架。推荐顺序如下: -1. 在 =src/core/= 下建立最小骨架,至少放 =wm_types/state/plan/layout/policy/policy_config/runtime/service= 的头文件和空实现,先不要替换现有 =src/= 逻辑。 +1. 在 =src/core/= 下建立最小骨架,至少放 =wm_types/binding/state/plan/layout/policy/policy_config/runtime/service= 的头文件和空实现,先不要替换现有 =src/= 逻辑。 2. 优先实现 =state + query + plan= 这一层,把动态数组管理、=find_*= 查询函数和 =stack_order[]= 操作补齐。 @@ -404,11 +444,15 @@ Workspace-Output 配置示例和最佳实践。 =SET_LAYOUT= =CYCLE_LAYOUT= =TOGGLE_FLOATING= - =TOGGLE_MAXIMIZE= + =SET_MAXIMIZED= =MOVE_FLOATING_WINDOW= + =BEGIN_MOVE_FLOATING_INTERACTION= + =BEGIN_RESIZE_FLOATING_INTERACTION= 4. 给上述命令建立一个无 X11 依赖的测试入口,直接构造 =wm_command_t= ,检查 =wm_state_t= 和 =wm_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= 暂时不要先动。它们属于核心外服务,应在最小核心链路稳定后再迁移。 diff --git a/docs/min_core_draft/WM_COMMAND_RULES.org b/docs/min_core_draft/WM_COMMAND_RULES.org index 3a24f8f..797c22a 100644 --- a/docs/min_core_draft/WM_COMMAND_RULES.org +++ b/docs/min_core_draft/WM_COMMAND_RULES.org @@ -21,8 +21,8 @@ 都是 bootstrap 后固定集合;运行时不允许增删。 10. 文档里出现的可选行为,应优先固化到 =wm_policy_config_t= ,而不是继续留在 prose 里。 11. =frame_rect= 和 =float_rect= 都表示包含边框后的外框矩形。 -12. =border_width= 不是 =wm_window_t= 真状态;runtime 在提交 - =WM_EFFECT_CONFIGURE_WINDOW= 时按全局配置解析它。 +12. =border_width= / =border_color= 都不是 =wm_window_t= 真状态;runtime 在提交 + =WM_EFFECT_CONFIGURE_WINDOW= 时按全局配置和焦点关系解析它们。 * 管理类命令 @@ -82,6 +82,11 @@ 1. 若 =policy.focus_raises == true= ,则在实现中额外执行 =raise_window= 逻辑。 +副作用: + +1. =WM_DIRTY_DECORATION= +2. =WM_DIRTY_RENDER= + ** =WM_COMMAND_FOCUS_DIRECTION= 状态变化: @@ -90,6 +95,23 @@ 2. 候选窗口应排除 =geometry_mode == WM_GEOMETRY_MINIMIZED= 的窗口。 3. =sticky= 窗口是否参与方向焦点切换,由 =policy.sticky_windows_participate_in_direction_focus= 决定。 +副作用: + +1. =WM_DIRTY_DECORATION= +2. =WM_DIRTY_RENDER= + +** =WM_COMMAND_SET_WINDOW_URGENT= + +状态变化: + +1. 将目标窗口的 =urgent= 设为命令给定值。 +2. 不修改 =focused_window_id= 。 +3. 不修改 =stack_order[]= 。 + +副作用: + +1. =WM_DIRTY_RENDER= + ** =WM_COMMAND_RAISE_WINDOW= 状态变化: @@ -259,57 +281,60 @@ 1. =WM_DIRTY_LAYOUT= 2. =WM_DIRTY_RENDER= -** =WM_COMMAND_TOGGLE_MAXIMIZE= +** =WM_COMMAND_SET_MAXIMIZED= 状态变化: -1. 若当前 =geometry_mode == WM_GEOMETRY_MAXIMIZED= : +1. 若 =enabled == true= 且当前 =geometry_mode != WM_GEOMETRY_MAXIMIZED= : + 将 =geometry_mode= 设为 =WM_GEOMETRY_MAXIMIZED= 。 + 这会覆盖此前的 =FULLSCREEN= / =MINIMIZED= 状态。 +2. 若 =enabled == false= 且当前 =geometry_mode == WM_GEOMETRY_MAXIMIZED= : 设回 =WM_GEOMETRY_NORMAL= 。 -2. 若当前 =geometry_mode == WM_GEOMETRY_FULLSCREEN= : - 直接切换为 =WM_GEOMETRY_MAXIMIZED= 。 -3. 若当前 =geometry_mode == WM_GEOMETRY_MINIMIZED= : - 直接切换为 =WM_GEOMETRY_MAXIMIZED= 。 -4. 其余情况: - 设为 =WM_GEOMETRY_MAXIMIZED= 。 -5. 不改变 =floating= 。 -6. 若窗口原本是 floating,退出 maximize 后回到 =float_rect= 。 -7. 若窗口原本不是 floating,退出 maximize 后重新参与 layout。 -8. maximize 不改变全局边框策略;有效边框宽度仍由 runtime 统一解析。 +3. 其余情况不产生状态变化。 +4. 不改变 =floating= 。 +5. 若窗口原本是 floating,退出 maximize 后回到 =float_rect= 。 +6. 若窗口原本不是 floating,退出 maximize 后重新参与 layout。 +7. maximize 不改变全局边框策略;有效边框宽度仍由 runtime 统一解析。 副作用: 1. =WM_DIRTY_LAYOUT= 2. =WM_DIRTY_RENDER= -** =WM_COMMAND_TOGGLE_FULLSCREEN= +** =WM_COMMAND_SET_FULLSCREEN= 状态变化: -1. 若当前 =geometry_mode == WM_GEOMETRY_FULLSCREEN= : +1. 若 =enabled == true= 且当前 =geometry_mode != WM_GEOMETRY_FULLSCREEN= : + 将 =geometry_mode= 设为 =WM_GEOMETRY_FULLSCREEN= 。 + 这会覆盖此前的 =MAXIMIZED= / =MINIMIZED= 状态。 +2. 若 =enabled == false= 且当前 =geometry_mode == WM_GEOMETRY_FULLSCREEN= : 设回 =WM_GEOMETRY_NORMAL= 。 -2. 否则: - 设为 =WM_GEOMETRY_FULLSCREEN= 。 -3. 不改变 =floating= 。 -4. fullscreen 只决定几何占用 =output.geometry= ,不自动改变 =stack_order[]= 。 -5. fullscreen 的有效边框宽度由 runtime 解析为 0 。 +3. 其余情况不产生状态变化。 +4. 不改变 =floating= 。 +5. fullscreen 只决定几何占用 =output.geometry= ,不自动改变 =stack_order[]= 。 +6. fullscreen 的有效边框宽度由 runtime 解析为 0 。 副作用: 1. =WM_DIRTY_LAYOUT= 2. =WM_DIRTY_RENDER= -** =WM_COMMAND_TOGGLE_MINIMIZE= +** =WM_COMMAND_SET_MINIMIZED= 状态变化: -1. 若当前 =geometry_mode == WM_GEOMETRY_MINIMIZED= : +1. 若 =enabled == true= 且当前 =geometry_mode != WM_GEOMETRY_MINIMIZED= : + 将 =geometry_mode= 设为 =WM_GEOMETRY_MINIMIZED= 。 + 在 X11 后端上,这同时覆盖 ICCCM =IconicState= / =WM_CHANGE_STATE(Iconic)= + 语义;core 不单独建模 =iconic= 。 +2. 若 =enabled == false= 且当前 =geometry_mode == WM_GEOMETRY_MINIMIZED= : 设回 =WM_GEOMETRY_NORMAL= 。 -2. 否则: - 设为 =WM_GEOMETRY_MINIMIZED= 。 -3. 不改变 =floating= 。 -4. 不改变 =sticky= 。 -5. minimized 后窗口不再参与显示和焦点候选。 -6. 若目标窗口是当前焦点,是否清空或重选焦点由 =policy.minimize_clears_focus= 决定。 +3. 其余情况不产生状态变化。 +4. 不改变 =floating= 。 +5. 不改变 =sticky= 。 +6. minimized 后窗口不再参与显示和焦点候选。 +7. 若本次将目标窗口切入 minimized,且它正是当前焦点,是否清空或重选焦点由 =policy.minimize_clears_focus= 决定。 副作用: @@ -363,15 +388,19 @@ * 边框约定 -1. 当前最小核心只支持全局统一边框,不支持每窗口边框宽度。 +1. 当前最小核心只支持全局统一边框宽度和全局边框颜色集,不支持每窗口独立配置。 2. 全局边框宽度由 =wm_runtime_bootstrap_t.border_width= 提供。 -3. =layout= 和 =floating= 都产出包含边框后的外框矩形。 -4. =WM_EFFECT_CONFIGURE_WINDOW.rect= 也是外框矩形。 -5. runtime 在把最终矩形转成 =configure= effect 时解析有效边框宽度: +3. 全局边框颜色由 =wm_runtime_bootstrap_t.border_palette= 提供。 +4. =layout= 和 =floating= 都产出包含边框后的外框矩形。 +5. =WM_EFFECT_CONFIGURE_WINDOW.rect= 也是外框矩形。 +6. runtime 在把最终矩形转成 =configure= effect 时解析有效边框宽度: =WM_GEOMETRY_FULLSCREEN= -> 0 其余模式 -> 全局边框宽度 +7. runtime 在把最终矩形转成 =configure= effect 时解析有效边框颜色: + focused window -> =border_palette.focused_rgba= + 其余窗口 -> =border_palette.normal_rgba= -* redraw 和 quit +* redraw 和 runtime 控制命令 ** =WM_COMMAND_REDRAW= @@ -389,6 +418,43 @@ 1. 不再接收新事件,交由 =runtime= 退出主循环。 +副作用: + +1. 不产生 =wm_plan_t= 平台 effect。 + +** =WM_COMMAND_BEGIN_MOVE_FLOATING_INTERACTION= + +状态变化: + +1. 不修改 =wm_state_t= 。 +2. 由 =runtime= 在命令应用外层拦截,并把 =interaction.mode= 设为 + =WM_INTERACTION_MOVE_FLOATING= 。 +3. 只有当当前事件是 =WM_EVENT_POINTER_BUTTON_PRESS= 、目标窗口存在,且窗口 + 满足 =floating == true= 时,交互才真正开始。 + +副作用: + +1. 不产生 =wm_plan_t= 平台 effect。 + +** =WM_COMMAND_BEGIN_RESIZE_FLOATING_INTERACTION= + +状态变化: + +1. 不修改 =wm_state_t= 。 +2. 由 =runtime= 在命令应用外层拦截,并把 =interaction.mode= 设为 + =WM_INTERACTION_RESIZE_FLOATING= 。 +3. 只有当当前事件是 =WM_EVENT_POINTER_BUTTON_PRESS= 、目标窗口存在,且窗口 + 满足 =floating == true= 时,交互才真正开始。 + +副作用: + +1. 不产生 =wm_plan_t= 平台 effect。 + +补充约定: + +1. =WM_COMMAND_QUIT= / =WM_COMMAND_BEGIN_*_INTERACTION= 属于 runtime control commands。 +2. 它们应在 =wm_policy_apply_command()= 外层被 =runtime= 拦截,而不是走状态写入分支。 + * 可见性推导建议 建议实现顺序如下: diff --git a/docs/min_core_draft/WM_EVENT_ROUTING.org b/docs/min_core_draft/WM_EVENT_ROUTING.org index 7f31f6e..68cbe21 100644 --- a/docs/min_core_draft/WM_EVENT_ROUTING.org +++ b/docs/min_core_draft/WM_EVENT_ROUTING.org @@ -24,15 +24,21 @@ void wm_runtime_process_event(wm_runtime_t *runtime, const wm_event_t *event) { // 3. 常规事件路由 if (!consumed) { - wm_policy_route_event(&runtime->state, event, &runtime->command_buffer); + wm_policy_route_event( + &runtime->state, + &runtime->keybindings, + &runtime->pointer_bindings, + event, + &runtime->command_buffer + ); } // 4. 应用命令 for (size_t i = 0; i < runtime->command_buffer.count; i++) { const wm_command_t *command = &runtime->command_buffer.items[i]; - if (command->type == WM_COMMAND_QUIT) { - quit_requested = true; + if (runtime_handle_control_command(runtime, event, command, + &quit_requested)) { continue; } @@ -52,6 +58,16 @@ void wm_runtime_process_event(wm_runtime_t *runtime, const wm_event_t *event) { } #+END_SRC +这里的 runtime control command 至少包括: + +1. =WM_COMMAND_QUIT= +2. =WM_COMMAND_BEGIN_MOVE_FLOATING_INTERACTION= +3. =WM_COMMAND_BEGIN_RESIZE_FLOATING_INTERACTION= + +=QUIT= 只修改 runtime 主循环状态; +=BEGIN_*_INTERACTION= 只在当前事件是 =WM_EVENT_POINTER_BUTTON_PRESS= 且目标窗口 +有效时更新 =runtime->interaction= ,不进入 =wm_policy_apply_command()= 。 + * 辅助事件处理规则 辅助事件直接更新 runtime 或广播给服务层,不经过命令系统: @@ -59,7 +75,7 @@ void wm_runtime_process_event(wm_runtime_t *runtime, const wm_event_t *event) { #+BEGIN_SRC c static bool is_auxiliary_event(const wm_event_t *event) { return event->type == WM_EVENT_WINDOW_METADATA_CHANGED || - event->type == WM_EVENT_STATUS_TICK; + event->type == WM_EVENT_WINDOW_HINTS_CHANGED; } static void runtime_handle_auxiliary_event(wm_runtime_t *runtime, @@ -69,8 +85,8 @@ static void runtime_handle_auxiliary_event(wm_runtime_t *runtime, runtime_handle_metadata_event(runtime, event); return; - case WM_EVENT_STATUS_TICK: - runtime_handle_status_tick(runtime, event); + case WM_EVENT_WINDOW_HINTS_CHANGED: + runtime_handle_hints_event(runtime, event); return; default: @@ -120,25 +136,46 @@ void runtime_handle_metadata_event(wm_runtime_t *runtime, const wm_event_t *even } #+END_SRC -**=WM_EVENT_STATUS_TICK=** +**=WM_EVENT_WINDOW_HINTS_CHANGED=** -状态服务更新事件,也不经过命令系统: +客户端属性 / hint 更新事件,也不经过命令系统: #+BEGIN_SRC c -void runtime_handle_status_tick(wm_runtime_t *runtime, const wm_event_t *event) { - (void)event; +void runtime_handle_hints_event(wm_runtime_t *runtime, const wm_event_t *event) { + const wm_window_hints_changed_event_t *e = &event->data.window_hints_changed; + const wm_window_t *win = wm_state_window_get(&runtime->state, e->window); - // 通知状态服务更新 - wm_service_event_t service_event = { - .type = WM_SERVICE_EVENT_STATUS_TICK, - }; - wm_service_registry_emit(&runtime->services, &service_event, &runtime->state); + if (!win) { + return; + } + + if (e->changed_fields & WM_WINDOW_HINT_CHANGED_URGENT) { + wm_state_window_set_urgent(&runtime->state, e->window, e->urgent); + } + if (e->changed_fields & WM_WINDOW_HINT_CHANGED_FIXED_SIZE) { + wm_state_window_set_fixed_size(&runtime->state, e->window, e->fixed_size); + runtime->plan.dirty_flags |= WM_DIRTY_LAYOUT; + } + if (e->changed_fields & WM_WINDOW_HINT_CHANGED_SKIP_TASKBAR) { + wm_state_window_set_skip_taskbar(&runtime->state, e->window, + e->skip_taskbar); + } + + runtime->plan.dirty_flags |= WM_DIRTY_RENDER; } #+END_SRC -* 快捷键绑定系统 +这里的边界是: -**配置格式**: +1. `window hints changed` 不是 control request,不生成 command。 +2. `fixed_size` 属于 capability hint,不等同于 `floating`; + 但当前最小实现允许 state setter 在 `fixed_size == true` 时派生出 + `floating == true` 这种约束。 +3. `skip_taskbar` 主要影响核心外 taskbar/pager 服务的可见性过滤。 + +* 输入绑定系统 + +**键盘绑定配置格式**: #+BEGIN_SRC c // keybindings.conf @@ -180,46 +217,141 @@ keybindings = [ ] #+END_SRC -**快捷键匹配优先级**: +**鼠标按键绑定配置格式**: + +#+BEGIN_SRC c +// pointer_bindings.conf +pointer_bindings = [ + { + // 点击窗口聚焦。window_id == INVALID 表示“使用当前点击的窗口” + button: "BUTTON1", + target: "window", + command: { + type: WM_COMMAND_FOCUS_WINDOW, + window_id: WM_WINDOW_ID_INVALID + } + }, + { + // SUPER + 左键拖动 floating 窗口 + button: "SUPER+BUTTON1", + target: "window", + command: { + type: WM_COMMAND_BEGIN_MOVE_FLOATING_INTERACTION, + window_id: WM_WINDOW_ID_INVALID + } + }, + { + // SUPER + 右键调整 floating 窗口大小 + button: "SUPER+BUTTON3", + target: "window", + command: { + type: WM_COMMAND_BEGIN_RESIZE_FLOATING_INTERACTION, + window_id: WM_WINDOW_ID_INVALID + } + } +] +#+END_SRC + +**输入绑定匹配优先级**: 1. 完全匹配(keysym + 所有 modifiers) 2. 通配符匹配(keysym + 部分修饰符) -3. 默认绑定(如 SUPER+1 切换到预设工作区) 配置加载阶段应先把 =SUPER+m= 这类字符串解析成规范化按键组合。 这里的 =keysym= 指规范化后的逻辑主键编码,不是平台原始扫描码: 事件里的 =keycode= 只是保留 backend 的物理键位信息; 常规绑定仍以 =keysym + modifiers= 为准。 +像 =SUPER+1= 这类行为也必须显式写成一条绑定,而不是由 core 内建生成。 +鼠标按键绑定只区分 =root= / =window= 两类命中目标,不覆盖 bar/status 点击区域。 #+BEGIN_SRC c -typedef struct keybinding_t { +typedef enum wm_binding_match_mode_t { + WM_BINDING_MATCH_EXACT, + WM_BINDING_MATCH_ALLOW_EXTRA_MODIFIERS, +} wm_binding_match_mode_t; + +typedef struct wm_key_binding_t { wm_keysym_t keysym; wm_modifier_mask_t modifiers; - wm_command_type_t command_type; -} keybinding_t; + wm_binding_match_mode_t match_mode; + wm_command_t command; +} wm_key_binding_t; + +typedef struct wm_key_binding_table_t { + const wm_key_binding_t *items; + size_t count; +} wm_key_binding_table_t; + +typedef enum wm_pointer_binding_target_t { + WM_POINTER_BINDING_TARGET_ANY, + WM_POINTER_BINDING_TARGET_ROOT, + WM_POINTER_BINDING_TARGET_WINDOW, +} wm_pointer_binding_target_t; + +typedef struct wm_pointer_binding_t { + wm_button_t button; + wm_modifier_mask_t modifiers; + wm_binding_match_mode_t match_mode; + wm_pointer_binding_target_t target; + wm_command_t command; +} wm_pointer_binding_t; + +typedef struct wm_pointer_binding_table_t { + const wm_pointer_binding_t *items; + size_t count; +} wm_pointer_binding_table_t; #+END_SRC +绑定表属于 bootstrap 固定输入: + +#+BEGIN_SRC c +typedef struct wm_runtime_bootstrap_t { + ... + wm_key_binding_table_t keybindings; + wm_pointer_binding_table_t pointer_bindings; + ... +} wm_runtime_bootstrap_t; +#+END_SRC + +若 backend 需要平台侧被动抓键 / 抓按钮,则通过 +=wm_backend_api_t.set_keybindings()= / =set_pointer_bindings()= 接收同一组表。 + **=wm_policy_route_event()= 实现**: #+BEGIN_SRC c bool wm_policy_route_event(const wm_state_t *state, + const wm_key_binding_table_t *keybindings, + const wm_pointer_binding_table_t *pointer_bindings, const wm_event_t *event, wm_command_buffer_t *out) { switch (event->type) { case WM_EVENT_KEY_PRESS: - return route_key_press(state, &event->data.key_press, out); + return route_key_press(state, keybindings, + &event->data.key_press, out); case WM_EVENT_POINTER_BUTTON_PRESS: - return route_pointer_press(state, &event->data.pointer_button_press, out); + return route_pointer_press(state, pointer_bindings, + &event->data.pointer_button_press, out); case WM_EVENT_WINDOW_MAP_REQUEST: return route_map_request(state, &event->data.window_map_request, out); - case WM_EVENT_WINDOW_UNMAP: - return route_unmap_notify(state, &event->data.window_unmap, out); + case WM_EVENT_WINDOW_REMOVE: + return route_window_remove(state, &event->data.window_remove, out); - case WM_EVENT_WINDOW_DESTROY: - return route_destroy_notify(state, &event->data.window_destroy, out); + case WM_EVENT_WINDOW_ACTIVATE_REQUEST: + return route_activate_request( + state, + &event->data.window_activate_request, + out + ); + + case WM_EVENT_WINDOW_STATE_REQUEST: + return route_window_state_request( + state, + &event->data.window_state_request, + out + ); case WM_EVENT_CONFIGURE_REQUEST: return route_configure_request(state, &event->data.configure_request, out); @@ -231,10 +363,12 @@ bool wm_policy_route_event(const wm_state_t *state, // 键盘事件路由 static bool route_key_press(const wm_state_t *state, + const wm_key_binding_table_t *keybindings, const wm_key_press_event_t *e, wm_command_buffer_t *out) { - // 1. 查找匹配的快捷键绑定 - const keybinding_t *binding = keybinding_find( + // 1. 查找匹配的键盘绑定 + const wm_key_binding_t *binding = wm_key_binding_find( + keybindings, e->keysym, e->modifiers ); @@ -243,13 +377,140 @@ static bool route_key_press(const wm_state_t *state, return false; // 没有匹配的绑定 } - // 2. 将绑定转换为命令 - wm_command_t cmd = { - .type = binding->command_type, - // ... 填充命令参数 - }; + // 2. 直接复制绑定里的命令模板 + return wm_command_buffer_push(out, binding->command); +} + +static bool resolve_pointer_window_target(wm_command_t *cmd, + wm_window_id_t event_window) { + switch (cmd->type) { + case WM_COMMAND_UNMANAGE_WINDOW: + if (cmd->as.unmanage_window.window_id == WM_WINDOW_ID_INVALID) { + cmd->as.unmanage_window.window_id = event_window; + } + return cmd->as.unmanage_window.window_id != WM_WINDOW_ID_INVALID; + + case WM_COMMAND_FOCUS_WINDOW: + if (cmd->as.focus_window.window_id == WM_WINDOW_ID_INVALID) { + cmd->as.focus_window.window_id = event_window; + } + return cmd->as.focus_window.window_id != WM_WINDOW_ID_INVALID; + + case WM_COMMAND_RAISE_WINDOW: + if (cmd->as.raise_window.window_id == WM_WINDOW_ID_INVALID) { + cmd->as.raise_window.window_id = event_window; + } + return cmd->as.raise_window.window_id != WM_WINDOW_ID_INVALID; + + case WM_COMMAND_LOWER_WINDOW: + if (cmd->as.lower_window.window_id == WM_WINDOW_ID_INVALID) { + cmd->as.lower_window.window_id = event_window; + } + return cmd->as.lower_window.window_id != WM_WINDOW_ID_INVALID; + + case WM_COMMAND_SEND_WINDOW_TO_WORKSPACE: + if (cmd->as.send_window_to_workspace.window_id == WM_WINDOW_ID_INVALID) { + cmd->as.send_window_to_workspace.window_id = event_window; + } + return cmd->as.send_window_to_workspace.window_id != + WM_WINDOW_ID_INVALID; + + case WM_COMMAND_SEND_WINDOW_TO_OUTPUT: + if (cmd->as.send_window_to_output.window_id == WM_WINDOW_ID_INVALID) { + cmd->as.send_window_to_output.window_id = event_window; + } + return cmd->as.send_window_to_output.window_id != + WM_WINDOW_ID_INVALID; + + case WM_COMMAND_TOGGLE_FLOATING: + if (cmd->as.toggle_floating.window_id == WM_WINDOW_ID_INVALID) { + cmd->as.toggle_floating.window_id = event_window; + } + return cmd->as.toggle_floating.window_id != WM_WINDOW_ID_INVALID; + + case WM_COMMAND_TOGGLE_STICKY: + if (cmd->as.toggle_sticky.window_id == WM_WINDOW_ID_INVALID) { + cmd->as.toggle_sticky.window_id = event_window; + } + return cmd->as.toggle_sticky.window_id != WM_WINDOW_ID_INVALID; + + case WM_COMMAND_SET_MAXIMIZED: + if (cmd->as.set_maximized.window_id == WM_WINDOW_ID_INVALID) { + cmd->as.set_maximized.window_id = event_window; + } + return cmd->as.set_maximized.window_id != WM_WINDOW_ID_INVALID; + + case WM_COMMAND_SET_FULLSCREEN: + if (cmd->as.set_fullscreen.window_id == WM_WINDOW_ID_INVALID) { + cmd->as.set_fullscreen.window_id = event_window; + } + return cmd->as.set_fullscreen.window_id != WM_WINDOW_ID_INVALID; + + case WM_COMMAND_SET_MINIMIZED: + if (cmd->as.set_minimized.window_id == WM_WINDOW_ID_INVALID) { + cmd->as.set_minimized.window_id = event_window; + } + return cmd->as.set_minimized.window_id != WM_WINDOW_ID_INVALID; + + case WM_COMMAND_MOVE_FLOATING_WINDOW: + if (cmd->as.move_floating_window.window_id == WM_WINDOW_ID_INVALID) { + cmd->as.move_floating_window.window_id = event_window; + } + return cmd->as.move_floating_window.window_id != + WM_WINDOW_ID_INVALID; + + case WM_COMMAND_RESIZE_FLOATING_WINDOW: + if (cmd->as.resize_floating_window.window_id == WM_WINDOW_ID_INVALID) { + cmd->as.resize_floating_window.window_id = event_window; + } + return cmd->as.resize_floating_window.window_id != + WM_WINDOW_ID_INVALID; + + case WM_COMMAND_BEGIN_MOVE_FLOATING_INTERACTION: + if (cmd->as.begin_move_floating_interaction.window_id == + WM_WINDOW_ID_INVALID) { + cmd->as.begin_move_floating_interaction.window_id = event_window; + } + return cmd->as.begin_move_floating_interaction.window_id != + WM_WINDOW_ID_INVALID; + + case WM_COMMAND_BEGIN_RESIZE_FLOATING_INTERACTION: + if (cmd->as.begin_resize_floating_interaction.window_id == + WM_WINDOW_ID_INVALID) { + cmd->as.begin_resize_floating_interaction.window_id = event_window; + } + return cmd->as.begin_resize_floating_interaction.window_id != + WM_WINDOW_ID_INVALID; + + default: + return true; + } +} + +static bool route_pointer_press(const wm_state_t *state, + const wm_pointer_binding_table_t *pointer_bindings, + const wm_pointer_button_event_t *e, + wm_command_buffer_t *out) { + const wm_pointer_binding_t *binding = wm_pointer_binding_find( + pointer_bindings, + e->button, + e->modifiers, + e->window != WM_WINDOW_ID_INVALID + ); + + wm_command_t cmd; + + (void)state; + + if (!binding) { + return false; + } + + cmd = binding->command; + if (!resolve_pointer_window_target(&cmd, e->window)) { + return false; + } - // 3. 添加到命令缓冲 return wm_command_buffer_push(out, cmd); } @@ -258,7 +519,7 @@ static bool route_map_request(const wm_state_t *state, const wm_window_map_request_event_t *e, wm_command_buffer_t *out) { // 1. 检查窗口是否已存在 - if (wm_state_window_get(state, e->window)) { + if (wm_state_window_get(state, e->info.id)) { return false; // 窗口已管理,忽略 } @@ -266,15 +527,175 @@ static bool route_map_request(const wm_state_t *state, wm_command_t cmd = { .type = WM_COMMAND_MANAGE_WINDOW, .as.manage_window = { - .window_id = e->window, - // 应用规则确定初始状态 + .window_id = e->info.id, + // route 层结合 e->info / transient_for / is_dialog + // 推导 initial_state、workspace_id 和初始 float_rect } }; return wm_command_buffer_push(out, cmd); } + +static bool route_window_remove(const wm_state_t *state, + const wm_window_remove_event_t *e, + wm_command_buffer_t *out) { + if (!wm_state_window_get(state, e->window)) { + return false; + } + + // 当前最小核心不区分 withdrawn / destroy,统一转成 unmanage。 + (void)e->reason; + + return wm_command_buffer_push(out, (wm_command_t){ + .type = WM_COMMAND_UNMANAGE_WINDOW, + .as.unmanage_window = { + .window_id = e->window, + }, + }); +} + +static bool route_activate_request(const wm_state_t *state, + const wm_window_activate_request_event_t *e, + wm_command_buffer_t *out) { + const wm_window_t *win = wm_state_window_get(state, e->window); + const wm_workspace_t *ws; + const wm_output_t *output; + wm_command_t cmd; + + if (!win) { + return false; + } + + if (e->source == WM_WINDOW_ACTIVATION_SOURCE_PAGER) { + ws = &state->workspaces[win->workspace_id]; + output = &state->outputs[ws->output_id]; + + if (output->current_workspace_id != ws->id) { + cmd = (wm_command_t){ + .type = WM_COMMAND_SWITCH_WORKSPACE, + .as.switch_workspace = { + .output_id = output->id, + .workspace_id = ws->id, + }, + }; + if (!wm_command_buffer_push(out, cmd)) { + return false; + } + } + + cmd = (wm_command_t){ + .type = WM_COMMAND_FOCUS_WINDOW, + .as.focus_window = { + .window_id = win->id, + }, + }; + return wm_command_buffer_push(out, cmd); + } + + cmd = (wm_command_t){ + .type = WM_COMMAND_SET_WINDOW_URGENT, + .as.set_window_urgent = { + .window_id = win->id, + .urgent = true, + }, + }; + return wm_command_buffer_push(out, cmd); +} + +static bool route_window_state_request(const wm_state_t *state, + const wm_window_state_request_event_t *e, + wm_command_buffer_t *out) { + const wm_window_t *win = wm_state_window_get(state, e->window); + bool current_enabled; + bool enabled; + wm_command_t cmd; + + if (!win) { + return false; + } + + switch (e->kind) { + case WM_WINDOW_STATE_REQUEST_FULLSCREEN: + current_enabled = win->geometry_mode == WM_GEOMETRY_FULLSCREEN; + break; + + case WM_WINDOW_STATE_REQUEST_MAXIMIZED: + current_enabled = win->geometry_mode == WM_GEOMETRY_MAXIMIZED; + break; + + case WM_WINDOW_STATE_REQUEST_MINIMIZED: + current_enabled = win->geometry_mode == WM_GEOMETRY_MINIMIZED; + break; + + default: + return false; + } + + switch (e->action) { + case WM_WINDOW_STATE_REQUEST_ACTION_ADD: + enabled = true; + break; + + case WM_WINDOW_STATE_REQUEST_ACTION_REMOVE: + enabled = false; + break; + + case WM_WINDOW_STATE_REQUEST_ACTION_TOGGLE: + enabled = !current_enabled; + break; + + default: + return false; + } + + switch (e->kind) { + case WM_WINDOW_STATE_REQUEST_FULLSCREEN: + cmd = (wm_command_t){ + .type = WM_COMMAND_SET_FULLSCREEN, + .as.set_fullscreen = { + .window_id = win->id, + .enabled = enabled, + }, + }; + break; + + case WM_WINDOW_STATE_REQUEST_MAXIMIZED: + cmd = (wm_command_t){ + .type = WM_COMMAND_SET_MAXIMIZED, + .as.set_maximized = { + .window_id = win->id, + .enabled = enabled, + }, + }; + break; + + case WM_WINDOW_STATE_REQUEST_MINIMIZED: + cmd = (wm_command_t){ + .type = WM_COMMAND_SET_MINIMIZED, + .as.set_minimized = { + .window_id = win->id, + .enabled = enabled, + }, + }; + break; + + default: + return false; + } + + return wm_command_buffer_push(out, cmd); +} #+END_SRC +这里的最小策略是: + +1. `pager` 来源请求可以切到目标窗口所属 workspace,并把焦点交给该窗口。 +2. `application` / `legacy` 来源不直接抢焦点,而是转成 =SET_WINDOW_URGENT(true)= 。 +3. `WM_EVENT_WINDOW_STATE_REQUEST` 保留 add/remove/toggle 语义,不在 backend 侧提前求最终布尔值。 +4. route 层结合当前 =geometry_mode= ,把它翻译成 =SET_FULLSCREEN= / =SET_MAXIMIZED= / =SET_MINIMIZED= 。 +5. 这层抽象可以同时承接 X11 的 =_NET_WM_STATE= 与 Wayland 的窗口状态请求。 +6. 若以后需要更细的 focus-stealing prevention,可以再下沉到 =wm_policy_config_t= 。 + * 交互态事件路由 交互态会覆盖正常的事件路由: @@ -334,21 +755,31 @@ static bool route_move_floating_event(wm_runtime_t *runtime, | 事件类型 | 可能生成的命令 | 条件 | |---------|---------------|------| -| KEY_PRESS | 任意命令(通过快捷键绑定) | 绑定存在 | -| POINTER_BUTTON_PRESS | FOCUS_WINDOW, RAISE_WINDOW | 点击窗口 | +| KEY_PRESS | 任意命令(通过键盘绑定) | 绑定存在 | +| POINTER_BUTTON_PRESS | 任意窗口命令 / BEGIN_*_INTERACTION(通过鼠标绑定) | 绑定存在 | | WINDOW_MAP_REQUEST | MANAGE_WINDOW | 窗口未管理 | -| WINDOW_UNMAP | (可能不生成命令) | - | -| WINDOW_DESTROY | UNMANAGE_WINDOW | 窗口已管理 | +| WINDOW_REMOVE | UNMANAGE_WINDOW | 窗口已管理 | +| WINDOW_ACTIVATE_REQUEST | SWITCH_WORKSPACE + FOCUS_WINDOW / SET_WINDOW_URGENT | source == pager / source != pager | +| WINDOW_STATE_REQUEST | SET_FULLSCREEN / SET_MAXIMIZED / SET_MINIMIZED | 窗口已管理 | | CONFIGURE_REQUEST | (可能不生成命令) | floating 窗口 | | POINTER_MOTION | MOVE/RESIZE 命令 | 交互态下 | +补充约定: + +1. `WM_EVENT_CONFIGURE_REQUEST` 只携带稀疏几何字段,不直接镜像 X11 全量协议载荷。 +2. 最小核心当前只会把这些几何请求映射到 floating 窗口的 move/resize 语义。 +3. `border_width` / `sibling` / `stack_mode` 这类协议细节留在 backend / adapter 层。 +4. fullscreen / maximize / minimize 这类窗口模式切换不属于 configure request。 +5. `WM_EVENT_WINDOW_STATE_REQUEST` 表达后端看到的原始 add/remove/toggle 请求; + core command 仍保持 =SET_*= 这种最终状态语义。 + * 事件处理的边界条件 **1. 辅助事件边界** 辅助事件满足以下条件时直接处理,不经过命令系统: - 事件类型是 `WM_EVENT_WINDOW_METADATA_CHANGED` -- 事件类型是 `WM_EVENT_STATUS_TICK` +- 事件类型是 `WM_EVENT_WINDOW_HINTS_CHANGED` **2. 交互态边界** @@ -366,16 +797,18 @@ static bool route_move_floating_event(wm_runtime_t *runtime, 窗口管理的边界条件: - `WM_EVENT_WINDOW_MAP_REQUEST`:窗口已存在则忽略 -- `WM_EVENT_WINDOW_DESTROY`:窗口不存在则忽略 -- `WM_EVENT_CONFIGURE_REQUEST`:只有 floating 窗口才响应 +- `WM_EVENT_WINDOW_REMOVE`:窗口不存在则忽略 +- `WM_EVENT_WINDOW_ACTIVATE_REQUEST`:窗口不存在则忽略 +- `WM_EVENT_WINDOW_STATE_REQUEST`:窗口不存在则忽略 +- `WM_EVENT_CONFIGURE_REQUEST`:只有 floating 窗口的几何请求才进入 core 命令链 * 事件处理顺序保证 为了保证确定性行为,事件处理必须按以下顺序进行: 1. **辅助事件优先**:元数据和状态服务更新不走命令系统 -2. **交互态优先**:拖拽操作优先于快捷键 -3. **快捷键匹配优先级**:完全匹配 → 通配符 → 默认 +2. **交互态优先**:拖拽操作优先于常规输入绑定 +3. **输入绑定匹配优先级**:完全匹配 → 通配符 4. **命令执行顺序**:命令缓冲中的命令按顺序执行 * 主执行流程实现 @@ -408,6 +841,13 @@ void wm_runtime_run(wm_runtime_t *runtime) { wm_runtime_relayout(runtime); } + // 3.1.5 若布局或装饰发生变化,重建 configure effects。 + // 这里需要同时补全 rect / border_width / border_rgba。 + if (runtime->plan.dirty_flags & + (WM_DIRTY_LAYOUT | WM_DIRTY_DECORATION)) { + wm_runtime_rebuild_configure_effects(runtime); + } + // 3.2 依次提交 effect。RENDER_OUTPUT 留给服务层处理。 for (size_t i = 0; i < runtime->plan.effect_count; i++) { const wm_effect_t *effect = &runtime->plan.effects[i]; @@ -447,7 +887,7 @@ void wm_runtime_run(wm_runtime_t *runtime) { 这个事件路由设计完全兼容现有 ZDWM 功能: -1. **快捷键系统**:通过 keybindings.conf 配置 +1. **输入绑定系统**:通过 keybindings.conf / pointer_bindings.conf 配置 2. **规则系统**:在 MANAGE_WINDOW 命令中应用 3. **布局切换**:通过 SET_LAYOUT / CYCLE_LAYOUT 命令 4. **拖拽交互**:通过交互态管理 @@ -462,6 +902,6 @@ void wm_runtime_run(wm_runtime_t *runtime) { 3. ✅ 路由接口(wm_policy.h) 4. ✅ 命令应用(WM_POLICY_APPLY_COMMAND_SKELETON.org) 5. ✅ **路由规则**(本文档补充) -6. ✅ **快捷键系统**(本文档补充) +6. ✅ **输入绑定系统**(本文档补充) 7. ✅ **交互态处理**(本文档补充) 8. ✅ **主执行流程**(本文档补充) diff --git a/docs/min_core_draft/WM_POLICY_APPLY_COMMAND_SKELETON.org b/docs/min_core_draft/WM_POLICY_APPLY_COMMAND_SKELETON.org index 729812e..23c83b1 100644 --- a/docs/min_core_draft/WM_POLICY_APPLY_COMMAND_SKELETON.org +++ b/docs/min_core_draft/WM_POLICY_APPLY_COMMAND_SKELETON.org @@ -16,10 +16,11 @@ 补充约定: -1. =border_width= 不是 =wm_window_t= 真状态。 +1. =border_width= / =border_color= 不是 =wm_window_t= 真状态。 2. =apply_command()= 只负责更新 =frame_rect= / =float_rect= 和脏标记。 3. runtime 在布局求值和 effect 提交阶段,根据全局边框配置补全 - =WM_EFFECT_CONFIGURE_WINDOW.border_width= 。 + =WM_EFFECT_CONFIGURE_WINDOW.border_width= / + =WM_EFFECT_CONFIGURE_WINDOW.border_rgba= 。 它不负责: @@ -43,8 +44,11 @@ bool wm_policy_apply_command(wm_state_t *state, if (!state || !policy || !command || !plan) return false; - generation_relevant = command->type != WM_COMMAND_REDRAW && - command->type != WM_COMMAND_QUIT; + generation_relevant = + command->type != WM_COMMAND_REDRAW && + command->type != WM_COMMAND_QUIT && + command->type != WM_COMMAND_BEGIN_MOVE_FLOATING_INTERACTION && + command->type != WM_COMMAND_BEGIN_RESIZE_FLOATING_INTERACTION; switch (command->type) { case WM_COMMAND_NONE: @@ -66,6 +70,10 @@ bool wm_policy_apply_command(wm_state_t *state, changed = apply_focus_direction(state, policy, command, plan); break; + case WM_COMMAND_SET_WINDOW_URGENT: + changed = apply_set_window_urgent(state, command, plan); + break; + case WM_COMMAND_RAISE_WINDOW: changed = apply_raise_window(state, command, plan); break; @@ -94,16 +102,16 @@ bool wm_policy_apply_command(wm_state_t *state, changed = apply_toggle_sticky(state, command, plan); break; - case WM_COMMAND_TOGGLE_MAXIMIZE: - changed = apply_toggle_maximize(state, command, plan); + case WM_COMMAND_SET_MAXIMIZED: + changed = apply_set_maximized(state, command, plan); break; - case WM_COMMAND_TOGGLE_FULLSCREEN: - changed = apply_toggle_fullscreen(state, command, plan); + case WM_COMMAND_SET_FULLSCREEN: + changed = apply_set_fullscreen(state, command, plan); break; - case WM_COMMAND_TOGGLE_MINIMIZE: - changed = apply_toggle_minimize(state, policy, command, plan); + case WM_COMMAND_SET_MINIMIZED: + changed = apply_set_minimized(state, policy, command, plan); break; case WM_COMMAND_MOVE_FLOATING_WINDOW: @@ -114,6 +122,11 @@ bool wm_policy_apply_command(wm_state_t *state, changed = apply_resize_floating_window(state, command, plan); break; + case WM_COMMAND_BEGIN_MOVE_FLOATING_INTERACTION: + case WM_COMMAND_BEGIN_RESIZE_FLOATING_INTERACTION: + case WM_COMMAND_QUIT: + return false; + case WM_COMMAND_SET_LAYOUT: changed = apply_set_layout(state, layouts, command, plan); break; @@ -126,9 +139,6 @@ bool wm_policy_apply_command(wm_state_t *state, changed = apply_redraw(state, command, plan); break; - case WM_COMMAND_QUIT: - changed = apply_quit(state, command, plan); - break; } if (changed && generation_relevant) state->generation++; @@ -264,7 +274,7 @@ static bool apply_focus_window(wm_state_t *state, push_restack_effect(state, plan); } - plan->dirty_flags |= WM_DIRTY_RENDER; + plan->dirty_flags |= WM_DIRTY_DECORATION | WM_DIRTY_RENDER; return true; } #+END_SRC @@ -320,7 +330,27 @@ static bool apply_focus_direction(wm_state_t *state, if (!next || ws->focused_window_id == next) return false; ws->focused_window_id = next; + plan->dirty_flags |= WM_DIRTY_DECORATION | WM_DIRTY_RENDER; + return true; +} +#+END_SRC + +** =apply_set_window_urgent()= + +#+BEGIN_SRC c +static bool apply_set_window_urgent(wm_state_t *state, + const wm_command_t *command, + wm_plan_t *plan) { + wm_window_t *win = + require_window(state, command->as.set_window_urgent.window_id); + bool urgent = command->as.set_window_urgent.urgent; + + if (!win) return false; + if (win->urgent == urgent) return false; + + win->urgent = urgent; plan->dirty_flags |= WM_DIRTY_RENDER; + push_render_all_outputs(state, plan); return true; } #+END_SRC @@ -452,74 +482,91 @@ static bool apply_toggle_floating(wm_state_t *state, } #+END_SRC -** =apply_toggle_maximize()= +** =apply_set_maximized()= #+BEGIN_SRC c -static bool apply_toggle_maximize(wm_state_t *state, - const wm_command_t *command, - wm_plan_t *plan) { - wm_window_t *win = require_window(state, command->as.toggle_maximize.window_id); +static bool apply_set_maximized(wm_state_t *state, + const wm_command_t *command, + wm_plan_t *plan) { + wm_window_t *win = require_window(state, command->as.set_maximized.window_id); if (!win) return false; - if (win->geometry_mode == WM_GEOMETRY_MAXIMIZED) { - win->geometry_mode = WM_GEOMETRY_NORMAL; - } else { - win->geometry_mode = WM_GEOMETRY_MAXIMIZED; + wm_window_geometry_mode_t next_mode = win->geometry_mode; + + if (command->as.set_maximized.enabled) { + next_mode = WM_GEOMETRY_MAXIMIZED; + } else if (win->geometry_mode == WM_GEOMETRY_MAXIMIZED) { + next_mode = WM_GEOMETRY_NORMAL; } + if (next_mode == win->geometry_mode) return false; + + win->geometry_mode = next_mode; plan->dirty_flags |= WM_DIRTY_LAYOUT | WM_DIRTY_RENDER; push_render_all_outputs(state, plan); return true; } #+END_SRC -实际实现中应按 [[file:WM_COMMAND_RULES.org][WM_COMMAND_RULES.org]] 处理与 =FULLSCREEN= 和 =MINIMIZED= 的切换优先级。 +实际实现中应按 [[file:WM_COMMAND_RULES.org][WM_COMMAND_RULES.org]] 处理 =enabled= +的幂等性,以及与 =FULLSCREEN= / =MINIMIZED= 的互斥切换关系。 -** =apply_toggle_fullscreen()= +** =apply_set_fullscreen()= #+BEGIN_SRC c -static bool apply_toggle_fullscreen(wm_state_t *state, - const wm_command_t *command, - wm_plan_t *plan) { +static bool apply_set_fullscreen(wm_state_t *state, + const wm_command_t *command, + wm_plan_t *plan) { wm_window_t *win = - require_window(state, command->as.toggle_fullscreen.window_id); + require_window(state, command->as.set_fullscreen.window_id); if (!win) return false; - if (win->geometry_mode == WM_GEOMETRY_FULLSCREEN) { - win->geometry_mode = WM_GEOMETRY_NORMAL; - } else { - win->geometry_mode = WM_GEOMETRY_FULLSCREEN; + wm_window_geometry_mode_t next_mode = win->geometry_mode; + + if (command->as.set_fullscreen.enabled) { + next_mode = WM_GEOMETRY_FULLSCREEN; + } else if (win->geometry_mode == WM_GEOMETRY_FULLSCREEN) { + next_mode = WM_GEOMETRY_NORMAL; } + if (next_mode == win->geometry_mode) return false; + + win->geometry_mode = next_mode; plan->dirty_flags |= WM_DIRTY_LAYOUT | WM_DIRTY_RENDER; push_render_all_outputs(state, plan); return true; } #+END_SRC -** =apply_toggle_minimize()= +** =apply_set_minimized()= #+BEGIN_SRC c -static bool apply_toggle_minimize(wm_state_t *state, - const wm_policy_config_t *policy, - const wm_command_t *command, - wm_plan_t *plan) { +static bool apply_set_minimized(wm_state_t *state, + const wm_policy_config_t *policy, + const wm_command_t *command, + wm_plan_t *plan) { wm_window_t *win = - require_window(state, command->as.toggle_minimize.window_id); + require_window(state, command->as.set_minimized.window_id); if (!win) return false; wm_workspace_t *ws = require_workspace(state, win->workspace_id); + wm_window_geometry_mode_t next_mode = win->geometry_mode; - if (win->geometry_mode == WM_GEOMETRY_MINIMIZED) { - win->geometry_mode = WM_GEOMETRY_NORMAL; - } else { - win->geometry_mode = WM_GEOMETRY_MINIMIZED; + if (command->as.set_minimized.enabled) { + next_mode = WM_GEOMETRY_MINIMIZED; + } else if (win->geometry_mode == WM_GEOMETRY_MINIMIZED) { + next_mode = WM_GEOMETRY_NORMAL; + } - if (policy->minimize_clears_focus && - ws && - ws->focused_window_id == win->id) { - maybe_refocus_workspace(state, ws); - } + if (next_mode == win->geometry_mode) return false; + + win->geometry_mode = next_mode; + + if (command->as.set_minimized.enabled && + policy->minimize_clears_focus && + ws && + ws->focused_window_id == win->id) { + maybe_refocus_workspace(state, ws); } plan->dirty_flags |= WM_DIRTY_LAYOUT | WM_DIRTY_RENDER; @@ -669,29 +716,18 @@ static bool apply_redraw(wm_state_t *state, } #+END_SRC -** =apply_quit()= - -#+BEGIN_SRC c -static bool apply_quit(wm_state_t *state, - const wm_command_t *command, - wm_plan_t *plan) { - (void)state; - (void)command; - (void)plan; - return true; -} -#+END_SRC - -通常情况下 =WM_COMMAND_QUIT= 会由 runtime 在命令应用外层直接拦截并调用 -=wm_runtime_stop()= 。保留 =apply_quit()= 这个空实现,只是为了让 switch -结构保持完整。 +通常情况下 =WM_COMMAND_QUIT= / =WM_COMMAND_BEGIN_*_INTERACTION= 都会由 +=runtime= 在命令应用外层直接拦截,因此 =wm_policy_apply_command()= 不应真正看见 +这些 runtime control commands。 * 推荐的后处理步骤 在 =apply_*()= 返回后,建议由上层统一做这些事,而不是在每个分支里直接跑布局: 1. 如果有 =WM_DIRTY_LAYOUT= ,则对每个 output 的当前 workspace 重新计算 layout。 -2. 根据窗口的 =geometry_mode= 、=floating= 和 layout 结果解析最终矩形。 +2. 若有 =WM_DIRTY_LAYOUT= 或 =WM_DIRTY_DECORATION= ,则根据窗口的 + =geometry_mode= 、=floating= 、焦点关系和全局边框配置解析最终矩形、 + 有效边框宽度与边框颜色。 =sticky= 窗口由于满足 =sticky => floating= ,因此同样走 =float_rect= 路径。 3. 生成 =WM_EFFECT_CONFIGURE_WINDOW= 。 4. 若有 =WM_DIRTY_STACK= ,生成 =WM_EFFECT_RESTACK_WINDOWS= 。 diff --git a/docs/min_core_draft/wm_backend.h b/docs/min_core_draft/wm_backend.h index 95e6f27..a212894 100644 --- a/docs/min_core_draft/wm_backend.h +++ b/docs/min_core_draft/wm_backend.h @@ -1,5 +1,6 @@ #pragma once +#include "wm_binding.h" #include "wm_event.h" #include "wm_plan.h" @@ -18,6 +19,22 @@ typedef enum wm_backend_next_result_t { typedef struct wm_backend_api_t { bool (*init)(wm_backend_t *backend); + /* + * 安装 bootstrap 固定下来的键盘绑定表。 + * X11 这类需要被动抓键的平台可以在这里建立平台侧 grab; + * 总能收到所有键盘事件的 backend 可以把它留空。 + */ + bool (*set_keybindings)(wm_backend_t *backend, + const wm_key_binding_table_t *keybindings); + /* + * 安装 bootstrap 固定下来的鼠标按键绑定表。 + * X11 这类需要被动抓按钮的平台可以把它缓存下来,并在 root 或新受管窗口 + * 上按 target 规则建立 grab;总能收到所有 pointer button 事件的 backend + * 可以把它留空。 + */ + bool (*set_pointer_bindings)( + wm_backend_t *backend, + const wm_pointer_binding_table_t *pointer_bindings); /* * 返回一个 backend 轮询结果。 * 只有返回 WM_BACKEND_NEXT_EVENT 时,out 才包含一个完整的 runtime 输入事件。 diff --git a/docs/min_core_draft/wm_binding.h b/docs/min_core_draft/wm_binding.h new file mode 100644 index 0000000..496d852 --- /dev/null +++ b/docs/min_core_draft/wm_binding.h @@ -0,0 +1,86 @@ +#pragma once + +#include "wm_command.h" +#include "wm_types.h" +#include +#include + +/* + * 输入绑定只匹配 backend 已归一化后的输入事件。 + * backend 负责把平台原始输入翻译为 wm_event_t,并决定是否折叠 auto-repeat。 + */ +typedef enum wm_binding_match_mode_t { + // modifiers 必须与事件完全一致 + WM_BINDING_MATCH_EXACT, + // 事件允许带有更多修饰键,但必须至少包含 binding.modifiers + WM_BINDING_MATCH_ALLOW_EXTRA_MODIFIERS, +} wm_binding_match_mode_t; + +typedef struct wm_key_binding_t { + wm_keysym_t keysym; + wm_modifier_mask_t modifiers; + wm_binding_match_mode_t match_mode; + // 命中的绑定会直接复制这条命令模板进入 command buffer + wm_command_t command; +} wm_key_binding_t; + +typedef struct wm_key_binding_table_t { + const wm_key_binding_t *items; + size_t count; +} wm_key_binding_table_t; + +/* + * 核心只关心 root / window 两类命中目标: + * - bar / status / widget 等点击区域属于核心外服务,不进入最小核心绑定系统 + * - 当事件未命中受管窗口时,wm_event_t.window == WM_WINDOW_ID_INVALID + */ +typedef enum wm_pointer_binding_target_t { + WM_POINTER_BINDING_TARGET_ANY, + WM_POINTER_BINDING_TARGET_ROOT, + WM_POINTER_BINDING_TARGET_WINDOW, +} wm_pointer_binding_target_t; + +/* + * 最小核心当前只定义“按下触发”的鼠标绑定; + * button release 主要保留给 move/resize 交互结束逻辑。 + * + * 若 command 是窗口目标命令,且模板中的 window_id 为 + * WM_WINDOW_ID_INVALID,则 route_pointer_press() 应把它替换为当前点击的 + * event->window。 + */ +typedef struct wm_pointer_binding_t { + wm_button_t button; + wm_modifier_mask_t modifiers; + wm_binding_match_mode_t match_mode; + wm_pointer_binding_target_t target; + wm_command_t command; +} wm_pointer_binding_t; + +typedef struct wm_pointer_binding_table_t { + const wm_pointer_binding_t *items; + size_t count; +} wm_pointer_binding_table_t; + +/* + * 查找与当前按键事件匹配的绑定。 + * 匹配顺序固定为: + * 1. 所有 exact 绑定,按数组顺序扫描 + * 2. 所有 allow-extra-modifiers 绑定,按数组顺序扫描 + * + * 当前最小核心不内建“默认绑定生成规则”;像 SUPER+1 这类行为也必须显式 + * 出现在 key binding table 里。 + */ +const wm_key_binding_t *wm_key_binding_find( + const wm_key_binding_table_t *table, wm_keysym_t keysym, + wm_modifier_mask_t modifiers); + +/* + * 查找与当前按钮按下事件匹配的鼠标绑定。 + * hits_window == true 表示事件命中了某个受管窗口; + * hits_window == false 表示事件落在 root / 空白区域。 + * + * 匹配顺序与键盘绑定一致:先 exact,再 allow-extra-modifiers。 + */ +const wm_pointer_binding_t *wm_pointer_binding_find( + const wm_pointer_binding_table_t *table, wm_button_t button, + wm_modifier_mask_t modifiers, bool hits_window); diff --git a/docs/min_core_draft/wm_command.h b/docs/min_core_draft/wm_command.h index cdac935..df0cc32 100644 --- a/docs/min_core_draft/wm_command.h +++ b/docs/min_core_draft/wm_command.h @@ -16,6 +16,7 @@ typedef enum wm_command_type_t { WM_COMMAND_UNMANAGE_WINDOW, WM_COMMAND_FOCUS_WINDOW, WM_COMMAND_FOCUS_DIRECTION, + WM_COMMAND_SET_WINDOW_URGENT, WM_COMMAND_RAISE_WINDOW, WM_COMMAND_LOWER_WINDOW, WM_COMMAND_SWITCH_WORKSPACE, @@ -23,11 +24,13 @@ typedef enum wm_command_type_t { WM_COMMAND_SEND_WINDOW_TO_OUTPUT, WM_COMMAND_TOGGLE_FLOATING, WM_COMMAND_TOGGLE_STICKY, - WM_COMMAND_TOGGLE_MAXIMIZE, - WM_COMMAND_TOGGLE_FULLSCREEN, - WM_COMMAND_TOGGLE_MINIMIZE, + WM_COMMAND_SET_MAXIMIZED, + WM_COMMAND_SET_FULLSCREEN, + WM_COMMAND_SET_MINIMIZED, WM_COMMAND_MOVE_FLOATING_WINDOW, WM_COMMAND_RESIZE_FLOATING_WINDOW, + WM_COMMAND_BEGIN_MOVE_FLOATING_INTERACTION, + WM_COMMAND_BEGIN_RESIZE_FLOATING_INTERACTION, WM_COMMAND_SET_LAYOUT, WM_COMMAND_CYCLE_LAYOUT, WM_COMMAND_REDRAW, @@ -58,6 +61,11 @@ typedef struct wm_command_t { wm_focus_direction_t direction; } focus_direction; + struct { + wm_window_id_t window_id; + bool urgent; + } set_window_urgent; + struct { wm_window_id_t window_id; } raise_window; @@ -92,15 +100,18 @@ typedef struct wm_command_t { struct { wm_window_id_t window_id; - } toggle_maximize; + bool enabled; + } set_maximized; struct { wm_window_id_t window_id; - } toggle_fullscreen; + bool enabled; + } set_fullscreen; struct { wm_window_id_t window_id; - } toggle_minimize; + bool enabled; + } set_minimized; struct { wm_window_id_t window_id; @@ -117,6 +128,14 @@ typedef struct wm_command_t { int16_t dh; } resize_floating_window; + struct { + wm_window_id_t window_id; + } begin_move_floating_interaction; + + struct { + wm_window_id_t window_id; + } begin_resize_floating_interaction; + struct { wm_workspace_id_t workspace_id; wm_layout_id_t layout_id; diff --git a/docs/min_core_draft/wm_desc.h b/docs/min_core_draft/wm_desc.h new file mode 100644 index 0000000..c5b5913 --- /dev/null +++ b/docs/min_core_draft/wm_desc.h @@ -0,0 +1,36 @@ +#pragma once + +#include "wm_types.h" + +typedef struct wm_output_info_t { + char *name; + wm_rect_t geometry; +} wm_output_info_t; + +/* + * 由 backend 提供的窗口基础信息。 + * + * 这些字符串由调用方提供;state 如需长期持有,应自行复制。 + */ +typedef struct wm_window_info_t { + wm_window_id_t id; + wm_rect_t frame_rect; + + const char *title; + const char *app_id; + const char *class_name; + const char *instance_name; + + wm_window_geometry_mode_t geometry_mode; + bool urgent; + bool fixed_size; + bool skip_taskbar; +} wm_window_info_t; + +typedef struct wm_workspace_desc_t { + size_t output_index; + const char *name; + const wm_layout_id_t *layout_ids; + size_t layout_count; + wm_layout_id_t initial_layout_id; +} wm_workspace_desc_t; diff --git a/docs/min_core_draft/wm_event.h b/docs/min_core_draft/wm_event.h index dbb0804..737c42c 100644 --- a/docs/min_core_draft/wm_event.h +++ b/docs/min_core_draft/wm_event.h @@ -1,6 +1,6 @@ #pragma once -#include "wm_types.h" +#include "wm_desc.h" /* * wm_event_t 表示“runtime 真正关心的输入事实”: @@ -10,17 +10,19 @@ * 的返回结果表达,而不是伪装成一个事件类型 */ typedef enum wm_event_type_t { + // 最小核心里的快捷键只由 key press 触发,不单独暴露 key release 事件 WM_EVENT_KEY_PRESS, + // 鼠标绑定只匹配 button press;button release 主要供交互态结束拖拽使用 WM_EVENT_POINTER_BUTTON_PRESS, WM_EVENT_POINTER_BUTTON_RELEASE, WM_EVENT_POINTER_MOTION, WM_EVENT_WINDOW_MAP_REQUEST, - WM_EVENT_WINDOW_UNMAP, - WM_EVENT_WINDOW_DESTROY, + WM_EVENT_WINDOW_REMOVE, WM_EVENT_WINDOW_METADATA_CHANGED, + WM_EVENT_WINDOW_HINTS_CHANGED, + WM_EVENT_WINDOW_ACTIVATE_REQUEST, + WM_EVENT_WINDOW_STATE_REQUEST, WM_EVENT_CONFIGURE_REQUEST, - WM_EVENT_TIMER_TICK, - WM_EVENT_STATUS_TICK, } wm_event_type_t; typedef struct wm_key_press_event_t { @@ -63,16 +65,27 @@ typedef struct wm_pointer_motion_event_t { } wm_pointer_motion_event_t; typedef struct wm_window_map_request_event_t { - wm_window_id_t window; + wm_window_info_t info; + // 仅用于 manage 路由判断,不进入持久 state + wm_window_id_t transient_for; + // 仅用于 manage 路由判断,不进入持久 state + bool is_dialog; } wm_window_map_request_event_t; -typedef struct wm_window_unmap_event_t { - wm_window_id_t window; -} wm_window_unmap_event_t; +/* + * 归一化后的窗口移除事件。 + * backend 把 withdraw / destroy 这类平台差异折叠成单一 remove 事件, + * core 只按 reason 区分“为何离开 managed 集合”。 + */ +typedef enum wm_window_remove_reason_t { + WM_WINDOW_REMOVE_WITHDRAWN, + WM_WINDOW_REMOVE_DESTROY, +} wm_window_remove_reason_t; -typedef struct wm_window_destroy_event_t { +typedef struct wm_window_remove_event_t { wm_window_id_t window; -} wm_window_destroy_event_t; + wm_window_remove_reason_t reason; +} wm_window_remove_event_t; typedef enum wm_window_meta_changed_flags_t { WM_WINDOW_META_CHANGED_NONE = 0, @@ -97,19 +110,91 @@ typedef struct wm_window_metadata_changed_event_t { const char *instance_name; } wm_window_metadata_changed_event_t; +typedef enum wm_window_hint_changed_flags_t { + WM_WINDOW_HINT_CHANGED_NONE = 0, + WM_WINDOW_HINT_CHANGED_URGENT = 1u << 0, + WM_WINDOW_HINT_CHANGED_FIXED_SIZE = 1u << 1, + WM_WINDOW_HINT_CHANGED_SKIP_TASKBAR = 1u << 2, +} wm_window_hint_changed_flags_t; + +/* + * 客户端属性 / hint 变化的归一化结果。 + * 这类事件不是控制请求,不经过 command 路由; + * runtime 直接把结果同步到 wm_window_t 中的 hint/capability 字段。 + */ +typedef struct wm_window_hints_changed_event_t { + wm_window_id_t window; + uint32_t changed_fields; + bool urgent; + bool fixed_size; + bool skip_taskbar; +} wm_window_hints_changed_event_t; + +/* + * 归一化后的窗口激活请求来源。 + * 取值对齐 EWMH _NET_ACTIVE_WINDOW 的 source indication: + * - 0: 旧客户端 / 未指明来源 + * - 1: 应用自身发起 + * - 2: pager / taskbar / window switcher 发起 + */ +typedef enum wm_window_activation_source_t { + WM_WINDOW_ACTIVATION_SOURCE_LEGACY = 0, + WM_WINDOW_ACTIVATION_SOURCE_APPLICATION = 1, + WM_WINDOW_ACTIVATION_SOURCE_PAGER = 2, +} wm_window_activation_source_t; + +typedef struct wm_window_activate_request_event_t { + wm_window_id_t window; + wm_window_activation_source_t source; +} wm_window_activate_request_event_t; + +/* + * 归一化后的窗口模式切换请求。 + * backend 保留 add/remove/toggle 这类原始协议语义; + * route_event() 再结合当前 wm_state_t 翻译成最终的 SET_* 命令。 + */ +typedef enum wm_window_state_request_kind_t { + WM_WINDOW_STATE_REQUEST_FULLSCREEN, + WM_WINDOW_STATE_REQUEST_MAXIMIZED, + WM_WINDOW_STATE_REQUEST_MINIMIZED, +} wm_window_state_request_kind_t; + +typedef enum wm_window_state_request_action_t { + WM_WINDOW_STATE_REQUEST_ACTION_ADD, + WM_WINDOW_STATE_REQUEST_ACTION_REMOVE, + WM_WINDOW_STATE_REQUEST_ACTION_TOGGLE, +} wm_window_state_request_action_t; + +typedef struct wm_window_state_request_event_t { + wm_window_id_t window; + wm_window_state_request_kind_t kind; + wm_window_state_request_action_t action; +} wm_window_state_request_event_t; + +typedef enum wm_configure_request_field_t { + WM_CONFIGURE_REQUEST_FIELD_NONE = 0, + WM_CONFIGURE_REQUEST_FIELD_X = 1u << 0, + WM_CONFIGURE_REQUEST_FIELD_Y = 1u << 1, + WM_CONFIGURE_REQUEST_FIELD_WIDTH = 1u << 2, + WM_CONFIGURE_REQUEST_FIELD_HEIGHT = 1u << 3, +} wm_configure_request_field_t; + +/* + * 归一化后的窗口配置请求。 + * 这类事件只表达稀疏几何请求,不承载 fullscreen/maximize/minimize + * 之类窗口模式切换;后者应进入独立的 state request event。 + * + * x / y / width / height 中只有 changed_fields 指示的成员才有意义。 + */ typedef struct wm_configure_request_event_t { wm_window_id_t window; - wm_rect_t requested_rect; + uint32_t changed_fields; + int32_t x; + int32_t y; + int32_t width; + int32_t height; } wm_configure_request_event_t; -typedef struct wm_timer_tick_event_t { - wm_time_ms_t now_ms; -} wm_timer_tick_event_t; - -typedef struct wm_status_tick_event_t { - wm_time_ms_t now_ms; -} wm_status_tick_event_t; - typedef struct wm_event_t { wm_event_type_t type; // backend 归一化后的事件时间值(毫秒),不是 wall-clock 时间戳 @@ -120,11 +205,11 @@ typedef struct wm_event_t { wm_pointer_button_event_t pointer_button_release; wm_pointer_motion_event_t pointer_motion; wm_window_map_request_event_t window_map_request; - wm_window_unmap_event_t window_unmap; - wm_window_destroy_event_t window_destroy; + wm_window_remove_event_t window_remove; wm_window_metadata_changed_event_t window_metadata_changed; + wm_window_hints_changed_event_t window_hints_changed; + wm_window_activate_request_event_t window_activate_request; + wm_window_state_request_event_t window_state_request; wm_configure_request_event_t configure_request; - wm_timer_tick_event_t timer_tick; - wm_status_tick_event_t status_tick; } data; } wm_event_t; diff --git a/docs/min_core_draft/wm_plan.h b/docs/min_core_draft/wm_plan.h index 00d94d8..d4920bc 100644 --- a/docs/min_core_draft/wm_plan.h +++ b/docs/min_core_draft/wm_plan.h @@ -8,7 +8,9 @@ typedef enum wm_dirty_flags_t { WM_DIRTY_LAYOUT = 1u << 1, WM_DIRTY_STACK = 1u << 2, WM_DIRTY_OUTPUT = 1u << 3, - WM_DIRTY_RENDER = 1u << 4, + // 只影响窗口装饰,不改变 layout 结果;例如焦点切换导致边框颜色变化 + WM_DIRTY_DECORATION = 1u << 4, + WM_DIRTY_RENDER = 1u << 5, } wm_dirty_flags_t; typedef enum wm_effect_type_t { @@ -36,6 +38,7 @@ typedef struct wm_effect_t { wm_window_id_t window_id; wm_rect_t rect; // 最终外框矩形(包含边框) uint16_t border_width; // runtime 解析出的有效边框宽度 + wm_rgba32_t border_rgba; // runtime 解析出的有效边框颜色 } configure_window; struct { diff --git a/docs/min_core_draft/wm_policy.h b/docs/min_core_draft/wm_policy.h index d68f3c7..bcecd18 100644 --- a/docs/min_core_draft/wm_policy.h +++ b/docs/min_core_draft/wm_policy.h @@ -1,5 +1,6 @@ #pragma once +#include "wm_binding.h" #include "wm_command.h" #include "wm_event.h" #include "wm_layout.h" @@ -19,7 +20,10 @@ void wm_command_buffer_shutdown(wm_command_buffer_t *buffer); bool wm_command_buffer_push(wm_command_buffer_t *buffer, wm_command_t command); -bool wm_policy_route_event(const wm_state_t *state, const wm_event_t *event, +bool wm_policy_route_event(const wm_state_t *state, + const wm_key_binding_table_t *keybindings, + const wm_pointer_binding_table_t *pointer_bindings, + const wm_event_t *event, wm_command_buffer_t *out); bool wm_policy_apply_command(wm_state_t *state, diff --git a/docs/min_core_draft/wm_runtime.h b/docs/min_core_draft/wm_runtime.h index a900296..a1dfd06 100644 --- a/docs/min_core_draft/wm_runtime.h +++ b/docs/min_core_draft/wm_runtime.h @@ -1,5 +1,6 @@ #pragma once +#include "wm_binding.h" #include "wm_backend.h" #include "wm_layout.h" #include "wm_plan.h" @@ -23,6 +24,11 @@ typedef struct wm_interaction_state_t { wm_rect_t window_origin_rect; } wm_interaction_state_t; +typedef struct wm_border_palette_t { + wm_rgba32_t normal_rgba; + wm_rgba32_t focused_rgba; +} wm_border_palette_t; + typedef struct wm_runtime_bootstrap_t { /* * 输出配置(启动时扫描显示器后填充)。 @@ -55,6 +61,18 @@ typedef struct wm_runtime_bootstrap_t { */ wm_policy_config_t policy; + /* + * 不可变键盘绑定表。 + * 当前草案把它视为 bootstrap 提供的只读视图;items 的生命周期必须覆盖 runtime。 + */ + wm_key_binding_table_t keybindings; + + /* + * 不可变鼠标按键绑定表。 + * bar / status 点击属于核心外服务,因此这里只覆盖 root / window 目标。 + */ + wm_pointer_binding_table_t pointer_bindings; + /* * 全局统一边框宽度。 * 它不是窗口真状态。runtime 在提交 configure effect 时: @@ -63,6 +81,14 @@ typedef struct wm_runtime_bootstrap_t { */ uint16_t border_width; + /* + * 全局边框颜色配置。 + * 它不是窗口真状态。runtime 在提交 configure effect 时根据当前焦点关系解析: + * - 当前 workspace 的 focused window -> focused_rgba + * - 其余窗口 -> normal_rgba + */ + wm_border_palette_t border_palette; + /* * Existing windows discovered during startup should be translated into these * commands instead of being injected into wm_state_t directly. @@ -80,7 +106,10 @@ typedef struct wm_runtime_t { wm_command_buffer_t command_buffer; wm_layout_registry_t layouts; wm_policy_config_t policy; + wm_key_binding_table_t keybindings; + wm_pointer_binding_table_t pointer_bindings; uint16_t border_width; + wm_border_palette_t border_palette; wm_interaction_state_t interaction; wm_backend_t backend; wm_service_registry_t services; diff --git a/docs/min_core_draft/wm_service.h b/docs/min_core_draft/wm_service.h index 8aa3256..01efb8a 100644 --- a/docs/min_core_draft/wm_service.h +++ b/docs/min_core_draft/wm_service.h @@ -9,7 +9,6 @@ typedef enum wm_service_event_type_t { WM_SERVICE_EVENT_RUNTIME_STARTED, WM_SERVICE_EVENT_RUNTIME_STOPPING, WM_SERVICE_EVENT_WINDOW_METADATA_CHANGED, - WM_SERVICE_EVENT_STATUS_TICK, WM_SERVICE_EVENT_RENDER_OUTPUT, } wm_service_event_type_t; diff --git a/docs/min_core_draft/wm_state.h b/docs/min_core_draft/wm_state.h index e22683f..78d5f18 100644 --- a/docs/min_core_draft/wm_state.h +++ b/docs/min_core_draft/wm_state.h @@ -16,6 +16,7 @@ typedef struct wm_window_t { bool floating; bool sticky; bool urgent; + bool fixed_size; // 几何信息(均为包含边框后的外框矩形) wm_rect_t float_rect; // floating 模式下记忆的外框矩形 @@ -164,6 +165,9 @@ void wm_state_window_set_sticky(wm_state_t *state, wm_window_id_t window_id, bool sticky); void wm_state_window_set_urgent(wm_state_t *state, wm_window_id_t window_id, bool urgent); +void wm_state_window_set_fixed_size(wm_state_t *state, + wm_window_id_t window_id, + bool fixed_size); void wm_state_window_set_skip_taskbar(wm_state_t *state, wm_window_id_t window_id, bool skip_taskbar); diff --git a/docs/min_core_draft/wm_types.h b/docs/min_core_draft/wm_types.h index de41cdc..c4c30d5 100644 --- a/docs/min_core_draft/wm_types.h +++ b/docs/min_core_draft/wm_types.h @@ -23,6 +23,11 @@ typedef uint32_t wm_time_ms_t; */ typedef uint32_t wm_keysym_t; typedef uint32_t wm_modifier_mask_t; +/* + * 通用 32-bit RGBA 颜色值,编码为 0xRRGGBBAA。 + * backend 若不支持 alpha,可忽略最低 8 bit 或在提交平台 effect 时自行量化。 + */ +typedef uint32_t wm_rgba32_t; // 无效 ID 标记 #define WM_WINDOW_ID_INVALID ((wm_window_id_t)0) @@ -74,6 +79,14 @@ typedef struct wm_rect_t { int32_t height; } wm_rect_t; +/* + * 核心统一使用 geometry_mode 表示窗口的显示几何模式。 + * 其中 WM_GEOMETRY_MINIMIZED 覆盖所有“被最小化/被图标化”的不可见状态: + * - 在 X11 上,它对应 ICCCM 的 IconicState + * - 来自 WM_CHANGE_STATE(Iconic) 的请求也应折叠到这个模式 + * + * 最小核心不单独暴露 "iconic" 枚举,避免与 minimized 形成两套并行语义。 + */ typedef enum wm_window_geometry_mode_t { WM_GEOMETRY_NORMAL, WM_GEOMETRY_MAXIMIZED, diff --git a/src/backend/x11.c b/src/backend/x11.c index 9faa58b..7953f26 100644 --- a/src/backend/x11.c +++ b/src/backend/x11.c @@ -10,8 +10,8 @@ #include #include "core/backend.h" +#include "core/wm_desc.h" #include "utils.h" -#include "wm_desc.h" struct wm_backend_t { xcb_connection_t *conn; diff --git a/src/core/backend.h b/src/core/backend.h index 05bdc0e..90f0dbe 100644 --- a/src/core/backend.h +++ b/src/core/backend.h @@ -2,7 +2,7 @@ #include -#include "wm_desc.h" +#include "core/wm_desc.h" typedef struct wm_backend_t wm_backend_t; diff --git a/src/core/event.h b/src/core/event.h index 9db237d..cc2c508 100644 --- a/src/core/event.h +++ b/src/core/event.h @@ -1,7 +1,8 @@ #pragma once #include -#include "core/types.h" + +#include "core/wm_desc.h" typedef enum wm_event_type_t { WM_EVENT_KEY_PRESS, @@ -9,9 +10,11 @@ typedef enum wm_event_type_t { WM_EVENT_POINTER_BUTTON_RELEASE, WM_EVENT_POINTER_MOTION, WM_EVENT_WINDOW_MAP_REQUEST, - WM_EVENT_WINDOW_UNMAP, - WM_EVENT_WINDOW_DESTROY, + WM_EVENT_WINDOW_REMOVE, WM_EVENT_WINDOW_METADATA_CHANGED, + WM_EVENT_WINDOW_HINTS_CHANGED, + WM_EVENT_WINDOW_ACTIVATE_REQUEST, + WM_EVENT_WINDOW_STATE_REQUEST, WM_EVENT_CONFIGURE_REQUEST, } wm_event_type_t; @@ -51,6 +54,99 @@ typedef struct wm_pointer_motion_event_t { wm_point_t local; } wm_pointer_motion_event_t; +typedef struct wm_window_map_request_event_t { + wm_window_info_t info; + wm_window_id_t transient_for; + bool is_dialog; +} wm_window_map_request_event_t; + +typedef enum wm_window_remove_reason_t { + WM_WINDOW_REMOVE_WITHDRAWN, + WM_WINDOW_REMOVE_DESTROY, +} wm_window_remove_reason_t; + +typedef struct wm_window_remove_event_t { + wm_window_id_t window; + wm_window_remove_reason_t reason; +} wm_window_remove_event_t; + +typedef enum wm_window_metadata_change_flags_t { + WM_WINDOW_METADATA_CHANGE_TITLE = 1u << 0, + WM_WINDOW_METADATA_CHANGE_APP_ID = 1u << 1, + WM_WINDOW_METADATA_CHANGE_CLASS = 1u << 2, + WM_WINDOW_METADATA_CHANGE_INSTANCE = 1u << 3, +} wm_window_metadata_change_flags_t; + +typedef struct wm_window_metadata_change_event_t { + wm_window_id_t window; + uint32_t changed_fields; + const char *title; + const char *app_id; + const char *class_name; + const char *instance_name; +} wm_window_metadata_change_event_t; + +typedef enum wm_window_hint_changed_flags_t { + WM_WINDOW_HINT_CHANGED_NONE = 0, + WM_WINDOW_HINT_CHANGED_URGENT = 1u << 0, + WM_WINDOW_HINT_CHANGED_FIXED_SIZE = 1u << 1, + WM_WINDOW_HINT_CHANGED_SKIP_TASKBAR = 1u << 2, +} wm_window_hint_changed_flags_t; + +typedef struct wm_window_hints_changed_event_t { + wm_window_id_t window; + uint32_t changed_fields; + bool urgent; + bool fixed_size; + bool skip_taskbar; +} wm_window_hints_changed_event_t; + +typedef enum wm_window_activation_source_t { + WM_WINDOW_ACTIVATION_SOURCE_LEGACY = 0, + WM_WINDOW_ACTIVATION_SOURCE_APPLICATION = 1, + WM_WINDOW_ACTIVATION_SOURCE_PAGER = 2, +} wm_window_activation_source_t; + +typedef struct wm_window_activate_request_event_t { + wm_window_id_t window; + wm_window_activation_source_t source; +} wm_window_activate_request_event_t; + +typedef enum wm_window_state_request_kind_t { + WM_WINDOW_STATE_REQUEST_FULLSCREEN, + WM_WINDOW_STATE_REQUEST_MAXIMIZED, + WM_WINDOW_STATE_REQUEST_MINIMIZED, +} wm_window_state_request_kind_t; + +typedef enum wm_window_state_request_action_t { + WM_WINDOW_STATE_REQUEST_ACTION_ADD, + WM_WINDOW_STATE_REQUEST_ACTION_REMOVE, + WM_WINDOW_STATE_REQUEST_ACTION_TOGGLE, +} wm_window_state_request_action_t; + +typedef struct wm_window_state_request_event_t { + wm_window_id_t window; + wm_window_state_request_kind_t kind; + wm_window_state_request_action_t action; +} wm_window_state_request_event_t; + +typedef enum wm_configure_request_field_t { + WM_CONFIGURE_REQUEST_FIELD_NONE = 0, + WM_CONFIGURE_REQUEST_FIELD_X = 1u << 0, + WM_CONFIGURE_REQUEST_FIELD_Y = 1u << 1, + WM_CONFIGURE_REQUEST_FIELD_WIDTH = 1u << 2, + WM_CONFIGURE_REQUEST_FIELD_HEIGHT = 1u << 3, +} wm_configure_request_field_t; + +typedef struct wm_configure_request_event_t { + wm_window_id_t window; + uint32_t changed_fields; + int32_t x; + int32_t y; + int32_t width; + int32_t height; +} wm_configure_request_event_t; + typedef struct wm_event_t { wm_event_type_t type; uint32_t time_ms; @@ -59,5 +155,12 @@ typedef struct wm_event_t { wm_pointer_button_event_t pointer_button_press; wm_pointer_button_event_t pointer_button_release; wm_pointer_motion_event_t pointer_motion; + wm_window_map_request_event_t window_map_request; + wm_window_remove_event_t window_remove; + wm_window_metadata_change_event_t window_metadata_change; + wm_window_hints_changed_event_t window_hints_changed; + wm_window_activate_request_event_t window_activate_request; + wm_window_state_request_event_t window_state_request; + wm_configure_request_event_t configure_request; } data; } wm_event_t; diff --git a/src/core/runtime.c b/src/core/runtime.c index 0b7d81c..658950c 100644 --- a/src/core/runtime.c +++ b/src/core/runtime.c @@ -5,8 +5,8 @@ #include "core/backend.h" #include "core/state.h" #include "core/types.h" +#include "core/wm_desc.h" #include "utils.h" -#include "wm_desc.h" bool wm_runtime_init(wm_runtime_t *runtime) { p_clear(runtime, 1); diff --git a/src/core/state.c b/src/core/state.c index d6f9bb5..b23ce7e 100644 --- a/src/core/state.c +++ b/src/core/state.c @@ -4,8 +4,8 @@ #include #include "core/types.h" +#include "core/wm_desc.h" #include "utils.h" -#include "wm_desc.h" void wm_state_init(wm_state_t *state, const wm_output_info_t *outputs, size_t output_count, const wm_workspace_desc_t *workspaces, diff --git a/src/core/state.h b/src/core/state.h index de6aba0..373520f 100644 --- a/src/core/state.h +++ b/src/core/state.h @@ -3,7 +3,7 @@ #include #include "core/types.h" -#include "wm_desc.h" +#include "core/wm_desc.h" /* 核心实体定义 */ typedef struct wm_window_t { @@ -75,7 +75,8 @@ typedef struct wm_state_t { * - wm_state_init() / wm_state_cleanup() 必须成对调用 * - 同一个 state 生命周期内,wm_state_init() 不允许重复调用 * - 除 wm_state_init() 外,其余 state 相关接口都要求 state 已初始化 - * - workspaces[i] 必须满足 wm_workspace_desc_valid(&workspaces[i], output_count) + * - workspaces[i] 必须满足 wm_workspace_desc_valid(&workspaces[i], + * output_count) * - 传入空指针、未初始化对象或违反前置条件属于调用方错误 * * 初始化时根据描述表构建 outputs[] 与 workspaces[]。 @@ -84,8 +85,7 @@ typedef struct wm_state_t { * workspace;若某个 output 没有任何归属 workspace,wm_state_init() 会失败。 */ void wm_state_init(wm_state_t *state, const wm_output_info_t *outputs, - size_t output_count, - const wm_workspace_desc_t *workspaces, + size_t output_count, const wm_workspace_desc_t *workspaces, size_t workspace_count); void wm_state_cleanup(wm_state_t *state); diff --git a/src/wm_desc.h b/src/core/wm_desc.h similarity index 100% rename from src/wm_desc.h rename to src/core/wm_desc.h