[TOC]
向用户显示您的应用程序只是I/O等式的一半——大多数应用程序还需要处理输入。为此,座椅提供了一种对Wayland上输入事件的抽象。从哲学上讲,Wayland的座椅指的是用户坐在其上操作计算机的“座位”,并与最多一个键盘和最多一个“指针”设备(即鼠标或触摸板)相关联。对于触摸屏、绘图板设备等也有类似的关系定义。
重要的是要记住,这是一种抽象,并且在Wayland显示上表示的座椅可能与现实世界1:1对应。实际上,在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。
这个接口相当简单。服务器将向客户端发送一个能力事件,以表明这个座位支持的输入设备的种类——用一组能力值的位掩码表示——客户端可以根据需要绑定到相应的输入设备。例如,如果服务器发送的能力中(caps & WL_SEAT_CAPABILITY_KEYBOARD) > 0
为真,客户端可以使用 get_keyboard 请求为这个座位获取一个 wl_keyboard
对象。每个特定输入设备的语义将在其余章节中介绍。
在介绍这些之前,我们先来谈谈一些常见的语义。
事件序列
Wayland客户端执行的一些操作需要进行一种简单的身份验证,即输入事件序列。例如,一个客户端打开弹出窗口(用右键单击召唤的上下文菜单是一种弹出窗口)可能希望在弹出窗口被撤销之前从受影响的座位获取所有服务器端的输入事件。为了防止滥用此功能,服务器可以为每个发送的输入事件分配序列号,并要求客户端在请求中包含其中一个序列号。
当服务器收到这样的请求时,它会查找与给定序列相关联的输入事件并做出判断。如果事件太久以前发生,或者对于错误的表面,或者不是正确类型的事件-例如,当您摇动鼠标时可以拒绝抓取,但单击时允许抓取-它可以拒绝请求。
从服务器的角度来看,他们可以简单地为每个输入事件发送一个递增的整数,并记录对特定用例有效的序列号以供稍后验证。客户端从其输入事件处理程序接收这些序列号,并可以立即将它们传递回去以执行所需的操作。
稍后,当我们开始介绍需要输入事件序列进行验证的具体请求时,我们将更详细地讨论这些问题。
输入帧
来自输入设备的单个输入事件可能会因实际原因而分解成多个Wayland事件。例如,wl_pointer会为您使用滚轮时发出轴事件,但会单独发出一个事件告诉您轴的种类是什么:滚轮、触摸板上的手指、将滚轮倾斜到侧面等。来自同一输入源的相同输入事件可能还包括一些鼠标移动,或者如果用户足够快速地点击按钮,也会有一些按钮点击。
这些相关事件的语义分组与输入类型之间略有不同,但帧事件在它们之间通常是通用的。简而言之,如果您缓冲来自设备的所有输入事件,然后等待帧事件发出信号,表明您已收到单个输入“帧”的所有事件,您可以将缓冲的Wayland事件解释为单个输入事件,然后重置缓冲并开始收集下一个帧的事件。
如果这听起来太复杂,不用担心。许多应用程序不需要担心输入帧。只有当您开始进行更复杂的输入事件处理时,您才会想关注这一点。
释放设备
当您完成使用设备时,每个接口都有一个释放请求,您可以使用它来清理它。它看起来像这样:
<request name="release" type="destructor" />
很容易。
9.1 鼠标输入
使用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>
一旦指针进入您的表面,您将开始收到更多的事件,我们稍后将讨论这些事件。然而,您可能首先想要做的是提供光标图像。过程如下:
- 使用
wl_compositor
创建一个新的wl_surface。 - 使用
wl_pointer.set_cursor
将该表面附加到指针。 - 将光标图像
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>
在这里,序列号必须来自“enter”事件。hotspot_x和hotspot_y参数指定光标表面局部坐标的“热点”,或指针在光标图像中的有效位置(例如箭头尖端)。请注意,表面可以为空——用于完全隐藏光标。
如果您正在寻找光标图像的良好来源,libwayland附带了一个单独的wayland-cursor库,可以从磁盘加载X光标主题并为其创建wl_buffers。有关详细信息,请参阅wayland-cursor.h,或参阅第9.5章中我们的示例客户端的更新。
注意:wayland-cursor包括处理动画光标的代码,这在1998年甚至还不酷。如果我是你,我不会费心去处理这个。从来没有人抱怨过我的Wayland客户端不支持动画光标。
当光标进入您的表面并且您已经附加了适当的光标后,您就可以开始处理输入事件了。有运动、按钮和轴事件。
指针帧
服务器上的单个输入处理帧可以携带大量信息的变化——例如,一次鼠标轮询可以在单个数据包中返回一个更新的位置和按钮的释放。服务器将这些变化作为单独的Wayland事件发送,并使用“frame”事件将它们组合在一起。
<event name="frame"></event>
客户端应将所有接收到的wl_pointer事件累积起来,然后在“frame”事件收到后处理挂起的输入作为一个单独的指针事件。
移动事件
移动事件与“enter”事件使用相同的坐标空间进行指定,并且非常简单:
<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中找到这些值,最常用的可能是由常量BTN_LEFT、BTN_RIGHT和BTN_MIDDLE表示的。还有更多,我会让您自己浏览头文件。
轴事件
轴事件用于滚动操作,例如旋转滚轮或从左到右摇动滚轮。最基本的形式如下:
<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>
轴源事件告诉您哪种轴被驱动——一个滚轮、一个手指触摸板、一个侧向倾斜的摇杆,或者更新颖的东西。这个事件很简单,但其余的事件更复杂:
<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中的摘要。简而言之,轴离散事件用于从任意尺度上消除轴事件的歧义,例如滚动轮的离散步骤中的歧义。轴停止事件表示一个离散的用户运动已经完成,并用于在几个帧中记录滚动事件的情况。未来的任何事件应被解释为单独的运动。
9.2 XKB简述
在我们讨论键盘输入之前,需要停止并给您一些额外的背景信息。键盘映射是涉及键盘输入的重要细节,而XKB是Wayland中处理它们推荐的方式。
当您在键盘上按下一个键时,它会向计算机发送一个扫描码,这只是一个分配给该物理键的数字。在我的键盘上,扫描码1是Escape键,'1'键是扫描码2,'a'是30,Shift是42,依此类推。我使用的是美国ANSI键盘布局,但还有许多其他布局,并且它们的扫描码不同。在我朋友的德国键盘上,扫描码12产生'ß',而我的产生'-'。
为了解决这个问题,我们使用了一个名为"xkbcommon"的库,这是由于其作用是将XKB(X键盘)中的通用代码提取为独立库而命名的。XKB定义了大量的键符号,例如XKB_KEY_A和XKB_KEY_ssharp(来自德语的'ß')以及XKB_KEY_kana_WO(来自日语的'を')。
识别这些键并与键符号进行关联只是问题的一部分。'a'可以通过按住Shift键来产生'A','を'在Katakana模式下被写为'ヲ',尽管存在严格意义上的'ß'的大写版本,但它几乎从未使用过,也从未被输入过。像Shift这样的键被称为修饰符,而像平假名和片假名这样的组被称为组。有些修饰符可以锁定,如大写锁定。XKB具有处理所有这些情况的基元,并维护一个状态机来跟踪您的键盘正在做什么并确定用户试图输入的确切Unicode码点。
使用XKB
那么xkbcommon实际上是如何使用的呢?首先,需要链接到它并获取头文件xkbcommon/xkbcommon.h。大多数利用xkbcommon的程序将需要管理三个对象:
- xkb_context:用于配置其他XKB资源的句柄
- xkb_keymap:从扫描码到键符号的映射
- xkb_state:将键符号转换为UTF-8字符串的状态机
设置此过程通常如下:
- 使用
xkb_context_new
创建一个新的xkb_context
,除非您正在做一些奇怪的事情,否则将其传递给XKB_CONTEXT_NO_FLAGS
。 - 获取一个作为字符串的键图。*
- 使用
xkb_keymap_new_from_string
创建一个针对此键图的xkb_keymap
。您将为格式参数传递XKB_KEYMAP_FORMAT_TEXT_V1
,这是唯一的键图格式。同样,除非您正在做一些奇怪的事情,否则您将使用XKB_KEYMAP_COMPILE_NO_FLAGS
作为标志。 - 使用
xkb_state_new
创建一个具有您的键图的xkb_state
。状态将增加键图的引用计数,因此如果您自己完成了它,请使用xkb_keymap_unref
。 - 从键盘获取扫描码。*
- 将扫描码输入到
xkb_state_key_get_one_sym
以获取键符号,并输入到xkb_state_key_get_utf8
以获取UTF-8字符串。Tada!
这些步骤将在下一部分讨论。
从代码的角度来看,这个过程看起来像这样:
#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 --cflags xkbcommon
和 pkgconf --libs xkbcommon
,或者使用您的构建系统推荐的消费 pc 文件的方式。
9.3 键盘输入
有了对如何使用XKB的理解,让我们扩展我们的Wayland代码,为我们提供键事件来输入。与获取wl_pointer资源类似,我们可以使用wl_seat.get_keyboard请求为具有WL_SEAT_CAPABILITY_KEYBOARD能力的座位创建wl_keyboard。当你完成后,你应该发送“释放”请求:
<request name="release" type="destructor" since="3">
</request>
这将允许服务器清理与此键盘相关的资源。
但是,你如何实际使用它呢?让我们从基础知识开始。
键映射
当你绑定到wl_keyboard时,服务器可能发送的第一个事件是键映射。
<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>
键映射格式枚举是在我们为键映射提出新格式时提供的,但在编写时,服务器可能发送的唯一格式是XKB键映射。
像这样的批量数据是通过文件描述符传输的。我们可以直接从文件描述符中读取,但通常建议使用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”事件还包括一个当前按下的键的数组。这是一个32位无符号整数的数组,每个整数代表一个按下的键的扫描码。
输入事件
一旦键盘进入你的表面,你就可以期待开始接收输入事件了。
<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>
“键”事件是在用户按下或释放一个键时发送的。像许多输入事件一样,还包括一个序列号,您可以使用它来将未来的请求与这个输入事件相关联。“键”是按下的键的扫描码,而“状态”是该键的按下或释放状态。
重要提示:此事件的扫描码来自Linux evdev。要将此扫描码转换为XKB扫描码,您必须将evdev扫描码加8。
修饰符事件包括类似的序列号,以及当前正在使用的输入组的被压、锁住和锁定修饰符的掩码。修饰符在被压时,例如,当你按住Shift键时。修饰符可以锁住,例如,当启用了粘性键时按下Shift键——在下一个非修饰符键被按下后它将停止生效。修饰符还可以被锁定,例如,当大写锁定被切换开或关时。输入组用于在各种键盘布局之间切换,例如在ISO和ANSI布局之间切换,或用于更语言特定的功能。
修饰符的解释是键映射特定的。您应该将它们都转发给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中,客户端负责实现“键重复”功能——只要您保持键按住,就会继续输入字符。此事件是向客户端发送用户的键重复设置的首选通知。在键重复开始之前的延迟时间是以毫秒为单位的键被按下持续时间,“速率”是释放键之前每秒重复的字符数。
9.4 触摸输入
在表面上,触摸屏输入相当简单,您的实现也可以很简单。然而,该协议为您提供了很多深度,应用程序可以利用这些深度提供更细致的触摸驱动的手势和反馈。
大多数触摸屏设备支持多点触控:它们可以跟踪屏幕被触摸的多个位置。每个“触摸点”都被分配一个ID,该ID在所有当前活动点中是唯一的,但如果您抬起手指并再次按下,可能会被重复使用。1
与其他输入设备类似,您可以通过wl_seat.get_touch获取wl_touch资源,并在处理完之后发送“释放”请求。
触摸帧
与指针一样,单个帧的触摸处理在服务器上可能包含许多更改的信息,但服务器将这些信息作为离散的Wayland事件发送。wl_touch.frame事件用于将这些事件组合在一起。
<event name="frame"></event>
客户端应该累积收到的所有wl_touch事件,然后在“帧”事件收到时处理挂起的输入作为一个单一的触摸事件。
触摸和释放
我们将考虑的第一个事件是“下”和“上”,当您将手指按在设备上时,以及当您从设备上移开手指时,它们分别被触发。
<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”参数中给出——的坐标空间中的定点坐标。时间是具有任意纪元并以毫秒为单位的单调递增时间戳。2请注意还包括一个序列号,该序列号可以包含在将来的请求中以将它们与该输入事件相关联。
移动
在收到具有特定触摸ID的“下”事件后,您将开始收到描述该触摸点在设备上移动的运动事件。
<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”坐标是在“进入”事件发送的表面的相对坐标空间中。
手势取消
触摸事件通常在识别为手势之前必须满足一定的阈值。例如,从左到右在屏幕上滑动可以由Wayland合成器用来在应用程序之间切换。但是,直到一些阈值被越过——比如在一定时间内达到屏幕的中点——合成器才会识别这种行为作为手势。
在达到这个阈值之前,合成器将为正在触摸的表面发送正常的触摸事件。一旦识别出手势,合成器将发送一个“取消”事件来通知您,合成器将接管。
<event name="cancel"></event>
当您收到此事件时,所有活动的触摸点都将被取消。
形状和方向
一些高端触摸硬件能够确定更多关于用户与其交互方式的信息。对于希望采用更高级交互或触摸反馈的合适硬件的用户和应用程序,提供了“形状”和“方向”事件。
<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>
“形状”事件定义了触摸屏幕的对象的椭圆近似,其中长轴和短轴以触摸表面的坐标空间中的单位表示。方向事件通过指定长轴和触摸表面的Y轴之间的角度(以度为单位)来旋转这个椭圆。
触摸是Wayland协议支持的输入设备中的最后一个。有了这些知识,让我们更新我们的示例代码。
1强调“可能”——不要基于重复使用触摸点ID做出任何假设。
2这意味着可以比较单独的时间戳来获得事件之间的时间,但它们与实际时钟时间不可比较。
9.5 扩展示例代码
在前面的章节中,我们构建了一个可以在显示器上显示其表面的简单客户端。让我们扩展此代码,以构建一个可以接收输入事件的客户端。为了简单起见,我们只是将输入事件记录到stderr。
这需要比我们迄今为止所处理的代码多得多,所以系好安全带。我们要做的第一件事是设置座位。
设置Seats
首先我们需要一个座位的引用。我们将将其添加到我们的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是一个由这个座位支持的设备类型的位掩码——与一个能力进行位与(&)运算,如果支持的话会产生一个非零值。然后,如果我们有一个指针并且还没有配置它,我们走第一条分支,使用wl_seat_get_pointer来获取一个指针引用并将其存储在我们的状态中。如果座位不支持指针,但我们已经有了一个配置好的指针,我们使用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;
+}
“进入”和“离开”事件相当简单,为后续的实现做好了铺垫。我们更新事件掩码以包括适当的事件,然后用提供的数据填充它。 “移动”和“按钮”事件相当相似:
+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”布尔值的使用:我们可能会接收到一个只更新一个轴但不更新另一个轴的指针帧,所以我们使用这个“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,
+};
现在,我们来看看改变的关键部分。让我们从键盘布局开始:
+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——我们在这里使用它来确保键盘布局格式是我们期望的。然后,我们使用mmap将组合器发送给我们的文件描述符映射到一个我们可以传递给xkb_keymap_new_from_string的char *指针。之后别忘了解除映射和关闭该fd——然后我们设置XKB状态。请注意,我们也取消引用了在此函数先前调用中设置的任何先前XKB键盘或状态,以防组合器在运行时更改键盘布局。1
+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);
+ }
+}
当键盘“进入”我们的表面时,我们就获得了键盘焦点。组合器转发当时已经按下的键的列表,我们在这里只是枚举它们并记录它们的键符号名和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状态。至于处理键重复——这有很多特定于你的应用程序的约束。你想重复文本输入吗?你想重复键盘快捷键吗?这些的时序如何与你的事件循环交互?这些问题的答案留给你来决定。
如果你再次编译这个,你应该能够开始在窗口中输入,并看到你的输入打印到日志中。好极了!
为触摸设备做准备
最后,我们将添加对触摸设备的支持。就像指针一样,存在一个“帧”事件用于触摸设备。然而,它们更复杂,因为在一个帧内可能更新多个触摸点。我们将添加一些更多的结构和枚举来代表累积的状态:
+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个触摸点,并假设大多数用户只会使用这么多手指。对于更大的多用户触摸屏,你可能需要更高的限制。此外,一些触摸硬件支持的触摸点数比10还要少——8也很常见,而支持更少的硬件在旧设备中也很常见。
我们将把这个结构添加到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];
+}
该函数的基本目的是从我们添加到触摸事件结构中的数组中选择一个触摸点,基于我们接收事件的触摸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;
+}
触摸取消事件有所不同,因为它一次“取消”所有活动的触摸点。我们只需将此存储在触摸事件的高级事件掩码中。
+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 这种情况实际上确实会发生!