カーソル形状の変更
今回は、マウスカーソルの形状を変更してみます。
※カーソル画像を読み込むのに、libwayland-cursor のライブラリが必要なので、リンクします。
カーソルがウィンドウ内に入る度に (enter イベントが呼ばれる度に)、マウスカーソルを "wait" と "text" の2つに、交互に切り替えます。
カーソルがアニメーション付きの場合は、アニメーション処理も行っています。
ウィンドウ内で中ボタンを押すと終了します。
<09-cursor.c>
$ 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 のディレクトリに置かれているカーソルテーマから、任意のカーソル画像を読み込んで使うことができます。
wayland-cursor では、/usr/share/icons や ~/.icons のディレクトリに置かれているカーソルテーマから、任意のカーソル画像を読み込んで使うことができます。
カーソルテーマの読み込み
まずは、カーソルテーマを読み込みます。
引数 name には、カーソルのテーマ名を指定します。
NULL で、デフォルトのテーマ ("default") になります。
テーマ名は、インストールされている各テーマの、先頭のディレクトリ名を指定します ("Adwaita" など)。
テーマ内の index.theme ファイルで定義されている名前では読み込めません。
size は、使用するカーソル画像の、おおよその px サイズです。
とりあえず、32 にしておけば良いです。
shm には、wl_shm のポインタを指定します。
共有メモリを使って画像を読み込むので、wl_shm が必要になります。
//テーマ読み込み 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 から、使用するカーソルを読み込みます。
name は、カーソルの名前です。
テーマの cursors ディレクトリ内にある、カーソル画像のファイル名を指定します。
読み込めなかった場合は、NULL が返ります。
※戻り値の wl_cursor は、クライアント側で解放処理を行う必要はありません。
通常の矢印カーソルの場合は、"default" や "left_ptr" といった名前になります。
カーソルのファイル名には、ある程度規則があります。
カーソル画像やテーマ自体は、X11 で使われてきたものをそのまま使用できるので、X11 のデスクトップ環境の規格をまとめている freedesktop.org には、基本的なカーソルの名前規則が掲載されています。
https://www.freedesktop.org/wiki/Specifications/cursor-spec/
なお、一つの形状に対して、複数の名前が存在する場合があります。
基本的にリンクファイルで対応している場合が多いですが、テーマによっては、一部の名前しか対応していない場合もあるので、カーソルを読み込む際は、一つのカーソル形状に対して、複数名での読み込みを試してみた方がいいでしょう。
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 構造体は、直接中身を参照することができます。
image_count は、画像の数です。
アニメーションの画像数となり、アニメーションがない場合は「1」となります。
images には、image_count 分の、wl_cursor_image のポインタが格納されています。
wl_cursor_image は、各画像の情報です。
「幅」「高さ」「ホットスポット位置」「アニメーションにおいて、その画像を表示する時間」が入っています。
このプログラムの場合は、初期化時に、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 にイメージとしてセットできるバッファです。
※ここで取得した 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_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 単位で設定するので、ウィンドウごとにカーソル形状をセットするということはできません。
serial は、wl_pointer の enter イベントなどで渡された serial 値を、そのまま指定します。
surface は、カーソル画像の wl_surface です。
hotspot_x, hotspot_y は、ホットスポット位置 (画像内でどの位置を原点とするか) です。
wl_cursor_image の hotspot_x, hotspot_y の値を、そのまま指定します。
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 が再描画を行う時のタイミングで、wl_callback によるコールバックが実行されます。
※wl_surface_commit() を実行した時に適用されるので、注意してください。
つまり、画像を切り替える時間になったら、新しい 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 引数がないので、開始時間が決められないため)
以降のコールバック時は、「現在時間 - 開始時間」で経過時間を算出し、それを元に、表示するフレーム画像を取得して、カーソル形状を変更します。
time は、アニメーション開始時点からの総経過時間 (ms) です。
アニメーション全体の時間を超えている場合は、繰り返しループするように処理されます。
戻り値のフレームインデックスは、「0 〜 image_count - 1」の値です。
そのまま、wl_cursor の images の配列インデックス値となります。
(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() を使った場合、カーソルがウィンドウ内に入っている間だけ、コールバックが通知されます。
カーソルがウィンドウ外にある場合は、送られてきません。
そうすると無駄に CPU を消費することになるので、基本的に、wl_surface_frame() だけを使ったアニメーション処理は推奨されません。
GUI のイベントループ内で、タイマーを実装するなどの処理を行うと良いでしょう。
ちなみに、カーソル画像として使う wl_surface を対象に wl_surface_frame() を使った場合、カーソルがウィンドウ内に入っている間だけ、コールバックが通知されます。
カーソルがウィンドウ外にある場合は、送られてきません。