feat: 为 workspace 添加可用布局列表功能

- wm_workspace_t 新增 available_layouts 和 layout_count 字段
- 新增 WM_COMMAND_CYCLE_LAYOUT 命令支持循环切换布局
- 添加 workspace 布局列表操作 API
- 完善命令规则和实现骨架
- 新增布局列表配置示例和文档
This commit is contained in:
2026-03-11 05:54:30 +08:00
parent 329943ab3e
commit 7de9cd500b
6 changed files with 179 additions and 1 deletions

View File

@@ -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=

View File

@@ -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=

View File

@@ -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

View File

@@ -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 命令在可用布局间循环切换

View File

@@ -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;

View File

@@ -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);