扩展我们的示例代码

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

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