Files
zdwm/docs/tray-design.org

14 KiB
Raw Permalink Blame History

ZDWM Tray 系统托盘设计

背景与动机

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 交叉依赖的落点(详见 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_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);

关键设计点

  • *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=truecreate_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::vectorsize()/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 的 regionlayout 后的坐标) */
bar_x_region_t cell_get_region(zdwm_bar_item_t *, size_t index);

bar_cell_tint32_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_drawupdate 多一个 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,    /* 返回 NONEicon 真实子窗口自收点击 */
  .after_layout  = bar_tray_after_layout,
  .destroy_state = bar_tray_destroy_state,
};
  • create_state=:拷贝 =tray_t=,调 =set_listener 注册回调(回调置 =state->dirty = true=)。
  • update==dirtyset_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 重算 regioncell 数变了)
  -> tray 的 after_layout -> backend place(handle, region.start)
  -> 后端 move 容器 + configure 每个 icon 到 (region.start + i*icon_size, 0)

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 visualbackend_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_tfixed_width==bar_ttray
src/bar/cell.h / cell.c cell_apicell_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_barenable_tray=、存 =bar->tray
include/zdwm/config.h zdwm_bar_config_ttray_output_index

复用现有

  • =backend_create_bar_window==src/backend/x11/backend.c=bar 窗口已是 ARGBtray 容器继承透明。
  • zdwm_bar_item_type_t vtable 框架 + workspaces.ctray item 同模式落地。
  • zdwm_bar_click_params_t==after_layout / after_draw 的 params 打包仿此先例。
  • cell_api 机制tray update 通过它操作 cell与 workspaces 一致)。