14 KiB
ZDWM Tray 系统托盘设计
- 背景与动机
- 设计目标与约束
- 方案选型:嵌入式容器子窗口(A')
- 后端接口设计
- bar item 框架扩展
- bar tray item
- runtime 桥接
- 配置
- 数据流(触发链)
- 实现范围(第一版)
- 关键文件清单
- 复用现有
背景与动机
ZDWM 当前没有系统托盘(system tray / systray)功能。本文档描述如何在 bar 中实现 X11 系统托盘,让外部应用(输入法、音量、蓝牙、网络等)的托盘图标显示在状态栏右侧并正常交互,同时保持现有前后端分离架构,为未来 Wayland 后端预留扩展空间。
设计目标与约束
前后端分离
tray 协议(=_NET_SYSTEM_TRAY= + XEMBED)必须*闭环在后端内部*。core 的 event_t / policy / plan 完全不感知 tray——tray 不产生 =event_t=、不进 policy 路由、不进 plan/effect。
这与现有架构的三条解耦通道一致:
backend -> core=:后端把平台原始事件归一化为 =event_t- =core -> backend=:core 产出 =effect_t=,后端执行
runtime作为唯一桥接点:bar 模块本身拿不到 backend,所有平台访问经 runtime 中转
Wayland 预留
bar 与 runtime 层必须后端无关、零 xcb。未来加 Wayland 后端时,只需新增一个后端实现——Wayland 用 StatusNotifierItem(SNI)over D-Bus,icon 是像素数据而非真实窗口,由前端 cairo 绘制。只要后端接口形状不变,上层(bar/runtime)零改动。
tray 是 bar 的附属组件
没有 bar 就没有 tray。tray 的生命周期随 bar 窗口,*不*作为独立公共接口(不进 =include/zdwm/=)。tray 能力内嵌在 bar 窗口的创建/销毁里。
方案选型:嵌入式容器子窗口(A')
X11 系统托盘协议只要求:tray 创建一个 window 成为 _NET_SYSTEM_TRAY_S<n> 的 selection owner,客户端向 owner 发 _NET_SYSTEM_TRAY_REQUEST_DOCK 请求,tray 用 XEMBED reparent icon。协议本身*不*规定 icon reparent 到 bar 窗口还是独立窗口。
为何不用独立 top-level 窗口
完全独立的 top-level tray 窗口(如 stalonetray)是为*没有状态栏*的环境设计的。ZDWM 已有 bar,用独立窗口会引入"两个窗口的几何/对齐同步"问题(tray 窗口要贴 bar 右端、随 icon 数动态 resize、跟随 bar 移动),且无功能收益。
为何用专门的容器子窗口
虽然直接 reparent 到 bar 的 cairo 绘制窗口技术上可行(bar 已是 ARGB,子窗口叠加在 cairo 内容之上),但用一个*专门的 tray 容器子窗口*做 selection owner 更正统、更干净:
- *职责分离*:bar 窗口专注 cairo 绘制,tray 容器专注 XEMBED / icon 管理。dock 请求和
_XEMBED消息定向到容器,不进 bar 的 =handle_client_message=。 - *事件隔离*:tray 协议事件在容器窗口闭环,bar 的窗口管理事件流零污染。
- *视觉不变*:容器是 bar 窗口的子窗口,位于 tray region 内,icon 跟随 bar 移动/resize(X 子窗口语义),不占额外屏幕空间。
后端接口设计
tray 契约独立成头 interface/tray.h —— 落 interface 层:不放 core(tray 与 core 无关),也不放 bar(backend 也要实现它)。bar 和 backend 作为两个对等实现层,都依赖 interface/tray.h 来解耦——这正是分层消除 bar↔backend 交叉依赖的落点(详见 module-layering.org 的 tray 落点)。它只依赖 interface/types.h=(=window_id_t=),不含 =backend_t=,所以 bar include 它而不碰 =interface/backend.h 的内部。=interface/backend.h= 反过来 include interface/tray.h=,让 =backend_bar_window_t 内嵌 =tray_t=。
typedef void (*tray_icons_change_cb_t)(void *user_data);
typedef struct tray_api_t {
/* 返回 tray 当前挂载的 bar window_id;invalid 表示无 tray 挂载 */
window_id_t (*host_window)(void *handle);
/* 当前 icon 数量 */
size_t (*icon_count)(void *handle);
/* icon 边长(= bar height),bar 据此算 cell 宽度 */
int32_t (*icon_size)(void *handle);
/* 把容器 + icon 摆到 region 起点 x */
void (*place)(void *handle, int32_t x);
/* 注册 icon 数量变化回调 */
void (*set_listener)(void *handle,
tray_icons_change_cb_t cb,
void *user_data);
} tray_api_t;
typedef struct tray_t {
tray_api_t api; /* 值类型,后端总填充,一直可用 */
void *handle; /* 指向 backend(bar 不知其类型) */
} tray_t;
typedef struct backend_bar_window_t {
window_id_t window_id;
cairo_t *cr;
tray_t tray;
} backend_bar_window_t;
/* 扩展:加 enable_tray;icon_size 直接用 geometry.height */
backend_bar_window_t backend_create_bar_window(
backend_t *backend, rect_t geometry, uint32_t bg_pixel, bool enable_tray);
关键设计点
- *api 是值类型*:后端用一个全局
static constvtable 填充,保证一直可用,bar 不需判 NULL。这与项目现有 =zdwm_bar_item_type_t=(纯 vtable)+ =zdwm_bar_item_t=(实例持有 api + state)的模式一致。 - *handle 总有效*:实际指向
backend_t*=(tray api 函数内用 =backend->conn=、=backend->trayhost),对 bar 透明(=void*=)。有无 tray 统一由host_window()返回值判断(invalid = 无),不靠 api 是否 NULL,避免"先判 api、再判 host_window"两个判断点。 - *icon_size 来自 handle*(单一数据源):后端 configure icon 用的尺寸 = bar 查布局用的尺寸,杜绝两边各算导致 icon 错位/溢出。这比在
tray_t冗余存一份快照更纯粹。 - *生命周期随 bar/backend*:=backend_create_bar_window(enable_tray=true)= 时创建/迁移 host;=backend_destroy= 释放。无
tray_host_create/destroy独立入口,体现"tray 是 bar 附属"。
挂载语义
X11 的 _NET_SYSTEM_TRAY_S0 selection owner *整个 display 只能有一个*,但 backend_create_bar_window 是 per-output 调用的。处理方式:
- tray host 是后端单例(内联在 =backend_t=)。
- 每次传
enable_tray=true的create_bar_window把容器 reparent 到本次 bar 窗口,最终挂在*最后一个* true 的 output 上。 host_window()返回当前挂载的 bar window_id,bar 据此决定哪个 output 渲染 tray。
为何 set_listener 不配 remove_listener
tray icon 变化的唯一消费者是 bar 的 tray item,且其生命周期 = host 生命周期(=backend_destroy= 时自然消失)。与 core 的 listeners 模块一致(listener 跟宿主容器走,靠容器销毁回收),不引入 =remove_listener=。
bar item 框架扩展
tray 作为 right-side 的普通 item,复用 zdwm_bar_item_type_t 框架。为支持 tray 的两个本质差异(cell 无文本 + 需要在 layout 后把结果同步到后端真实窗口),对框架做最小*通用*扩展——不为 tray 特例化。
cell_api 全包 cell 相关(不引入 item_api)
set_cell_count / get_cell_count 本质是 *cell 集合的管理*(类比 std::vector 的 size()/resize() 与 operator[] 同属 vector),留在 =zdwm_bar_cell_api_t=。
新增两个 cell 级接口:
/* 固定宽度:>0 = 固定宽度,0 = 走文本测量(零值默认),<0 = 隐藏 */
void cell_set_fixed_width(zdwm_bar_item_t *, size_t index, int32_t width);
/* 单个 cell 的 region(layout 后的坐标) */
bar_x_region_t cell_get_region(zdwm_bar_item_t *, size_t index);
bar_cell_t 加 int32_t fixed_width=,默认 =0=(走文本测量)。把零值留给最常见的默认行为(测量),=p_clear 清零后新 cell 天然正确,=set_cell_count= 无需补初始化;=-1= 当"隐藏"哨兵(负数宽度本就无意义),与 =0=(测量)明确区分,没有歧义。
bar_output_layout 改一处:
int32_t width;
if (cell->fixed_width > 0) width = cell->fixed_width; /* 固定 */
else if (cell->fixed_width == 0) width = 文本测量; /* 测量(默认)*/
else width = 0; /* 隐藏 */
after_layout / after_draw 钩子
普通 item 是 cairo 绘制;tray 是"layout 后把 region 应用到后端真实窗口"。给 zdwm_bar_item_type_t 加两个*可空*通用钩子,参数打包(仿现有 zdwm_bar_click_params_t 先例):
typedef struct zdwm_bar_item_layout_params_t {
zdwm_bar_item_t *item;
const zdwm_bar_cell_api_t *cell_api;
bar_x_region_t region;
void *state;
} zdwm_bar_item_layout_params_t;
void (*after_layout)(const zdwm_bar_item_layout_params_t *); /* 可空 */
void (*after_draw)(const zdwm_bar_item_layout_params_t *); /* 可空 */
设计要点:
- 钩子为 NULL 则跳过,普通 item 零影响。
after_layout/after_draw比update多一个region=(它们在 layout 之后调用),且通过 =cell_api可调cell_get_region取 per-cell 位置。update不打包参数(3 个参数不多,保持现有workspaces/binding/windows三个 item 零改动)。
bar_draw 流程:
bar_output_layout(bo, ctx);
for each item: if (after_layout) after_layout(params); /* params 含 region */
bar_output_draw(bo, ctx, bg);
for each item: if (after_draw) after_draw(params);
bar tray item
新增 =src/bar/tray.c=,作为普通 right-side item(参考 workspaces.c 的实现模式):
zdwm_bar_item_type_t bar_tray = {
.create_state = bar_tray_create_state,
.update = bar_tray_update,
.on_click = bar_tray_on_click, /* 返回 NONE:icon 真实子窗口自收点击 */
.after_layout = bar_tray_after_layout,
.destroy_state = bar_tray_destroy_state,
};
create_state=:拷贝 =tray_t=,调 =set_listener注册回调(回调置 =state->dirty = true=)。update=:=dirty时set_cell_count(icon_count)+ 循环 =cell_set_fixed_width(i, icon_size)=。- =after_layout=:=place(handle, params->region.start)=。
tray cell 不画 cairo 内容(text 为空,=bar_item_draw= 只画融入 bar 的背景色块,被真实 icon 窗口盖住)。
runtime 桥接
=runtime_init_bar=(=src/runtime/runtime.c=)的 create 循环:
for (size_t i = 0; i < count; ++i) {
bool enable_tray = ((int32_t)i == runtime->bar.config.tray_output_index);
auto bar_window = backend_create_bar_window(backend, bar_rect, color, enable_tray);
...
runtime->bar.tray = bar_window.tray; /* 每个返回都一样,存一份 */
}
bar_init(&runtime->bar, &runtime->listeners);
bar_init 遍历 bar_output=,对 =host_window() 匹配自身 window_id 的那个 output 调 =bar_output_add_tray=(right side),其余不加。tray 生命周期随 =backend_destroy=,runtime 无特殊清理。
配置
=zdwm_bar_config_t=(=include/zdwm/config.h=)加:
int32_t tray_output_index; /* 默认 0 = 第一个 output */
配置 setup 中可通过 output 信息确定具体 index。
数据流(触发链)
icon 增删
-> 后端 set_listener 回调 -> state->dirty = true
-> 下个 timerfd tick -> bar_update -> bar_tray_update
-> set_cell_count + cell_set_fixed_width -> item 标 dirty
-> bar_draw -> bar_output_layout 重算 region(cell 数变了)
-> tray 的 after_layout -> backend place(handle, region.start)
-> 后端 move 容器 + configure 每个 icon 到 (region.start + i*icon_size, 0)
bar 重绘是 timerfd(fps)驱动,非事件驱动。tray icon 是真实窗口,X server 自管绘制,不依赖 cairo 重绘。
实现范围(第一版)
第一版做*基础可用*:
_NET_SYSTEM_TRAY_S0selection owner + MANAGER 广播- 基础 XEMBED(reparent 容器、=_XEMBED_EMBEDDED_NOTIFY=)
- icon 显示与点击
- icon 尺寸 = bar 高度(统一,不缩放)
放后续迭代:
- 完整 XEMBED 焦点 / 激活转发
- icon 尺寸协商与缩放
- 客户端异常断开的清理
*透明背景*第一版基本白送——bar 窗口已是 ARGB visual(见 backend_create_bar_window 调用 =window_get_visual(backend, true)=),tray 容器作为子窗口继承。
关键文件清单
| 文件 | 改动 |
|---|---|
interface/tray.h |
新增 tray 契约:=tray_t= / tray_api_t / =tray_icons_change_cb_t=(bar 与 backend 共享,落 interface 解耦) |
interface/backend.h |
include interface/tray.h=;=backend_bar_window_t 内嵌 =tray_t=;扩展 =backend_create_bar_window=(加 =enable_tray=) |
src/backend/x11/tray.c |
*新增*:selection / MANAGER / 基础 XEMBED / icon 管理 / place |
src/backend/x11/internal.h |
ATOM_LIST 追加 tray/XEMBED atoms;=backend_t= 加 tray 字段 |
src/backend/x11/event.c |
消化 tray 相关 client message(不产 =event_t=) |
src/backend/x11/backend.c |
backend_create_bar_window 实现 enable_tray 分支;=backend_destroy= 释放 tray |
src/bar/types.h |
bar_cell_t 加 fixed_width=;=bar_t 加 tray |
src/bar/cell.h / cell.c |
cell_api 加 cell_set_fixed_width / cell_get_region |
src/bar/bar.c |
layout 支持 fixed_width=;=bar_draw 调 after 钩子;=bar_init= 加 tray item;新增 layout params 类型 |
src/bar/tray.c |
*新增*:tray item 实现 |
src/runtime/runtime.c |
runtime_init_bar 传 enable_tray=、存 =bar->tray |
include/zdwm/config.h |
zdwm_bar_config_t 加 tray_output_index |
复用现有
- =backend_create_bar_window=(=src/backend/x11/backend.c=):bar 窗口已是 ARGB,tray 容器继承透明。
zdwm_bar_item_type_tvtable 框架 + workspaces.c:tray item 同模式落地。zdwm_bar_click_params_t=:=after_layout/after_draw的 params 打包仿此先例。cell_api机制:trayupdate通过它操作 cell(与 workspaces 一致)。