Files
zdwm/docs/tray-design.org

257 lines
14 KiB
Org Mode
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#+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 用 StatusNotifierItemSNIover D-Busicon 是像素数据而非真实窗口,由前端 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 移动/resizeX 子窗口语义),不占额外屏幕空间。
* 后端接口设计
tray 契约独立成头 =interface/tray.h= —— 落 interface 层:不放 coretray 与 core 无关),也不放 barbackend 也要实现它。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_idinvalid 表示无 tray 挂载 */
window_id_t (*host_window)(void *handle);
/* 当前 icon 数量 */
size_t (*icon_count)(void *handle);
/* icon 边长(= bar heightbar 据此算 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; /* 指向 backendbar 不知其类型) */
} tray_t;
typedef struct backend_bar_window_t {
window_id_t window_id;
cairo_t *cr;
tray_t tray;
} backend_bar_window_t;
/* 扩展:加 enable_trayicon_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_idbar 据此决定哪个 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 的 regionlayout 后的坐标) */
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, /* 返回 NONEicon 真实子窗口自收点击 */
.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 重算 regioncell 数变了)
-> tray 的 after_layout -> backend place(handle, region.start)
-> 后端 move 容器 + configure 每个 icon 到 (region.start + i*icon_size, 0)
#+end_example
bar 重绘是 timerfdfps驱动非事件驱动。tray icon 是真实窗口X server 自管绘制,不依赖 cairo 重绘。
* 实现范围(第一版)
第一版做*基础可用*
- =_NET_SYSTEM_TRAY_S0= selection owner + MANAGER 广播
- 基础 XEMBEDreparent 容器、=_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 窗口已是 ARGBtray 容器继承透明。
- =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 一致)。