Focus is all you need
作者的 Lafa 合成器在 2026 年 9 月 21 日四舍五入能用了,目测已经达到了 45% 的项目初版进度。
焦点是 WM 里最为关键的状态,需要小心维护。
Clients
对于客户端来说,焦点似乎没什么牵扯,或者说整个状态对客户端不透明。客户端最多能知道自己是否被激活:
// include/wlr/types/wlr_xdg_shell.h
// types/xdg_shell/wlr_xdg_toplevel.c
/**
* Request that this toplevel show itself in an activated or deactivated
* state. Returns the associated configure serial.
*/
uint32_t wlr_xdg_toplevel_set_activated(struct wlr_xdg_toplevel *toplevel,
bool activated);
对应到 xdg_toplevel::configure 里的 states 数组的 activated 元素:
The surface is now activated
Client window decorations should be painted as if the window is active. Do not assume this means that the window actually has keyboard or pointer focus.
激活后有一些视觉上的变化和窗口状态提示,比如 foot 的光标从空心变成实心,与真实焦点状态可能并不一致。
Seat
// TODO 鼠标、键盘都是从哪里来的,一个seat只有一套键鼠吗?
Keyboard
wl_keyboard::key 事件里没有 surface 参数,按键的分发由之前建立的 logical state 决定焦点:
In the
wl_keyboardlogical state, this event adds the key to the keys currently logically down…
也就是说非焦点的 surface 没有能力感知按键。在 wlroots 中这个状态由 keyboard_state.focused_client 保存:
// types/seat/wlr_seat_keyboard.c
void wlr_seat_keyboard_send_key(struct wlr_seat *wlr_seat, uint32_t time,
uint32_t key, uint32_t state) {
struct wlr_seat_client *client = wlr_seat->keyboard_state.focused_client;
if (!client) {
return;
}
uint32_t serial = wlr_seat_client_next_serial(client);
struct wl_resource *resource;
wl_resource_for_each(resource, &client->keyboards) {
if (seat_client_from_keyboard_resource(resource) == NULL) {
continue; // 过滤掉“存在但已失效”的 wl_keyboard 绑定
}
wl_keyboard_send_key(resource, serial, time, key, state);
}
}
这个焦点在 wlroots 中由 wlr_seat_keyboard_notify_enter 修改:
- leave the previously entered surface
wl_keyboard_send_leave - enter the current surface
wl_keyboard_send_enter - reinitialize the focus destroy events
- update
keyboard_state.focused_clientandkeyboard_state.focused_surface - tell new client about any modifier change last
wl_keyboard_send_modifiers - emit
wl_signalforkeyboard_state.events.focus_change
注意到这里存了 focused_client 和 focused_surface 两个状态,后者表示协议里挂的 wl_surface,前者是拥有
wl_keyboard 的 client。一个 client 有 N 个 surface 和 N 个 keyboard 绑定。
Pointer
四舍五入在焦点这块和键盘没区别,但这又带来了新问题:键盘焦点和鼠标焦点哪个才是主焦点? 从用户动作上来看,鼠标是主要的触发动作,比如点击窗口的标题栏等,会改变键盘的输入焦点; 但是在窗口激发状态这一点上,也可能会出现不一致的行为,如激活的 A 窗口打字的时候,鼠标在 B 窗口上下滚动。
// TODO 触控板理论上也属于鼠标范畴
Touch
触摸就更复杂了,多点触控可能涉及到多个窗口及显示器,暂时略。
Focus Stealing
Wayland 的协议设计默认阻止了客户端的焦点抢夺,但在很多情况下又需要焦点的传递:
- 启动台作为 Client 需要拉起新 Client 并给予焦点
- 用户点击 IM 中的链接,浏览器响应并激活到前台
因此有了 xdg-activation-v1 协议处理这种“文明”请求:
The client that intends to activate another toplevel uses the
xdg_activation_v1.get_activation_tokenrequest to get an activation token. This token is then forwarded to the client, which is supposed to activate one of its surfaces, through a separate band of communication.The token the activating client gets may be ineffective either already at the time it receives it, for example if it was not focused, for focus stealing prevention. The activating client will have no way to discover the validity of the token, and may still forward it to the to be activated client.
协议顺便举例说可以用环境变量或 D-Bus 来传递 token,刚好对应前面的两种场景。