The Wayland Protocol 中文版

阅读地址:https://wayland.arktoria.org/
英文原版:https://wayland-book.com/
版权许可:Creative Commons Attribution-ShareAlike 4.0 International License

译者水平有限,出现问题首先怀疑翻译,其次怀疑版本太老,欢迎提出质疑。 以下几点需要明确一下,避免造成阅读阻碍:

  1. Wayland 是新一代图形显示协议

    部分场景中支持没有 X11 好,如录屏、截图、远程、输入法等,Wayland 就像安卓高版本的新 API,应用程序未适配,接口本身也可能在 unstable 状态。

    目前 X11 兼容层 XWayland 在 HiDPI 等方面不尽人意,如果应用支持 Wayland 原生显示,请尽量通过设置环境变量等调整为 Wayland。

  2. Wayland 的新架构 & 名词定义

    Wayland 是 C/S 模型工作的,文中所说 Client、客户端……都指 Application 应用程序,如火狐浏览器。

    在另一端的 Wayland Server,不仅负责与 Clients 通信,更要把接收到来的图像,合成后呈现到显示器上,也就是说 在 Wayland 中 Server、Window Manager 是二位一体的。 因此文中的 Server、服务端、合成器、混成器、窗口管理器等所指相同。

  3. 有关 Wayland 的更多资料

    项目官网 https://wayland.freedesktop.org/ 上除了代码仓库和邮箱列表过于复杂外,其余链接都值得花时间看。

    本文原作者 Drew DeVault 是 sway 及 wlroots 的创始人,现致力于 sr.ht 代码协作平台(GitHub 类似)、hare 编程语言(C 语言类似)等新项目,Wayland 项目已交由其他人来做,因此本书在停更状态。 他的博客 https://drewdevault.com/ 上也有一些参考信息。

banner

绪论

Wayland 是类 UNIX 系统的新一代图像显示服务。 该项目由经典的 Xorg 的原班人马打造,是将应用程序的图形化界面(GUI)显示到用户屏幕的最佳选择。 之前使用过 X11 的读者会对 Wayland 的改进感到惊喜,而之前未接触过 Unix 图形学的新人,也会发现 Wayland 在构建 GUI 显示方面的强大、灵活之处。

这本书将会帮助您深入理解 Wayland 的概念、设计和实现,并为您提供构建构建 Wayland 客户端 和 Wayland 合成器所需的工具。 随着阅读的进行,我们会建立起 Wayland 的理论架构并了解其设计理念。 在阅读中,你会感到惊喜,因为 Wayland 的设计非常的直观而且清晰的,这应该有助于你继续看下去。欢迎探索开源图形学的未来!

这还是个草稿,1-10 章基本完善,不过会有后续更新,11 章后很多内容有待撰写。

TODO:

  • Expand on resource lifetimes and avoiding race conditions in chapter 2.4
  • Move linux-dmabuf details to the appendix, add note about wl_drm & Mesa
  • Rewrite the introduction text
  • Add example code for interactive move, to demonstrate the use of serials
  • Use — instead of - where appropriate
  • Prepare PDFs and EPUBs

英文原版

在线阅读 wayland-book.com
项目地址 git.sr.ht/~sircmpwn/wayland-book

关于作者

用 Drew 的密切合作者 Preston Carpenter 的话来说:

从 sway 的建造开始,Drew DeVault 进入到 Wayland 的世界。 sway 是最受欢迎的平铺式窗口管理器 i3wm 在 Wayland 的克隆,从任何角度来说,包括用户、开发提交数量和影响力,sway 都是目前 Wayland 下最受欢迎的平铺式窗口管理器。 随着 sway 的成功,Drew 回馈 Wayland 社区 - 开启项目 wlroots:用于构建 Wayland 合成器的高定制化、组件化模块包。 现今,已有数十个不同的混成器在 wlroots 基础之上而构造,而 Drew 也成为 Wayland 领域的重要专家之一。

Wayland 上层设计

您的电脑有“输入”和“输出”设备,分别负责接收您的信息和显示信息。 输入设备通常有以下几种:

  • 键盘
  • 鼠标
  • 触控板
  • 触摸屏
  • 数位板

输出设备一般是桌面上的显示器、笔记本或其他移动设备的屏幕。 应用程序之间将会共享这些屏幕,而Wayland 合成器的作用便是分配输入事件到正确的Wayland 客户端,并将他们的程序窗口恰当地显示在您的显示器上。 将所有应用程序的窗口组合起来,一起显示在屏幕上的过程被称为“合成”,而执行这一过程的程序被称作“合成器”。

Wayland Compositor,“合成器”、“混成器”只是不同翻译

具体实践

桌面生态系统包含有许多程序组件,比如渲染用的 Mesa(及其驱动)、Linux KMS/DRM 子系统、负责缓冲区分配的 GBM、用户态的 libdrm、libinput、evdev 等。 不用担心,理解 Wayland 几乎不需要这些专业知识,何况这些内容都远超本书范围。

实际上,Wayland 协议是相当谨慎和高度封装的,很容易就能构建出一个 Wayland 桌面,并在其上面成功运行多数的应用程序,而无需牵连这些应用程序。 话虽如此,大致了解这些东西是什么以及它们是如何工作的,仍非常有用。 让我们从底层开始,逐步向上展开。

硬件部分

一台典型的计算机配备了一些重要的硬件。 在机箱外面,我们有显示器、键盘、鼠标,或许还有麦克风和一个可爱的 USB 加热杯垫。 机箱内部有一系列组件与这些设备接口相连。 例如,如果您的电脑有 USB 接口 - 那您的键鼠大概率会插在 USB 接口上,显示器正连接着显卡 GPU。

这些硬件各司其职。 例如,GPU 有以显存形式提供的像素缓冲区,并将这些像素扫描输出到显示器上。 GPU 还提供特制的处理器,可以很好地处理高度并行的任务(例如为 1080P 显示器上的 2,073,600 个像素计算正确的颜色),但除此之外的任务都不擅长。 USB 控制器的工作同样复杂的令人称奇,它要实现枯燥的 USB 协议,接受来自键盘的输入事件,或精心调控杯垫的温度,从而避免被人投诉做出了令人不快的冷咖啡。

在这个层面上,硬件几乎不了解系统上正在运行着哪些应用程序。 硬件提供了执行工作的指令接口,并被告知相应的操作的指令,但不在乎指令是谁发出的。 基于此,只有一个组件被允许和硬件交流。

内核部分

这一任务落归内核组件。 内核是一头复杂的“野兽”,因此我们只关注与 Wayland 相关的部分。 Linux 内核的任务是给硬件提供一个抽象,这样在用户态可以安全的访问它们,我们的 Wayland 合成器也运行在用户态。 对于 Display 框架, 被称为 DRM(direct rendering manager),可以在用户态有效地给 GPU 分配任务。 DRM 有一个重要的子系统 KMS(kernel mode setting),用于遍历计算机中所有连接的显示器并设置其属性,比如他们选定的分辨率(也称为“模式”)等。 输入设备通过名为 evdev 的接口进行抽象。

大多数内核接口都以特殊文件的形式存在于 /dev 供用户态调用。 以 DRM 为例,这些文件位于 /dev/dri/,通常,以主要节点 primary node(如 card0)的形式进行模式设置等特权操作,以渲染节点 render node(如 renderD128)的形式进行渲染、视频解码等非特权操作,而对于设备节点 device nodes 则位于 /dev/input/event*

$ ls /dev/dri/
by-path  card0  renderD128

用户态

现在我们来看用户态。 在这里,应用程序与硬件隔离,必须通过内核提供的设备节点(device nodes)进行工作。

libdrm

大多数 Linux 内核接口都有一个对应的用户态接口,为使用这些设备节点(device nodes)提供合适的 C 语言 API。 libdrm 库是其中之一,它是 DRM 子系统的用户态部分。 Wayland 混成器使用它进行模式设置(modesetting)和其他 DRM 操作,但 Wayland 客户端通常不会直接使用 libdrm。

Mesa

Mesa 是 Linux 图形栈中最为重要的部分之一。 它除了为 Linux 提供 OpenGL(和 Vulkan)的厂家优化实现之外,还提供了 GBM(Generic Buffer Management)库,这是一种在 libdrm 之上的抽象层,用于在 GPU 上分配缓冲区。 大多数 Wayland 混成器会通过 Mesa 同时使用 GBM 和 OpenGL,多数 Wayland 客户端至少使用 OpenGL 或 Vulkan 其中一种。

libinput

如同 libdrm 是 DRM 子系统的抽象那样,libinput 提供了 evdev 用户态的抽象。 它负责从内核接收输入设备的输入事件,将其解码为可用的形式,并传递给 Wayland 混成器。 混成器需要特殊的权限才能使用 evdev 设备文件,强制要求 Wayland 客户端通过混成器接收输入事件,这样可以防止键盘使用日志被记录等。

(e)udev

用户态还要负责协调来自内核的新设备、配置 /dev 的设备节点的权限、并将事件变动发送给系统上正在运行的程序。 大多数系统使用 udev(或 eudev)以进行这项工作。 Wayland 就是用 udev 来遍历输入设备和 GPU,并在设备变动(比如插入新设备或拔出旧设备)时接收通知。

xkbcommon

XKB(X Keyboard)最初是 Xorg 服务处理键盘的子系统。 几年前开发者将它从 Xorg 代码中分离出来,成为了一个独立的键盘处理库,从此不再与 X 有任何实际的联系。 Libinput(以及 Wayland 混成器)以扫描码(scancodes)的形式提供键盘事件,扫描码的准确含义因键盘而异。 xkbcommon 负责将这些扫描码转化为更有意义的通用键盘信号,如 65 转化为 XKB_KEY_Space。 它还包含了一个状态机,负责将在按住 shift 键的时的 1 会变成

pixman

这是一个简单的像素操作(pixel manipulation)库,Wayland 客户端和混成器都会使用,它可以高效的处理像素缓冲区、处理图像相关的数学运算,比如矩阵相交,以及执行其他类似的像素操作任务。

libwayland

libwayland 是 Wayland 协议最常用的实现,它由 C 语言编写,能处理大部分的底层传输协议。 libwayland 同时也提供了一个从 Wayland 协议(XML 文件)生成高级代码的工具。 我们将从第 1.3 章开始,在本书中详细讨论 libwayland。

其它

到目前为止,提到的每个部分在整个 Linux 桌面生态系统中都是一致的,除此以外还存在更多的组件。 许多图形应用程序根本不知晓 Wayland,而是选择诸如 GTK、QT、SDL 和 GLFW 之类的库来进行代处理。 许多混成器选择像 wlroots 这样的软件来抽象简化很多功能,而其它的一类混成器则在内部实现所有功能。

目标和受众

本书的目标是让您了解 Wayland 协议及其高级用法。 您将会对 Wayland 核心协议的所有内容有扎实的了解,同时掌握实际生产、评估和实现各种扩展协议所需的必要知识。 首先,本书通过 Wayland 的客户端来介绍其架构。 此外,它也会为 Wayland 合成器的开发提供一些实用的工具。

自由系统桌面(freedesktop)生态十分复杂,它由许多分离的部分组成。 我们将很少讨论 libdrm、libinput、evdev 等部分内容,本书不是构建 Wayland 混成器的详尽指南。 在本书中你也找不到图像绘制技术的相关信息,比如 Cairo、Pango、GTK+ 等,尽管他们对开发 Wayland 客户端非常有帮助。 因此,本书同样不是一个完备的 Wayland 客户端实现指南。 相反,我们只专注于 Wayland 协议的细节。

本书仅涵盖 Wayland 协议和 libwayland。 如果你正在编写一个客户端应用程序,并有自己熟悉的 UI 渲染库,那么请带上自家的“像素”,本书将帮助您在 Wayland 上显示它们。 如果你已经对操控合成器、显示器和输入设备所需的技术有所了解,那么本书将帮助您学习如何与客户端进行通信。

Wayland 软件包

在 Linux 发行版中安装 "wayland",您最终会获得 freedesktop.org 分发的 libwayland-client、libwayland-server、wayland-scanner 和 wayland.xml。 它们分别位于 /usr/lib/usr/include/usr/bin/usr/share/wayland。 此包代表了 Wayland 协议最主流的实现,但它不是唯一选择。 本书第三章详细介绍这种实现;这本书其余部分同样适用于 Wayland 的任何实现。

wayland.xml

Wayland 协议通过 XML 文件进行定义。 如果能够定位并以文本格式打开 "wayland.xml" 文件,您将会找到 Wayland 核心协议 XML 规范。 这是一个高级协议,它建立在我们将在下一章要讨论的 wire 协议之上。 本书的大部分内容致力于解释该文件。

wayland-scanner

"wayland-scanner" 工具能够处理上述 XML 文件并生成对应代码。 其最常用的实现正如你现在所见,它可以由 wayland.xml 之类的文件生成 C 语言的头文件和上下文胶水代码。 也有其他语言对应的 scanner,如 wayland-rs (Rust)、waymonad-scanner (Haskell) 等。

libwayland

libwayland-client 和 libwayland-server 这两个库包含了一个 wire 协议的双端通信实现。 libwayland 同时提供一些常用工具,用于处理 Wayland 数据结构、简易事件循环等。 此外,libwayland 还包含一份使用 wayland-scanner 生成的 Wayland 核心协议的预编译副本。

协议设计

Wayland 协议由多层抽象结构组成。 它从一个基本的 Wire 协议格式开始(该格式可以用事先约定的接口解码信息流), 然后用更高层次的程序枚举接口、创建符合这些接口的资源、交换相关信息, 这便是 Wayland 协议及其扩展协议的内容。 除此以外,我们还有一些更广泛的模式,这些模式在 Wayland 协议的设计中经常使用。 我们将在本章节中介绍所有相关内容。

让我们自下而上,开始学习。

基础 Wire 协议

如果仅仅打算使用 libwayland,那么本节选读并可以直接跳至下节。

Wire 协议是由 32 位值所组成的流,使用当前机器的字节顺序进行编码(例如 x86 系列 CPU 上的小端序)。 其中包含以下几种基础类型:

  • intuint
    32 位 有符号及无符号整型
  • fixed
    24 位整数 + 8 位小数 有符号浮点数
  • object
    32 位 对象 ID
  • new_id
    32 位 对象 ID(收到对象时需要分配)

除了上述基础类型之外,还有一些常用的类型:

  • string
    字符串以 32 位整数开头,这个整数表示字符串的长度(以字节为单位), 接下来是字符串的内容和 NUL 终结符,最后用未定义数据对齐填充 32 位。 编码没有规定,但是实践中使用 UTF-8。

  • array
    任意数据的二进制块,以 32 位整数开头,指定块长度(以字节为单位), 然后是数组的逐字内容,最后用未定义数据对齐 32 位。

  • fd
    主体传输的是一个 0 bit 的值,但是会通过 Unix Socket 消息(msg_control)中的辅助数据将文件描述符(fd)传输到另一端。

  • enum
    一个单独的值(或 bitmap),用于已知常量的枚举,编码为 32 位整型。

消息

Wire 协议是使用这些原语构建而成的消息流。 每条消息都代表着某个对象 object 相关的一次 event 事件(服务端到客户端)或 request 请求 (客户端到服务端)。

消息头由两个字段组成。 第一个字段是操作的对象 ID。 第二个字段是两个 16 位值:高 16 位是这条消息的大小(包括头本身),低 16 位是这次事件或请求的操作码。 接下来是基于双方事先约定的消息签名的消息参数。 接收方会查找对象 ID 的接口、事件或请求的操作码,以确认消息的签名和属性。

为了解析一条消息,客户端和服务端必须先创建对象。 ID 1 预分配给了 Wayland 显示单例对象,它被用于引导产生其它对象。 我们将在第四章中对此进行讨论。 下一章将假设您已经有了一个对象 ID,进一步讨论什么是接口、请求和事件怎么运行。

对象 ID

new_id 参数随某条消息而来,发送者会给它分配一个对象 ID (新对象的接口通过其它额外的参数传递,或事先双方约定)。 此对象 ID 能在后续的消息头或者其它对象的参数中使用。 客户端在 [1, 0xFEFFFFFF] 而服务端在 [0xFF000000, 0xFFFFFFFF] 内分配 ID。 ID 从低位边界开始,并随每次新对象的分配递增。

对象的 ID 为 0 代表空(null)对象,即对象不存在或者空缺。

传输

迄今为止,所有的 Wayland 实现均通过 Unix Socket 工作。 这有个很特别的原因:文件描述符消息。 Unix Socket 是最实用的跨进程文件描述符传输方法,这对大文件传输(如键盘映射、像素缓冲区、剪切板)来说非常必要。 理论上其它传输协议(比如 TCP)是可行的,但是需要开发者实现大文件传输的替代方案。

为了找到 Unix Socket 并连接,大部分实现要做的事和 libwayland 所做的一样:

  1. 如果 WAYLAND_SOCKET 已设置,则假设父进程已经为我们配置了连接,将 WAYLAND_SOCKET 解析为文件描述符。
  2. 如果 WAYLAND_DISPLAY 已设置,则与 XDG_RUNTIME_DIR 路径连接,尝试建立 Unix Socket。
  3. 假设 Socket 名称为 wayland-0 并连接 XDG_RUNTIME_DIR 为路径,尝试建立 Unix Socket。
  4. 失败放弃。

接口与事件请求

Wayland 协议通过发出作用于对象的请求和事件来工作。 每个对象都有自己的接口,定义了可行的请求事件以及对应的签名。 让我们来考虑一个简单的示例接口:wl_surface

wl_surface

Requests 请求

Surface 是可以在屏幕上显示的像素区域, 是我们构建应用程序窗口之类的东西的基本元素之一。 它有个请求名为“damage”(损坏),客户端发送这个请求表示某个 surface 的某些部分发生了变化,需要重绘。 下面是 wire 中的一个 damage 消息的带注释示例(16 进制):

0000000A    对象 ID (10)
00180002    消息长度 (24) 和请求操作码 (2)
00000000    X 坐标       (int): 0
00000000    Y 坐标       (int): 0
00000100    宽度         (int): 256
00000100    高度         (int): 256

这是 session 会话的小片段:surface 已经提前分配,它的 ID 为 10。 当服务端收到这条消息后,服务端会查找 ID 为 10 的对象,发现它是一个 wl_surface 实例。 基于此,服务端用请求的 opcode 操作码 2 查找请求的签名。 然后就知道了接下来有四个整型作为参数,这样服务器就能解码这条消息,分派到内部处理。

Events 事件

请求是从客户端发送到服务端,反之,服务端也可以给客户端广播消息,叫做“事件”。 例如,其中一个事件是 wl_surface 的 enter 事件,在 surface 被显示到指定的 output 时,服务端将发送该事件 (客户端可能会响应这一事件,比如为 HiDPI 高分屏调整缩放的比例因数)。 这样一条消息的示例如下:

0000000A    对象 ID (10)
000C0000    消息长度 (12) 和事件操作码 (0)
00000005    Output (object ID): 5

这条消息通过 ID 引用了另一对象:wl_output,surface 就是在这对象上显示。 客户端收到该消息后,行为与服务端类似:查找 ID 为 10 的对象、将其与 wl_surface 接口关联、查找操作码 0 对应事件的签名。 它相应地解码其余信息(查找 ID 为 5wl_output),然后分派内部处理。

Interfaces 接口

接口定义了请求和事件的列表清单,操作码、签名与之一一对应,双方事先约定,就用可以解码消息。 欲知后事如何,且听下回分解。

上层协议

在 1.3 节有说,wayland.xml 基本上会随着 Wayland 包安装在你的系统里,找到并用文本编辑器打开这个文件。 就是通过此类文件,我们定义了 Wayland 客户端或服务端所支持的接口。

协议在线版查看:https://wayland.app/

Linux 下路径一般为:/usr/share/wayland/wayland.xml

此文件使用 XML 格式,其中定义了每个接口,以及对应的请求、事件和各自的签名。 看看上一章中讨论的 wl_surface 示例:

<interface name="wl_surface" version="4">
  <request name="damage">
    <arg name="x" type="int" />
    <arg name="y" type="int" />
    <arg name="width" type="int" />
    <arg name="height" type="int" />
  </request>

  <event name="enter">
    <arg name="output" type="object" interface="wl_output" />
  </event>
</interface>

片段为了简洁有删减,如果你有找到本机的 wayland.xml 文件,请亲自查看这个接口,它带有额外的文档,这些文档解释了每个请求事件的目的、准确语义。

在处理此 XML 文件时,我们按照它们出现的顺序给每个请求和事件分配一个操作码(都是从 0 开始编号,依次递增)。 结合参数列表,我们可以解码 Wire 协议传输来的请求和事件,并且根据 XML 文档,决定如何进行软件编程来做出相应的处理。 解析 XML 的工作通常不需要手写,而是由代码生成器自动生成,我们将在第 3 章讨论 libwayland 如何实现这一点。

从第 4 章开始,本书其余大部分内容专门用于解释该协议文件,以及一些补充扩展协议。

协议设计规范

在大多数 Wayland 协议的设计中已经应用了一些关键性的理念,我们简述一下。 这些模式在上层的 Wayland 协议和协议扩展中都有体现(至少1在 Wayland 核心协议中是这样)。 如果您正在编写自己的协议扩展,那么推荐您遵守并应用这些规范。

原子性

Wayland 协议设计规范中最重要的是原子性。 Wayland 的一个既定目标是 "每帧都完美"。 为此,大多数接口允许以事务的方式更新,使用多个请求来创建一个新的表示状态,然后一次性提交所有请求。 例如,以下几个属性可以在 wl_surface 上配置:

  • 附加的像素缓冲区
  • 需要重新绘制的变更区域
  • 出于优化而设置为不透明的区域
  • 可接受输入事件的区域
  • 变换,比如旋转 90 度
  • 缓冲区的缩放比例,用于 HiDPI

接口为这些配置提供了许多独立的请求,但它们都处于挂起状态(pending)。 仅当发送提交(commit)请求时,挂起状态才会合并到当前状态(current)。 从而可以在单帧内,原子地更新所有这些属性。 结合其他一些关键性的设计决策,Wayland 合成器可以在每一帧中都完美地渲染一切,没有撕裂或更新一半的窗口,每个像素都在它应在的位置,静静地显示。

资源(Resource)的生命周期

另一个重要的规范是关于资源的生命周期的:尽量避免服务端和客户端向无效对象发送事件或请求。 因此,接口往往会给局部 resource 资源定义有关生命周期的请求和事件,客户端或服务器可以声明他们意图不再向该对象发送请求或事件。 只有当两边都同意释放,才能异步地销毁为对象分配的资源。

Wayland 是一个完全异步的协议,它只能保证同一发送者的消息按照发送的顺序到达。 例如,当客户端决定销毁其键盘设备时,服务端可能有多个输入事件正在排队。 客户端必须正确处理不再需要的对象事件,直到服务端同步。 同理,如果客户端在释放资源请求之前还存在其它请求,那么请求队列就必须以正确的顺序发送,避免操作操作空。

版本相关

Wayland 协议中的版本有两种模式:Unstable 不稳定版和 Stable 稳定版。 这两种模式的协议都向后兼容,但协议从不稳定版过渡到稳定时,会允许最后一次不兼容的变动。 这样给协议提供了孵化期,期间可以进行实际测试,最后大变动可以完善协议,增加日后2的可用性。

为了向后兼容,新的事件请求只能在当前接口协议的末尾添加,枚举同理。 此外,每个实现所用的接口事件,另一端必须支持。 我们将在第 5 章讨论如何确定双方接口的版本。

1

除了那个接口之外。看,至少我们尝试了,对吧?

2

请注意,许多有用的协议在撰写本文时仍不稳定。它们可能有一些不太完善的地方,但仍然得到广泛使用,这就是为什么向后兼容性很重要的原因。当将一个协议从不稳定状态推广到稳定状态时,需要采取一种方式,使软件同时支持不稳定和稳定的协议,以实现更平滑的过渡。

libwayland

我们在 1.3 节中介绍了 libwayland 这一最受欢迎的 Wayland 实现。 本书的大部分内容适用于任何实现,不过接下来的两章将使你熟悉 libwayland。

Wayland 软件包包括用于 Wayland-Client 和 Wayland-Server 的 pkg-config 规范——请参阅构建系统的文档以获取有关它们的链接介绍。当然,大多数应用程序只会链接到其中一个。该库包含一些简单的原语(如链表)和 Wayland 的核心协议——wayland.xml 的预编译版本。

我们将从原语开始介绍。

wayland-util 原语

wayland-util.h 是客户端和服务端共同使用的库,它定义了许多实用的结构、函数、宏,建立了一些用于 Wayland 应用程序的原语。 其中包括:

  • 用于序列化与反序列化 Wayland 协议消息的代码
  • 链表 wl_list 的实现
  • 数组 wl_array 的实现(用于相应的 Wayland 原语)
  • 用于 Wayland 标量(如定点小数)和 C 语言类型转化的程序
  • Debug/Log 工具,获取来自 libwayland 内部传出的信息

头文件自身包含了许多非常好的注释,您应当自己实际读一读。 我们将在接下来的几节中详细介绍如何使用这些原语。

wayland-scanner

Wayland 包里有一个二进制可执行文件:wayland-scanner。 正如本书 2.3 章所说,该工具负责预处理,读取 Wayland 协议的 XML 文件生成 C 语言头文件和对应的胶水代码。 该工具在 Wayland 包的构建过程中会处理核心协议 wayland.xml,并生成名为 wayland-client-protocol.hwayland-server-protocol.h 的头文件以及胶水代码,此外 Wayland 包中通常还包括对协议进行封装的头文件: wayland-client.hwayland-server.h,一般直接 include 后者而不是手动使用前者。

该工具的用法非常简单(见 wayland-scanner -h),可概述如下:

生成客户端头文件:

$ wayland-scanner client-header < protocol.xml > protocol_client.h

生成服务端头文件:

$ wayland-scanner server-header < protocol.xml > protocol_server.h

生成胶水代码:

$ wayland-scanner private-code < protocol.xml > protocol.c

不同的构建系统有不同的方法来配置命令,具体查阅您的工具链文档。 一般来说,您需要在构建时运行 wayland-scanner,然后编译并链接您的程序到胶水代码。

如果条件允许,现在就能用任意的一个 Wayland 协议来执行此操作(如 wayland.xml 基本上在 /usr/share/wayland)。 阅读后续章节时,记得随时查看这些胶水代码和头文件,以了解 libwayland 原语生成的代码如何实际应用。


以下内容来自 wlroots/tinywl/Makefile 供参考

WAYLAND_PROTOCOLS=$(shell pkg-config --variable=pkgdatadir wayland-protocols)
WAYLAND_SCANNER=$(shell pkg-config --variable=wayland_scanner wayland-scanner)
LIBS=\
	 $(shell pkg-config --cflags --libs wlroots) \
	 $(shell pkg-config --cflags --libs wayland-server) \
	 $(shell pkg-config --cflags --libs xkbcommon)

# wayland-scanner is a tool which generates C headers and rigging for Wayland
# protocols, which are specified in XML. wlroots requires you to rig these up
# to your build system yourself and provide them in the include path.
xdg-shell-protocol.h:
	$(WAYLAND_SCANNER) server-header \
		$(WAYLAND_PROTOCOLS)/stable/xdg-shell/xdg-shell.xml $@

tinywl: tinywl.c xdg-shell-protocol.h
	$(CC) $(CFLAGS) \
		-g -Werror -I. \
		-DWLR_USE_UNSTABLE \
		-o $@ $< \
		$(LIBS)

clean:
	rm -f tinywl xdg-shell-protocol.h xdg-shell-protocol.c

.DEFAULT_GOAL=tinywl
.PHONY: clean

资源与代理

对象是客户端和服务端达成共识的实体,它具有一些状态,可以通过 Wire 协议更改。 在客户端,libwayland 通过 wl_proxy 接口引用这些对象。 这些接口对 C 语言友好,它们是抽象对象的具体代理,提供了客户端会间接使用的函数,用于 Wayland 消息的序列化。 查看 wayland-client-core.h 头文件,您会发现为实现该目的的一些底层函数。 它们通常不会被直接使用。

# 查找该文件位置
$ find /usr/include -name wayland-client-core.h

在服务端,对象通过 wl_resource 进行抽象,这点与客户端非常相似,不过有一点更复杂 —— 服务端必须知晓哪个对象属于哪个客户端。 每个 wl_resource 都归单独的某一客户端所有,除此之外,接口大致相同,也有提供把事件发送给客户端的底层抽象。 在服务端上直接使用 wl_resource 的频次将比客户端直接使用 wl_proxy 更高。 这种用法的一个例子是获取拥有你想在上下文之外操纵的资源的 wl_client 的引用,或者在客户端尝试无效操作的时候发送协议错误。

此外还有另一组高级接口,大多数 Wayland 客户端和服务端的代码都与之交互以完成其大部分任务。 我们将在下一节中讨论它们。

接口与监听

终于,我们到达了 libwayland 顶层的抽象:接口与监听。 前几章讨论的 wl_proxy、wl_resource 以及原语是 libwayland 中的单一实现,它们的存在是为了支持本层抽象。 调用 wayland-scanner 处理 XML 文件时,它会针对高级协议中的每个接口,生成接口、监听及其与底层 Wire 协议之间的胶水代码。

回想一下,Wayland 连接上的每个参与者都可以接收和发送消息。客户端监听事件并发送请求,服务端监听请求并发送事件。 各方都使用特定名称的 wl_listener 监听另一方的消息。 下面是这个接口的一个例子:

译者注:surface 有多重含义,但就 wayland 的使用场景来说往往指代窗口上的内容,而 shell surface 实际上就指代传统意义上的窗口。 下面这个监听器包含进入和离开事件

struct wl_surface_listener {
    /** surface enters an output */
    void (*enter)(
        void *data,
        struct wl_surface *wl_surface,
        struct wl_output *output);

    /** surface leaves an output */
    void (*leave)(
        void *data,
        struct wl_surface *wl_surface,
        struct wl_output *output);
};

这是客户端 wl_surface 对应的监听。 用来生成该片段的 XML 如下:

<interface name="wl_surface" version="4">
  <event name="enter">
    <arg name="output"
      type="object"
      interface="wl_output"/>
  </event>

  <event name="leave">
    <arg name="output"
      type="object"
      interface="wl_output"/>
  </event>
  <!-- additional details omitted for brevity -->
</interface>

应该非常清晰地可以看到事件是如何转变成监听接口的: 每个函数指针接收用户数据、事件涉及资源的引用、事件的参数。 我们可以像这样将监听绑定到 wl_surface 上:

static void wl_surface_enter(
    void *data,
    struct wl_surface *wl_surface,
    struct wl_output *output) {
    // ...
}

static void wl_surface_leave(
    void *data,
    struct wl_surface *wl_surface,
    struct wl_output *output) {
    // ...
}

static const struct wl_surface_listener surface_listener = {
    .enter = wl_surface_enter,
    .leave = wl_surface_leave,
};

// ...略...

struct wl_surface *surf;
wl_surface_add_listener(surf, &surface_listener, NULL);

wl_surface 接口也定义了客户端可以进行的一些请求:

<interface name="wl_surface" version="4">
  <request name="attach">
    <arg name="buffer"
      type="object"
      interface="wl_buffer"
      allow-null="true"/>
    <arg name="x" type="int"/>
    <arg name="y" type="int"/>
  </request>
  <!-- additional details omitted for brevity -->
</interface>

wayland-scanner 生成以下原型,以及序列化消息的胶水代码:

void wl_surface_attach(
    struct wl_surface *wl_surface,
    struct wl_buffer *buffer,
    int32_t x, int32_t y);

服务端接口和监听的代码是相同的,但要逆转过来——为请求生成监听,为事件生成胶水代码。 当 libwayland 收到消息时,它会查找对象的 ID、接口,然后用找到的接口解码消息的剩余部分。 再寻找这个对象上的监听,用消息上带的参数调用监听函数。

这便是 libwayland 的全貌! 我们花了好几层抽象才到这一步,您现在应该了解事件如何从服务端开始,成为 Wire 的消息,再被客户端理解并分派。 然而,还有一个悬而未决的问题: 所有这些都假设你已经拥有了对 Wayland 对象的引用,而它又是如何得到的呢?

Wayland Display 显示

到目前为止,我们在解释 Wayland 如何管理客户端和服务器之间对象的所有权时,遗漏了一个关键细节: 对象首先是如何创建出来的? Wayland Display,即 wl_display 隐式存在于每次 Wayland 连接中,它具有以下接口:

<interface name="wl_display" version="1">
  <request name="sync">
    <arg name="callback" type="new_id" interface="wl_callback"
       summary="callback object for the sync request"/>
  </request>

  <request name="get_registry">
    <arg name="registry" type="new_id" interface="wl_registry"
      summary="global registry object"/>
  </request>

  <event name="error">
    <arg name="object_id" type="object" summary="object where the error occurred"/>
    <arg name="code" type="uint" summary="error code"/>
    <arg name="message" type="string" summary="error description"/>
  </event>

  <enum name="error">
    <entry name="invalid_object" value="0" />
    <entry name="invalid_method" value="1" />
    <entry name="no_memory" value="2" />
    <entry name="implementation" value="3" />
  </enum>

  <event name="delete_id">
    <arg name="id" type="uint" summary="deleted object ID"/>
  </event>
  <!-- event 相当于接口的声明,其内部指定了变量名称,类型和说明 -->
</interface>

对于普通 Wayland 用户来说,其中最有趣的是 get_registry,我们将会在接下来的章节中讨论其细节。 简而言之,registry 注册函数是用来分配其他对象的。 其余接口用于连接的状态维护,通常不重要,除非您想编写自己的 libwayland 替代实现。

所以,本章会关注 libwayland 中一些与 wl_display 有关——用于建立和维护 Wayland 连接的函数。 这些函数能操作 libwayland 的内部状态,避免您在 Wire 协议层直接涉及相关的请求和事件。

我们将从函数中最重要的一个开始:建立 display 显示。 对于客户端,它涵盖了连接的实际过程;对于服务器,则是配置显示以供连接。

创建一个显示

启动你的文本编辑器——是时候编写我们的第一行代码了。

Wayland 客户端

连接到 Wayland 服务端并创建一个 wl_display 来管理连接状态是非常容易的:

#include <stdio.h>
#include <wayland-client.h>

int
main(int argc, char *argv[])
{
    struct wl_display *display = wl_display_connect(NULL);
    if (!display) {
        fprintf(stderr, "Failed to connect to Wayland display.\n");
        return 1;
    }
    fprintf(stderr, "Connection established!\n");

    wl_display_disconnect(display);
    return 0;
}

让我们来编译并运行这个程序。假设你在阅读文本的时候使用的是 Wayland 混成器,那么结果应该是这样的:

$ cc -o client client.c -lwayland-client # cc 实际上是 gcc 的软链接
$ ./client
Connection established!

wl_display_connect 是客户端建立 Wayland 连接最常见的方式,其声明如下:

struct wl_display *wl_display_connect(const char *name);

参数 name 是 Wayland 显示服务的名称,通常是 "wayland-0"(可以通过 $WAYLAND_DISPLAY 环境变量查看)。你可以在我们的测试客户端中把 NULL 换成这个,然后自己试试——这很可能是可行的。这与 $XDG_RUNTIME_DIR 中的 Unix 套接字的名称相对应。但是 NULL 是推荐选项,如果选用,libwayland 会有如下操作:

    1. 如果 $WAYLAND_DISPLAY 已经被设置,则尝试连接到 $XDG_RUNTIME_DIR/$WAYLAND_DISPLAY
    1. 试图连接 $XDG_RUNTIME_DIR/wayland-0
    1. 失败

这允许用户通过设置 $WAYLAND_DISPLAY 变量来特别指定他们想在哪个 Wayland 显示器上运行他们的客户端。如果有更复杂的需求,你也可以自行建立连接,并从文件描述符中创建一个 Wayland 显示服务:

struct wl_display *wl_display_connect_to_fd(int fd);

你也可以通过 wl_display_get_fd 获得 wl_display 正在使用的文件描述符,无论你是如何创建这个显示服务的。

int wl_display_get_fd(struct wl_display *display);

Wayland 服务端

这个过程对服务端来说也是相当简单的。显示服务的创建和套接字绑定是分离的,以使得你有时间在任何客户端能够连接到显示服务之前配置它。这里是另一个简例:

#include <stdio.h>
#include <wayland-server.h>

int
main(int argc, char *argv[])
{
    struct wl_display *display = wl_display_create();
    if (!display) {
        fprintf(stderr, "Unable to create Wayland display.\n");
        return 1;
    }

    const char *socket = wl_display_add_socket_auto(display);
    if (!socket) {
        fprintf(stderr, "Unable to add socket to Wayland display.\n");
        return 1;
    }

    fprintf(stderr, "Running Wayland display on %s\n", socket);
    wl_display_run(display);

    wl_display_destroy(display);
    return 0;
}

让我们继续编译并运行:

$ cc -o server server.c -lwayland-server
$ ./server &
Running Wayland display on wayland-1
$ WAYLAND_DISPLAY=wayland-1 ./client
Connection established!

使用 wl_display_add_socket_auto 将会允许 libwayland 自动决定显示服务的名称,默认为 wayland-0,或者 wayland-$n,这取决于是否有其他的 Wayland 混成器在 $XDG_RUNTIME_DIR 中存有套接字。然而,与客户端一样,你还有一些其他的选项来配置显示服务:

int wl_display_add_socket(struct wl_display *display, const char *name);

int wl_display_add_socket_fd(struct wl_display *display, int sock_fd);

在添加套接字后,调用 wl_display_run 将会运行 libwayland 的内部事件循环,并阻塞至调用 wl_display_terminate 终止。这个事件循环是什么?让我们翻开下一页就明白了!

加入一个事件循环

libwayland 为 Wayland 服务端提供了自己的事件循环实现,但维护者需要知道这是一种设计上的僭越行为。 对于客户端,没有这样的等价物。无论如何,Wayland 服务端事件循环已经足够有用了。

Wayland 服务端事件循环

libwayland-server 创建的每一个 wl_display 都有一个对应的 wl_event_loop,你可以通过 wl_display_get_event_loop 来获取其引用。如果你正在写一个新的 Wayland 混成器,你很可能想把它作为唯一的事件循环。你可以用 wl_event_loop_add_fd 来添加一个文件描述符,用 wl_event_loop_add_timer 来添加一个计时器。还可以通过 wl_event_loop_add_signal 来处理信号,这可能是非常便捷的做法。

可以根据你的喜好配置事件循环,以监控混成器所需响应的全部事件。你可以通过调用 wl_display_run 来一次性处理事件和调度 Wayland 客户端。它将处理并陷入事件循环,直到通过 wl_display_terminate 进行终止。大多数 Wayland 混成器从一开始就考虑到 Wayland 的这种用法(而不是从 X11 移植过来)。

然而,也可以采用轮询的方式将 Wayland 显示服务纳入你自己的事件循环。wl_display 在内部使用事件循环来处理客户端,你可以选择自己监控 Wayland 事件循环,在必要的时候对其进行调度,或者也可以完全忽略,手动处理客户端的更新请求。如果你希望让 Wayland 事件循环自己运行,并将其视作你自己事件循环的附属品,你可以使用 wl_event_loop_get_fd 来过的一个可以回调的文件描述符,然后在该文件描述符发生活动时调用 wl_event_loop_dispatch 来处理事件。当你有数据需要写入客户端时,你也需要调用 wl_display_flush_clients

Wayland 客户端事件循环

另一方面,libwayland-client 并没有自己的事件循环。然而,由于通常只有一个文件描述符,所以没有自己的事件循环更容易管理。如果你的程序期望响应唯一的 Wayland 事件,那么这个简单的循环就足够了。

while (wl_display_dispatch(display) != -1) {
    /* This space deliberately left blank */
}

然而,如果你有一个更复杂的应用程序,你可以以任何方式建立你自己的事件循环,并通过 wl_display_get_fd 获得 Wayland 显示器的文件描述符。在 POLLIN 事件中调用 wl_display_dispatch 来处理传入的事件。要刷新输出的请求则用 wl_display_flush

小节

至此,你已经拥有了所有设置 Wayland 显示器和处理事件和请求的背景知识。剩下的唯一步骤是分配对象,以便与连接的对方通讯。为此,我们使用 registry 注册。在下一章结束时,我们将拥有自己第一个可用的 Wayland 客户端。

全局和注册

如果你还能回想起 2.1 章的内容——每个请求和事件都有一个对象 ID 与之关联。但到目前为止,我们还没有讨论对象是如何被创建的。当我们收到一个 Wayland 消息时,我们必须知道对象 ID 所代表的接口,以便对其进行编码。我们还必须以某种方式协商可用的对象,创建新的对象,以及为它们分配 ID。在 Wayland 上,我们同时解决这两个问题——当我们绑定一个对象 ID 时,我们认为在未来所有的消息中使用其接口,并在我们本地状态中存储一个对象 ID 到接口的映射。

为了引导(bootstrap)这些,服务端提供了一个全局对象的列表。这些全局对象根据自身已有的特点来提供信息和功能,且最常见的是它们被用来代理其他对象以实现各种目的——比如创建应用程序的窗口。这些全局对象它们自己也有对象 ID 和接口,我们必须以某种方式来分配和这些对象并对接口定义达成共识。

毫无疑问地,你现在已经想到了鸡生蛋的问题,我们将揭示其奥秘:对象 ID 为 1 的已经被隐式分配给了 wl_display 接口。当你想要调用这个接口时,注意使用 wl_display::get_registry 请求:

<interface name="wl_display" version="1">
  <request name="sync">
    <arg name="callback" type="new_id" interface="wl_callback" />
  </request>

  <request name="get_registry">
    <arg name="registry" type="new_id" interface="wl_registry" />
  </request>

  <!-- cotd -->
</interface>

wl_display::get_registry 请求可以用来将一个对象 ID 绑定定到 wl_registry 接口,这是在 wayland.xml 中找到的下一个接口。鉴于 wl_display 总是有 ID 为 1 的对象,下面的线程信息应该是有意义的(以大端序为例)

C->S    00000001 000C0001 00000002            .... .... ....

当我们将其分解时,第一串序列是对象 ID,第二串序列中前 16 位是消息总长度(以字节为单位),其后的位是请求操作码。剩下的字(只有一个)是参数。简而言之,这是对象 ID 1 (wl_display) 上调用的请求 1 (从 0 开始),它接受一个参数:一个新对象生成的 ID。请注意在 XML 文档中这个新 ID 是提前定义好的,由 wl_registry 接口来管理。

<interface name="wl_registry" version="1">
  <request name="bind">
    <arg name="name" type="uint" />
    <arg name="id" type="new_id" />
  </request>

  <event name="global">
    <arg name="name" type="uint" />
    <arg name="interface" type="string" />
    <arg name="version" type="uint" />
  </event>

  <event name="global_remove">
    <arg name="name" type="uint" />
  </event>
</interface>

这些接口我们将在后续的章节中讨论。

绑定全局变量

创建注册表(registry)对象后,服务端将为服务器上每个可用的全局对象发起全局事件。 然后,您可以绑定到所需要的全局对象。

这个为已知对象分配 ID 的过程称为对象绑定。 一旦客户端像这样绑定到注册表,服务器就会多次发起全局事件,以告知它支持的接口。 每个全局对象都有一个独一无二的 ID 名称,这个 ID 名称是一个无符号整数。 接口字符串映射到协议中找到的接口名称:在上面 XML 中的 wl_display 就是这样一个名称的示例。 此外,版本号也在这里定义——关于接口版本的更多信息,请查看附录 C。

要绑定到任意一个接口时,我们需要使用绑定请求,其工作方式类似于我们绑定到 wl_registry 的神奇过程。 例如,考虑下面的 wire 协议交换:

C->S    00000001 000C0001 00000002            .... .... ....

S->C    00000002 001C0000 00000001 00000007   .... .... .... ....
        776C5f73 686d0000 00000001            wl_s hm.. ....
        [...]

C->S    00000002 00100000 00000001 00000003   .... .... .... ....

第一个消息与我们已经剖析过的消息相同。 第二个消息是来自服务端的事件:对象 2 (客户端在第一个消息中分配了 wl_registry)操作码 0("global"), 参数 1、wl_shm 和 1 分别是这个全局对象的名称、接口和版本。 客户端通过调用对象 ID 2(wl_registry::bind)的操作码 0 进行响应, 并将对象 ID 3 分配给全局名称 1 ——即绑定到全局的 wl_shm。 这个对象的未来事件和请求是由 wl_shm (shared memory support)协议定义, 你可以在 wayland.xml (/usr/share/wayland/wayland.xml) 中找到。

一旦您创建了这个对象,你就可以利用其接口来完成各种任务——在 wl_shm 的例子中, 管理客户端和服务端之间的共享内存。 本书剩下的大部分内容都致力于解释这些全局对象的用法。

有了这些信息,我们就可以写出我们第一个有用的 Wayland 客户端: 它可以简单地打印出服务端上所有可用的全局接口。

#include <stdint.h>
#include <stdio.h>
#include <wayland-client.h>

static void
registry_handle_global(void *data, struct wl_registry *registry,
        uint32_t name, const char *interface, uint32_t version)
{
    printf("interface: '%s', version: %d, name: %d\n",
            interface, version, name);
}

static void
registry_handle_global_remove(void *data, struct wl_registry *registry,
        uint32_t name)
{
    // This space deliberately left blank
}

static const struct wl_registry_listener
registry_listener = {
    .global = registry_handle_global,
    .global_remove = registry_handle_global_remove,
};

int
main(int argc, char *argv[])
{
    struct wl_display *display = wl_display_connect(NULL);
    struct wl_registry *registry = wl_display_get_registry(display);
    wl_registry_add_listener(registry, &registry_listener, NULL);
    wl_display_roundtrip(display);
    return 0;
}

请参考之前的章节来解释这个程序。 我们连接到显示器(4.1 章),获得注册表(本章),然后给它添加一个监听器(3.4 章), 最后打印这个混成器上可用的全局接口来处理全局事件。 自己试试吧。

$ cc -o globals -lwayland-client globals.c

执行程序后输出结果如下:

interface: 'wl_shm', version: 1, name: 1
interface: 'wl_drm', version: 2, name: 2
interface: 'zwp_linux_dmabuf_v1', version: 3, name: 3
...

注意:本章是我们最后一次用十六进制展示线程协议输出,可能也是你最后一次在文中看到它们总体的情况。 追踪你的 Wayland 客户端或服务端的一个更好的方法是,在运行你的程序之前, 将环境中的 WAYLAND_DEBUG 变量设为 1。 现在就用 globals 程序试试吧!

[4144282.115]  -> wl_display@1.get_registry(new id wl_registry@2)
[4144282.149]  -> wl_display@1.sync(new id wl_callback@3)
[4144282.551] wl_display@1.delete_id(3)
[4144282.575] wl_registry@2.global(1, "wl_shm", 1)
interface: 'wl_shm', version: 1, name: 1
[4144282.605] wl_registry@2.global(2, "wl_drm", 2)
interface: 'wl_drm', version: 2, name: 2
[4144282.625] wl_registry@2.global(3, "zwp_linux_dmabuf_v1", 3)
interface: 'zwp_linux_dmabuf_v1', version: 3, name: 3
[4144282.644] wl_registry@2.global(4, "wl_compositor", 4)
interface: 'wl_compositor', version: 4, name: 4
[4144282.661] wl_registry@2.global(5, "wl_subcompositor", 1)
interface: 'wl_subcompositor', version: 1, name: 5
[4144282.678] wl_registry@2.global(6, "wl_data_device_manager", 3)
interface: 'wl_data_device_manager', version: 3, name: 6
[4144282.696] wl_registry@2.global(7, "zwlr_gamma_control_manager_v1", 1)
interface: 'zwlr_gamma_control_manager_v1', version: 1, name: 7
[4144282.719] wl_registry@2.global(8, "zxdg_output_manager_v1", 3)
interface: 'zxdg_output_manager_v1', version: 3, name: 8
[4144282.738] wl_registry@2.global(9, "org_kde_kwin_idle", 1)
interface: 'org_kde_kwin_idle', version: 1, name: 9
[4144282.759] wl_registry@2.global(10, "zwp_idle_inhibit_manager_v1", 1)
interface: 'zwp_idle_inhibit_manager_v1', version: 1, name: 10
[4144282.775] wl_registry@2.global(11, "zwlr_layer_shell_v1", 4)
interface: 'zwlr_layer_shell_v1', version: 4, name: 11
[4144282.793] wl_registry@2.global(12, "xdg_wm_base", 2)
interface: 'xdg_wm_base', version: 2, name: 12
[4144282.810] wl_registry@2.global(13, "zwp_tablet_manager_v2", 1)
interface: 'zwp_tablet_manager_v2', version: 1, name: 13
[4144282.828] wl_registry@2.global(14, "org_kde_kwin_server_decoration_manager", 1)
interface: 'org_kde_kwin_server_decoration_manager', version: 1, name: 14
[4144282.843] wl_registry@2.global(15, "zxdg_decoration_manager_v1", 1)
interface: 'zxdg_decoration_manager_v1', version: 1, name: 15
[4144282.872] wl_registry@2.global(16, "zwp_relative_pointer_manager_v1", 1)
interface: 'zwp_relative_pointer_manager_v1', version: 1, name: 16
[4144282.890] wl_registry@2.global(17, "zwp_pointer_constraints_v1", 1)
interface: 'zwp_pointer_constraints_v1', version: 1, name: 17
[4144282.905] wl_registry@2.global(18, "wp_presentation", 1)
interface: 'wp_presentation', version: 1, name: 18
[4144282.924] wl_registry@2.global(19, "zwlr_output_manager_v1", 2)
interface: 'zwlr_output_manager_v1', version: 2, name: 19
[4144282.942] wl_registry@2.global(20, "zwlr_output_power_manager_v1", 1)
interface: 'zwlr_output_power_manager_v1', version: 1, name: 20
[4144282.962] wl_registry@2.global(21, "zwp_input_method_manager_v2", 1)
interface: 'zwp_input_method_manager_v2', version: 1, name: 21
[4144282.978] wl_registry@2.global(22, "zwp_text_input_manager_v3", 1)
interface: 'zwp_text_input_manager_v3', version: 1, name: 22
[4144282.995] wl_registry@2.global(23, "zwlr_foreign_toplevel_manager_v1", 3)
interface: 'zwlr_foreign_toplevel_manager_v1', version: 3, name: 23
[4144283.012] wl_registry@2.global(24, "zwlr_export_dmabuf_manager_v1", 1)
interface: 'zwlr_export_dmabuf_manager_v1', version: 1, name: 24
[4144283.029] wl_registry@2.global(25, "zwlr_screencopy_manager_v1", 3)
interface: 'zwlr_screencopy_manager_v1', version: 3, name: 25
[4144283.044] wl_registry@2.global(26, "zwlr_data_control_manager_v1", 2)
interface: 'zwlr_data_control_manager_v1', version: 2, name: 26
[4144283.060] wl_registry@2.global(27, "zwp_primary_selection_device_manager_v1", 1)
interface: 'zwp_primary_selection_device_manager_v1', version: 1, name: 27
[4144283.078] wl_registry@2.global(28, "wp_viewporter", 1)
interface: 'wp_viewporter', version: 1, name: 28
[4144283.102] wl_registry@2.global(29, "zxdg_exporter_v1", 1)
interface: 'zxdg_exporter_v1', version: 1, name: 29
[4144283.119] wl_registry@2.global(30, "zxdg_importer_v1", 1)
interface: 'zxdg_importer_v1', version: 1, name: 30
[4144283.137] wl_registry@2.global(31, "zxdg_exporter_v2", 1)
interface: 'zxdg_exporter_v2', version: 1, name: 31
[4144283.161] wl_registry@2.global(32, "zxdg_importer_v2", 1)
interface: 'zxdg_importer_v2', version: 1, name: 32
[4144283.177] wl_registry@2.global(33, "zwp_virtual_keyboard_manager_v1", 1)
interface: 'zwp_virtual_keyboard_manager_v1', version: 1, name: 33
[4144283.196] wl_registry@2.global(34, "zwlr_virtual_pointer_manager_v1", 2)
interface: 'zwlr_virtual_pointer_manager_v1', version: 2, name: 34
[4144283.212] wl_registry@2.global(35, "zwlr_input_inhibit_manager_v1", 1)
interface: 'zwlr_input_inhibit_manager_v1', version: 1, name: 35
[4144283.229] wl_registry@2.global(36, "zwp_keyboard_shortcuts_inhibit_manager_v1", 1)
interface: 'zwp_keyboard_shortcuts_inhibit_manager_v1', version: 1, name: 36
[4144283.250] wl_registry@2.global(37, "wl_seat", 7)
interface: 'wl_seat', version: 7, name: 37
[4144283.274] wl_registry@2.global(38, "zwp_pointer_gestures_v1", 1)
interface: 'zwp_pointer_gestures_v1', version: 1, name: 38
[4144283.296] wl_registry@2.global(39, "wl_output", 3)
interface: 'wl_output', version: 3, name: 39
[4144283.317] wl_callback@3.done(56994)

这里展示 wayland.xml 协议内的具体定义,可以参考其事件及参数配置如下, 可以看到 void *datastruct wl_registry *registry 两个参数是固定的, 其余参数由文件中的 <arg ... /> 定义。

 146   │     <event name="global">
 147   │       <description summary="announce global object">
 148   │     Notify the client of global objects.
 149   │
 150   │     The event notifies the client that a global object with
 151   │     the given name is now available, and it implements the
 152   │     given version of the given interface.
 153   │       </description>
 154   │       <arg name="name" type="uint" summary="numeric name of the global object"/>
 155   │       <arg name="interface" type="string" summary="interface implemented by the object"/>
 156   │       <arg name="version" type="uint" summary="interface version"/>
 157   │     </event>
 158   │
 159   │     <event name="global_remove">
 160   │       <description summary="announce removal of global object">
 161   │     Notify the client of removed global objects.
 162   │
 163   │     This event notifies the client that the global identified
 164   │     by name is no longer available.  If the client bound to
 165   │     the global using the bind request, the client should now
 166   │     destroy that object.
 167   │
 168   │     The object remains valid and requests to the object will be
 169   │     ignored until the client destroys it, to avoid races between
 170   │     the global going away and a client sending a request to it.
 171   │       </description>
 172   │       <arg name="name" type="uint" summary="numeric name of the global object"/>
 173   │     </event>

注册全局对象

libwayland-server 注册全局对象的方式与之前有些不同。 当你用wayland-scanner 生成服务端代码时,它会创建接口(类似于监听器)和发送事件的胶水代码。 第一项任务是注册全局对象,当全局对象被绑定时,用一个函数来启动资源1。 就代码而言,其结果看起来像这样。

static void
wl_output_handle_bind(struct wl_client *client, void *data,
    uint32_t version, uint32_t id)
{
    struct my_state *state = data;
    // TODO
}

int
main(int argc, char *argv[])
{
    struct wl_display *display = wl_display_create();
    struct my_state state = { ... };
    // ...
    wl_global_create(wl_display, &wl_output_interface,
        1, &state, wl_output_handle_bind);
    // ...
}

如果你采用这段代码,例如把它修补到 4.1 章的服务端示例中, 你会让一个 wl_output 全局接口对我们上次写的 "globals" 程序可见2。 然而,任何试图绑定到这个全局接口的对象都会将运行到我们的 TODO 处。 为了完善这一点,我们还需要提供一个 wl_output 接口的实现。

static void
wl_output_handle_resource_destroy(struct wl_resource *resource)
{
    struct my_output *client_output = wl_resource_get_user_data(resource);

    // TODO: Clean up resource

    remove_to_list(client_output->state->client_outputs, client_output);
}

static void
wl_output_handle_release(struct wl_client *client, struct wl_resource *resource)
{
    wl_resource_destroy(resource);
}

static const struct wl_output_interface
wl_output_implementation = {
    .release = wl_output_handle_release,
};

static void
wl_output_handle_bind(struct wl_client *client, void *data,
    uint32_t version, uint32_t id)
{
    struct my_state *state = data;

    struct my_output *client_output = calloc(1, sizeof(struct client_output));

    struct wl_resource *resource = wl_resource_create(
        client, &wl_output_interface, wl_output_interface.version, id);

    wl_resource_set_implementation(resource, &wl_output_implementation,
        client_output, wl_output_handle_resource_destroy);

    client_output->resource = resource;
    client_output->state = state;

    // TODO: Send geometry event, et al

    add_to_list(state->client_outputs, client_output);
}

光这样是很难理解的,因此让我们来逐一解释。 在底部,我们已经扩展了我们的 "bind handle",以创建一个 wl_resource 来跟踪这个对象的服务端状态(使用客户端 ID)。 当我们这样做的时候,我们向 wl_resource_create 提供了一个指向我们的接口实现的指针,即 wl_output_implementation, 在这段代码中是一个常量静态结构体。 这个类型 (struct wl_output_interface) 是由 wayland-scanner 生成的,包含了这个接口所支持的每个请求的一个函数指针。 我们还借此机会分配了一个小容器, 用于存储我们需要的、libwayland 不为我们处理的、任何额外状态,其具体性质因协议不同而不同。

注意: 这里有两个不同的东西,但是使用同一个名字: struct wl_output_interface 是接口的实例, 另一个 wl_output_interfacewayland-scanner 生成的一个全局常规变量, 它包含与实现有关的元数据(比如上面例子中使用的版本)。

当客户端发送释放请求的时候,我们的 wl_output_handle_release 函数就会被调用, 表明它们不再需要这个资源——所以我们应该销毁它。 这反过来触发了 wl_output_handle_resource_destroy 函数, 稍后我们将扩展该函数以释放我们先前为它本配的所有状态。 这个函数也被传递到 wl_resource_create 中作为析构器, 如果客户端在没有明确发送释放请求的情况下意外终止,它将会被调用。

我们代码中剩下的另一个 "TODO" 是发送 "name" 以及其他一些事件。 如果我们回顾一下 wayland.xml,我们会在接口上看到这个事件:

<event name="geometry">
  <description summary="properties of the output">
The geometry event describes geometric properties of the output.
The event is sent when binding to the output object and whenever
any of the properties change.

The physical size can be set to zero if it doesn't make sense for this
output (e.g. for projectors or virtual outputs).
  </description>
  <arg name="x" type="int" />
  <arg name="y" type="int" />
  <arg name="physical_width" type="int" />
  <arg name="physical_height" type="int" />
  <arg name="subpixel" type="int" enum="subpixel" />
  <arg name="make" type="string" />
  <arg name="model" type="string" />
  <arg name="transform" type="int" enum="transform" />
</event>

当输出被绑定后,应该交由我们来发送这个事件,这很容易实现:

static void
wl_output_handle_bind(struct wl_client *client, void *data,
    uint32_t version, uint32_t id)
{
    struct my_state *state = data;

    struct my_output *client_output = calloc(1, sizeof(struct client_output));

    struct wl_resource *resource = wl_resource_create(
        client, &wl_output_implementation, wl_output_interface.version, id);

    wl_resource_set_implementation(resource, wl_output_implementation,
        client_output, wl_output_handle_resource_destroy);

    client_output->resource = resource;
    client_output->state = state;

    // 发送事件(对应于 wayland.xml 中声明的 event arg)
    wl_output_send_geometry(resource, 0, 0, 1920, 1080,
        WL_OUTPUT_SUBPIXEL_UNKNOWN, "Foobar, Inc",
        "Fancy Monitor 9001 4K HD 120 FPS Noscope",
        WL_OUTPUT_TRANSFORM_NORMAL);

    add_to_list(state->client_outputs, client_output);
}

注意: 这里所用的 wl_output::geometry 是为了解释说明,但在实践中对它的使用有一些特殊考虑。在你的客户端或服务端中实现这个事件之前,请查看协议的 XML 文件定义。

1

资源代表每个客户端对象实例的服务器端状态。

2

如果你对更为健壮的程序有兴趣,我们还可以从 Wayland-utils (之前在 Weston) 项目中获取一种稍微复杂一些的"globals"程序版本,名为 wayland-info

缓冲区和表面

译注:surface 取了直译,实际上即为像素最后渲染到屏幕前的显示面

显然,这个系统的全部意义在于向用户显示信息,并接收他们的反馈以进行额外处理。在这一章,我们将会探讨这些任务其中之一:在屏幕上显示像素。

为实现这一目的,我们有两个基本概念需要了解:缓冲区和表面,分别由 wl_bufferwl_surface 接口管理。缓冲区作为一些底层像素的存储,并且由客户端来提供一些实现方法——共享内存缓冲区和 GPU 句柄是最为常见的方法。

使用 wl_compositor

有人说给事物命名是计算机科学中最为复杂的问题之一,而在这也是如此,并且证据确凿。wl_compositor 是全局的混成器,也是混成器的一部分。通过这个接口,你可以向服务端发送你要展示的窗口,以便与旁边其他的窗口进行混成。混成器有两项工作:创建表面和区域。

此处引用规范来介绍:一个 Wayland 表面有一个矩形区域,可以显示在 0 号(默认显示器)或者更多的输出设备上,递交缓冲区,接受用户输入,并定义一个相对坐标系。我们将在后期详细讨论这些问题,但从最根本的部分开始:获得一个表面并为其强加缓冲区。要获得一个表面,首先我们要自己的混成器绑定到全局 wl_compositor。通过扩展 5.1 章的例子,我们可以得到如下结果:

struct our_state {
    // ...
    struct wl_compositor *compositor;
    // ...
};

static void
registry_handle_global(void *data, struct wl_registry *wl_registry,
		uint32_t name, const char *interface, uint32_t version)
{
    struct our_state *state = data;
    if (strcmp(interface, wl_compositor_interface.name) == 0) {
        state->compositor = wl_registry_bind(
            wl_registry, name, &wl_compositor_interface, 4);
    }
}

int
main(int argc, char *argv[])
{
    struct our_state state = { 0 };
    // ...
    wl_registry_add_listener(registry, &registry_listener, &state);
    // ...
}

注意,我们在调用 wl_registry_bind 时指定了版本 4,这是写作时的最新版本。有了这个引用的保证,我们就可以创建一个 wl_surface

struct wl_surface *surface = wl_compositor_create_surface(state.compositor);

在我们能够显示它之前,我们必须首先给它附加一个像素源:一个 wl_buffer

共享内存缓冲区

从客户端获取像素到混成器最简单,也是唯一被载入 wayland.xml 的方法,就是 wl_shm ——共享内存。简单地说,它允许你为混成器传输一个文件描述符到带有 MAP_SHARED 的内存映射(mmap),然后从这个池中共享像素缓冲区。添加一些简单的同步原语,以防止缓冲区竞争,然后你就有了一个可行且可移植的解决方案。

绑定到 wl_shm

在 5.1 章节中解释的全局注册表监听器将在 wl_shm 全局可用时进行公告。绑定到它是相当直接的。扩展第 5.1 章中的例子,我们可以得到如下结果:

struct our_state {
    // ...
    struct wl_shm *shm;
    // ...
};

static void
registry_handle_global(void *data, struct wl_registry *registry,
		uint32_t name, const char *interface, uint32_t version)
{
    struct our_state *state = data;
    if (strcmp(interface, wl_shm_interface.name) == 0) {
        state->shm = wl_registry_bind(
            wl_registry, name, &wl_shm_interface, 1);
    }
}

int
main(int argc, char *argv[])
{
    struct our_state state = { 0 };
    // ...
    wl_registry_add_listener(registry, &registry_listener, &state);
    // ...
}

一旦绑定,我们可以选择通过 wl_shm_add_listener 添加一个监听器。混成器将通过这个监听器公布器其所支持的像素格式。可用的像素格式的完整列表在 wayland.xml 中给出。有两种格式是必须支持的:ARGB (各 8 位色深) 和 XRGB (各 8 位色深),它们是 24 位颜色,分别有和没有透明度 (alpha) 通道。

分配共享内存工具

可以利用 POSIX shm_open 和随机文件名的组合来创建一个适合这一目的的文件,并利用 ftruncate 分配合适的大小。下面的模板可以在公共领域或 CC0 下自由使用:

#define _POSIX_C_SOURCE 200112L
#include <errno.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <time.h>
#include <unistd.h>

static void
randname(char *buf)
{
	struct timespec ts;
	clock_gettime(CLOCK_REALTIME, &ts);
	long r = ts.tv_nsec;
	for (int i = 0; i < 6; ++i) {
		buf[i] = 'A'+(r&15)+(r&16)*2;
		r >>= 5;
	}
}

static int
create_shm_file(void)
{
	int retries = 100;
	do {
		char name[] = "/wl_shm-XXXXXX";
		randname(name + sizeof(name) - 7);
		--retries;
		int fd = shm_open(name, O_RDWR | O_CREAT | O_EXCL, 0600);
		if (fd >= 0) {
			shm_unlink(name);
			return fd;
		}
	} while (retries > 0 && errno == EEXIST);
	return -1;
}

int
allocate_shm_file(size_t size)
{
	int fd = create_shm_file();
	if (fd < 0)
		return -1;
	int ret;
	do {
		ret = ftruncate(fd, size);
	} while (ret < 0 && errno == EINTR);
	if (ret < 0) {
		close(fd);
		return -1;
	}
	return fd;
}

希望这些代码能浅显易懂。有了这个,客户端可以相当简单地创建一个共享内存池。比如,我们想显示一个 1920x1080 的窗口,我们需要两个缓冲区来进行双缓冲,所以这将是 4,147,200 像素。假设像素格式是 WL_SHM_FORMAT_XRGB8888,那么每个像素将有 4 个字节,总池大小为 16,588,800 字节。如第 5.1 章所述,从注册表中绑定全局 wl_shm,然后像这样来使用它创建一个可以容纳这些缓冲区的共享内存池:

const int width = 1920, height = 1080;
const int stride = width * 4;
const int shm_pool_size = height * stride * 2;

int fd = allocate_shm_file(shm_pool_size);
uint8_t *pool_data = mmap(NULL, shm_pool_size,
    PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);

struct wl_shm *shm = ...; // Bound from registry
struct wl_shm_pool *pool = wl_shm_create_pool(shm, fd, shm_pool_size);

从池中创建缓存区域

一旦这个消息传到混成器,它也会对这个文件描述符进行内存映射。不过 Wayland 是异步的,所以我们可以马上开始从这个池子里分配缓冲区。由于我们为两个缓冲区分配了空间,所以需要为每个缓冲区各分配一个索引,并将这些索引转换成池中的字节偏移量。有了这些信息后,我们可以创建一个 wl_buffer

int index = 0;
int offset = height * stride * index;
struct wl_buffer *buffer = wl_shm_pool_create_buffer(pool, offset,
    width, height, stride, WL_SHM_FORMAT_XRGB8888);

我们现在也可以将图像写入此缓冲区。例如,将其设置为纯白色:

uint32_t *pixels = (uint32_t *)&pool_data[offset];
memset(pixels, 0, width * height * 4);

或者,为了更有趣,这里有一个棋盘格图案:

uint32_t *pixels = (uint32_t *)&pool_data[offset];
for (int y = 0; y < height; ++y) {
  for (int x = 0; x < width; ++x) {
    if ((x + y / 8 * 8) % 16 < 8) {
      pixels[y * width + x] = 0xFF666666;
    } else {
      pixels[y * width + x] = 0xFFEEEEEE;
    }
  }
}

舞台已经设置好后,我们需要把缓冲区连接到我们的界面,把整个表面标记为 “损坏”1,并提交:

wl_surface_attach(surface, buffer, 0, 0);
wl_surface_damage(surface, 0, 0, UINT32_MAX, UINT32_MAX);
wl_surface_commit(surface);

如果你运用这些新学到的知识来编写一个 Wayland 客户端,当你的缓冲区没有显示在屏幕上时,你可能会感到疑惑。我们错过了关键的最后一步——给你的表面分配一个角色(role)。

1

“损坏” 意味着 “这个区域需要重新绘制”

服务端的 wl_shm

在到达那一步之前,服务端的部分也值得注意。libwayland 提供了一些辅助程序,让 wl_shm 使用起来更容易。若要配置显示器,它只需要以下内容:

int
wl_display_init_shm(struct wl_display *display);

uint32_t *
wl_display_add_shm_format(struct wl_display *display, uint32_t format);

前者创建了全局对象,并设置了内部实现,后者添加了一个支持的像素格式(记得至少添加 ARGB8888 和 XRGB8888)。一旦客户端将缓冲区添加到它的一个表面,你就可以将缓冲区资源传入 wl_shm_buffer_get 以获得一个 wl_shm_buffer 引用,并像下面这样利用它:

void
wl_shm_buffer_begin_access(struct wl_shm_buffer *buffer);

void
wl_shm_buffer_end_access(struct wl_shm_buffer *buffer);

void *
wl_shm_buffer_get_data(struct wl_shm_buffer *buffer);

int32_t
wl_shm_buffer_get_stride(struct wl_shm_buffer *buffer);

uint32_t
wl_shm_buffer_get_format(struct wl_shm_buffer *buffer);

int32_t
wl_shm_buffer_get_width(struct wl_shm_buffer *buffer);

int32_t
wl_shm_buffer_get_height(struct wl_shm_buffer *buffer);

如果你用 begin_accessend_access 来保护你对缓冲区数据的访问,libwayland 将会为你处理锁的问题。

Linux dmabuf

大多数 Wayland 混成器在 GPU 上进行渲染,而许多 Wayland 客户端也同样在 GPU 上进行渲染。在这种情况下,使用共享内存的方法,从客户端向混成器发送缓冲区是非常低效的。 因为客户端必须将它们的数据从 GPU 读到 CPU,然后混成器必须将其从 CPU 中读回 GPU 以进行渲染。

Linux 上的 DRM (直接渲染管理器 Direct Rendering Manager) 接口(在一些 BSD 中也有实现)为我们提供了一种向 GPU 资源暴露句柄的方法。Mesa 是用户态 Linux 图形驱动的主要实现方式,它实现了一个协议,允许 EGL 用户将 GPU 缓冲区的句柄从客户端传输到混成器上进行渲染,而无需将数据复制到 GPU 上 (原文此处写成 GPU,也可以理解为从 GPU 到 CPU 再到 GPU,这一二次传递过程,总的来说是省略了多余的 GPU 通信过程)1

这个协议的内部工作原理不在本书讨论范围内,那些专注于 Mesa 或 Linux DRM 的资源更适合进一步学习。然而,我们也可以提供一个关于使用的简短总结:

  1. eglGetPlatformDisplayEXTEGL_PLATFORM_WAYLAND_KHR 一起使用来创建一个 EGL 显示。
  2. 照常配置显示,选择一个适合自己情况的配置,将 EGL_SURFACE_TYPE 设置成 EGL_WINDOW_BIT
  3. 使用 wl_egl_window_create 来为一个给定的 wl_surface 创建一个 wl_egl_window
  4. 使用 eglCreatePlatformWindowSurfaceEXTwl_egl_window 创建一个 EGLSurface
  5. 照常使用 EGL,例如,使用 eglMakeCurrent 让表面的 EGL 上下文处于当前状态,使用 eglSwapBuffers 向混成器发送最新的缓冲区并提交表面内容。

如果你之后需要改变 wl_egl_window 的大小,可以使用 wl_egl_window_resize 来实现。

但我真的想要知道内部实现

一些不使用 libwayland 的 Wayland 程序员抱怨说,这种方法将 Mesa 和 libwayland 捆绑在一起,诚然如此。然而,解偶也并非不能——它只是需要你自己以 linux-dmabuf 的形式做大量的实现。关于协议的细节请参考 Wayland 扩展的 XML,以及 Mesa 在 src/egl/drivers/dri2/platform_wayland.c 的实现(在撰写本文时的文件路径)。祝你好运!

对于服务端

不幸的是,混成器的细节既复杂又超出了本书的范围。不过我可以给你指出正确的方向:wlroots 的实现很简单2,应该可以让你走上正确的道路。

1

感谢 quininer 老师对此处翻译细节的指正

2

截至写作时,应该可以在 types/wlr_linux_dmabuf_v1.c 中找到

表面的 “角色” (Surface roles)

我们已经创建了一个像素缓冲区,把它送到了服务端上,并将其连接到一个表面,按理说我们可以通过这个表面向用户显示它。然而,要赋予表面意义还缺少一个关键部分:它的角色。

像素缓冲区可能会向用户表现出许多不同种的情况,而每种情况都需要有不同的语义。例如你的光标图像或是桌面壁纸。为了对比应用程序窗口和光标的语义,我们可以举个例子:你的光标能否被最小化,又或是你的应用窗口是否应该被黏在鼠标上,跟随鼠标移动。出于这个原因,角色提供了另一层抽象,它允许你为表面分配适当的语义。

在接下来的第 6 章中,你可能想给你的程序分配一个应用程序窗口的角色。下一章节介绍了实现这一目的机制:XDG shell

XDG shell 基础

XDG (cross-desktop group) shell 是 Wayland 的一个标准扩展协议,描述了应用窗口的语义。它定义了两个 wl_surface 角色:"toplevel" 用于你的顶层应用窗口;"popup" 则用于诸如上下文菜单、下拉菜单、工具提示等等——它们是顶层窗口的子集。有了这些,你可以将其归结于一个树状结构,顶层是根,弹出式或附加式窗口处于顶层的子叶上。该协议还定义了一个定位器接口,用于辅助定位弹窗,并提供有关窗口周围事物的那些信息。

xdg-shell,作为一个扩展协议,它并没有在 wayland.xml 中定义。取而代之的是你将会在 wayland-protocols 包中找到它。在你的系统中,它可能被安装在类似于 /usr/share/wayland-protocols/stable/xdg-shell/xdg-shell.xml 的路径下。

...
1074   │   <interface name="xdg_popup" version="3">
1075   │     <description summary="short-lived, popup surfaces for menus">
...

xdg_wm_base

xdg_wm_base 是规范中定义的唯一一个全局接口,它提供了创建你所需要的其他每个对象的请求。最基本的实现是从处理 "ping" 事件开始的——当混成器发送该事件时,你应该及时响应 "pong" 请求,以表明你还没有陷入死锁。另一个请求涉及到定位器的创建,也就是先前有提到的,我们将把这些细节留到第十章。首先我们要研究的请求是 get_xdg_surface

XDG 表面

xdg-shell 领域内的表面被称为 xdg_surfaces,这个接口带来了两种 XDG 表面所共有的功能——toplevels 和 popups(也即之前提到的顶层窗口和弹窗)。每种 XDG 表面的语义仍然不同,所以必须通过一个额外的角色来明确指定它们。

xdg_surface 接口提供了额外的请求来分配更具体的 popup 和 toplevel 角色。一旦我们将一个全局对象绑定到全局接口 xdg_wm_base,我们就可以使用 get_xdg_surface 请求来获得一个 wl_suraface

<request name="get_xdg_surface">
  <arg name="id" type="new_id" interface="xdg_surface"/>
  <arg name="surface" type="object" interface="wl_surface"/>
</request>

xdg_surface 接口除了要求你给表面分配一个更具体的 toplevel 或 popup 角色外,还包括一些两个角色共有的重要功能。在我们继续讨论这二者的具体语义之前,先让我们回顾一下:

<event name="configure">
  <arg name="serial" type="uint" summary="serial of the configure event"/>
</event>

<request name="ack_configure">
  <arg name="serial" type="uint" summary="the serial from the configure event"/>
</request>

xdg-surface 最重要的 API 就是 configureack_configure 这一对。你可能还记得,Wayland 的一个目标是让每一帧都完美呈现。这意味着任何一帧都没有应用了一半的状态变化(原子性,避免画面撕裂),为了实现这个目标,我们必须要在客户端和服务端之间同步这些变化。对于 XDG 表面来说,这对消息(这两个 API 传递的内容)正是实现这一目的的机制。

我们目前只关注基础内容,因此我们会总结这两个事件的重点如下:当来自服务端的事件通知你配置(或重新配置)一个表面时,将它们设置到一个待定状态。当一个 configure 事件到来时,会应用先前准备好的变化,使用 ack_configure 来确定你已经这样做了,然后渲染并提交一个新的帧。我们将在下一章节中展示这一做法,并在 8.1 章中详细解释。

<request name="set_window_geometry">
  <arg name="x" type="int"/>
  <arg name="y" type="int"/>
  <arg name="width" type="int"/>
  <arg name="height" type="int"/>
</request>

set_window_geometry 请求主要用于使用应用程序的 CSD(client-side decorations),以区分其表面上被认为是窗口和不是窗口的部分。它最常用于排除窗口后面渲染的客户端阴影,使其不被视为窗口的一部分(即窗口阴影和窗口本体分离)。混成器可以使用这些信息来管理它自己的行为,以布置窗口和交互。

应用程序窗口

我们历尽艰难终于走到了这里,但现在是时候了:XDG toplevel 是我们最终要来显示一个应用程序的接口。XDG toplevel 接口管理有许多管理应用程序窗口的请求和事件,包括最小化和最大化状态,设置窗口标题等。我们将在以后的章节中详细讨论它的每一部分,因此先让我们来关注最基本的内容。

基于上一章的知识,我们知道可以从 wl_surface 获得一个 xdg_surface,但这只是第一步:把一个 surface 夹带进 XDG shell。下一步是把 XDG 表面变成一个 XDG toplevel——一个 “顶层” 应用程序窗口,它因最终处于 XDG shell 创建的窗口和弹出菜单的顶层而得名。要创建一个这样的窗口,我们可以使用 xdg_surface 接口来合理请求。

<request name="get_toplevel">
  <arg name="id" type="new_id" interface="xdg_toplevel"/>
</request>

这个新的 xdg_toplevel 接口为我们提供了许多请求和事件,用于管理应用程序窗口的生命周期。第 10 章深入讨论了这些问题,但我知道你很想先在屏幕上得到一些东西。如果你按照这些的步骤,处理好上一章 XDG 表面的 configureack_configure 上下文,并将一个 wl_buffer 提交到我们的 wl_surface,一个应用程序窗口就会出现,并向用户展示你的缓冲区内容。下一章将提供这样的示例代码,示例还利用了一个额外的 XDG toplevel 请求,我们目前还没有涉及:

<request name="set_title">
  <arg name="title" type="string"/>
</request>

不过这应该是不言自明的。还有一个类似的请求我们在示例代码中没有使用,但它可能适合你的应用:

<request name="set_app_id">
  <arg name="app_id" type="string"/>
</request>

标题通常显示在窗口装饰,任务栏等地方,而应用 ID 则用于识别你的应用程序或将你的窗口组合到一起。你可以通过将你的窗口标题设置为 "Application windows - The Wayland Protocol - Firefox",以及将你的应用程序 ID 设为 "firefox" 的方式来使用它。

总而言之,以下步骤将会带你从零开始创建一个屏幕上的窗口:

  1. 绑定到 wl_compositor 并使用它来创建一个 wl_surface
  2. 绑定到 xdg_wm_base 并用它为你的 wl_surface 创建一个 xdg_surface
  3. 通过 xdg_surface.get_toplevelxdg_surface 创建一个 xdg_toplevel
  4. xdg_surface 创建一个监听器,并且等待 configure 事件的发生。
  5. 绑定到你选择的缓冲区分配机制(如 wl_shm),并分配一个共享缓冲区,然后将你要显示的内容渲染后传入。
  6. 使用 wl_surface.attachwl_buffer 附加到 wl_surface 上。
  7. 使用 xdg_surface.ack_configureconfigure 的序列信息传给它,确认你已经准备好了一个合适的帧。
  8. 发送一个 wl_surface.commit 请求。

翻到下一页,可以看到这些步骤的具体操作。

扩展示例代码

利用我们目前为止所学到的知识,我们现在可以写一个 Wayland 客户端,以在屏幕上显示一些东西。下面的代码是一个完整的 Wayland 应用程序,它可以打开一个 XDG 顶层窗口,并在上面显示一个 640x480 像素的棋盘格。

preview

可以像这样编译它:

wayland-scanner private-code \
  < /usr/share/wayland-protocols/stable/xdg-shell/xdg-shell.xml \
  > xdg-shell-protocol.c
wayland-scanner client-header \
  < /usr/share/wayland-protocols/stable/xdg-shell/xdg-shell.xml \
  > xdg-shell-client-protocol.h
cc -o client client.c xdg-shell-protocol.c -lwayland-client -lrt

然后运行 ./client 来查看其情况,或者运行 WAYLAND_DEBUG=1 ./client 来包含更多有用的调试信息。在未来的章节中,我们将在这个客户端的基础上进行开发,所以要将这些代码妥善保管。

#define _POSIX_C_SOURCE 200112L
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <stdbool.h>
#include <string.h>
#include <sys/mman.h>
#include <time.h>
#include <unistd.h>
#include <wayland-client.h>
#include "xdg-shell-client-protocol.h"

/* Shared memory support code */
static void
randname(char *buf)
{
    // 返回一个随机的名字
    struct timespec ts;
    clock_gettime(CLOCK_REALTIME, &ts);
    long r = ts.tv_nsec;
    for (int i = 0; i < 6; ++i) {
        buf[i] = 'A'+(r&15)+(r&16)*2;
        r >>= 5;
    }
}

static int
create_shm_file(void)
{
    int retries = 100;
    do {
        char name[] = "/wl_shm-XXXXXX";
        randname(name + sizeof(name) - 7);
        --retries;
        int fd = shm_open(name, O_RDWR | O_CREAT | O_EXCL, 0600);
        if (fd >= 0) {
            shm_unlink(name);
            return fd;
        }
    } while (retries > 0 && errno == EEXIST);
    return -1;
}

static int
allocate_shm_file(size_t size)
{
    int fd = create_shm_file();
    if (fd < 0)
        return -1;
    int ret;
    do {
        /* 
        * 关于 ftruncate 的内容可以参考
        * https://www.man7.org/linux/man-pages/man3/ftruncate.3p.html
        */
        ret = ftruncate(fd, size);
    } while (ret < 0 && errno == EINTR);
    if (ret < 0) {
        close(fd);
        return -1;
    }
    return fd;
}

/* Wayland code */
struct client_state {
    /* Globals */
    struct wl_display *wl_display;
    struct wl_registry *wl_registry;
    struct wl_shm *wl_shm;
    struct wl_compositor *wl_compositor;
    struct xdg_wm_base *xdg_wm_base;
    /* Objects */
    struct wl_surface *wl_surface;
    struct xdg_surface *xdg_surface;
    struct xdg_toplevel *xdg_toplevel;
};

static void
wl_buffer_release(void *data, struct wl_buffer *wl_buffer)
{
    /* Sent by the compositor when it's no longer using this buffer */
    wl_buffer_destroy(wl_buffer);
}

static const struct wl_buffer_listener wl_buffer_listener = {
    .release = wl_buffer_release,
};

static struct wl_buffer *
draw_frame(struct client_state *state)
{
    const int width = 640, height = 480;
    int stride = width * 4;
    int size = stride * height;

    int fd = allocate_shm_file(size);
    if (fd == -1) {
        return NULL;
    }

    uint32_t *data = mmap(NULL, size,
            PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    if (data == MAP_FAILED) {
        close(fd);
        return NULL;
    }

    // 创建一个共享内存池
    struct wl_shm_pool *pool = wl_shm_create_pool(state->wl_shm, fd, size);

    // 创建一个缓冲区 
    struct wl_buffer *buffer = wl_shm_pool_create_buffer(pool, 0,
            width, height, stride, WL_SHM_FORMAT_XRGB8888);
    wl_shm_pool_destroy(pool);
    close(fd);

    /* Draw checkerboxed background */
    for (int y = 0; y < height; ++y) {
        for (int x = 0; x < width; ++x) {
            if ((x + y / 8 * 8) % 16 < 8)
                data[y * width + x] = 0xFF666666;
            else
                data[y * width + x] = 0xFFEEEEEE;
        }
    }

    munmap(data, size);

    // 添加监听器用于检测释放缓冲区的事件
    wl_buffer_add_listener(buffer, &wl_buffer_listener, NULL);
    return buffer;
}

static void
xdg_surface_configure(void *data,
        struct xdg_surface *xdg_surface, uint32_t serial)
{
    struct client_state *state = data;

    // 返回一个 ack_configure 以示确认
    xdg_surface_ack_configure(xdg_surface, serial);

    // 向缓冲区中绘制内容
    struct wl_buffer *buffer = draw_frame(state);

    // 将缓冲区内容附加到表面
    wl_surface_attach(state->wl_surface, buffer, 0, 0);

    // 提交表面
    wl_surface_commit(state->wl_surface);
}

static const struct xdg_surface_listener xdg_surface_listener = {
    .configure = xdg_surface_configure,
};

static void
xdg_wm_base_ping(void *data, struct xdg_wm_base *xdg_wm_base, uint32_t serial)
{
    xdg_wm_base_pong(xdg_wm_base, serial);
}

static const struct xdg_wm_base_listener xdg_wm_base_listener = {
    .ping = xdg_wm_base_ping,
};

static void
registry_global(void *data, struct wl_registry *wl_registry,
        uint32_t name, const char *interface, uint32_t version)
{
    struct client_state *state = data;

    // 绑定到全局
    if (strcmp(interface, wl_shm_interface.name) == 0) {
        state->wl_shm = wl_registry_bind(
                wl_registry, name, &wl_shm_interface, 1);
    } else if (strcmp(interface, wl_compositor_interface.name) == 0) {
        state->wl_compositor = wl_registry_bind(
                wl_registry, name, &wl_compositor_interface, 4);
    } else if (strcmp(interface, xdg_wm_base_interface.name) == 0) {
        state->xdg_wm_base = wl_registry_bind(
                wl_registry, name, &xdg_wm_base_interface, 1);
        xdg_wm_base_add_listener(state->xdg_wm_base,
                &xdg_wm_base_listener, state);
    }
}

static void
registry_global_remove(void *data,
        struct wl_registry *wl_registry, uint32_t name)
{
    /* This space deliberately left blank */
}

static const struct wl_registry_listener wl_registry_listener = {
    .global = registry_global,
    .global_remove = registry_global_remove,
};

int
main(int argc, char *argv[])
{
    // 初始化状态
    struct client_state state = { 0 };
    
    // 获取默认显示器
    state.wl_display = wl_display_connect(NULL);
    
    // 注册到默认显示器
    state.wl_registry = wl_display_get_registry(state.wl_display);
    
    // 添加事件监听
    wl_registry_add_listener(state.wl_registry, &wl_registry_listener, &state);

    // 以 wl_display 作为代理连接到混成器
    wl_display_roundtrip(state.wl_display);

    // 从混成器创建一个表面
    state.wl_surface = wl_compositor_create_surface(state.wl_compositor);

    // 从 wl_surface 创建一个 xdg_surface
    state.xdg_surface = xdg_wm_base_get_xdg_surface(
            state.xdg_wm_base, state.wl_surface);

    // 添加事件监听
    xdg_surface_add_listener(state.xdg_surface, &xdg_surface_listener, &state);

    // 获得顶层窗口
    state.xdg_toplevel = xdg_surface_get_toplevel(state.xdg_surface);

    // 设置标题
    xdg_toplevel_set_title(state.xdg_toplevel, "Example client");

    // 提交表面
    wl_surface_commit(state.wl_surface);

    while (wl_display_dispatch(state.wl_display)) {
        /* This space deliberately left blank */
    }

    return 0;
}

深入理解表面

到目前为止,我们所演示的 Surfaces 基本接口已经足以向用户展示数据,但 Surface 接口还提供了许多额外的请求和事件,以提高使用效率。对于大多数应用来说不需要每一帧都重新绘制整个表面。甚至决定何时绘制下一帧也最好在混成器的协助下完成。本章我们将深入讨论 wl_surface 的特性。

表面的生命周期

我们在前面提到,Wayland 的设计是为了原子化地更新所有的东西,这样就不会有无效或者中间状态的帧出现。正是 wl_surface 本身的机制保障了应用程序窗口和其他表面多种属性配置时原子性。

每个表面都有一个待定状态和一个应用状态,而在它刚被创建的时候则无任何状态。待定状态是通过客户端的请求和服务端的事件来协商的,当双方都认为它代表一个一致的表面状态时,表面就会被提交——待定状态会被应用到表面的当前状态。在这之前,混成器将继续渲染最后的一致状态;一旦提交,将从下一帧开始使用新的状态。

其中原子操作更新的状态有:

  • 所附加的 wl_buffer 或构成表面内容的像素
  • 在上一帧中被 “破坏” 的区域,需要重新绘制。
  • 接受输入事件的区域。
  • 被认为是不透明的区域 1
  • 对附加的 wl_buffer 进行变换,以旋转或呈现缓冲区的一个子集
  • 缓冲区的缩放系数,用于高分辨率 (HiDPI) 显示。

除了表面的这些特征之外,表面的角色还可以有额外的双缓冲区。所有这些状态,以及与角色相关的任何状态,都会在发达送 wl_surface.commit 时应用。如果你改变主意,你可以多次发送这些请求,当表面最终被提交的时候只会考虑这些属性的最新值(即可最后一次提交有效)。

当你第一次创建表面时,初始状态是无效的。为了使其有效(或映射到表面),你需要提供必要的信息来初次为该曲面建立一致的状态。这包括给它一个角色(比如 xdg_toplevel),分配和附加一个缓冲区,以及配置该表面的任何角色特定状态。当你发送了一个 wl_surface.commit 并正确配置了这个状态时,这个表面就会生效(或被映射)并被混成器呈现。

下一个问题是:我该何时准备一个新的帧?

1

这是以优化混成器为目的而采取的措施

帧回调

更新表面最简单的方法是:在需要改变时简单的渲染和附加新的帧。这种方法很好用,例如,在事件驱动的应用中,用户按下了一个按键,文本框需要重新渲染,那么你就可以立即开始重新渲染,将相应的区域标记为 “损坏”,并附加一个新的缓冲区,在下一帧中呈现。

然而,有些程序可能希望连续地渲染帧。比如你可能正在渲染视频游戏、回放视频、或者动画的帧。你的显示器有一个固定的刷新率,既它能够显示的最快的刷新速度(通常是一个数字,如 60Hz、144Hz 等)。超出这个范围渲染帧的速度在快也没有意义,而且这样做只会浪费资源——CPU、GPU、甚至是用户的电量。如果你在每次显示器刷新间隔之内发送多个帧,那么除了最后一个帧以外,其它的帧都会被丢掉,渲染它们没有意义。

此外,某些情况下,混成器甚至可能不想为你显示新的帧。比如你的应用程序可能离开屏幕、被最小化或者隐藏在其它窗口后面、或者只显示了你应用程序的缩略图,所以混成器可能想以比较低的帧速率渲染你的应用,以节省资源。因此,在 Wayland 客户端中连续渲染帧的最好方法是让混成器告诉你什么时候它准备好接收新的帧:使用帧回调。

<interface name="wl_surface" version="4">
  <!-- ... -->

  <request name="frame">
    <arg name="callback" type="new_id" interface="wl_callback" />
  </request>

  <!-- ... -->
</interface>

这一请求将会分配一个 wl_callback 对象,它有一个相当简单的接口:

<interface name="wl_callback" version="1">
  <event name="done">
    <arg name="callback_data" type="uint" />
  </event>
</interface>

当你在一个表面上请求一个帧回调时,一旦这个表面的新帧准备好了,混成器会向回调对象发送一个完成事件。在帧事件的情况下,callback_data 被设置为从一个未指定的时期开始到当前时间,以毫秒为单位计算。你可以将其与上一帧进行比较,以计算动画的进度或对输入事件进行调整。

有了帧回调这个工具,我们为什么不更新一下第 7.3 章节中的应用程序,让它每一帧都滚动一下呢?让我们先在我们的 client_state 结构体中添加一点状态:

--- a/client.c
+++ b/client.c
@@ -71,6 +71,8 @@ struct client_state {
 	struct xdg_surface *xdg_surface;
 	struct xdg_toplevel *xdg_toplevel;
+	/* State */
+	float offset;
+	uint32_t last_frame;
 };
 
 static void wl_buffer_release(void *data, struct wl_buffer *wl_buffer) {

然后我们将更新我们的 draw_frame 函数以考虑偏移量。

@@ -107,9 +109,10 @@ draw_frame(struct client_state *state)
 	close(fd);
 
 	/* Draw checkerboxed background */
+	int offset = (int)state->offset % 8;
 	for (int y = 0; y < height; ++y) {
 		for (int x = 0; x < width; ++x) {
-			if ((x + y / 8 * 8) % 16 < 8)
+			if (((x + offset) + (y + offset) / 8 * 8) % 16 < 8)
 				data[y * width + x] = 0xFF666666;
 			else
 				data[y * width + x] = 0xFFEEEEEE;

在主函数中,让我们为我们的第一个新帧注册一个回调。

@@ -195,6 +230,9 @@ main(int argc, char *argv[])
 	xdg_toplevel_set_title(state.xdg_toplevel, "Example client");
 	wl_surface_commit(state.wl_surface);
 
+	struct wl_callback *cb = wl_surface_frame(state.wl_surface);
+	wl_callback_add_listener(cb, &wl_surface_frame_listener, &state);
+
 	while (wl_display_dispatch(state.wl_display)) {
 		/* This space deliberately left blank */
 	}

然后这样实现它:

@@ -147,6 +150,38 @@ static const struct xdg_wm_base_listener xdg_wm_base_listener = {
 	.ping = xdg_wm_base_ping,
 };
 
+static const struct wl_callback_listener wl_surface_frame_listener;
+
+static void
+wl_surface_frame_done(void *data, struct wl_callback *cb, uint32_t time)
+{
+	/* Destroy this callback */
+	wl_callback_destroy(cb);
+
+	/* Request another frame */
+	struct client_state *state = data;
+	cb = wl_surface_frame(state->wl_surface);
+	wl_callback_add_listener(cb, &wl_surface_frame_listener, state);
+
+	/* Update scroll amount at 24 pixels per second */
+	if (state->last_frame != 0) {
+		int elapsed = time - state->last_frame;
+		state->offset += elapsed / 1000.0 * 24;
+	}
+
+	/* Submit a frame for this event */
+	struct wl_buffer *buffer = draw_frame(state);
+	wl_surface_attach(state->wl_surface, buffer, 0, 0);
+	wl_surface_damage_buffer(state->wl_surface, 0, 0, INT32_MAX, INT32_MAX);
+	wl_surface_commit(state->wl_surface);
+
+	state->last_frame = time;
+}
+
+static const struct wl_callback_listener wl_surface_frame_listener = {
+	.done = wl_surface_frame_done,
+};
+
 static void
 registry_global(void *data, struct wl_registry *wl_registry,
 		uint32_t name, const char *interface, uint32_t version)

现在,每一帧中,我们将:

  1. 销毁现在使用的帧回调
  2. 为下一帧请求一个新的回调
  3. 渲染并提交新的帧

第三步细分为:

  1. 用一个新的偏移量来更新,使用同上一帧一致的速度滚动。
  2. 准备一个新的 wl_buffer 并为其渲染一帧。
  3. 将新的 wl_buffer 附加到我们的表面。
  4. 将整个表面标记为 “损坏”。
  5. 提交表面。

步骤 3 和 4 更新表面的待定状态,为其赋予一个新的缓冲区,并表示整个表面状态已经改变。第 5 步提交这个待定状态,并在下一帧中使用它。原子化应用这个新的缓冲区意味着我们永远不会只显示最后帧的一半,从而产生一个更好的无撕裂体验。编译并运行更新后的客户端,亲身体验一下吧。

1

想要更准确的描述吗?在第 12.1 章中我们谈到了一个扩展协议,它能以纳秒级的分辨率告诉你每一帧画面是何时呈现给用户的。

损坏表面

你可能已经注意到,在上一个例子中,当我们向表面提交一个新的帧的时候,在代码中添加了这样一行:

wl_surface_damage_buffer(state->wl_surface, 0, 0, INT32_MAX, INT32_MAX);

如果是这样的话,请擦亮眼睛!这段代码损坏了我们的表面,向混成器表明其需要重新被绘制。在这里,我们损坏了整个表面(甚至远远超过其范围),但我们也可以只损坏其中的一部分。

例如,你写了一个 GUI 工具,用户正在向一个文本框中输入信息。这个文本框可能只占窗口的一小部分,而每个新的字符所占的部分就更小了。当用户按下一个按键,你可以只渲染文本上的新字符,然后只标记表面的那一部分即可。然后,混成器可以只复制表面的一小部分,这可以大大加快速度——特别是对于嵌入式设备。当光标在字符之间闪烁的时候,你仅需要提交更新部分的损坏,而当用户改变视图的时候,才可能会损坏整个表面。这样,每项改动的开销都减少了,并且用户会感谢你提高了他们的电池寿命。

注意: Wayland 协议为破坏表面提供了两个请求 damagedamage_buffer。前者实际上已经弃用了,你应该只使用后者。二者的区别在于,damage 考虑到了影响曲面的所有变换,比如旋转、缩放比例、缓冲区位置和剪切。后者则是相对于缓冲区标记损坏,这样更容易解释。

表面区域

我们已经通过 wl_compositor 的接口 wl_compositor.create_surface 创建了一个 wl_surfaces。然而,请注意,这里还有第二个请求:create_region

<interface name="wl_compositor" version="4">
  <request name="create_surface">
    <arg name="id" type="new_id" interface="wl_surface" />
  </request>

  <request name="create_region">
    <arg name="id" type="new_id" interface="wl_region" />
  </request>
</interface>

wl_region 接口定义了一组矩形,它们共同组成了一个任意形状的几何区域。其定义的请求允许你对其所定义的几何体进行位操作,即从其中添加或减去矩形。

<interface name="wl_region" version="1">
  <request name="destroy" type="destructor" />

  <request name="add">
    <arg name="x" type="int" />
    <arg name="y" type="int" />
    <arg name="width" type="int" />
    <arg name="height" type="int" />
  </request>

  <request name="subtract">
    <arg name="x" type="int" />
    <arg name="y" type="int" />
    <arg name="width" type="int" />
    <arg name="height" type="int" />
  </request>
</interface>

例如,要制作一个有孔的矩形,你可以这样:

  1. 发送 wl_compositor.create_region 请求来分配一个 wl_region 对象。
  2. 发送 wl_region.add(0, 0, 512, 512) 来创建一个 512x512 的矩形。
  3. 发送 wl_region.subtract(128, 128, 256, 256),从区域中间移除一个 256x256 的矩形。

这些区域也可以是没有交集的,它不需要是一个连续的多边形。一旦你创建了这些区域的其中之一,你就可以把它传递给 wl_surface 接口,即用 set_opaque_regionset_input_region 请求。

<interface name="wl_surface" version="4">
  <request name="set_opaque_region">
    <arg name="region" type="object" interface="wl_region" allow-null="true" />
  </request>

  <request name="set_input_region">
    <arg name="region" type="object" interface="wl_region" allow-null="true" />
  </request>
</interface>

不透明区域是给混成器的一个提示,告诉它你的表面哪些部分被认为是不透明的。基于这些信息,混成器可以优化它的渲染过程。例如,你的表面是完全不透明的,并且遮挡了它下面的另一个窗口,那么混成器就不会在重新绘制下面窗口上浪费任何时间。默认情况下是没有这个提示的, 它假定你表面的任何部分都可能是透明的。这使得默认情况下绘制效率最低,但效果也最正确。

输入区域表示你的表面哪些部分可以接受光标和触摸输入事件。例如,你可以在你的表面下绘制一个下拉阴影,但发生在这个区域的输入事件应该被传递到你内部的的客户端。或者,如果你的窗口是一个不规则的形状,你也可以在此创建一个符合形状的输入区域。在默认情况下,对于大多数表面类型,你的整个表面都接受输入。

这两个请求都可以通过传入 null (而不是 wl_region 对象)来设置一个空的区域。它们也都是带有双缓冲区的——所以发送一个 wl_surface.commit 来使得你的改变生效。一旦你发送了 set_opaque_regionset_input_region 请求,你就可以销毁 wl_region 对象以释放其资源。在你发送这些请求后,再更新这一区域不会更新表面的状态。

子表面

在核心 Wayland 协议中,wayland.xml 只定义了一种1表面角色:子表面。它们拥有一个相对于父表面但并不受到其父表面边界的限制的 X、Y 位置,以及一个相对于其兄弟和父表面的 Z 轴顺序。

这个功能的一些使用情况包括:以其原始像素格式播放带有视频的表面,并在上面显示 RGBA 用户界面或字幕;使用 OpenGL 表面作为你主要的应用界面,并在软件中使用子表面来渲染窗口装饰,或在用户界面各个部分移动而不必在客户端重新绘制。在硬件表面的帮助下,混成器也可能不需要绘制任何东西来更新你的子表面。特别是在嵌入式系统上,当它符合你的使用情况时,这可能特别有用。一个巧妙设计的应用程序可以利用子表面来提高运行效率。

wl_subcompositor 接口来管理这些请求。get_subcompositor 请求是 subcompositor 的主要接入点:

<request name="get_subsurface">
  <arg name="id" type="new_id" interface="wl_subsurface" />
  <arg name="surface" type="object" interface="wl_surface" />
  <arg name="parent" type="object" interface="wl_surface" />
</request>

一旦你有了一个与 wl_surface 关联的 wl_subsurface 对象,那么它就会成为这个表面的子表面。子表面本身也可以有子表面,从而在任何顶层(top-level)表面下形成一个有序的表面树。对这些子表面的操作是通过 wl_subsurface 接口完成的。

<request name="set_position">
  <arg name="x" type="int" summary="x coordinate in the parent surface"/>
  <arg name="y" type="int" summary="y coordinate in the parent surface"/>
</request>

<request name="place_above">
  <arg name="sibling" type="object" interface="wl_surface" />
</request>

<request name="place_below">
  <arg name="sibling" type="object" interface="wl_surface" />
</request>

<request name="set_sync" />
<request name="set_desync" />

一个子表面的 Z 轴顺序可以放在任何与它有相同父表面的兄弟或父表面本身的上方或者下方。

wl_subsurface 的各种属性同步在这里需要做出一些解释。这些位置和 Z 轴属性是与父表面的生命周期同步的。当主表面的 wl_surface.commit 请求被发送的时候,它所有的子表面的位置和 Z 轴顺序的变化都会一并被应用。

然而,与这个子表面相关的 wl_surface 状态,例如,缓冲区的附加和损坏区域的累积,则不需要与父表面的生命周期相联系。这就是 set_syncset_desync 请求的目的。与父表面同步的子表面会在父表面提交时提交其所有的状态。解除同步的表面会像别的表面一样管理自己提交的生命周期。

简而言之,同步和解同步的请求是无缓冲的,会立即被应用。位置和 Z 轴顺序请求是有缓冲的,并不受曲面的同步或异步属性影响——它们总是和父表面一同提交。在相关的 wl_surface 上,剩下的表面状态,会根据子表面的同步或异步状态来提交。

1

忽略弃用的 wl_shell 接口

高分辨率表面(HiDPI)

在过去的几年时间里,高端显示器的像素密度取得了巨大的飞跃,新的显示器在相同的物理显示面积上装入了两倍于我们过去几年所见的像素量。我们称这些显示器为 “HiDPI” 显示器(高分屏),是 “每英寸高像素密度” 的简称。然而这些显示器要远远领先于它们的 “低分屏” 同行,要正确利用它们,必须在应用层面作出改变。通过在相同的空间内将屏幕的分辨率提高一倍,如果我们不特别考虑它们的话,我们所有的用户界面尺寸都会减少一半。对于大多数显示器而言,这将使得文字无法阅读,交互元素也会变得很小,令人不悦。

然而,作为交换,其为我们的矢量图形提高了图形的保真度,最明显的是在文本渲染方面。Wayland 通过为每个输出添加一个 “比例因子”(scale factor) 来解决这个问题,而客户端也被期望将这个比例因子应用到它们的界面上。此外,没有意识到 HiDPI 的客户端通过无动作来传达这一限制信号,让混成器放大他们的缓冲区来弥补这一限制。混成器通过适当的事件发出每个输出的比例因子信号:

<interface name="wl_output" version="3">
  <!-- ... -->
  <event name="scale" since="2">
    <arg name="factor" type="int" />
  </event>
</interface>

请注意,这是在版本 2 中添加的,因此当绑定到 wl_output 全局量的时候你需要将版本至少设置为 2 以接收这些事件。然而, 这还不足以决定你在客户端用上 HiDPI。为了进行这一调用,混成器还必须为你的 wl_surface 发送 enter 事件,以表明它已经 “进入”(正在显示在)一个或多个特定的输出端。

<interface name="wl_surface" version="4">
  <!-- ... -->
  <event name="enter">
    <arg name="output" type="object" interface="wl_output" />
  </event>
</interface>

一旦你知道客户端的输出集合开始显示,就应该取比例因子中的最大值,将其中缓冲区的大小(以像素为单位)乘以这个值,然后以 2 倍或 3 倍(或 N 倍)的比例渲染 UI。然后像这样指出缓冲区准备的比例:

<interface name="wl_surface" version="4">
  <!-- ... -->
  <request name="set_buffer_scale" since="3">
    <arg name="scale" type="int" />
  </request>
</interface>

注意: 这需要版本 3 或者更新的 wl_surface。当你与 wl_compositor 绑定时,你应该把这个版本号传递给 wl_registry

在下一次 wl_surface.commit 时,你的表面会假定这个比例因子。如果它大于表面显示的比例系数,混成器会将其缩小。如果它小于输出的比例系数,混成器则会将其放大。

Seats: 处理输入

向用户显示你的应用程序只是 I/O 方式中的一半,大多数应用程序还需要处理输入。为此,座位 (Seats) 为 Wayland 上的输入事件提供了一个抽象的概念。从哲学的角度上讲,一个 Wayland 座位是指用户作者操作电脑的一个座位,它与最多一个键盘和最多一个 “指针” 设备(即鼠标或触摸板)相关。类似的关系也被定义为触摸屏、数位板设备等。

重要的是要记住,这只是一个抽象概念,Wayland 显示屏上显示的座位情况可能与实际情况不完全一致。在实践中,Wayland 会话中很少有超过一个座位的情况。如果你在电脑上插入第二个键盘,它通常会被分配到与第一个键盘相同的座位上,当你开始在每个座位上打字的时候,键盘布局等都会动态地切换。这些是实施细节留给 Wayland 混成器去考虑吧。

从客户端的角度来看,这是很直接的。如果你绑定了全局的 wl_seat,那么你可以访问以下接口:

<interface name="wl_seat" version="7">
  <enum name="capability" bitfield="true">
    <entry name="pointer" value="1" />
    <entry name="keyboard" value="2" />
    <entry name="touch" value="4" />
  </enum>

  <event name="capabilities">
    <arg name="capabilities" type="uint" enum="capability" />
  </event>

  <event name="name" since="2">
    <arg name="name" type="string" />
  </event>

  <request name="get_pointer">
    <arg name="id" type="new_id" interface="wl_pointer" />
  </request>

  <request name="get_keyboard">
    <arg name="id" type="new_id" interface="wl_keyboard" />
  </request>

  <request name="get_touch">
    <arg name="id" type="new_id" interface="wl_touch" />
  </request>

  <request name="release" type="destructor" since="5" />
</interface>

注意: 这个接口已经更新了很多次——当你绑定到全局接口的时候要注意版本。本书假设你绑定的是最新的版本,在撰写本书的时候是第 7 版。

这个接口相对来说比较简单的。服务端向客户端发送一个能力事件,以表明本座位支持哪些类型的输入设备——用能力值 (capability values) 的位域表示——客户端可以相应地绑定到它希望能够使用的输入设备。例如,如果服务端发送的能力中 (caps & WL_SEAT_CAPABILITY_KEYBOARD) > 0 为真,那么客户端就可以使用 get_keyboard 请求来获取这个座位的 wl_keyboard 对象。每个特定输入设备的语义将在其余章节中介绍。

在我们讨论这些问题之前,让我们先谈一谈一些常见的语义。

事件序列

Wayland 客户端可能在执行某些操作的时候需要以序列的形式输入事件,以进行一些常用形式的身份验证。例如,一个打开弹窗的客户端(用右键调出的上下文菜单是弹出窗口的一种)可能希望在服务端 “抓取” 受影响座位上的所有输入事件,直到弹窗被取消。为了防止这个功能被滥用,服务端可以给它发送的每个输入事件分配序列,并要求客户端在请求中包括这些序列之一。

当服务端收到这样的请求时,它会查找与给定序列相关的输入事件,并作出判断。如果该事件发生的事件太长,或是在错误的表面,亦或是事件类型不正确——例如,当你摆动鼠标的时候,拒绝抓取,而当你点击的时候却要允许抓取——这样的请求服务端可以拒绝。

对于服务端而言,它们可以简单地在每个输入事件中发送一个递增的整数,并记录被认定为对特定使用情况有效的序列,以便以后验证。客户端从它们的输入事件处理程序中收到这些序列,并可以简单地将它们回传,以执行所需要的操作。

我们将在后面的章节中更详细地讨论这些问题,届时我们将开始涉及需要输入事件序列来验证的具体请求。

输入帧

由于现实原因,一个来自输入设备的单一输入事件可能会被分解成几个 Wayland 事件。例如,当你使用滚轮的时候,一个 wl_pointer 会发出一套轴事件,它会分别发出一个事件告诉你这是哪种轴:滚轮、手指在触摸板上、将滚轮倾斜到一边,等等。如果用户的输入动作足够快的话,来自输入源的同一个输入事件可能还包括鼠标的一些动作,或者点击一个按钮。

这些相关事件的语义分组在不同的输入类型中略有不同,但帧事件在它们之间通常是相通的。简而言之,如果你把从设备上采集到的所有输入事件都放入缓冲,然后等待帧事件发出信号,表示你已经受到了一个输入 “帧” 的所有事件,你就可以把缓冲区里的 Wayland 事件解释为一整个输入事件,然后重置缓冲区,开始收集下一帧的事件。

如果这听起来太过复杂,请不要担心。许多应用程序并不需要担心输入帧。只有当你开始做更复杂的输入事件处理的时候,才会想去关心这个。

释放设备

当你使用完一个设备后,每个接口都有一个释放请求,你可以用它们来清理,就像下面这样:

<request name="release" type="destructor" />

这已经足够简单了。

光标指针输入

使用 wl_seat.get_pointer 请求,客户端可以获得一个 wl_pointer 对象。只要用户移动他们的指针、按下鼠标按钮、使用滚轮等——只要指针在你的一个表面上,服务端就会向它发送事件。我们可以通过 wl_pointer.enter 事件来判断是否满足条件。

<event name="enter">
  <arg name="serial" type="uint" />
  <arg name="surface" type="object" interface="wl_surface" />
  <arg name="surface_x" type="fixed" />
  <arg name="surface_y" type="fixed" />
</event>

当指针在我们的一个表面上移动的时候,服务端将发送这一事件,并指定 “进入” 的表面,以及指针所处的表面本地坐标(从左上角开始)。这里的坐标使用 fixed 类型指定,你可能还记得第 2.1 章节,它代表一个 24 位长度(8 位色深)的固定精度数字(wl_fixed_to_double 会把它转换成 C 语言的 double 类型)。

当指针从你的表面移开时,相应的事件就会更短小:

<event name="leave">
  <arg name="serial" type="uint" />
  <arg name="surface" type="object" interface="wl_surface" />
</event>

一旦指针进入到你的表面,你将会接受到它的额外事件,我们将很快对此作出讨论。然而,你可能想做的第一件事是提供一个光标的图像。这个过程如下:

  1. wl_compositor 创建一个新的 wl_surface
  2. 使用 wl_pointer.set_cursot 将表面附加到指针上。
  3. 将光标图像的 wl_buffer 附加到该表面并提交。

这里唯一引入的新 API 是 wl_pointer.set_cursor

<request name="set_cursor">
  <arg name="serial" type="uint" />
  <arg name="surface" type="object" interface="wl_surface" allow-null="true" />
  <arg name="hotspot_x" type="int" />
  <arg name="hotspot_y" type="int" />
</request>

这里的序列必须来自输入事件。hotspot_xhotspot_y 参数 指定了光标 “热点” 在表面的本地坐标,或者指针在光标图像中的有效位置(例如,在尖头的顶端)。还要注意,表面可以是空的——用它来完全隐藏光标。

如果你正在寻找一个好的指针图标来源,libwayland 带有一个单独的 wayland-cursor 库,它可以从磁盘上加载 X 光标主题并为它们创建 wl_buffers。详见 wayland-cursor.h,或者参考第 9.5 章中对我们客户端示例的更新。

注意:wayland-cursor 包括处理动画光标的代码,这即便是在 1998 年也不酷。如果我是你,我就不会去管这些。从来没有人抱怨过我的 Wayland 客户端不支持光标动画。

在光标进入你的表面,并且你附加了一个合适的光标图片后,你就可以开始处理输入事件了。有运动、按钮和轴事件。

指针帧

服务端上的一帧输入处理可以携带许多变化的信息——例如,轮询一次鼠标可以在一个数据包中返回一个更新的位置和一个释放的按钮。服务端将这些变化作为单独的 Wayland 事件发送,并使用 “帧” 事件将它们组合在一起。

<event name="frame"></event>

客户端应该在受到所有 wl_pointer 事件时将它们累积起来,一旦受到 “帧” 事件,就将这些未决的输入作为一二个单一的指针事件来处理。

运动事件

运动事件与进入事件使用的相同坐标空间,并在进入时指定,且其定义也非常直接:

<event name="motion">
  <arg name="time" type="uint" />
  <arg name="surface_x" type="fixed" />
  <arg name="surface_y" type="fixed" />
</event>

就像所有包含时间戳的输入事件一样,时间值是一个与此输入事件相关的,单调增长的,毫秒级的时间戳。

按键事件

按键事件大多数都不言而喻:

<enum name="button_state">
  <entry name="released" value="0" />
  <entry name="pressed" value="1" />
</enum>

<event name="button">
  <arg name="serial" type="uint" />
  <arg name="time" type="uint" />
  <arg name="button" type="uint" />
  <arg name="state" type="uint" enum="button_state" />
</event>

然而,按键的参数值值得作一些额外的解释。这些数字是一个特定平台的输入事件,尽管注意到 FreeBSD 平台也重用了 Linux 的数值。你可以在 linux/input-event-codes.h (一般由 linux-headers 或者 linux-api-headers 包提供)中找到这些 Linux 下的数值,最有用的数值可能是由常量 BTN_LEFTBTN_RIGHTBTN_MIDDLE 表示的。除此之外还有更多定义,在你闲暇的时候可以浏览一下头文件。

 342   │ #define BTN_MISC        0x100
 343   │ #define BTN_0           0x100
 344   │ #define BTN_1           0x101
 345   │ #define BTN_2           0x102
 346   │ #define BTN_3           0x103
 347   │ #define BTN_4           0x104
 348   │ #define BTN_5           0x105
 349   │ #define BTN_6           0x106
 350   │ #define BTN_7           0x107
 351   │ #define BTN_8           0x108
 352   │ #define BTN_9           0x109

轴事件

轴事件用于描述滚轮动作,例如旋转你的滚轮或者左右摇动它。最基本的形式看起来像这样:

<enum name="axis">
  <entry name="vertical_scroll" value="0" />
  <entry name="horizontal_scroll" value="1" />
</enum>

<event name="axis">
  <arg name="time" type="uint" />
  <arg name="axis" type="uint" enum="axis" />
  <arg name="value" type="fixed" />
</event>

然而,轴事件是复杂的,这也是 wl_pointer 接口中多年来受到最多关注的部分。有几个额外的事件存在,它们增加了轴事件的特殊性:

<enum name="axis_source">
  <entry name="wheel" value="0" />
  <entry name="finger" value="1" />
  <entry name="continuous" value="2" />
  <entry name="wheel_tilt" value="3" />
</enum>

<event name="axis_source" since="5">
  <arg name="axis_source" type="uint" enum="axis_source" />
</event>

axis_source 事件告诉你哪种轴被驱动了——滚轮、触摸板上的手指移动、向旁边倾斜的摇杆、或更新颖的东西。这个事件本身很简单,但其余的就不那么简单了:

<event name="axis_stop" since="5">
  <arg name="time" type="uint" />
  <arg name="axis" type="uint" enum="axis" />
</event>

<event name="axis_discrete" since="5">
  <arg name="axis" type="uint" enum="axis" />
  <arg name="discrete" type="int" />
</event>

这两个事件的精确语义很复杂,如果你想利用它们,我建议仔细阅读 wayland.xml 中的摘要。简而言之,axis_discrete 事件用于区分任意规模的轴事件和离散的步骤,例如,滚轮的每一次 “点击” 代表书轴值的一次离散的变化。axis_stop 事件标志着一个离散的用户运动行为已经完成,并用于计算发生在几个帧之间的滚动事件。任何未来的事件都应该被解释为一个单独的运动。

XBK 简介

X_keyboard_extension

我们清单上的下一个输入设备是键盘,但在讨论它们之前,我们需要先停下来补充一些额外的背景知识。键位映射 (Keymaps) 是键盘输入中涉及到的一个重要的细节,XKB 是 Wayland 上推荐的处理键盘的方式。

当你按下键盘上的一个键时,它会向计算机发送一个编码,这只是分配给该物理按键的一个数字,在我的键盘上,编码 1 是 Escape 键,Shift 是 42,以此类推。我使用的是 US ANSI 键盘布局,但还有许多其他的布局,它们的编码也不相同。在我朋友的德国键盘上,编码 12 产生 'ß',而我的则产生 '-'。

为了解决这一问题,我们使用了一个叫 "xkbcommon" 的库,它的名字源自于它的作用是将 XKB (X KeyBoard) 的通用编码提取到一个独立的库中。XKB 定义了大量的按键符号,如 XKB_KEY_AXKB_KEY_ssharp (ß,来自德语),以及 XKB_KEY_kana_WO (を,来自日语)。

然而,识别这些按键并将它们与这样的按键符号联系起来只是问题的其中一部分。如果按住 Shift 键,'a' 可以产生 'A','を' 在片假名模式下被写成 'ヲ',虽然严格来说 'ß' 有一个大写版本,但它几乎不被使用,理所当然也不会被打出来。向 Shift 这样的键被称为修饰键,而像平假名和片假名这样的被称之为组。有些修饰键可以锁定,比如 Caps Lock。XKB 有处理这些情况的基元,并维护一个状态机,跟踪你的键盘在做什么,并准确找出用户试图输入的 Unicode 编码点。

使用 XKB

那么,xkbcommon 究竟是如何使用的呢?第一步是链接到它,然后抓取头文件 xkbcommon/xkbcommon.h1

大多数使用 xkbcommon 的程序都必须管理以下三个对象:

  • xkb_context: 一个用于配置其他 XKB 资源的句柄
  • xkb_keymap: 一个从编码到键盘符号的映射
  • xkb_state: 一个将键盘符号转化为 UTF-8 字符串的状态机

设置的过程通常如下:

  1. 使用 xkb_context_new 创建一个新的 xkb_context,通常将 XKB_CONTEXT_NO_FLAGS 传递给它,除非你在做一些特殊的事情。
  2. 获取一个字符串形式的键映射(key map)
  3. 使用 xkb_keymap_new_from_string 来为这个键映射创建一个 xkb_keymap。这里只有一种键映射格式:XKB_KEYMAP_FORMAT_TEXT_V1,你将其作为格式参数传给函数。同样,除非你有特殊安排,否则应使用 XKB_KEYMAP_COMPILE_NO_FLAGS 作为标志传入。
  4. 使用 xkb_state_new 为你的键映射创建一个 xkb_state。这个状态会增加键映射的引用计数 (refcount),所以如果你自己已经用完了,请使用 xkb_keymap_unref 来解引用。
  5. 从一个按键上获得编码。
  6. 将扫描到的编码传入 xkb_state_key_get_one_sym 以获得 keysyms,并传入 xkb_state_key_get_utf8 获得 UTF-8 字符串就大功告成了!

*这些步骤将在下一节中具体讨论。

就代码而言,这个过程看起来如下:

#include <xkbcommon/xkbcommon.h> // -lxkbcommon
/* ... */

const char *keymap_str = /* ... */;

/* Create an XKB context */
struct xkb_context *context = xkb_context_new(XKB_CONTEXT_NO_FLAGS);

/* Use it to parse a keymap string */
struct xkb_keymap *keymap = xkb_keymap_new_from_string(
    xkb_context, keymap_str, XKB_KEYMAP_FORMAT_TEXT_V1,
    XKB_KEYMAP_COMPILE_NO_FLAGS);

/* Create an XKB state machine */
struct xkb_state *state = xkb_state_new(keymap);

然后处理扫描到的编码:

int scancode = /* ... */;

xkb_keysym_t sym = xkb_state_key_get_one_sym(xkb_state, scancode);
if (sym == XKB_KEY_F1) {
    /* Do the thing you do when the user presses F1 */
}

char buf[128];
xkb_state_key_get_utf8(xkb_state, scancode, buf, sizeof(buf));
printf("UTF-8 input: %s\n", buf);

有了这些细节,我们已经准备好解决键盘输入的处理问题。

1

xkbcommon 带有一个 .pc 文件:使用 pkgconf --clflags xkbcommonpkgconf --libs xkbcommon,或是你的编译系统喜欢的方式来获取 pc 文件。

键盘输入

在了解如何使用 XKB 之后,让我们来扩展我们的 Wayland 代码,为我们的键入事件提供输入。与我们获得 wl_pointer 资源的方法类似,我们可以使用 wl_sear.get_keyboard 请求来为一个有着 WL_SEAT_CAPABILITY_KEYBOARD 功能的座位(seat)创建一个 wl_keyboard。当你创建完成后,你应该发送 "release" 来释放请求:

<request name="release" type="destructor" since="3">
</request>

这将使服务器能够清理与该键盘相关的资源。

但是,你实际上如何使用它呢?让我们从基础知识开始。

键位映射

当你绑定到 wl_keyboard 时,服务端可能发送的第一个事件是 keymap

<enum name="keymap_format">
  <entry name="no_keymap" value="0" />
  <entry name="xkb_v1" value="1" />
</enum>

<event name="keymap">
  <arg name="format" type="uint" enum="keymap_format" />
  <arg name="fd" type="fd" />
  <arg name="size" type="uint" />
</event>

keymap_format 枚举类型是我们想出一种新的 keymaps 格式的情况下提供的(预留),但在本文撰写时,XKB keymaps 仍旧是服务端可能发送的唯一格式。

像这样的批量数据是通过文件描述符传输的。我们可以简单地从文件描述符中读取,但一般来说,建议用 mmap 代替。在 C 语言中,类似可能的实现如下:

#include <sys/mman.h>
// ...

static void wl_keyboard_keymap(void *data, struct wl_keyboard *wl_keyboard,
        uint32_t format, int32_t fd, uint32_t size) {
    assert(format == WL_KEYBOARD_KEYMAP_FORMAT_XKB_V1);
    struct my_state *state = (struct my_state *)data;

    char *map_shm = mmap(NULL, size, PROT_READ, MAP_PRIVATE, fd, 0);
    assert(map_shm != MAP_FAILED);

    struct xkb_keymap *keymap = xkb_keymap_new_from_string(
        state->xkb_context, map_shm, XKB_KEYMAP_FORMAT_TEXT_V1,
        XKB_KEYMAP_COMPILE_NO_FLAGS);
    munmap(map_shm, size);
    close(fd);

    // ...do something with keymap...
}

一旦我们有了一个键位映射,我们就可以为这个 wl_keyboard 解释未来的按键事件。请注意,服务端一可以在任何时候发送一个新的键映射,所有未来的按键事件都应该从这个新的映射来解释。

键盘焦点

<event name="enter">
  <arg name="serial" type="uint" />
  <arg name="surface" type="object" interface="wl_surface" />
  <arg name="keys" type="array" />
</event>

<event name="leave">
  <arg name="serial" type="uint" />
  <arg name="surface" type="object" interface="wl_surface" />
</event>

就像 wl_pointer 里的 "enter" 和 "leave" 事件是当指针在你的表面上移动的时候发出的,服务端在表面收到键盘焦点时发送 wl_keyboard.enter,而失去焦点的时候发送 wl_keyboard.leave。许多应用程序会在这些条件下改变它们的外观——比如,开始绘制一个闪烁的光标。

"enter" 事件还包括 array 数组,里面涵盖了当前输入的按键。这是一个由 32 位无符号整数组成的数组,每一个都代表一个所按按键的扫描编码 scancode。

输入事件

一旦将键盘进入你的表面,你就可以期待开始接受输入事件。

<enum name="key_state">
  <entry name="released" value="0" />
  <entry name="pressed" value="1" />
</enum>

<event name="key">
  <arg name="serial" type="uint" />
  <arg name="time" type="uint" />
  <arg name="key" type="uint" />
  <arg name="state" type="uint" enum="key_state" />
</event>

<event name="modifiers">
  <arg name="serial" type="uint" />
  <arg name="mods_depressed" type="uint" />
  <arg name="mods_latched" type="uint" />
  <arg name="mods_locked" type="uint" />
  <arg name="group" type="uint" />
</event>

"key" 事件在用户按下或者释放一个键的时候被发送。像许多输入事件一样,它包括一个序列,你可以用它来将未来的请求与这个输入事件联系起来。"key" 是所按按键的编码,"state" 是该按键的按下或释放状态。

重要: 这个事件的 scancode 是 Linux evdev scancode。若要将其转换为 XKB 的 scancode,你必须在 evdev scancode 中加 8。

修饰事件包括一个类似的序列,还有按下、锁存和锁定修饰键的掩码,以及当前正在使用的输入组的索引。一个修饰键被按下,就像当你按住 Shift 的时候。修饰键可以锁存,例如启用粘滞键(专为同时按下两个键或多个按键有困难的人而设计的)后先按下一个修饰键 Shift 松开,直到再按下另一个非修饰键时生效。修饰键也可以被锁定,比如当大写锁定开关被打开或关闭时。输入组用于在各种键盘布局之间切换,例如在 ISO 和 ANSI 布局之间,或者用于更多特殊语言的特性。

修饰键的解释因 keymap 而异。你应该把它们都转发给 XKB 来处理。大多数 “修饰键” 事件的实现是非常直接的:

static void wl_keyboard_modifiers(void *data, struct wl_keyboard *wl_keyboard,
        uint32_t serial, uint32_t depressed, uint32_t latched,
        uint32_t locked, uint32_t group) {
    struct my_state *state = (struct my_state *)data;
    xkb_state_update_mask(state->xkb_state,
        depressed, latched, locked, 0, 0, group);
}

按键重复

最后让我们来考虑 "repeat_info" 事件:

<event name="repeat_info" since="4">
  <arg name="rate" type="int" />
  <arg name="delay" type="int" />
</event>

在 Wayland 中,客户端负责实现 “按键重复”——只要你按住按键,就会持续输入字符的功能。发送这个事件是为了将用户对重复事设置的偏好通知给客户端。延迟 "delay" 是指在按键重复启动前的需要保持按下的毫秒数,速率 "rate" 是指直到按键被释放每秒重复输入的字符数。

触控输入

在表面上,触摸屏输入是相当简单的,你的实现也可也非常简单。然而,该协议为你提供了很多深度 (depth),应用程序可以利用这些深度来提供更细致的触摸驱动手势和反馈。

大多数触摸屏设备都支持多点触控:它们可以跟踪屏幕被触摸的多个位置。这些 “触摸点” 中的每一个都被分配了一个 ID,这个 ID 在当前所有触摸屏的活动点中是唯一的,但如果你抬起手指再按一次,ID 就可能被重复使用1

与其它输入设备类似,你可以用 wl_sear.get_touch 获得一个 wl_touch 资源,当你用完它时,你应该发送一个 "release" 请求来释放资源。

触控帧

就像光标指针一样,服务端上的一帧触控处理也可能带有许多变化的信息,但服务端会将这些信息作为离散的 Wayland 事件来发送。wl_touch.frame 事件是用来将这些事件组合到一起的。

<event name="frame"></event>

客户端应该累积所有收到的 wl_touch 事件,然后在收到 "frame" 事件时将待处理的输入作为一个单一的触控事件进行处理。

触摸和释放

我们要看的第一个事件是 "down" 和 "up",当你把手指按在设备上,以及把手指从设备上移开的时候,这两个事件分别被触发。

<event name="down">
  <arg name="serial" type="uint" />
  <arg name="time" type="uint" />
  <arg name="surface" type="object" interface="wl_surface" />
  <arg name="id" type="int" />
  <arg name="x" type="fixed" />
  <arg name="y" type="fixed" />
</event>

<event name="up">
  <arg name="serial" type="uint" />
  <arg name="time" type="uint" />
  <arg name="id" type="int" />
</event>

"x" 和 "y" 坐标是触摸表面所处坐标空间中的定点坐标,在 "surface" 参数中给出。"time" 是一个单调递增的时间戳,具有任意的 epoch,并以毫秒为单位2。还请注意这里包含了一个序列 "serial",它可以包含在未来与此输入事件相关联的请求中。

运动

在你收到某个特定 ID 触点的 "down" 事件后,你将会开始接收到运动事件,描述该触摸点在设备上的移动。

<event name="motion">
  <arg name="time" type="uint" />
  <arg name="id" type="int" />
  <arg name="x" type="fixed" />
  <arg name="y" type="fixed" />
</event>

这里的 "x" 和 "y" 坐标是发送 "enter" 事件表面的相对空间坐标。

手势结束

触摸事件在被识别为手势之前,往往必须满足一些阈值。例如,从左到右轻扫屏幕可以被 Wayland 混成器用来在不同的应用程序之间切换。然而,直到越过某些阈值:例如,在一定时间内到达屏幕的中点,混成器才会将这种行为识别为手势。

到达这个阈值之前,混成器都为被触摸的表面发送正常的触摸事件。一旦手势被识别,混成器将发送一个 "cancel" 事件,让你直到混成器正在接管。

<event name="cancel"></event>

当你受到这个事件后,所有活动的触点都被取消了。

形状和方式

一些高端的触摸硬件能够确定更多用户交互方式信息。对于希望采用更高级的交互或触摸反馈的有合适硬件和应用的用户,提供了 "shape" 和 "orientation" 事件。

<event name="shape" since="6">
  <arg name="id" type="int" />
  <arg name="major" type="fixed" />
  <arg name="minor" type="fixed" />
</event>

<event name="orientation" since="6">
  <arg name="id" type="int" />
  <arg name="orientation" type="fixed" />
</event>

"shape" 事件定义了一个椭圆值来近似触屏物体的形状,其长轴和短轴在被接触表面坐标空间中的单元表示。方向事件通过指定触摸表面的长轴和 Y 轴之间的夹角来旋转该椭圆。


触摸是 Wayland 协议所支持的最后一种输入设备。有了这些知识,让我们来更新我们的示例代码。

1

强调 “可能” ——不要根据重复使用的一个触点 ID 而作出任何假设。 2: 这意味着独立的时间戳可以相互比较,以获得事持续的事件,但不能与壁钟时间 (Wall-Clock Time,即执行时间) 相较。

扩展我们的示例代码

在前面几章中,我们建立了一个简单的客户端,它可以在显示器上展示其表面。让我们把这个代码扩展一下,建立一个可以接收输入事件的客户端。为了简单起见,我们仅仅将输入事件记录到 stderr。

这需要更多的代码,而不仅仅是将到目前为止的工作绑在一起。我们需要做的第一件事就是设置座位。

设置座位

我们首先需要的是一个对座位的引用。我们将把它添加到我们的 client_state 结构体中,并添加键盘、指针和触摸对象供后期使用。

        struct wl_shm *wl_shm;
        struct wl_compositor *wl_compositor;
        struct xdg_wm_base *xdg_wm_base;
+       struct wl_seat *wl_seat;
        /* Objects */
        struct wl_surface *wl_surface;
        struct xdg_surface *xdg_surface;
+       struct wl_keyboard *wl_keyboard;
+       struct wl_pointer *wl_pointer;
+       struct wl_touch *wl_touch;
        /* State */
        float offset;
        uint32_t last_frame;
        int width, height;

我们还需要更新 registry_global,为该座位注册一个监听器。

                                wl_registry, name, &xdg_wm_base_interface, 1);
                xdg_wm_base_add_listener(state->xdg_wm_base,
                                &xdg_wm_base_listener, state);
+       } else if (strcmp(interface, wl_seat_interface.name) == 0) {
+               state->wl_seat = wl_registry_bind(
+                               wl_registry, name, &wl_seat_interface, 7);
+               wl_seat_add_listener(state->wl_seat,
+                               &wl_seat_listener, state);
        }
 }

请注意,我们绑定的是最新版本的座位接口,即第 7 版。让我们把监听器也加上:

static void
wl_seat_capabilities(void *data, struct wl_seat *wl_seat, uint32_t capabilities)
{
       struct client_state *state = data;
       /* TODO */
}

static void
wl_seat_name(void *data, struct wl_seat *wl_seat, const char *name)
{
       fprintf(stderr, "seat name: %s\n", name);
}

static const struct wl_seat_listener wl_seat_listener = {
       .capabilities = wl_seat_capabilities,
       .name = wl_seat_name,
};

如果你现在编译 (cc -o client client.c xdg-shell-protocol.c) 并运行这个,你的座位名字就应该被打印到 stderr。

接入指针事件

让我们来谈谈光标指针事件。如果你还记得,前面我们提到来自 Wayland 服务端的指针事件会被累积为一个单一逻辑事件。因此,我们需要定义一个结构体来存储这些事件。

enum pointer_event_mask {
       POINTER_EVENT_ENTER = 1 << 0,
       POINTER_EVENT_LEAVE = 1 << 1,
       POINTER_EVENT_MOTION = 1 << 2,
       POINTER_EVENT_BUTTON = 1 << 3,
       POINTER_EVENT_AXIS = 1 << 4,
       POINTER_EVENT_AXIS_SOURCE = 1 << 5,
       POINTER_EVENT_AXIS_STOP = 1 << 6,
       POINTER_EVENT_AXIS_DISCRETE = 1 << 7,
};

struct pointer_event {
       uint32_t event_mask;
       wl_fixed_t surface_x, surface_y;
       uint32_t button, state;
       uint32_t time;
       uint32_t serial;
       struct {
               bool valid;
               wl_fixed_t value;
               int32_t discrete;
       } axes[2];
       uint32_t axis_source;
};

这里我们使用一个位掩码来识别我们接受到的单个指针帧中的事件,并将每个事件的相关信息存储到各自的字段中。让我们也将此添加到我们的状态结构体中:

        /* State */
        float offset;
        uint32_t last_frame;
        int width, height;
        bool closed;
+       struct pointer_event pointer_event;
 };

然后我们需要更新我们的 wl_seat_capabilities,为有光标指针输入功能的座位指定指针对象。

 static void
 wl_seat_capabilities(void *data, struct wl_seat *wl_seat, uint32_t capabilities)
 {
        struct client_state *state = data;
-       /* TODO */
+
+       bool have_pointer = capabilities & WL_SEAT_CAPABILITY_POINTER;
+
+       if (have_pointer && state->wl_pointer == NULL) {
+               state->wl_pointer = wl_seat_get_pointer(state->wl_seat);
+               wl_pointer_add_listener(state->wl_pointer,
+                               &wl_pointer_listener, state);
+       } else if (!have_pointer && state->wl_pointer != NULL) {
+               wl_pointer_release(state->wl_pointer);
+               state->wl_pointer = NULL;
+       }
}

这里值得解释一下。回想一下,功能 capabilities 是此座位支持的设备类型的位掩码,即如果支持,则进行位与运算 (&) 将产生非零值。然后,如果我们有一个光标指针,并且还没有配置它,我们就访问第一个分支 (第一个 if),使用 wl_seat_get_pointer 来分配一个光标指针的引用并将它存储在我们的状态 (state) 中。如果座位不支持光标指针,但我们却已经配置了一个,那么需要使用 wl_pointer_release 来释放这个引用。请记住,一个座位的 capabilities 可能在运行时改变,例如,当用户重新插拔他们的鼠标时座位所拥有的功能就会改变。

我们还为指针配置了一个监听器。让我们将它也添加到结构体中:

static const struct wl_pointer_listener wl_pointer_listener = {
       .enter = wl_pointer_enter,
       .leave = wl_pointer_leave,
       .motion = wl_pointer_motion,
       .button = wl_pointer_button,
       .axis = wl_pointer_axis,
       .frame = wl_pointer_frame,
       .axis_source = wl_pointer_axis_source,
       .axis_stop = wl_pointer_axis_stop,
       .axis_discrete = wl_pointer_axis_discrete,
};

指针拥有许多事件,让我们来看看它们。

static void
wl_pointer_enter(void *data, struct wl_pointer *wl_pointer,
               uint32_t serial, struct wl_surface *surface,
               wl_fixed_t surface_x, wl_fixed_t surface_y)
{
       struct client_state *client_state = data;
       client_state->pointer_event.event_mask |= POINTER_EVENT_ENTER;
       client_state->pointer_event.serial = serial;
       client_state->pointer_event.surface_x = surface_x,
               client_state->pointer_event.surface_y = surface_y;
}

static void
wl_pointer_leave(void *data, struct wl_pointer *wl_pointer,
               uint32_t serial, struct wl_surface *surface)
{
       struct client_state *client_state = data;
       client_state->pointer_event.serial = serial;
       client_state->pointer_event.event_mask |= POINTER_EVENT_LEAVE;
}

进入 "enter" 和离开 "leave" 事件是非常直截了当的,它们为其余的执行工作提供了舞台。我们更新事件掩码以包括适当的事件,然后用我们提供的数据填充进去。运动 "motion" 和按钮 "button" 事件也是十分类似的:

static void
wl_pointer_motion(void *data, struct wl_pointer *wl_pointer, uint32_t time,
               wl_fixed_t surface_x, wl_fixed_t surface_y)
{
       struct client_state *client_state = data;
       client_state->pointer_event.event_mask |= POINTER_EVENT_MOTION;
       client_state->pointer_event.time = time;
       client_state->pointer_event.surface_x = surface_x,
               client_state->pointer_event.surface_y = surface_y;
}

static void
wl_pointer_button(void *data, struct wl_pointer *wl_pointer, uint32_t serial,
               uint32_t time, uint32_t button, uint32_t state)
{
       struct client_state *client_state = data;
       client_state->pointer_event.event_mask |= POINTER_EVENT_BUTTON;
       client_state->pointer_event.time = time;
       client_state->pointer_event.serial = serial;
       client_state->pointer_event.button = button,
               client_state->pointer_event.state = state;
}

轴事件有点复杂,因为存在两个方向的轴:水平和垂直。因此,我们的 pointer_event 结构体也包含具有两组轴事件的数组。我们处理这些的代码最终如下:

static void
wl_pointer_axis(void *data, struct wl_pointer *wl_pointer, uint32_t time,
               uint32_t axis, wl_fixed_t value)
{
       struct client_state *client_state = data;
       client_state->pointer_event.event_mask |= POINTER_EVENT_AXIS;
       client_state->pointer_event.time = time;
       client_state->pointer_event.axes[axis].valid = true;
       client_state->pointer_event.axes[axis].value = value;
}

static void
wl_pointer_axis_source(void *data, struct wl_pointer *wl_pointer,
               uint32_t axis_source)
{
       struct client_state *client_state = data;
       client_state->pointer_event.event_mask |= POINTER_EVENT_AXIS_SOURCE;
       client_state->pointer_event.axis_source = axis_source;
}

static void
wl_pointer_axis_stop(void *data, struct wl_pointer *wl_pointer,
               uint32_t time, uint32_t axis)
{
       struct client_state *client_state = data;
       client_state->pointer_event.time = time;
       client_state->pointer_event.event_mask |= POINTER_EVENT_AXIS_STOP;
       client_state->pointer_event.axes[axis].valid = true;
}

static void
wl_pointer_axis_discrete(void *data, struct wl_pointer *wl_pointer,
               uint32_t axis, int32_t discrete)
{
       struct client_state *client_state = data;
       client_state->pointer_event.event_mask |= POINTER_EVENT_AXIS_DISCRETE;
       client_state->pointer_event.axes[axis].valid = true;
       client_state->pointer_event.axes[axis].discrete = discrete;
}

除了更新受到影响的轴这一主要变化之外,其余部分也同样非常直截了当。请注意 "valid" 布尔值的使用:我们有可能受到更新了一个轴但没更新另一个的指针帧 (pointer frame),所以我们使用 "valid" 值来确定该帧事件中哪些轴被有效更新。

说到这里,现在是该集中注意力的地方了:我们的 "frame" 句柄。

static void
wl_pointer_frame(void *data, struct wl_pointer *wl_pointer)
{
       struct client_state *client_state = data;
       struct pointer_event *event = &client_state->pointer_event;
       fprintf(stderr, "pointer frame @ %d: ", event->time);

       if (event->event_mask & POINTER_EVENT_ENTER) {
               fprintf(stderr, "entered %f, %f ",
                               wl_fixed_to_double(event->surface_x),
                               wl_fixed_to_double(event->surface_y));
       }

       if (event->event_mask & POINTER_EVENT_LEAVE) {
               fprintf(stderr, "leave");
       }

       if (event->event_mask & POINTER_EVENT_MOTION) {
               fprintf(stderr, "motion %f, %f ",
                               wl_fixed_to_double(event->surface_x),
                               wl_fixed_to_double(event->surface_y));
       }

       if (event->event_mask & POINTER_EVENT_BUTTON) {
               char *state = event->state == WL_POINTER_BUTTON_STATE_RELEASED ?
                       "released" : "pressed";
               fprintf(stderr, "button %d %s ", event->button, state);
       }

       uint32_t axis_events = POINTER_EVENT_AXIS
               | POINTER_EVENT_AXIS_SOURCE
               | POINTER_EVENT_AXIS_STOP
               | POINTER_EVENT_AXIS_DISCRETE;
       char *axis_name[2] = {
               [WL_POINTER_AXIS_VERTICAL_SCROLL] = "vertical",
               [WL_POINTER_AXIS_HORIZONTAL_SCROLL] = "horizontal",
       };
       char *axis_source[4] = {
               [WL_POINTER_AXIS_SOURCE_WHEEL] = "wheel",
               [WL_POINTER_AXIS_SOURCE_FINGER] = "finger",
               [WL_POINTER_AXIS_SOURCE_CONTINUOUS] = "continuous",
               [WL_POINTER_AXIS_SOURCE_WHEEL_TILT] = "wheel tilt",
       };
       if (event->event_mask & axis_events) {
               for (size_t i = 0; i < 2; ++i) {
                       if (!event->axes[i].valid) {
                               continue;
                       }
                       fprintf(stderr, "%s axis ", axis_name[i]);
                       if (event->event_mask & POINTER_EVENT_AXIS) {
                               fprintf(stderr, "value %f ", wl_fixed_to_double(
                                                       event->axes[i].value));
                       }
                       if (event->event_mask & POINTER_EVENT_AXIS_DISCRETE) {
                               fprintf(stderr, "discrete %d ",
                                               event->axes[i].discrete);
                       }
                       if (event->event_mask & POINTER_EVENT_AXIS_SOURCE) {
                               fprintf(stderr, "via %s ",
                                               axis_source[event->axis_source]);
                       }
                       if (event->event_mask & POINTER_EVENT_AXIS_STOP) {
                               fprintf(stderr, "(stopped) ");
                       }
               }
       }

       fprintf(stderr, "\n");
       memset(event, 0, sizeof(*event));
}

毋庸置疑,这是最长的一串代码了。但愿它不会令人感到困惑。我们在这里所做的就是把这一帧期间累积的状态漂亮地打印到 stderr 上。如果你现在再编译并运行这个程序,你应该可以在窗口上晃动你的鼠标,并看到输入事件被打印出来!

接入键盘事件

让我们用一些字段更新我们的 client_state 结构,以存储 XKB 的状态。

@@ -105,6 +107,9 @@ struct client_state {
        int width, height;
        bool closed;
        struct pointer_event pointer_event;
+       struct xkb_state *xkb_state;
+       struct xkb_context *xkb_context;
+       struct xkb_keymap *xkb_keymap;
};

我们需要 xkbcommon 头文件来定义这些。通常当我们这样做的时候,我将会把 assert.h 也拉进来。

@@ -1,4 +1,5 @@
 #define _POSIX_C_SOURCE 200112L
+#include <assert.h>
 #include <errno.h>
 #include <fcntl.h>
 #include <limits.h>
@@ -9,6 +10,7 @@
 #include <time.h>
 #include <unistd.h>
 #include <wayland-client.h>
+#include <xkbcommon/xkbcommon.h>
 #include "xdg-shell-client-protocol.h"

我们还需要在我们的主函数中初始化 xkb_context:

@@ -603,6 +649,7 @@ main(int argc, char *argv[])
        state.height = 480;
        state.wl_display = wl_display_connect(NULL);
        state.wl_registry = wl_display_get_registry(state.wl_display);
+       state.xkb_context = xkb_context_new(XKB_CONTEXT_NO_FLAGS);
        wl_registry_add_listener(state.wl_registry, &wl_registry_listener, &state);
        wl_display_roundtrip(state.wl_display);

下一步,让我们来更新我们座位的功能函数,把我们的键盘监听器也接入。

        } else if (!have_pointer && state->wl_pointer != NULL) {
                wl_pointer_release(state->wl_pointer);
                state->wl_pointer = NULL;
        }
+
+       bool have_keyboard = capabilities & WL_SEAT_CAPABILITY_KEYBOARD;
+
+       if (have_keyboard && state->wl_keyboard == NULL) {
+               state->wl_keyboard = wl_seat_get_keyboard(state->wl_seat);
+               wl_keyboard_add_listener(state->wl_keyboard,
+                               &wl_keyboard_listener, state);
+       } else if (!have_keyboard && state->wl_keyboard != NULL) {
+               wl_keyboard_release(state->wl_keyboard);
+               state->wl_keyboard = NULL;
+       }
 }

我们也要在这里定义我们使用的 wl_keyboard_listener

static const struct wl_keyboard_listener wl_keyboard_listener = {
       .keymap = wl_keyboard_keymap,
       .enter = wl_keyboard_enter,
       .leave = wl_keyboard_leave,
       .key = wl_keyboard_key,
       .modifiers = wl_keyboard_modifiers,
       .repeat_info = wl_keyboard_repeat_info,
};

现在开始有了一些变化,让我们从 keymap 开始:

static void
wl_keyboard_keymap(void *data, struct wl_keyboard *wl_keyboard,
               uint32_t format, int32_t fd, uint32_t size)
{
       struct client_state *client_state = data;
       assert(format == WL_KEYBOARD_KEYMAP_FORMAT_XKB_V1);

       char *map_shm = mmap(NULL, size, PROT_READ, MAP_SHARED, fd, 0);
       assert(map_shm != MAP_FAILED);

       struct xkb_keymap *xkb_keymap = xkb_keymap_new_from_string(
                       client_state->xkb_context, map_shm,
                       XKB_KEYMAP_FORMAT_TEXT_V1, XKB_KEYMAP_COMPILE_NO_FLAGS);
       munmap(map_shm, size);
       close(fd);

       struct xkb_state *xkb_state = xkb_state_new(xkb_keymap);
       xkb_keymap_unref(client_state->xkb_keymap);
       xkb_state_unref(client_state->xkb_state);
       client_state->xkb_keymap = xkb_keymap;
       client_state->xkb_state = xkb_state;
}

现在我们可以看到为什么我们需要添加 assert.h——我们在这里用断言来确保 keymap 的格式是我们所期望的。然后,我们用 mmap 将混成器发送给我们的文件描述符 fd 映射成一个 char* 指针,我们可以将其传入 xkb_keymap_new_from_string。不要忘记 munmap 并在之后关闭这个文件描述符,然后设置我们的 XKB 状态。还要注意的是,我们也用 "*_unref" 去掉了先前在调用此函数时所设置的一切 XKB keymap 或 state 引用,以防混成器在运行时改变 keymap1

static void
wl_keyboard_enter(void *data, struct wl_keyboard *wl_keyboard,
               uint32_t serial, struct wl_surface *surface,
               struct wl_array *keys)
{
       struct client_state *client_state = data;
       fprintf(stderr, "keyboard enter; keys pressed are:\n");
       uint32_t *key;
       wl_array_for_each(key, keys) {
               char buf[128];
               xkb_keysym_t sym = xkb_state_key_get_one_sym(
                               client_state->xkb_state, *key + 8);
               xkb_keysym_get_name(sym, buf, sizeof(buf));
               fprintf(stderr, "sym: %-12s (%d), ", buf, sym);
               xkb_state_key_get_utf8(client_state->xkb_state,
                               *key + 8, buf, sizeof(buf));
               fprintf(stderr, "utf8: '%s'\n", buf);
       }
}

当键盘 "进入" 我们的表面时,我们已经获得了键盘的输入焦点。混成器会将这之前所按键的队列转发出来,这里我们只是枚举它们并记录它们的 keysym 名称和 UTF-8 等效值。当按键被按下的时候,我们会做类似如下的事情:

static void
wl_keyboard_key(void *data, struct wl_keyboard *wl_keyboard,
               uint32_t serial, uint32_t time, uint32_t key, uint32_t state)
{
       struct client_state *client_state = data;
       char buf[128];
       uint32_t keycode = key + 8;
       xkb_keysym_t sym = xkb_state_key_get_one_sym(
                       client_state->xkb_state, keycode);
       xkb_keysym_get_name(sym, buf, sizeof(buf));
       const char *action =
               state == WL_KEYBOARD_KEY_STATE_PRESSED ? "press" : "release";
       fprintf(stderr, "key %s: sym: %-12s (%d), ", action, buf, sym);
       xkb_state_key_get_utf8(client_state->xkb_state, keycode,
                       buf, sizeof(buf));
       fprintf(stderr, "utf8: '%s'\n", buf);
}

最后,我们增加了其余三个小事件的实现:

static void
wl_keyboard_leave(void *data, struct wl_keyboard *wl_keyboard,
               uint32_t serial, struct wl_surface *surface)
{
       fprintf(stderr, "keyboard leave\n");
}

static void
wl_keyboard_modifiers(void *data, struct wl_keyboard *wl_keyboard,
               uint32_t serial, uint32_t mods_depressed,
               uint32_t mods_latched, uint32_t mods_locked,
               uint32_t group)
{
       struct client_state *client_state = data;
       xkb_state_update_mask(client_state->xkb_state,
               mods_depressed, mods_latched, mods_locked, 0, 0, group);
}

static void
wl_keyboard_repeat_info(void *data, struct wl_keyboard *wl_keyboard,
               int32_t rate, int32_t delay)
{
       /* Left as an exercise for the reader */
}

对于修饰符,我们可以进一步解码,但大多数应用程序不需要这样做。我们只是在这里更新 XKB 的状态。至于处理按键重复,这对于你的应用来说有诸多限制。比如,你想重复输入文本吗,想重复键盘快捷键吗,这些重复的所需的时间如何与你的事件循环进行互动?这些问题的答案需要由你自己来决定。

如果你再次编译并运行,你应该能够开始在窗口中开始打字,并看到你的输入被打印到终端日志中。这值得欢呼!

接入触摸事件

最后,我们将新增设备的触摸功能支持。就和指针事件一样,触摸设备也存在一个 "frame" 帧事件。然而,由于有多个触摸点可能在一帧内被更新,所以它们可能变得更加复杂。我们将增加一些结构体和枚举类型来表示状态的累积。

enum touch_event_mask {
       TOUCH_EVENT_DOWN = 1 << 0,
       TOUCH_EVENT_UP = 1 << 1,
       TOUCH_EVENT_MOTION = 1 << 2,
       TOUCH_EVENT_CANCEL = 1 << 3,
       TOUCH_EVENT_SHAPE = 1 << 4,
       TOUCH_EVENT_ORIENTATION = 1 << 5,
};

struct touch_point {
       bool valid;
       int32_t id;
       uint32_t event_mask;
       wl_fixed_t surface_x, surface_y;
       wl_fixed_t major, minor;
       wl_fixed_t orientation;
};

struct touch_event {
       uint32_t event_mask;
       uint32_t time;
       uint32_t serial;
       struct touch_point points[10];
};

请注意,我在这里选择了 10 个触摸点,假设大多数用户只会使用这么多手指。而对于较大的多用户触摸屏,你可能需要一个更高的上限。此外,有些触摸硬件同时支持的触摸点少于十个,仅有八个也是常见的,而支持触摸点数量更少的硬件在老旧设备中也十分常见。

我们把这个结构体添加到 client_state:

@@ -110,6 +135,7 @@ struct client_state {
        struct xkb_state *xkb_state;
        struct xkb_context *xkb_context;
        struct xkb_keymap *xkb_keymap;
+       struct touch_event touch_event;
 };

当触摸支持可用的时候,我们将更新座位的功能句柄,以介入一个监听器。

        } else if (!have_keyboard && state->wl_keyboard != NULL) {
                wl_keyboard_release(state->wl_keyboard);
                state->wl_keyboard = NULL;
        }
+
+       bool have_touch = capabilities & WL_SEAT_CAPABILITY_TOUCH;
+
+       if (have_touch && state->wl_touch == NULL) {
+               state->wl_touch = wl_seat_get_touch(state->wl_seat);
+               wl_touch_add_listener(state->wl_touch,
+                               &wl_touch_listener, state);
+       } else if (!have_touch && state->wl_touch != NULL) {
+               wl_touch_release(state->wl_touch);
+               state->wl_touch = NULL;
+       }
 }

我们对作为上触摸功能的出现和消失也做了同样处理,因此我们的代码在运行时设备热插拔处理方面都很健壮。不过,触摸设备热插拔的情况在实际中不太常见。

这里是其自身的监听器:

static const struct wl_touch_listener wl_touch_listener = {
       .down = wl_touch_down,
       .up = wl_touch_up,
       .motion = wl_touch_motion,
       .frame = wl_touch_frame,
       .cancel = wl_touch_cancel,
       .shape = wl_touch_shape,
       .orientation = wl_touch_orientation,
};

为了解决多点触摸问题,我们需要写一个小的辅助函数:

+static struct touch_point *
+get_touch_point(struct client_state *client_state, int32_t id)
+{
+       struct touch_event *touch = &client_state->touch_event;
+       const size_t nmemb = sizeof(touch->points) / sizeof(struct touch_point);
+       int invalid = -1;
+       for (size_t i = 0; i < nmemb; ++i) {
+               if (touch->points[i].id == id) {
+                       return &touch->points[i];
+               }
+               if (invalid == -1 && !touch->points[i].valid) {
+                       invalid = i;
+               }
+       }
+       if (invalid == -1) {
+               return NULL;
+       }
+       touch->points[invalid].valid = true;
+       touch->points[invalid].id = id;
+       return &touch->points[invalid];
+}

这个函数的基本目的是从我们添加到 touch_event 结构体的数组中,根据我们要接收事件的触摸点 ID,挑选一个触摸点。如果我们找到了该 ID 的现有触摸点,我们就将其返回。如果没有,则会返回第一个可用的触摸点。如果我们都找完了还没有,就会返回 NULL

现在我们可以利用这点来实现我们的第一个功能:触摸。

static void
wl_touch_down(void *data, struct wl_touch *wl_touch, uint32_t serial,
               uint32_t time, struct wl_surface *surface, int32_t id,
               wl_fixed_t x, wl_fixed_t y)
{
       struct client_state *client_state = data;
       struct touch_point *point = get_touch_point(client_state, id);
       if (point == NULL) {
               return;
       }
       point->event_mask |= TOUCH_EVENT_UP;
       point->surface_x = wl_fixed_to_double(x),
               point->surface_y = wl_fixed_to_double(y);
       client_state->touch_event.time = time;
       client_state->touch_event.serial = serial;
}

和指针事件一样,我们也是简单地将这个状态累积起来,以便后续使用。我们还不知道这个事件是否代表一个完整的触摸帧。让我们为触摸添加一些类似的东西:

static void
wl_touch_up(void *data, struct wl_touch *wl_touch, uint32_t serial,
               uint32_t time, int32_t id)
{
       struct client_state *client_state = data;
       struct touch_point *point = get_touch_point(client_state, id);
       if (point == NULL) {
               return;
       }
       point->event_mask |= TOUCH_EVENT_UP;
}

以及运动:

static void
wl_touch_motion(void *data, struct wl_touch *wl_touch, uint32_t time,
               int32_t id, wl_fixed_t x, wl_fixed_t y)
{
       struct client_state *client_state = data;
       struct touch_point *point = get_touch_point(client_state, id);
       if (point == NULL) {
               return;
       }
       point->event_mask |= TOUCH_EVENT_MOTION;
       point->surface_x = x, point->surface_y = y;
       client_state->touch_event.time = time;
}

触摸事件的取消与之前有所不同,因为它一次性 “取消” 了所有活动的触摸点。我们只需要将其存储在 touch_event 的顶层事件掩码中。

static void
wl_touch_cancel(void *data, struct wl_touch *wl_touch)
{
       struct client_state *client_state = data;
       client_state->touch_event.event_mask |= TOUCH_EVENT_CANCEL;
}

然而,形状和方向事件类似于向上、向下和移动,因为它们告诉我们一个特定触摸点的尺寸。

static void
wl_touch_shape(void *data, struct wl_touch *wl_touch,
               int32_t id, wl_fixed_t major, wl_fixed_t minor)
{
       struct client_state *client_state = data;
       struct touch_point *point = get_touch_point(client_state, id);
       if (point == NULL) {
               return;
       }
       point->event_mask |= TOUCH_EVENT_SHAPE;
       point->major = major, point->minor = minor;
}

static void
wl_touch_orientation(void *data, struct wl_touch *wl_touch,
               int32_t id, wl_fixed_t orientation)
{
       struct client_state *client_state = data;
       struct touch_point *point = get_touch_point(client_state, id);
       if (point == NULL) {
               return;
       }
       point->event_mask |= TOUCH_EVENT_ORIENTATION;
       point->orientation = orientation;
}

最后,在收到一个帧事件时,我们可以将所有这些累积的状态解释为一个单一的输入事件,就像我们的光标指针代码一样。

static void
wl_touch_frame(void *data, struct wl_touch *wl_touch)
{
       struct client_state *client_state = data;
       struct touch_event *touch = &client_state->touch_event;
       const size_t nmemb = sizeof(touch->points) / sizeof(struct touch_point);
       fprintf(stderr, "touch event @ %d:\n", touch->time);

       for (size_t i = 0; i < nmemb; ++i) {
               struct touch_point *point = &touch->points[i];
               if (!point->valid) {
                       continue;
               }
               fprintf(stderr, "point %d: ", touch->points[i].id);

               if (point->event_mask & TOUCH_EVENT_DOWN) {
                       fprintf(stderr, "down %f,%f ",
                                       wl_fixed_to_double(point->surface_x),
                                       wl_fixed_to_double(point->surface_y));
               }

               if (point->event_mask & TOUCH_EVENT_UP) {
                       fprintf(stderr, "up ");
               }

               if (point->event_mask & TOUCH_EVENT_MOTION) {
                       fprintf(stderr, "motion %f,%f ",
                                       wl_fixed_to_double(point->surface_x),
                                       wl_fixed_to_double(point->surface_y));
               }

               if (point->event_mask & TOUCH_EVENT_SHAPE) {
                       fprintf(stderr, "shape %fx%f ",
                                       wl_fixed_to_double(point->major),
                                       wl_fixed_to_double(point->minor));
               }

               if (point->event_mask & TOUCH_EVENT_ORIENTATION) {
                       fprintf(stderr, "orientation %f ",
                                       wl_fixed_to_double(point->orientation));
               }

               point->valid = false;
               fprintf(stderr, "\n");
       }
}

编译并再次运行这个程序,你就可以看到当你与触摸设备交互时,触摸事件被答应到 stderr (假设你现在有支持触摸的设备)。现在我们的客户端终于了实现输入的支持!

接下来该做什么?

有很多不同种类的输入设备,因此扩展我们的代码以支持这些设备是一项相当庞大的工作——仅在本章中我们的代码量就增加了 2.5 倍。不过收获应该也是相当大的,因为你现在已经熟悉了足够多的 Wayland 概念(和代码),由此你可以实现多种多样的客户端了。

这之后还有更多的东西要学——在最后几章,我们将介绍弹出窗口、上下文菜单、交互式窗口的移动和大小调整、剪贴板和拖放支持,以及后来的一些有趣的扩展协议,以支持更多小众的使用场景。我强烈建议你在构建自己的客户端之前先读到第 10.1 章,因为它涵盖诸如根据混成器的要求调整窗口大小等内容。

1

这种情况在实践中确实发生了!

深入理解 XDG shell

到目前为止,我们已经成功地在屏幕上现实了一些顶层的应用程序窗口,但 XDG shell 还有更多的东西我们还没有完全理解。它让即便是最简单的应用程序也能正确地实现配置生命周期,而且 xdg-shell 也为更复杂的应用程序提供了有用的功能。

xdg-shell 的全部功能包括客户端或服务端协商窗口大小、多窗口的层次结构、客户端装饰 (CSD)、以及诸如上下文菜单窗口一样的语义定位。

配置和生命周期

先前,按我们的选择创建了一个固定尺寸的窗口:640x480。然而,混成器往往会对我们的窗口应假设什么样的尺寸有意见,而我们可能也想传达自己的偏好尺寸。未能这样做往往会导致非预期行为,比如你窗口的一部分被混成器裁切掉,而混成器试图告诉你让你的表面尺寸缩小。

混成器可以为应用程序提供额外的线索,了解其显示的上下文。它可以让你知道应用是否正处于最大化或者全屏状态,亦或其窗口边的一个或多个边缘正与其他窗口或显示器的边缘平铺、正处于焦点还是后台空闲状态,等等。由于 wl_surface 是用来在客户端和服务端之间原子化交流表面的变化,xdg_surface 提供接口提供了一下两个消息,供混成器建议一些变化和客户端确认:

<request name="ack_configure">
  <arg name="serial" type="uint" />
</request>

<event name="configure">
  <arg name="serial" type="uint" />
</event>

就它们本身而言,这些消息只能携带很小的信息量。然而,xdg_surface 的每个子类 (xdg_toplevelxdg_popup) 都有额外的事件,服务端可以在 "configure" 配置事件之前发送,以提出到目前为止所提到的各种建议。服务端将发送这些状态,如最大化、焦点、尺寸建议等,然后用 serial 来配置事件。当客户端的状态与这些建议一致的时候,它将嘎送一个带有相同序列的 ack_configure 请求来表明这一点。在下次提交到相关的 wl_surface 时,混成器将认为该状态是一致的。

XDG 顶层窗口的生命周期

我们第 7 章的示例代码虽然可以工作,但它不是桌面的最佳范式。它并没有假定混成器推荐的尺寸,而且如果用户试图关闭窗口,它也不会消失。若要响应这些由混成器提供的事件,这里涉及到两个 Wayland 事件:configureclose 即配置和关闭。

<event name="configure">
  <arg name="width" type="int"/>
  <arg name="height" type="int"/>
  <arg name="states" type="array"/>
</event>

<event name="close" />

这里的宽度 width 和高度 height 是混成器为窗口推荐的首选尺寸1,而状态 states 则是由以下数值构成的数组:

<enum name="state">
  <entry name="maximized" />
  <entry name="fullscreen" />
  <entry name="resizing" />
  <entry name="activated" />
  <entry name="tiled_left" />
  <entry name="tiled_right" />
  <entry name="tiled_top" />
  <entry name="tiled_bottom" />
</enum>

关闭事件有时可以被忽略,一个典型的原因是向用户显示一个确认对话框,以保存他们还未保存的工作。我们可以很轻松地更新第 7 章中的示例代码,以支持这些事件。

diff --git a/client.c b/client.c
--- a/client.c
+++ b/client.c
@@ -70,9 +70,10 @@ struct client_state {
 	struct xdg_surface *xdg_surface;
 	struct xdg_toplevel *xdg_toplevel;
 	/* State */
-	bool closed;
 	float offset;
 	uint32_t last_frame;
+	int width, height;
+	bool closed;
 };
 
 static void wl_buffer_release(void *data, struct wl_buffer *wl_buffer) {
@@ -86,7 +87,7 @@ static const struct wl_buffer_listener wl_buffer_listener = {
 static struct wl_buffer *
 draw_frame(struct client_state *state)
 {
-	const int width = 640, height = 480;
+	int width = state->width, height = state->height;
 	int stride = width * 4;
 	int size = stride * height;
 
@@ -124,6 +125,32 @@ draw_frame(struct client_state *state)
 	return buffer;
 }
 
+static void
+xdg_toplevel_configure(void *data,
+		struct xdg_toplevel *xdg_toplevel, int32_t width, int32_t height,
+		struct wl_array *states)
+{
+	struct client_state *state = data;
+	if (width == 0 || height == 0) {
+		/* Compositor is deferring to us */
+		return;
+	}
+	state->width = width;
+	state->height = height;
+}
+
+static void
+xdg_toplevel_close(void *data, struct xdg_toplevel *toplevel)
+{
+	struct client_state *state = data;
+	state->closed = true;
+}
+
+static const struct xdg_toplevel_listener xdg_toplevel_listener = {
+	.configure = xdg_toplevel_configure,
+	.close = xdg_toplevel_close,
+};
+
 static void
 xdg_surface_configure(void *data,
 		struct xdg_surface *xdg_surface, uint32_t serial)
@@ -163,7 +190,7 @@ wl_surface_frame_done(void *data, struct wl_callback *cb, uint32_t time)
 	cb = wl_surface_frame(state->wl_surface);
 	wl_callback_add_listener(cb, &wl_surface_frame_listener, state);
 
-	/* Update scroll amount at 8 pixels per second */
+	/* Update scroll amount at 24 pixels per second */
 	if (state->last_frame != 0) {
 		int elapsed = time - state->last_frame;
 		state->offset += elapsed / 1000.0 * 24;
@@ -217,6 +244,8 @@ int
 main(int argc, char *argv[])
 {
 	struct client_state state = { 0 };
+	state.width = 640;
+	state.height = 480;
 	state.wl_display = wl_display_connect(NULL);
 	state.wl_registry = wl_display_get_registry(state.wl_display);
 	wl_registry_add_listener(state.wl_registry, &wl_registry_listener, &state);
@@ -227,6 +256,8 @@ main(int argc, char *argv[])
 			state.xdg_wm_base, state.wl_surface);
 	xdg_surface_add_listener(state.xdg_surface, &xdg_surface_listener, &state);
 	state.xdg_toplevel = xdg_surface_get_toplevel(state.xdg_surface);
+	xdg_toplevel_add_listener(state.xdg_toplevel,
+			&xdg_toplevel_listener, &state);
 	xdg_toplevel_set_title(state.xdg_toplevel, "Example client");
 	wl_surface_commit(state.wl_surface);

如果你再次编译并运行这个客户端,你会注意到它的行为表现得比之前更加完善了。

请求改变状态

客户端也可以向混成器请求将自己置入这些状态中(最大、最小化等等),或者对窗口的大小进行限制。

<request name="set_max_size">
  <arg name="width" type="int"/>
  <arg name="height" type="int"/>
</request>

<request name="set_min_size">
  <arg name="width" type="int"/>
  <arg name="height" type="int"/>
</request>

<request name="set_maximized" />

<request name="unset_maximized" />

<request name="set_fullscreen" />
  <arg name="output"
    type="object"
    interface="wl_output"
    allow-null="true"/>
</request>

<request name="unset_fullscreen" />

<request name="set_minimized" />

混成器通过发送一个相应的 configure 配置事件来表明它对这些请求的确认。

1

这考虑到了客户端 set_window_geometry 请求所发送的窗口的几何形状。建议的尺寸仅包括窗口几何形状所代表的空间(即外接矩形)。

弹出窗口

在设计有应用程序窗口的软件时,存在许多较小的辅助表面被用于各种目的。例如,右键显示的上下文菜单,从一系列选项中选择一个值的下拉菜单,当你鼠标悬停在 UI 元素上时显示的上下文提示,或者贴着窗口顶部和底部的弹出的菜单栏和工具栏。通常这些都是嵌套的窗口,例如按照 “文件 → 最近的文档 → 例子.odt” 这样的路径。

Wayland 环境下,XDG shell 提供了管理这些弹出式窗口的工具。我们在前面看到了 xdg_surfaceget_toplevel 请求,用于创建顶层的应用程序窗口。在弹出式窗口的的情况下,使用 get_popup 请求代替。

<request name="get_popup">
  <arg name="id" type="new_id" interface="xdg_popup"/>
  <arg name="parent" type="object" interface="xdg_surface" allow-null="true"/>
  <arg name="positioner" type="object" interface="xdg_positioner"/>
</request>

第一个和第二个参数是不言而喻的,但第三个参数引入了一个新的概念:定位器。定位器的实现目的正如其名,是为了定位新的弹出式窗口。这是用来让混成器使用器特权信息参与弹出窗口的定位,例如避免弹出窗口延伸到显示器的边缘之外。我们将在第 10.4 章节中讨论定位器,现在你在没有进一步配置的情况下简单地创建一个定位器,并实现适当的 xdg_wm_base 请求。

<request name="create_positioner">
  <arg name="id" type="new_id" interface="xdg_positioner"/>
</request>

简而言之我们可以:

  1. 创建一个新的 wl_surface
  2. 为其分配一个 xdg_surface
  3. 创建一个新的 xdg_positioner 定位器,并按 10.4 章节中那样保存它的配置
  4. 从我们的 XDG 表面和定位器创建一个 xdg_popup 弹窗,将其父级分配给我们先前创建的 xdg_toplevel 顶层窗口

然后,我们可以通过先前讨论过的相同生命周期实现来渲染和附加缓冲区。我们还可以访问其他一些弹窗特有的功能。

配置

就像 XDG toplevel 顶层窗口的配置事件一样,它可以用来建议你弹出窗口的尺寸。然而,与 toplevel 不同的是,它还包括一个定位事件,用于通知客户端弹出窗口相对于其父表面的位置。

<event name="configure">
  <arg name="x" type="int"
 summary="x position relative to parent surface window geometry"/>
  <arg name="y" type="int"
 summary="y position relative to parent surface window geometry"/>
  <arg name="width" type="int" summary="window geometry width"/>
  <arg name="height" type="int" summary="window geometry height"/>
</event>

客户端可以通过 XDG 定位器来影响这些值,这也将在第 10.4 章节中讨论。

弹出式窗口输入抓取

弹出式界面通常希望能 “抓取” 所有的输入,例如允许用户使用方向键来选择不同的菜单项目。这可以通过抓取请求来实现。

<request name="grab">
  <arg name="seat" type="object" interface="wl_seat" />
  <arg name="serial" type="uint" />
</request>

响应这个请求的前提是接收到一个合格的输入事件,比如右键。这个输入事件的序列应该被用于该请求中。这些语义将在第 9 章中详细介绍。混成器可以在其后取消这个抓取,例如用户按了 escape 按键或者点击了弹出窗口之外的地方。

驳回弹窗 (Dismissal)

在这种情况下,混成器会驳回你的弹出窗口,例如按下 escape 键之后,会发送以下事件:

<event name="popup_done" />

为了避免发生竞争条件,混成器将弹出式窗口的结构体保留在内存中,即便弹出式窗口驳回后也会为它们的请求提供服务。关于对象的寿命和竞争条件的更多细节在第 2.4 章节中阐述过了。

销毁弹窗

客户端实现销毁弹窗是非常直截了当的:

<request name="destroy" type="destructor" />

然而,有一个细节值得一提:你必须自顶向下销毁弹出窗口。在任何时候,你唯一可以销毁的弹出窗口只能是最上层的那个。如果不这样做,你就会因为协议错误而被断开连接。

交互式移动和尺寸调整

许多应用程序窗口都有交互式 UI 元素,用户可以用它来拖动或者调整窗口大小。在默认情况下,许多 Wayland 客户端都希望负责自己的窗口装饰,并提供这些交互元素。在 X11 上,应用程序窗口可以在屏幕上的任何地方自行定位(即知道自己的绝对位置),并以此来推动这些交互。

然而,Wayland 的一个设计特性是让应用程序窗口不知道它们在屏幕上的确切位置或是与其他窗口的相对位置。这一决定为 Wayland 混成器提供了更多的灵活性,例如:窗口可以同时显示在几个地方,排列在 VR 场景的 3D 空间中,或以任何其他新颖的方式呈现。Wayland 的设计旨在通用,广泛适用与许多设备和不同外形。

为了平衡移动和调整尺寸这两种需要,XDG toplevels 提供了两个请求,可以用来要求混成器执行一个交互式移动或者调整大小的操作。部分相关接口如下:

<request name="move">
  <arg name="seat" type="object" interface="wl_seat" />
  <arg name="serial" type="uint" />
</request>

就像上一章节解释的弹出式窗口里创建请求一样,你必须提供一个输入事件序列来执行一个交互式操作。例如,当你收到一个向下的鼠标移动的事件时,你可以使用该事件的序列来执行交互式移动操作。混成器将从此处接管,并且在其内部的坐标空间中对窗口进行交互式操作。

调整尺寸则相对复杂一些,因为需要指定操作中涉及到窗口的哪些边或角。

<enum name="resize_edge">
  <entry name="none" value="0"/>
  <entry name="top" value="1"/>
  <entry name="bottom" value="2"/>
  <entry name="left" value="4"/>
  <entry name="top_left" value="5"/>
  <entry name="bottom_left" value="6"/>
  <entry name="right" value="8"/>
  <entry name="top_right" value="9"/>
  <entry name="bottom_right" value="10"/>
</enum>

<request name="resize">
  <arg name="seat" type="object" interface="wl_seat" />
  <arg name="serial" type="uint" />
  <arg name="edges" type="uint" />
</request>

但除此之外,它的功能大致相同。如果用户沿着你窗口的左下角点击并拖动,你可能想发送一个交互式调整大小的请求,并将边缘参数设置为 buttom_left

对于自行实现 CSD 的客户端来说,有一个必要的额外请求:

<request name="show_window_menu">
  <arg name="seat" type="object" interface="wl_seat" />
  <arg name="serial" type="uint" />
  <arg name="x" type="int" />
  <arg name="y" type="int" />
</request>

当点击窗口装饰时,通常会出现一个提供窗口操作的上下文菜单,例如关闭或最小化窗口。对于窗口装饰由其自行管理的客户端来说,这有助于将客户端驱动的交互事件与混成器驱动的元操作(如最小化窗口)联系起来。如果你的客户端使用了 CSD,则可以为此目的使用此请求。

xdg-decoration

在讨论客户端 CSD 的行为时,最后一个值得一提的细节是管理其初次用于协商的协议。不同的 Wayland 客户端和服务端可能对 CSD (client-side decoration) 或 SSD (server-side decoration) 有不同的偏好。为了表达这一意图,我们使用了一个扩展协议:xdg-decoration。它可以在 wayland-protocols 中找到,该协议提供了一个全局的接口:

<interface name="zxdg_decoration_manager_v1" version="1">
  <request name="destroy" type="destructor" />

  <request name="get_toplevel_decoration">
    <arg name="id" type="new_id" interface="zxdg_toplevel_decoration_v1"/>
    <arg name="toplevel" type="object" interface="xdg_toplevel"/>
  </request>
</interface>

你可以将你的 xdg_toplevel 对象传递到 get_toplevel_decoration 请求中,以获得一个具有以下接口的对象:

<interface name="zxdg_toplevel_decoration_v1" version="1">
  <request name="destroy" type="destructor" />

  <enum name="mode">
    <entry name="client_side" value="1" />
    <entry name="server_side" value="2" />
  </enum>

  <request name="set_mode">
    <arg name="mode" type="uint" enum="mode" />
  </request>

  <request name="unset_mode" />

  <event name="configure">
    <arg name="mode" type="uint" enum="mode" />
  </event>
</interface>

set_mode 请求用于表达客户端的偏好,unset_mode 用于表达没有偏好。然后,混成器将使用 configure 事件来告知客户端是否使用 CSD。更多细节请查阅完整的 XML。

指针

当我们在前几页介绍弹出式窗口的时候,我们注意到创建弹出式窗口时你必须提供一个定位器对象。之所以我们告诉你不必担心这个问题,只需要使用默认值,是因为这是一个复杂的接口,在当时是无关紧要的。现在,我们将深入探讨这个复杂的接口。

当你打开一个弹窗时候,它是在一个窗口系统中显示的,该系统有些你客户端不知道的限制。例如,Wayland 客户端不知道它们在屏幕上显示的位置。因此,如果右键单机窗口,则客户端缺失了必要信息来进行决策,可能出现弹窗越过屏幕边缘的情况。定位器就是为了解决这些问题而设计的,它让客户端指定弹出式窗口移动方式或调整大小的某些约束条件,然后混成器在完全掌握全局的情况下,可以对如何使用作出最终决定。

基本请求

<request name="destroy" type="destructor"></request>

当你完成的时候,这个请求会销毁定位器。你可以在创建弹出窗口后调用此功能。

<request name="set_size">
  <arg name="width" type="int" />
  <arg name="height" type="int" />
</request>

set_size 请求用于设置正在创建的弹出窗口的大小。

所有使用使用定位器的客户端都将会用到这两个请求。现在,让我们来看一个更有趣的请求。

锚定