From 7de9cd500bffa09d908111905ba196caff1ccee3 Mon Sep 17 00:00:00 2001 From: Zedhugh Chen Date: Wed, 11 Mar 2026 05:54:30 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=B8=BA=20workspace=20=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=E5=8F=AF=E7=94=A8=E5=B8=83=E5=B1=80=E5=88=97=E8=A1=A8?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - wm_workspace_t 新增 available_layouts 和 layout_count 字段 - 新增 WM_COMMAND_CYCLE_LAYOUT 命令支持循环切换布局 - 添加 workspace 布局列表操作 API - 完善命令规则和实现骨架 - 新增布局列表配置示例和文档 --- docs/min_core_draft/README.org | 8 +++ docs/min_core_draft/WM_COMMAND_RULES.org | 20 ++++++ .../WM_POLICY_APPLY_COMMAND_SKELETON.org | 43 ++++++++++++ .../WORKSPACE_CONFIG_EXAMPLES.org | 66 +++++++++++++++++++ docs/min_core_draft/wm_command.h | 6 ++ docs/min_core_draft/wm_state.h | 37 ++++++++++- 6 files changed, 179 insertions(+), 1 deletion(-) diff --git a/docs/min_core_draft/README.org b/docs/min_core_draft/README.org index 6ee4d48..caba726 100644 --- a/docs/min_core_draft/README.org +++ b/docs/min_core_draft/README.org @@ -268,6 +268,11 @@ move/resize 这类拖拽交互不属于 =wm_state_t= 真状态,但也不能继 工作区名称直接存储在 =wm_workspace_t= 中,用于状态栏显示。 核心算法不应依赖此字段。 +8. =workspace layouts= +每个 workspace 维护一个可用布局列表(=available_layouts=),在启动时根据配置分配,运行时固定。 +workspace 的 =layout_id= 字段指向当前活动的布局(列表中的一个)。 +使用 =CYCLE_LAYOUT= 命令在可用布局间循环切换。 + * 核心不变量 1. 每个窗口必须且只能属于一个工作区。 @@ -283,6 +288,7 @@ move/resize 这类拖拽交互不属于 =wm_state_t= 真状态,但也不能继 11. layout 的 name 字段同时用于状态栏显示,应为 1-2 个字符的简短标识符(如 "T", "M")。 12. 窗口标题、类名等元数据存储在 =wm_window_t= 中,与控制状态一同管理。 13. 工作区名称存储在 =wm_workspace_t= 中,核心算法不应依赖此字段。 +14. 每个 workspace 的可用布局列表在启动时分配,运行时固定,只能通过 =CYCLE_LAYOUT= 或 =SET_LAYOUT= 命令切换当前活动布局。 * Generation 字段 @@ -365,6 +371,8 @@ Workspace-Output 配置示例和最佳实践。 =MANAGE_WINDOW= =UNMANAGE_WINDOW= =SWITCH_WORKSPACE= + =SET_LAYOUT= + =CYCLE_LAYOUT= =TOGGLE_FLOATING= =TOGGLE_MAXIMIZE= =MOVE_FLOATING_WINDOW= diff --git a/docs/min_core_draft/WM_COMMAND_RULES.org b/docs/min_core_draft/WM_COMMAND_RULES.org index a26c567..7691c09 100644 --- a/docs/min_core_draft/WM_COMMAND_RULES.org +++ b/docs/min_core_draft/WM_COMMAND_RULES.org @@ -188,6 +188,26 @@ 1. =WM_DIRTY_LAYOUT= 2. =WM_DIRTY_RENDER= +** =WM_COMMAND_CYCLE_LAYOUT= + +前置条件: + +1. 目标 workspace 存在。 +2. Workspace 的可用布局列表非空(=layout_count > 0=)。 + +状态变化: + +1. 若 =direction == 1= :切换到下一个布局(循环)。 +2. 若 =direction == -1= :切换到上一个布局(循环)。 +3. 在 workspace 的 =available_layouts[]= 数组中找到当前 =layout_id= 的索引。 +4. 按方向移动到相邻索引,循环回到数组开头或末尾。 +5. 将新的布局 ID 设置为 workspace 的 =layout_id= 。 + +副作用: + +1. =WM_DIRTY_LAYOUT= +2. =WM_DIRTY_RENDER= + * 模式切换命令 ** =WM_COMMAND_TOGGLE_FLOATING= 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 f66a9a0..e8a1c89 100644 --- a/docs/min_core_draft/WM_POLICY_APPLY_COMMAND_SKELETON.org +++ b/docs/min_core_draft/WM_POLICY_APPLY_COMMAND_SKELETON.org @@ -111,6 +111,10 @@ bool wm_policy_apply_command(wm_state_t *state, changed = apply_set_layout(state, layouts, command, plan); break; + case WM_COMMAND_CYCLE_LAYOUT: + changed = apply_cycle_layout(state, command, plan); + break; + case WM_COMMAND_REDRAW: changed = apply_redraw(state, command, plan); break; @@ -580,6 +584,45 @@ static bool apply_set_layout(wm_state_t *state, } #+END_SRC +** =apply_cycle_layout()= + +#+BEGIN_SRC c +static bool apply_cycle_layout(wm_state_t *state, + const wm_command_t *command, + wm_plan_t *plan) { + wm_workspace_t *ws = + require_workspace(state, command->as.cycle_layout.workspace_id); + if (!ws) return false; + if (ws->layout_count == 0) return false; + + // 找到当前布局在列表中的索引 + size_t current_idx = 0; + for (size_t i = 0; i < ws->layout_count; i++) { + if (ws->available_layouts[i] == ws->layout_id) { + current_idx = i; + break; + } + } + + // 计算下一个索引(支持前后循环) + size_t next_idx; + if (command->as.cycle_layout.direction > 0) { + next_idx = (current_idx + 1) % ws->layout_count; + } else { + next_idx = (current_idx == 0) ? (ws->layout_count - 1) : (current_idx - 1); + } + + // 如果新旧布局相同,直接返回 + wm_layout_id_t next_layout = ws->available_layouts[next_idx]; + if (next_layout == ws->layout_id) return false; + + ws->layout_id = next_layout; + plan->dirty_flags |= WM_DIRTY_LAYOUT | WM_DIRTY_RENDER; + push_render_all_outputs(state, plan); + return true; +} +#+END_SRC + ** =apply_redraw()= #+BEGIN_SRC c diff --git a/docs/min_core_draft/WORKSPACE_CONFIG_EXAMPLES.org b/docs/min_core_draft/WORKSPACE_CONFIG_EXAMPLES.org index fb684aa..5819fef 100644 --- a/docs/min_core_draft/WORKSPACE_CONFIG_EXAMPLES.org +++ b/docs/min_core_draft/WORKSPACE_CONFIG_EXAMPLES.org @@ -212,9 +212,75 @@ bool load_workspace_config(const char *path, -- 如果 workspace 4 当前在 DP-2 显示,窗口会在 DP-2 出现 #+END_SRC +** 布局列表配置 + +每个 workspace 可以配置自己的可用布局列表。 + +*** 基础配置 + +#+BEGIN_SRC lua +-- 声明布局注册表 +layouts = { + { id = 0, name = "T", fn = layout_tile }, + { id = 1, name = "M", fn = layout_monocle }, + { id = 2, name = "F", fn = layout_floating }, + { id = 3, name = "S", fn = layout_stack }, + { id = 4, name = "G", fn = layout_grid }, +} + +-- 单显示器:每个 workspace 可用所有布局 +workspaces = { + { id = 1, name = "1", output_id = 0, layouts = {0, 1, 2, 3, 4}, default_layout = 0 }, + { id = 2, name = "2", output_id = 0, layouts = {0, 1, 2, 3, 4}, default_layout = 0 }, + { id = 3, name = "3", output_id = 0, layouts = {0, 1, 2, 3, 4}, default_layout = 0 }, +} +#+END_SRC + +*** 主题化布局配置 + +不同用途的 workspace 配置不同的布局集合: + +#+BEGIN_SRC lua +workspaces = { + -- 代码 workspace:平铺、单列、堆叠(适合代码+终端) + { id = 1, name = "code", output_id = 0, + layouts = {0, 3, 4}, default_layout = 0 }, -- T, S, G + + -- 浏览器 workspace:单列、浮动、全屏(适合阅读) + { id = 2, name = "web", output_id = 0, + layouts = {1, 2}, default_layout = 1 }, -- M, F + + -- 游戏 workspace:仅浮动(游戏窗口通常需要浮动) + { id = 3, name = "game", output_id = 1, + layouts = {2}, default_layout = 2 }, -- F only + + -- 聊天 workspace:堆叠、单列 + { id = 4, name = "chat", output_id = 1, + layouts = {3, 1}, default_layout = 3 }, -- S, M +} +#+END_SRC + +*** 默认布局配置 + +如果不指定 layouts 字段,使用全局默认布局列表: + +#+BEGIN_SRC lua +-- 全局默认 +default_layouts = {0, 1, 2, 3, 4} -- 所有布局可用 + +-- workspace 省略 layouts 时使用全局默认 +workspaces = { + { id = 1, name = "1", output_id = 0 }, -- 使用全局默认 + { id = 2, name = "2", output_id = 0, + layouts = {0, 1}, default_layout = 0 }, -- 自定义 +} +#+END_SRC + ** 注意事项 1. *固定归属*:workspace.output_id 在创建后不应改变 2. *输出变化*:显示器配置变化时,WM 会重启,重新扫描和配置 3. *可见性*:窗口只在归属的 output 上可见 4. *独立性*:切换不同 output 的 workspace 互不影响 +5. *布局列表*:workspace.layouts 在启动时分配,运行时固定 +6. *布局切换*:使用 CYCLE_LAYOUT 命令在可用布局间循环切换 diff --git a/docs/min_core_draft/wm_command.h b/docs/min_core_draft/wm_command.h index 0742e88..3d83b92 100644 --- a/docs/min_core_draft/wm_command.h +++ b/docs/min_core_draft/wm_command.h @@ -31,6 +31,7 @@ typedef enum wm_command_type_t { WM_COMMAND_MOVE_FLOATING_WINDOW, WM_COMMAND_RESIZE_FLOATING_WINDOW, WM_COMMAND_SET_LAYOUT, + WM_COMMAND_CYCLE_LAYOUT, WM_COMMAND_REDRAW, WM_COMMAND_QUIT, } wm_command_type_t; @@ -133,5 +134,10 @@ typedef struct wm_command_t { wm_workspace_id_t workspace_id; wm_layout_id_t layout_id; } set_layout; + + struct { + wm_workspace_id_t workspace_id; + int direction; // 1: 下一个布局, -1: 上一个布局 + } cycle_layout; } as; } wm_command_t; diff --git a/docs/min_core_draft/wm_state.h b/docs/min_core_draft/wm_state.h index 3ee3955..620e8fd 100644 --- a/docs/min_core_draft/wm_state.h +++ b/docs/min_core_draft/wm_state.h @@ -31,9 +31,13 @@ typedef struct wm_window_t { typedef struct wm_workspace_t { wm_workspace_id_t id; wm_output_id_t output_id; // 固定归属某个输出 - wm_layout_id_t layout_id; + wm_layout_id_t layout_id; // 当前活动布局(可用布局列表中的一个) wm_window_id_t focused_window_id; + // 可用布局列表(启动时根据配置分配,运行时固定) + wm_layout_id_t *available_layouts; + size_t layout_count; + // 名称(核心算法不依赖,仅用于状态栏显示) char *name; } wm_workspace_t; @@ -91,6 +95,37 @@ size_t wm_state_workspace_get_by_output(const wm_state_t *state, wm_output_id_t output_id, wm_workspace_t **out_workspaces); +// ========== Workspace 布局操作 ========== + +/* + * 设置 workspace 的可用布局列表 + * 注意:应在 workspace 初始化时调用,运行时不应修改 + */ +void wm_workspace_set_layouts(wm_workspace_t *workspace, + const wm_layout_id_t *layouts, + size_t count); + +/* + * 获取 workspace 的可用布局列表 + */ +const wm_layout_id_t *wm_workspace_get_layouts(const wm_workspace_t *workspace, + size_t *out_count); + +/* + * 切换到下一个布局(循环) + */ +bool wm_workspace_cycle_layout(wm_workspace_t *workspace); + +/* + * 切换到指定索引的布局 + */ +bool wm_workspace_set_layout_by_index(wm_workspace_t *workspace, size_t index); + +/* + * 切换到指定 ID 的布局(如果该布局在可用列表中) + */ +bool wm_workspace_set_layout_by_id(wm_workspace_t *workspace, wm_layout_id_t id); + // ========== Output 访问 ========== wm_output_t *wm_state_output(wm_state_t *state, wm_output_id_t id);