Wayland - カーソル形状

カーソル形状の変更
enter イベントが来るごとに、マウスカーソルを "wait" と "text" の2つに交互に切り替えます。
カーソルがアニメーション付きの場合は、アニメ処理も行っています。
ウィンドウ内で中ボタンを押すと終了します。

コンパイル
> common1.h
> common1.c

カーソル画像を読み込むのに wayland-cursor のライブラリが必要なので、リンクします。

$ cc -o test 08_cursor.c common1.c -lwayland-client -lwayland-cursor -lrt

ソースコード
<08_cursor.c>
#include <stdio.h>
#include <string.h>
#include <linux/input.h>
#include <wayland-cursor.h>

#include "common1.h"

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

typedef struct
{
    struct wl_surface *surface;        //カーソル用サーフェス
    struct wl_callback *callback;    //アニメコールバック
    struct wl_cursor_theme *cursor_theme; //カーソルテーマ
    struct wl_cursor *cursor[2];    //各カーソルデータ

    uint32_t time_start;    //アニメ開始時間
    int cursor_no;            //現在のカーソル番号
}cursor_data;

typedef struct
{
    wayland base;
    cursor_data cur;
    uint32_t serial_enter;    //ポインタ enter 時の serial
    int loop;
}wayland_ex;

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

static void _surface_cursor_frame_callback(
    void *data,struct wl_callback *callback,uint32_t time);

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


/* カーソル画像変更 */

static void _set_cursor_image(wayland_ex *p,int index)
{
    struct wl_buffer *buffer;
    struct wl_cursor_image *img;

    img = p->cur.cursor[p->cur.cursor_no]->images[index];

    buffer = wl_cursor_image_get_buffer(img);
    if(!buffer) return;

    wl_surface_attach(p->cur.surface, buffer, 0, 0);
    wl_surface_damage(p->cur.surface, 0, 0, img->width, img->height);
    wl_surface_commit(p->cur.surface);

    wl_pointer_set_cursor(p->base.pointer, p->serial_enter,
        p->cur.surface,
        img->hotspot_x, img->hotspot_y);
}

/* アニメーションコールバック */

static const struct wl_callback_listener g_cursor_frame_listener = {
    _surface_cursor_frame_callback
};

void _surface_cursor_frame_callback(
    void *data,struct wl_callback *callback,uint32_t time)
{
    wayland_ex *p = (wayland_ex *)data;
    int index,commit = 1;

    wl_callback_destroy(callback);

    //コールバック再セット

    p->cur.callback = wl_surface_frame(p->cur.surface);
    wl_callback_add_listener(p->cur.callback, &g_cursor_frame_listener, p);

    //

    if(p->cur.time_start == 0)
        p->cur.time_start = time;
    else
    {
        index = wl_cursor_frame(p->cur.cursor[p->cur.cursor_no], time - p->cur.time_start);

        _set_cursor_image(p, index);

        commit = 0;
    }

    //コールバック適用
    //(カーソル画像が変わった時はすでに実行しているので呼ばない)

    if(commit)
        wl_surface_commit(p->cur.surface);
}

/* カーソル形状変更 */

static void _change_cursor(wayland_ex *p)
{
    struct wl_cursor *cursor;

    p->cur.cursor_no ^= 1;

    cursor = p->cur.cursor[p->cur.cursor_no];

    if(cursor)
    {
        if(p->cur.callback)
        {
            wl_callback_destroy(p->cur.callback);
            p->cur.callback = NULL;
        }

        //

        p->cur.time_start = 0;

        _set_cursor_image(p, 0);

        //アニメ開始

        if(cursor->image_count > 1)
        {
            p->cur.callback = wl_surface_frame(p->cur.surface);
            wl_callback_add_listener(p->cur.callback, &g_cursor_frame_listener, p);
            wl_surface_commit(p->cur.surface);

            printf("start anime: %d images\n", cursor->image_count);
        }
    }
}

//-----------  wl_pointer

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)
{
    wayland_ex *p = (wayland_ex *)data;

    p->serial_enter = serial;

    //カーソル変更

    _change_cursor(p);
}

static void _pointer_leave(void *data, struct wl_pointer *pointer,
    uint32_t serial, struct wl_surface *surface)
{
}

static void _pointer_motion(void *data, struct wl_pointer *pointer,
    uint32_t time, wl_fixed_t x, wl_fixed_t y)
{
}

static void _pointer_button(void *data, struct wl_pointer *pointer,
    uint32_t serial, uint32_t time, uint32_t button, uint32_t state)
{
    //中ボタンで終了

    if(button == BTN_MIDDLE)
        ((wayland_ex *)data)->loop = 0;
}

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)
{
}

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 _cursor_data_destroy(cursor_data *p)
{
    wl_cursor_theme_destroy(p->cursor_theme);

    if(p->callback)
        wl_callback_destroy(p->callback);

    wl_surface_destroy(p->surface);
}

static void _cursor_init(wayland_ex *p)
{
    p->cur.surface = wl_compositor_create_surface(p->base.compositor);

    p->cur.cursor_theme = wl_cursor_theme_load(NULL, 32, p->base.shm);

    p->cur.cursor[0] = wl_cursor_theme_get_cursor(p->cur.cursor_theme, "text");
    p->cur.cursor[1] = wl_cursor_theme_get_cursor(p->cur.cursor_theme, "wait");

    if(!p->cur.cursor[0] || !p->cur.cursor[1])
        printf("can not find cursor\n");
}

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

int main(void)
{
    wayland_ex *wl;
    window *win;

    wl = (wayland_ex *)wayland_new(sizeof(wayland_ex));

    wl->loop = 1;

    wl->base.init_flags = INIT_FLAGS_SEAT | INIT_FLAGS_POINTER;
    wl->base.pointer_listener = &g_pointer_listener;

    wayland_init(WAYLAND_PTR(wl));

    //カーソル

    _cursor_init(wl);

    //ウィンドウ

    win = window_create(WAYLAND_PTR(wl), 256, 256);

    imagebuf_fill(win->img, 0xff0000);
    window_update(win);

    //

    while(wl_display_dispatch(wl->base.display) != -1 && wl->loop);

    //解放

    window_destroy(win);

    _cursor_data_destroy(&wl->cur);

    wayland_destroy(WAYLAND_PTR(wl));

    return 0;
}
解説
カーソルテーマから画像を読み込むには、wayland-cursor.h のインクルードと、libwayland-cursor のリンクが必要です。
カーソルテーマの読み込み
まずは、カーソルテーマを読み込みます。
/usr/share/icons~/.icons にインストールされているテーマから、カーソル画像を読み込んで使うことができます。

# テーマ読み込み

struct wl_cursor_theme *wl_cursor_theme_load(const char *name, int size, struct wl_shm *shm);

# テーマ破棄

void wl_cursor_theme_destroy(struct wl_cursor_theme *theme);

引数 name には、テーマ名を指定します。NULL でデフォルトのテーマになります。
名前は、インストールされている各テーマの先頭ディレクトリ名を指定します ("Adwaita" など)。
テーマの index.theme 内で定義されている正式なテーマの名前で指定しても読み込まれません。

size は、カーソル画像のおおよその px サイズです。とりあえず 32 にしておけば良いです。

shm は、バインドした wl_shm のポインタを指定します。
共有メモリを使って読み込むので、wl_shm が必要になります。
各カーソルの読み込み
テーマから、使用する各カーソルを読み込みます。

struct wl_cursor *wl_cursor_theme_get_cursor(struct wl_cursor_theme *theme,const char *name);

name はカーソルの名前です。
テーマの cursors ディレクトリ内にあるファイル名を指定します。
読み込めなかった場合は NULL が返ります。

通常の矢印カーソルなら、"default", "left_ptr" などの名前となります。

名前の規則は複数あるので、通常はリンクファイルを使ってどの名前が使われてもいいようになっていますが、テーマによっては対応していない名前があるかもしれないので、きちんと対応するならば、一つのカーソルに対して複数名で読み込みを試すべきです。

wl_cursor は、クライアント側で解放する必要はありません。
カーソル形状の変更
まずは、カーソル用イメージとして扱うために、それ専用の wl_surface が必要になります。
初期化時に、wl_compositor_create_surface() を使って、カーソル画像用の wl_surface を作成しています。

wl_cursor 構造体
読み込んだ各カーソルの wl_cursor 構造体を見てみます。

struct wl_cursor {
    unsigned int image_count;  //イメージ数
    struct wl_cursor_image **images;  //イメージの配列
    char *name;
};

struct wl_cursor_image {
    uint32_t width;     //実際の幅
    uint32_t height;    //実際の高さ
    uint32_t hotspot_x; //hot spot x
    uint32_t hotspot_y; //hot spot y
    uint32_t delay;     //表示時間 (ms)
};

image_count には、一つのカーソルの画像数が入っています。
アニメーションがない場合は1、ある場合はアニメーションの画像数となります。

wl_cursor_image は、各画像の情報です。
幅、高さ、ホットスポット位置、アニメでその画像を表示する時間が入っています。

wl_buffer 取得
wl_cursor_image から、wl_buffer を取得します。

struct wl_buffer *wl_cursor_image_get_buffer(struct wl_cursor_image *image);

※ 取得した wl_buffer は、クライアント側で解放してはいけません。

wl_surface に wl_buffer を適用
取得した wl_buffer を、カーソル画像用の wl_surface に適用します。

wl_surface_attach(p->cur.surface, buffer, 0, 0);
wl_surface_damage(p->cur.surface, 0, 0, img->width, img->height);
wl_surface_commit(p->cur.surface);

カーソル形状を変更
カーソル画像が用意できたら、その wl_surface をカーソル形状としてセットします。

void wl_pointer_set_cursor(struct wl_pointer *wl_pointer,
    uint32_t serial, struct wl_surface *surface, int32_t hotspot_x, int32_t hotspot_y);

serial は、ポインタの enter イベント時に渡された serial 値を指定します。
hotspot_x, y は、ホットスポット位置 (画像内でどの位置を原点とするか) です。
wl_cursor_image の hotspot_x, y をそのまま指定します。
アニメーション
カーソルのアニメーション処理は、クライアントが自分で行わないといけません。
wl_surface_frame() を使って、コールバックで処理します。

ちなみに、wl_surface_frame() 後は、wl_surface_commit() を実行しないと、コールバックが適用されません。
忘れがちなので、気をつけておいてください。

まずは、最初のコールバック時に現在の時間を記録し、以降は、(現在時間 - 開始時間) から、表示するフレームを取得して、カーソル形状を変更しています。
GNOME の場合は、コールバック内で常に描画更新しないと正しく動作しないので、前回と同じフレームでも毎回描画しています。

経過時間から表示するフレームを取得する場合は、以下の関数を使います。

# フレームインデックスのみ取得

int wl_cursor_frame(struct wl_cursor *cursor, uint32_t time);

# フレームインデックスと残りの表示時間を取得

int wl_cursor_frame_and_duration(struct wl_cursor *_cursor, uint32_t time,
    uint32_t *duration);

wl_cursor_image の delay 値を参照すれば自力で計算することもできますが、一応こういった関数が用意されています。

time は、アニメーション開始からの総経過時間 (ms) です。
アニメーション全体の時間を超えている場合も、正しく処理されます。

戻り値のフレームインデックスは、[0 〜 image_count - 1] の範囲の値です。
そのまま wl_cursor::images のインデックス値となります。
アニメーションの注意点
通常はカーソルのアニメーションサークルは速いので、wl_surface_frame() で処理しても問題ありませんが、次のフレームまでの時間が極端に長い場合、その間に何回も同じフレームを描画することになります。

そうすると無駄に CPU を消費することになるため、frame コールバックだけを使うのは賢明ではありません。
次のフレームまでの時間が長い場合は、タイマーを使うなど、CPU を軽くする方法に切り替えるべきです。

ちなみに、カーソル画像として使う wl_surfacewl_surface_frame() を使った場合、
カーソルがウィンドウ内に入っている間だけコールバックが通知されます。
カーソルがウィンドウ外の場合は、送られてきません。