[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 這種情況實際上確實會發生!