Wayland: カーソル形状

カーソル形状の変更
今回は、マウスカーソルの形状を変更してみます。

$ cc -o test 09-cursor.c client.c imagebuf.c xdg-shell-protocol.c -lwayland-client -lwayland-cursor -lrt

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

カーソルがウィンドウ内に入る度に (enter イベントが呼ばれる度に)、マウスカーソルを "wait" と "text" の2つに、交互に切り替えます。
カーソルがアニメーション付きの場合は、アニメーション処理も行っています。

ウィンドウ内で中ボタンを押すと終了します。

<09-cursor.c>
#include <stdio.h>
#include <linux/input-event-codes.h>

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

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


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

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

    uint32_t time_start,    //アニメ開始時間
        serial_enter;         //enter 時の serial
    int cursor_no;            //現在のカーソル番号
}client_ex;

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

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



//=======================
// カーソル変更
//=======================


/* カーソル画像変更
 *
 * index: 複数枚ある場合、画像のインデックス */

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

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

    //wl_buffer 取得

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

    //

    wl_surface_attach(p->surface_cursor, buffer, 0, 0);
    wl_surface_damage(p->surface_cursor, 0, 0, img->width, img->height);
    wl_surface_commit(p->surface_cursor);

    //ポインタのカーソル形状としてセット
    //[!] ここで、enter 時の serial が必要

    wl_pointer_set_cursor(p->b.pointer, p->serial_enter,
        p->surface_cursor,
        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)
{
    client_ex *p = (client_ex *)data;
    int index,commit = 1;

    wl_callback_destroy(callback);

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

    p->callback = wl_surface_frame(p->surface_cursor);

    wl_callback_add_listener(p->callback, &g_cursor_frame_listener, p);

    //

    if(p->time_start == 0)
        //最初のコールバックの場合、開始時間をセット
        p->time_start = time;
    else
    {
        //2回目以降の場合、経過時間によって必要な画像を取得し、セット
    
        index = wl_cursor_frame(p->cursor[p->cursor_no], time - p->time_start);

        _set_cursor_image(p, index);

        commit = 0;
    }

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

    if(commit)
        wl_surface_commit(p->surface_cursor);
}

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

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

    p->cursor_no ^= 1;

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

    if(cursor)
    {
        //wl_callback 破棄
    
        if(p->callback)
        {
            wl_callback_destroy(p->callback);
            p->callback = NULL;
        }
        
        //カーソル画像セット
        
        p->time_start = 0;

        _set_cursor_image(p, 0);

        //複数枚ある場合、アニメーション開始

        if(cursor->image_count > 1)
        {
            p->callback = wl_surface_frame(p->surface_cursor);

            wl_callback_add_listener(p->callback, &g_cursor_frame_listener, p);

            wl_surface_commit(p->surface_cursor);

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


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

    p->serial_enter = serial;

    //カーソル変更

    _change_cursor(p);

    printf("change cursor: %d\n", p->cursor_no);
}

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
        && state == WL_POINTER_BUTTON_STATE_PRESSED)
    {
        CLIENT(data)->finish_loop = 1;
    }
}

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
};


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


/* client_ex 解放 */

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

    wl_cursor_theme_destroy(p->cursor_theme);

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

    wl_surface_destroy(p->surface_cursor);
}

/* カーソル初期化 */

static void _cursor_init(client_ex *p)
{
    //wl_surface 作成

    p->surface_cursor = wl_compositor_create_surface(p->b.compositor);

    //カーソルテーマ読み込み

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

    //カーソル読み込み

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

    if(!p->cursor[0]) printf("not found cursor 'text'\n");
    if(!p->cursor[1]) printf("not found cursor 'wait'\n");
}

/* 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;

    client_init(CLIENT(p));

    //カーソル初期化

    _cursor_init(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;
}
カーソルの読み込み
はじめに
カーソル画像を扱うために、「wayland-cursor.h」のインクルードと、「libwayland-cursor」のリンクが必要になります。

wayland-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 で、デフォルトのテーマ ("default") になります。

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

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

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

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

name は、カーソルの名前です。
テーマの cursors ディレクトリ内にある、カーソル画像のファイル名を指定します。

読み込めなかった場合は、NULL が返ります。
※戻り値の wl_cursor は、クライアント側で解放処理を行う必要はありません。

通常の矢印カーソルの場合は、"default""left_ptr" といった名前になります。

カーソルのファイル名には、ある程度規則があります。

カーソル画像やテーマ自体は、X11 で使われてきたものをそのまま使用できるので、X11 のデスクトップ環境の規格をまとめている freedesktop.org には、基本的なカーソルの名前規則が掲載されています。
https://www.freedesktop.org/wiki/Specifications/cursor-spec/

なお、一つの形状に対して、複数の名前が存在する場合があります。
基本的にリンクファイルで対応している場合が多いですが、テーマによっては、一部の名前しか対応していない場合もあるので、カーソルを読み込む際は、一つのカーソル形状に対して、複数名での読み込みを試してみた方がいいでしょう。
カーソル形状の変更
実際に、wl_cursor を元にカーソル形状を変更する場合は、カーソル画像を画面上に表示するために、wl_surface が必要になります。

このプログラムの場合は、初期化時に、wl_compositor_create_surface() を使って、カーソル画像用の wl_surface を1つ作成しています。

アニメーション付きのカーソルの場合、wl_cursor には複数のイメージが含まれているため、そこから、画面上に表示したい wl_cursor_image を取得します。

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; // hotspot x
    uint32_t hotspot_y; // hotspot y
    uint32_t delay;     // 表示時間 (ms)
};

image_count は、画像の数です。
アニメーションの画像数となり、アニメーションがない場合は「1」となります。

images には、image_count 分の、wl_cursor_image のポインタが格納されています。

wl_cursor_image は、各画像の情報です。
「幅」「高さ」「ホットスポット位置」「アニメーションにおいて、その画像を表示する時間」が入っています。
wl_buffer 取得
画面上に表示したい wl_cursor_image を取得したら、そのポインタから、wl_buffer を取得します。

wl_buffer は、wl_surface にイメージとしてセットできるバッファです。

struct wl_buffer *wl_cursor_image_get_buffer(struct wl_cursor_image *image);

※ここで取得した wl_buffer は、クライアント側で解放してはいけません。
wl_surface に wl_buffer を適用
カーソル画像を画面上に表示できるようにするため、wl_surface に、取得した wl_buffer セットします。
また、wl_surface_damage() でカーソル画像の範囲を更新させます。

最後に wl_surface_commit() で、設定を適用します。

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 が用意できたら、それを、現在のカーソル形状としてセットします。
wl_pointer 単位で設定するので、ウィンドウごとにカーソル形状をセットするということはできません。

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 は、wl_pointer の enter イベントなどで渡された serial 値を、そのまま指定します。
surface は、カーソル画像の wl_surface です。

hotspot_x, hotspot_y は、ホットスポット位置 (画像内でどの位置を原点とするか) です。
wl_cursor_image の hotspot_x, hotspot_y の値を、そのまま指定します。
アニメーション処理
カーソルのアニメーション処理は、クライアントが自分で行う必要があります。
つまり、画像を切り替える時間になったら、新しい wl_cursor_image を元に、カーソル形状を変更する必要があります。

今回の場合は、wl_surface_frame() を使って、コールバックで処理しています。
wl_surface_frame
struct wl_callback *wl_surface_frame(struct wl_surface *wl_surface);

//wl_callback のハンドラ構造体

struct wl_callback_listener {
    void (*done)(void *data, struct wl_callback *wl_callback, uint32_t callback_data);
};

指定した wl_surface が再描画を行う時のタイミングで、wl_callback によるコールバックが実行されます。
※wl_surface_commit() を実行した時に適用されるので、注意してください。

  • 一度の設定で、コールバックは一回しか呼ばれません。
  • wl_surface が画面上に表示されていない状態では、コールバックは実行されません。
  • done イベントの callback_data は、ミリ秒単位の現在の時間になります (開始位置は未定義)。
  • 再描画のタイミングは、基本的にモニタのリフレッシュレートが影響します。
    60 Hz の場合は、1秒間に 60 回描画するということなので、大体 1000 (ms) / 60 = 16.666.. (ms) ごとに呼ばれることになります。
アニメーション処理
まずは、最初のコールバックが来た時に、引数で渡された時間を、開始時間として記録します。
(enter イベント時は time 引数がないので、開始時間が決められないため)

以降のコールバック時は、「現在時間 - 開始時間」で経過時間を算出し、それを元に、表示するフレーム画像を取得して、カーソル形状を変更します。

経過時間からフレーム取得
経過時間を元に、wl_cursor から、表示するフレーム画像を取得する場合、wl_cursor_image の各 delay 値を参照すれば、自力で取得することもできますが、以下の関数を使うこともできます。

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

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);

time は、アニメーション開始時点からの総経過時間 (ms) です。
アニメーション全体の時間を超えている場合は、繰り返しループするように処理されます。

戻り値のフレームインデックスは、「0 〜 image_count - 1」の値です。
そのまま、wl_cursor の images の配列インデックス値となります。
注意点
通常、カーソルのアニメーションにおいて、1つの画像ごとの表示時間は短いので、今回のように wl_surface_frame() で処理しても問題はありませんが、これはタイマーではないので、次のフレームまでの時間が極端に長い場合、その間に何度もコールバックが呼ばれることになります。

そうすると無駄に CPU を消費することになるので、基本的に、wl_surface_frame() だけを使ったアニメーション処理は推奨されません。
GUI のイベントループ内で、タイマーを実装するなどの処理を行うと良いでしょう。

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