257 lines
14 KiB
Org Mode
257 lines
14 KiB
Org Mode
#+title: ZDWM Tray 系统托盘设计
|
||
#+OPTIONS: toc:2
|
||
|
||
* 背景与动机
|
||
|
||
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 更正统、更干净:
|
||
|
||
1. *职责分离*:bar 窗口专注 cairo 绘制,tray 容器专注 XEMBED / icon 管理。dock 请求和 =_XEMBED= 消息定向到容器,不进 bar 的 =handle_client_message=。
|
||
2. *事件隔离*:tray 协议事件在容器窗口闭环,bar 的窗口管理事件流零污染。
|
||
3. *视觉不变*:容器是 bar 窗口的子窗口,位于 tray region 内,icon 跟随 bar 移动/resize(X 子窗口语义),不占额外屏幕空间。
|
||
|
||
* 后端接口设计
|
||
|
||
tray 契约独立成头 =interface/tray.h= —— 落 interface 层:不放 core(tray 与 core 无关),也不放 bar(backend 也要实现它)。bar 和 backend 作为两个对等实现层,都依赖 =interface/tray.h= 来解耦——这正是分层消除 bar↔backend 交叉依赖的落点(详见 [[file:module-layering.org][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=。
|
||
|
||
#+begin_src c
|
||
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);
|
||
#+end_src
|
||
|
||
** 关键设计点
|
||
|
||
- *api 是值类型*:后端用一个全局 =static const= vtable 填充,保证一直可用,bar 不需判 NULL。这与项目现有 =zdwm_bar_item_type_t=(纯 vtable)+ =zdwm_bar_item_t=(实例持有 api + state)的模式一致。
|
||
- *handle 总有效*:实际指向 =backend_t*=(tray api 函数内用 =backend->conn=、=backend->tray= host),对 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 级接口:
|
||
|
||
#+begin_src c
|
||
/* 固定宽度:>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);
|
||
#+end_src
|
||
|
||
=bar_cell_t= 加 =int32_t fixed_width=,默认 =0=(走文本测量)。把零值留给最常见的默认行为(测量),=p_clear= 清零后新 cell 天然正确,=set_cell_count= 无需补初始化;=-1= 当"隐藏"哨兵(负数宽度本就无意义),与 =0=(测量)明确区分,没有歧义。
|
||
|
||
=bar_output_layout= 改一处:
|
||
|
||
#+begin_src c
|
||
int32_t width;
|
||
if (cell->fixed_width > 0) width = cell->fixed_width; /* 固定 */
|
||
else if (cell->fixed_width == 0) width = 文本测量; /* 测量(默认)*/
|
||
else width = 0; /* 隐藏 */
|
||
#+end_src
|
||
|
||
** after_layout / after_draw 钩子
|
||
|
||
普通 item 是 cairo 绘制;tray 是"layout 后把 region 应用到后端真实窗口"。给 =zdwm_bar_item_type_t= 加两个*可空*通用钩子,参数打包(仿现有 =zdwm_bar_click_params_t= 先例):
|
||
|
||
#+begin_src c
|
||
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 *); /* 可空 */
|
||
#+end_src
|
||
|
||
设计要点:
|
||
- 钩子为 NULL 则跳过,普通 item 零影响。
|
||
- =after_layout= / =after_draw= 比 =update= 多一个 =region=(它们在 layout 之后调用),且通过 =cell_api= 可调 =cell_get_region= 取 per-cell 位置。
|
||
- =update= 不打包参数(3 个参数不多,保持现有 =workspaces= / =binding= / =windows= 三个 item 零改动)。
|
||
|
||
=bar_draw= 流程:
|
||
|
||
#+begin_src c
|
||
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);
|
||
#+end_src
|
||
|
||
* bar tray item
|
||
|
||
新增 =src/bar/tray.c=,作为普通 right-side item(参考 [[file:../src/bar/workspaces.c][workspaces.c]] 的实现模式):
|
||
|
||
#+begin_src 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,
|
||
};
|
||
#+end_src
|
||
|
||
- =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 循环:
|
||
|
||
#+begin_src c
|
||
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);
|
||
#+end_src
|
||
|
||
=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=)加:
|
||
|
||
#+begin_src c
|
||
int32_t tray_output_index; /* 默认 0 = 第一个 output */
|
||
#+end_src
|
||
|
||
配置 setup 中可通过 output 信息确定具体 index。
|
||
|
||
* 数据流(触发链)
|
||
|
||
#+begin_example
|
||
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)
|
||
#+end_example
|
||
|
||
bar 重绘是 timerfd(fps)驱动,非事件驱动。tray icon 是真实窗口,X server 自管绘制,不依赖 cairo 重绘。
|
||
|
||
* 实现范围(第一版)
|
||
|
||
第一版做*基础可用*:
|
||
|
||
- =_NET_SYSTEM_TRAY_S0= selection 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_t= vtable 框架 + [[file:../src/bar/workspaces.c][workspaces.c]]:tray item 同模式落地。
|
||
- =zdwm_bar_click_params_t=:=after_layout= / =after_draw= 的 params 打包仿此先例。
|
||
- =cell_api= 机制:tray =update= 通过它操作 cell(与 workspaces 一致)。
|