Wayland - カーソル形状

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

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

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

08_cursor.c
/******************************
 * カーソル形状の変更
 *
 * 中ボタン押しで終了。
 ******************************/

#include <stdio.h>
#include <string.h>
#include <linux/input.h>

#include <wayland-client.h>
#include <wayland-cursor.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;            //現在のカーソル番号
}ClientEx;

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

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

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



//----------------------
// カーソル変更
//----------------------


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

static void _set_cursor_image(ClientEx *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)
{
    ClientEx *p = (ClientEx *)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(ClientEx *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)
{
    ClientEx *p = (ClientEx *)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)
{

}

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


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


/* ClientEx 解放 */

static void _clientex_destroy(Client *cl)
{
    ClientEx *p = (ClientEx *)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(ClientEx *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");
}


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


int main(void)
{
    ClientEx *p;
    Window *win;

    p = (ClientEx *)Client_new(sizeof(ClientEx));

    p->b.destroy = _clientex_destroy;
    p->b.init_flags = INIT_FLAGS_SEAT | INIT_FLAGS_POINTER;
    p->b.pointer_listener = &g_pointer_listener;

    Client_init(CLIENT(p));

    //カーソル初期化

    _cursor_init(p);
    
    //ウィンドウ

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

    ImageBuf_fill(win->img, 0xffff0000);
    Window_update(win);

    //

    Client_loop_simple(CLIENT(p));

    //解放

    Window_destroy(win);

    Client_destroy(CLIENT(p));

    return 0;
}

ウィンドウ (wl_surface) の enter イベントが来るごとに、マウスカーソルを "wait" と "text" の2つに、交互に切り替えます。

カーソルがアニメーション付きの場合は、アニメ処理も行っています。

ウィンドウ内で中ボタンを押すと終了します。
カーソルの読み込み
はじめに
カーソル画像を扱うために、「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 が返ります。

通常の矢印カーソルの場合は、「"default", "left_ptr"」などの名前となります。

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

※ 戻り値の wl_cursor は、クライアント側で解放処理を行う必要はありません。
カーソル形状の変更
wl_cursor を元にカーソル形状を変更する場合、まず、カーソル用のイメージとして、wl_surface が必要になります。

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

次に、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 を取得します。

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 は、wl_pointer の enter イベント時に渡された serial 値をそのまま指定します。

hotspot_x, hotspot_y は、ホットスポット位置 (画像内でどの位置を原点とするか) です。
wl_cursor_image の hotspot_x, hotspot_y をそのまま指定します。
アニメーション処理
カーソルのアニメーション処理は、クライアントが自分で行う必要があります。

ここでは、以前にも使った wl_surface_frame() を使用して、コールバックで処理します。

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

まずは、最初のコールバック時に、time 引数から現在の時間を記録し、以降は、「現在時間 - 開始時間」の経過時間から、表示するフレームを取得して、カーソル形状を変更しています。

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

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

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() を使った場合、カーソルがウィンドウ内に入っている間だけコールバックが通知されます。
カーソルがウィンドウ外にある場合は、送られてきません。