Wayland: ペンタブレット入力

ペンタブレット
ペンタブレットからの入力を行うため、拡張プロトコルの zwp_tablet_manager_v2 (stable) を使用します。

wayland-scanner で、stable/tablet/tablet-v2.xml ファイルから、「tablet-v2-client-protocol.h」「tablet-v2-protocol.c」を生成してください。

$ wayland-scanner client-header tablet-v2.xml tablet-v2-client-protocol.h
$ wayland-scanner private-code tablet-v2.xml tablet-v2-protocol.c

$ cc -o test 17-tablet.c client.c imagebuf.c \
 xdg-shell-protocol.c tablet-v2-protocol.c -lwayland-client -lrt

マウスの中ボタン押しで終了します。

<17-tablet.c>
#include <stdio.h>
#include <string.h>
#include <linux/input-event-codes.h>

#include <wayland-client.h>
#include "xdg-shell-client-protocol.h"
#include "tablet-v2-client-protocol.h"

#include "client.h"
#include "imagebuf.h"


//------------------

typedef struct
{
    client b;

    struct zwp_tablet_manager_v2 *manager;
    struct zwp_tablet_seat_v2 *tablet_seat;
}client_ex;

//------------------


//=====================
// zwp_tablet_tool_v2
//=====================


static void _tablettool_type(void *data,
    struct zwp_tablet_tool_v2 *zwp_tablet_tool_v2, uint32_t tool_type)
{
    printf("zwp_tablet_tool_v2 # type | tool_type:0x%X\n", tool_type);
}

static void _tablettool_hardware_serial(void *data,
    struct zwp_tablet_tool_v2 *zwp_tablet_tool_v2,
    uint32_t hardware_serial_hi, uint32_t hardware_serial_lo)
{
    printf("zwp_tablet_tool_v2 # hardware_serial | 0x%X%08X\n",
        hardware_serial_hi, hardware_serial_lo);
}

static void _tablettool_hardware_id_wacom(void *data,
    struct zwp_tablet_tool_v2 *zwp_tablet_tool_v2,
    uint32_t hardware_id_hi, uint32_t hardware_id_lo)
{
    printf("zwp_tablet_tool_v2 # hardware_id_wacom | 0x%X%08X\n",
        hardware_id_hi, hardware_id_lo);
}

static void _tablettool_capability(void *data,
    struct zwp_tablet_tool_v2 *zwp_tablet_tool_v2, uint32_t capability)
{
    printf("zwp_tablet_tool_v2 # capability | %u\n", capability);
}

static void _tablettool_done(void *data, struct zwp_tablet_tool_v2 *zwp_tablet_tool_v2)
{
    printf("zwp_tablet_tool_v2 # done\n");
}

static void _tablettool_removed(void *data, struct zwp_tablet_tool_v2 *zwp_tablet_tool_v2)
{
    printf("zwp_tablet_tool_v2 # removed\n");

    zwp_tablet_tool_v2_destroy(zwp_tablet_tool_v2);
}

static void _tablettool_proximity_in(void *data,
    struct zwp_tablet_tool_v2 *zwp_tablet_tool_v2, uint32_t serial,
    struct zwp_tablet_v2 *tablet, struct wl_surface *surface)
{
    printf("zwp_tablet_tool_v2 # proximity_in | serial:%u\n", serial);
}

static void _tablettool_proximity_out(void *data, struct zwp_tablet_tool_v2 *zwp_tablet_tool_v2)
{
    printf("zwp_tablet_tool_v2 # proximity_out\n");
}

static void _tablettool_down(void *data,
    struct zwp_tablet_tool_v2 *zwp_tablet_tool_v2, uint32_t serial)
{
    printf("zwp_tablet_tool_v2 # down | serial:%u\n", serial);
}

static void _tablettool_up(void *data, struct zwp_tablet_tool_v2 *zwp_tablet_tool_v2)
{
    printf("zwp_tablet_tool_v2 # up\n");
}

static void _tablettool_motion(void *data,
    struct zwp_tablet_tool_v2 *zwp_tablet_tool_v2, wl_fixed_t x, wl_fixed_t y)
{
    printf("zwp_tablet_tool_v2 # motion | (%.2f, %.2f)\n",
        x / 256.0, y / 256.0);
}

static void _tablettool_pressure(void *data,
    struct zwp_tablet_tool_v2 *zwp_tablet_tool_v2, uint32_t pressure)
{
    printf("zwp_tablet_tool_v2 # pressure | %u (%.4f)\n",
        pressure, pressure / 65535.0);
}

static void _tablettool_distance(void *data,
    struct zwp_tablet_tool_v2 *zwp_tablet_tool_v2, uint32_t distance)
{
    printf("zwp_tablet_tool_v2 # distance | %u (%.4f)\n",
        distance, distance / 65535.0);
}

static void _tablettool_tilt(void *data,
    struct zwp_tablet_tool_v2 *zwp_tablet_tool_v2, wl_fixed_t tilt_x, wl_fixed_t tilt_y)
{
    printf("zwp_tablet_tool_v2 # tilt | x:%.2f, y:%.2f\n",
        tilt_x / 256.0, tilt_y / 256.0);
}

static void _tablettool_rotation(void *data,
    struct zwp_tablet_tool_v2 *zwp_tablet_tool_v2, wl_fixed_t degrees)
{
    printf("zwp_tablet_tool_v2 # rotation | %.2f\n", degrees / 256.0);
}

static void _tablettool_slider(void *data,
    struct zwp_tablet_tool_v2 *zwp_tablet_tool_v2, int32_t position)
{
    printf("zwp_tablet_tool_v2 # slider | %d\n", position);
}

static void _tablettool_wheel(void *data,
    struct zwp_tablet_tool_v2 *zwp_tablet_tool_v2, wl_fixed_t degrees, int32_t clicks)
{
    printf("zwp_tablet_tool_v2 # wheel | degrees:%.2f, clicks:%d\n",
        degrees / 256.0, clicks);
}

static void _tablettool_button(void *data,
    struct zwp_tablet_tool_v2 *zwp_tablet_tool_v2,
    uint32_t serial, uint32_t button, uint32_t state)
{
    printf("zwp_tablet_tool_v2 # button | serial:%u, button:0x%X, state:%u\n",
        serial, button, state);
}

static void _tablettool_frame(void *data,
    struct zwp_tablet_tool_v2 *zwp_tablet_tool_v2, uint32_t time)
{
    printf("- zwp_tablet_tool_v2 # frame | time:%u\n", time);
}


static const struct zwp_tablet_tool_v2_listener g_tablet_tool_listener = {
    _tablettool_type, _tablettool_hardware_serial, _tablettool_hardware_id_wacom,
    _tablettool_capability, _tablettool_done, _tablettool_removed,
    _tablettool_proximity_in, _tablettool_proximity_out,
    _tablettool_down, _tablettool_up, _tablettool_motion,
    _tablettool_pressure, _tablettool_distance, _tablettool_tilt,
    _tablettool_rotation, _tablettool_slider, _tablettool_wheel,
    _tablettool_button, _tablettool_frame
};


//=====================
// zwp_tablet_v2
//=====================


static void _tablet_name(void *data, struct zwp_tablet_v2 *zwp_tablet_v2, const char *name)
{
    printf("zwp_tablet_v2 # name | name:\"%s\"\n", name);
}

static void _tablet_id(void *data, struct zwp_tablet_v2 *zwp_tablet_v2, uint32_t vid, uint32_t pid)
{
    printf("zwp_tablet_v2 # id | vid:0x%X, pid:0x%X\n", vid, pid);
}

static void _tablet_path(void *data, struct zwp_tablet_v2 *zwp_tablet_v2, const char *path)
{
    printf("zwp_tablet_v2 # path | path:\"%s\"\n", path);
}

static void _tablet_done(void *data, struct zwp_tablet_v2 *zwp_tablet_v2)
{
    printf("zwp_tablet_v2 # done\n");
}

static void _tablet_removed(void *data, struct zwp_tablet_v2 *zwp_tablet_v2)
{
    printf("zwp_tablet_v2 # removed\n");

    zwp_tablet_v2_destroy(zwp_tablet_v2);
}

static const struct zwp_tablet_v2_listener g_tablet_listener = {
    _tablet_name, _tablet_id, _tablet_path, _tablet_done, _tablet_removed
};


//=====================
// zwp_tablet_seat_v2
//=====================


static void _tabletseat_tablet_added(void *data, struct zwp_tablet_seat_v2 *zwp_tablet_seat_v2,
    struct zwp_tablet_v2 *id)
{
    printf("zwp_tablet_seat_v2 # tablet_added\n");

    zwp_tablet_v2_add_listener(id, &g_tablet_listener, data);
}

static void _tabletseat_tool_added(void *data, struct zwp_tablet_seat_v2 *zwp_tablet_seat_v2,
    struct zwp_tablet_tool_v2 *id)
{
    printf("zwp_tablet_seat_v2 # tool_added\n");

    zwp_tablet_tool_v2_add_listener(id, &g_tablet_tool_listener, data);
}

static void _tabletseat_pad_added(void *data, struct zwp_tablet_seat_v2 *zwp_tablet_seat_v2,
    struct zwp_tablet_pad_v2 *id)
{
    printf("zwp_tablet_seat_v2 # pad_added\n");
}

static const struct zwp_tablet_seat_v2_listener g_tablet_seat_listener = {
    _tabletseat_tablet_added, _tabletseat_tool_added, _tabletseat_pad_added
};


//=====================
// wl_pointer
//=====================


/* enter */

static void _pointer_enter(void *data, struct wl_pointer *pointer,
    uint32_t serial, struct wl_surface *surface, wl_fixed_t x, wl_fixed_t y)
{
    printf("wl_pointer # enter\n");
}

static void _pointer_leave(void *data, struct wl_pointer *pointer,
    uint32_t serial, struct wl_surface *surface)
{
    printf("wl_pointer # leave\n");
}

static void _pointer_motion(void *data, struct wl_pointer *pointer,
    uint32_t time, wl_fixed_t x, wl_fixed_t y)
{
    printf("wl_pointer # motion | (%d, %d)\n", x >> 8, y >> 8);
}

/* ボタン */

static void _pointer_button(void *data, struct wl_pointer *pointer,
    uint32_t serial, uint32_t time, uint32_t button, uint32_t state)
{
    printf("wl_pointer # button | %s, button:0x%X\n",
        (state == WL_POINTER_BUTTON_STATE_PRESSED)? "press":"release",
        button);

    if(state != WL_POINTER_BUTTON_STATE_PRESSED)
        return;

    switch(button)
    {
        //中ボタンで終了
        case BTN_MIDDLE:
            CLIENT(data)->finish_loop = 1;
            break;
    }
}

static void _pointer_axis(void *data, struct wl_pointer *pointer,
    uint32_t time, uint32_t axis, wl_fixed_t value)
{
}

static void _pointer_frame(void *data, struct wl_pointer *pointer)
{
}

static void _pointer_axis_source(void *data, struct wl_pointer *pointer, uint32_t axis_source)
{
}

static void _pointer_axis_stop(void *data, struct wl_pointer *pointer, uint32_t time, uint32_t axis)
{
}

static void _pointer_axis_discrete(void *data, struct wl_pointer *pointer, uint32_t axis, int32_t discrete)
{
}

//ver 5
static const struct wl_pointer_listener g_pointer_listener = {
    _pointer_enter, _pointer_leave, _pointer_motion, _pointer_button,
    _pointer_axis, _pointer_frame, _pointer_axis_source,
    _pointer_axis_stop, _pointer_axis_discrete
};


//===============


static void _registry_global(
    client *cl,struct wl_registry *reg,uint32_t id,const char *name,uint32_t ver)
{
    client_ex *p = (client_ex *)cl;

    if(strcmp(name, "zwp_tablet_manager_v2") == 0)
        p->manager = wl_registry_bind(reg, id, &zwp_tablet_manager_v2_interface, 1);
}

/* client_ex 解放 */

static void _clientex_destroy(client *cl)
{
    client_ex *p = (client_ex *)cl;

    if(p->tablet_seat)
        zwp_tablet_seat_v2_destroy(p->tablet_seat);

    if(p->manager)
        zwp_tablet_manager_v2_destroy(p->manager);
}

/* main */

int main(void)
{
    client_ex *p;
    Toplevel *win;

    p = (client_ex *)client_new(sizeof(client_ex));

    p->b.destroy = _clientex_destroy;
    p->b.init_flags = CLIENT_INIT_FLAGS_SEAT | CLIENT_INIT_FLAGS_POINTER;
    p->b.pointer_listener = &g_pointer_listener;
    p->b.registry_global = _registry_global;

    client_init(CLIENT(p));

    if(!p->manager)
    {
        client_destroy(CLIENT(p));
        printf("[!] not found 'zwp_tablet_manager_v2'\n");
        return 1;
    }

    //zwp_tablet_seat_v2

    p->tablet_seat = zwp_tablet_manager_v2_get_tablet_seat(p->manager, p->b.seat);

    zwp_tablet_seat_v2_add_listener(p->tablet_seat,
        &g_tablet_seat_listener, p);

    //メインウィンドウ

    win = toplevel_create(CLIENT(p), 256, 256, NULL);

    imagebuf_fill(win->sf.img, 0xffff0000);

    //イベントループ

    client_loop_simple(CLIENT(p));

    //解放

    toplevel_destroy(win);

    client_destroy(CLIENT(p));

    return 0;
}
解説
大まかな流れだけ説明します。
詳細は zwp_tablet_manager_v2 をご覧ください。

  • zwp_tablet_manager_v2 をバインド。
    2025年2月時点での最大バージョンは 1。
  • zwp_tablet_seat_v2 を作成。
    ※wl_seat が必要。
  • zwp_tablet_seat_v2 にハンドラを設定。
  • zwp_tablet_seat_v2 の各イベント内で、渡されたオブジェクトに対してハンドラを設定。
    tablet_added イベント: zwp_tablet_v2
    tool_added イベント: zwp_tablet_tool_v2
    pad_added イベント: zwp_tablet_pad_v2
  • 各オブジェクトのイベントを処理。

※今回は、zwp_tablet_pad_v2 の処理は省略しています。
zwp_tablet_pad_v2 では、タブレットデバイス上に物理的に存在している、ボタン・リングなどの操作を行いますが、そういったものが付いていないデバイスでは使用しません。
インターフェイス名について
バインド時のインターフェイス名は zwp_tablet_manager_v2 になっていますが、実際のバインド時に指定するバージョンは 1 です (2025年2月現在)。
これは、stable への移行時に、unstable の ver 2 の名前がそのまま引き継がれたためです。

サーバーとクライアントは、stable 版に対応するためにコードを書き換える必要がないのが、メリットになっていますが、名前は少々ややこしいことになっています。
zwp_tablet_v2
zwp_tablet_v2 は、タブレットのデバイスの情報を取得するためのインターフェイスです。
各イベントで、データが送られてきます。

zwp_tablet_seat_v2 # tablet_added
zwp_tablet_v2 # name | name:"Wacom Graphire3 Pen"
zwp_tablet_v2 # path | path:"/dev/input/event3"
zwp_tablet_v2 # id | vid:0x56A, pid:0x13
zwp_tablet_v2 # done
zwp_tablet_tool_v2
zwp_tablet_tool_v2 は、タブレットの各ツールの情報を取得するためのインターフェイスです。
座標などを取得できます。

ツールは、スタイラスペン (通常のペン先の部分) や、ペンの消しゴム部分など、使用用途によって分かれています。

ただし、アプリケーションを起動して、デバイスを認識した段階では、すべてのツールを列挙することはできません。
実際にタブレット上で各ツールを操作した時に、初めて認識されます。

イベントでは、ツールの初期情報や、座標・筆圧などを取得できます。
ペンを操作してウィンドウ内に入った
ペンを動かして、カーソルがクライアントのウィンドウ内に入った時のイベントは、以下のようになります。

zwp_tablet_seat_v2 # tool_added
zwp_tablet_tool_v2 # type | tool_type:0x140
zwp_tablet_tool_v2 # hardware_serial | 0x000000000
zwp_tablet_tool_v2 # hardware_id_wacom | 0x000000002
zwp_tablet_tool_v2 # capability | 2
zwp_tablet_tool_v2 # done

zwp_tablet_tool_v2 # proximity_in | serial:705
- zwp_tablet_tool_v2 # frame | time:0

zwp_tablet_tool_v2 # motion | (179.66, 0.73)
zwp_tablet_tool_v2 # pressure | 0 (0.0000)
- zwp_tablet_tool_v2 # frame | time:4810124

プログラムの起動後、初めてペンがクライアントのウィンドウ内に入った時に、zwp_tablet_seat_v2:tool_added イベントが来ます。

※同じツールを使っている状態で、一度ウィンドウから離れて再び入った時は、このイベントは来ませんが、消しゴムペンなど、別のツールに切り替わった時は再び来ます。

ここで、イベントで渡された zwp_tablet_tool_v2 にハンドラを設定すると、その直後に zwp_tablet_tool_v2 の各イベントが発生し、現在操作されているツールの情報が来ます。

上記の場合、ツールのタイプは「ZWP_TABLET_TOOL_V2_TYPE_PEN (通常のペン)」で、機能は ZWP_TABLET_TOOL_V2_CAPABILITY_PRESSURE (2) のみがあり、「筆圧」の情報を持っています。

proximity_in イベント
zwp_tablet_tool_v2:proximity_in イベントは、ツールが特定のサーフェスに焦点を合わせている状態の時、つまり、カーソルがサーフェス内にあって、ペンとタブレット面との距離が、動きを捕捉できる距離にある時に来ます。

GNOME の場合、タブレットペンを操作した時は、wl_pointer の enter イベントは来ないので、このイベントを enter の代わりとして扱う必要があります。

また、このプロトコルを使って処理をしない場合、代わりに wl_pointer でイベントが送られてくるということはないので、カーソルがウィンドウに入ると非表示になってしまったり、座標やボタンが全く取得できないことになるので、注意してください。
GNOME では、ペンがタブレット面から離れると、カーソルが非表示になります。
(2025年3月現在)
ペンのボタンを押した
zwp_tablet_tool_v2 # button | serial:891, button:0x14B, state:1
- zwp_tablet_tool_v2 # frame | time:4931133

ペンに付いているボタンを押した時は、zwp_tablet_tool_v2:button イベントが来ます。

※2025年3月現在、GNOME では、デスクトップ設定でタブレットのボタンの割り当てを行わないと、すべて down, up イベントでの扱いになります。

button の値は、"linux/input-event-codes.h" で定義されている値です。

0x14b は「BTN_STYLUS」、0x14c は「BTN_STYLUS2」です。
(タブレットペンに付いている最初のボタンと、2番目のボタンという意味)

GNOME では、ペンのボタンを中ボタンに割り当てると BTN_STYLE に、右ボタンに割り当てると BTN_STYLE2 になります。
右ボタンと言いつつ、Wayland プログラム内では BTN_RIGHT にならないので、注意してください。

X11 では、デフォルトで、ペンの下のボタンが中ボタン、上のボタンが右ボタンとなるので、X11 プログラムとの互換性を考えると、BTN_STYLE を中ボタン、BTN_STYLE2 を右ボタンとして扱うといいでしょう。
ツールを接地させた
zwp_tablet_tool_v2 # down | serial:786
- zwp_tablet_tool_v2 # frame | time:4849516

zwp_tablet_tool_v2 # motion | (45.00, 33.45)
zwp_tablet_tool_v2 # pressure | 338 (0.0052)
- zwp_tablet_tool_v2 # frame | time:4849524

zwp_tablet_tool_v2 # up
- zwp_tablet_tool_v2 # frame | time:4849796

ペンをタブレットの接地面に着けた時、zwp_tablet_tool_v2:down イベントが来ます。

グラブはサーバーの方で自動的に行われるので、ペンを着けている状態でウィンドウ外に出たとしても、ツールを接地面から離すか、ツールがタブレットから認識できなくなるまではイベントが来ます。
カーソル形状について
タブレットツールを操作している時のカーソル形状は、各ツールごとに変更する必要があります。

カーソル形状の変更は、zwp_tablet_tool_v2_set_cursor() を使ってください。
serial 引数には、proximity_in イベント時の値を指定しないと、正しくカーソルを変更できない場合があります。

(GNOME の場合) wl_pointer でセットされているカーソル形状は適用されないので、proximity_in イベントが来た時は、常にカーソルを変更する必要があります。
※ wl_pointer のカーソルとして使用している wl_surface は使わず、別の wl_surface を使うこと。