Wayland - ウィンドウの表示 (xdg-shell)

ウィンドウの表示 (xdg-shell)
wl_shell_surface を使うと、基本的なウィンドウの操作はできますが、最小限の機能しかないため、実際のウィンドウとして使うには物足りない場合があります。

もう少しウィンドウとしての機能を増やしたい場合は、拡張プロトコルとして定義されている 「xdg-shell」 を使います。
※ サーバー側でも xdg-shell が実装されている必要があります。

wl_shell_surface とほぼ同じような機能を持っていますが、最小化したり、最大化/フルスクリーンを戻すなどの機能が追加されています。
ただし、こちらも、ウィンドウに装飾は付きません。

Wayland 標準のプロトコルでは、かなり最低限の動作しか行えませんが、拡張プロトコルを使うと、より詳細な動作を行うことが出来ます。

ただし、拡張プロトコルを使うのに必要なヘッダファイルやソースファイルは、デフォルトで用意されていないため、プロトコルを定義した xml ファイルを元に、wayland-scanner ツールを使って、各ファイルを生成する必要があります。
プロトコル
Wayland では、各プロトコルの情報は、xml ファイルで定義されています。

wayland-scanner」ツールを使うと、その xml ファイルから、開発に必要なヘッダファイルとソースコードを生成できます。

このコマンドは、Wayland の開発パッケージ内に含まれています。

Wayland のソースファイルを見てみると、「protocol/wayland.xml」ファイルがあり、そこにデフォルトのプロトコルの情報が記述されています。
それを scanner で変換したヘッダファイルが、「wayland-client-protocol.h」となっています。

生成したヘッダファイルには、マクロや列挙型、インライン関数などが定義されています。
生成したソースファイルには、「<name>_interface」など、バインドする時に必要なグローバル変数などが定義されています。

wayland-protocols に、拡張プロトコルの各 xml ファイルがあります。
パッケージ
拡張プロトコルの xml ファイルは、Wayland の公式サイトからダウンロードすることもできますが、各ディストリビューションのパッケージにも存在しています。

Debian/Ubuntu
Arch Linux
wayland-protocols
Red Hatwayland-protocols-devel

Wayland プログラムのソースを配布する場合は、scanner で生成したヘッダファイルやソースファイルを同梱させるのではなく、これらのパッケージを依存パッケージとし、ビルド時に scanner で生成させるようにした方が良いでしょう。
wayland-scanner の使い方
$ wayland-scanner [option] [type] <input_file> <output_file>

▼ type (出力の種類)
client-headerクライアント用のヘッダファイルを生成します。
出力ファイル名は、「<name>-client-protocol.h」にするのが一般的です。
server-headerサーバー用のヘッダファイルを作成します。
private-codeクライアント/サーバーで必要なソースコードを生成します。
ファイル名は「<name>-protocol.c」にするのが一般的です。
public-code同じく、ソースコードを生成します。
とりあえずは、private-code の方を使えば良さそうです。

<使用例>
$ wayland-scanner client-header xdg-shell.xml xdg-shell-client-protocol.h
xdg-shell のファイルを生成
wayland-protocols パッケージでは、「/usr/share/wayland-protocols」ディレクトリ下に、各プロトコルの xml ファイルがインストールされています。

今回使用する「xdg-shell」の xml ファイルは、stable/xdg-shell ディレクトリ内にあります。
unstable の古いバージョンは、unstable/xdg-shell にあります。

クライアントで使う場合、クライアント用のヘッダファイルとソースファイルが必要なので、それらを生成します。

※ソースファイルを生成せずにコンパイルすると、「<name>_interface が定義されていない」というエラーが出ます。

$ wayland-scanner client-header xdg-shell.xml xdg-shell-client-protocol.h
$ wayland-scanner private-code xdg-shell.xml xdg-shell-protocol.c
Doxygen
scanner で生成したヘッダファイルには Doxygen 用のコメントが記述されているため、Doxygen を使って、関数などのドキュメントを生成することができます。

※ wayland-client-core.h、wayland-cursor.h の関数は、ヘッダファイル内にドキュメントが記述されていません。
これらに関しては、ヘッダファイルを見つつ、公式のドキュメントの方をご覧ください。


scanner で xml ファイルからヘッダファイルを生成するシェルスクリプトと、Doxygen の設定ファイルを用意したので、良ければ使ってみてください。

Download: wayland_doxy.tar.bz2

## include ディレクトリにヘッダファイルを生成
## 引数を指定すると、/usr/include/wayland-* のヘッダファイルも対象にする

$ ./header.sh 1

## include ディレクトリのファイルから HTML ドキュメント生成

$ doxygen doxy_wayland
unstable の扱いについて
拡張プロトコルでは、「stable (安定版)」と「unstable (不安定版)」が存在します。

最初は unstable から始まり、定義が成熟した段階で、stable へと移行します。

stable の場合は、オブジェクト名はそのままでバージョンが増える形になりますが、unstable の場合は、基本的に下位バージョンとの互換性がないため、バージョンごとにオブジェクトの名前が変わり、関数の名前も各バージョンごとに異なります。
(zxdg_exporter_v1、zxdg_exporter_v2 など)

unstable のプロトコルも実装することはできますが、バージョンが変わったり stable に移行した場合は、全体を書き直すか、各バージョンごとに処理を実装して、状況によって適切なものを使うなどの処理が必要となるため、扱いにくいです。
プログラム
$ cc -o test 06_xdgshell.c client.c imagebuf.c xdg-shell-protocol.c -lwayland-client -lrt

※ scanner で生成した「xdg-shell-client-protocol.h、xdg-shell-protocol.c」が必要。

06_xdgshell.c
/******************************
 * xdg-shell でウィンドウ表示
 ******************************/

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

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

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


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

/* クライアント */

typedef struct
{
    Client b;

    struct xdg_wm_base *xdg_wm_base;
}ClientEx;

/* ウィンドウ */

typedef struct
{
    ClientEx *client;
    struct wl_surface *surface;
    struct xdg_surface *xdg_surface;
    struct xdg_toplevel *xdg_toplevel;

    ImageBuf *img;
    uint8_t flag_configure;
}WindowXdg;

void WindowXdg_update(WindowXdg *p);

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


//--------------------
// xdg_surface
//--------------------


static void _xdg_surface_configure(
    void *data,struct xdg_surface *surface,uint32_t serial)
{
    WindowXdg *win = (WindowXdg *)data;

    printf("xdg_surface#configure | serial %u\n", serial);

    //commit の前に実行

    xdg_surface_ack_configure(surface, serial);

    //初期表示時、ウィンドウを更新

    if(win->flag_configure == 0)
    {
        win->flag_configure = 1;

        ImageBuf_fill(win->img, 0xffff0000);

        WindowXdg_update(win);
    }
}

static const struct xdg_surface_listener g_xdg_surface_listener = {
    _xdg_surface_configure
};


//--------------------
// xdg_toplevel
//--------------------


/* サイズ変更時や状態変更時 */

static void _xdg_toplevel_configure(
    void *data, struct xdg_toplevel *toplevel,
    int32_t width, int32_t height, struct wl_array *states)
{
    uint32_t *ps;

    printf("xdg_toplevel#configure | w:%d, h:%d, states:", width, height);

    wl_array_for_each(ps, states)
    {
        switch(*ps)
        {
            case XDG_TOPLEVEL_STATE_MAXIMIZED:
                printf("MAXIMIZED ");
                break;
            case XDG_TOPLEVEL_STATE_FULLSCREEN:
                printf("FULLSCREEN ");
                break;
            case XDG_TOPLEVEL_STATE_RESIZING:
                printf("RESIZING ");
                break;
            case XDG_TOPLEVEL_STATE_ACTIVATED:
                printf("ACTIVATED ");
                break;
        }
    }

    printf("\n");
}

/* ユーザーによって、ウィンドウを閉じる要求が行われた時 */

static void _xdg_toplevel_close(void *data,struct xdg_toplevel *toplevel)
{
    printf("xdg_toplevel#close\n");

    //終了させる

    ((WindowXdg *)data)->client->b.finish_loop = 1;
}

const struct xdg_toplevel_listener g_xdg_toplevel_listener = {
    _xdg_toplevel_configure, _xdg_toplevel_close
};


//------------------------
// WindowXdg
//------------------------


/* ウィンドウ作成 */

WindowXdg *WindowXdg_create(ClientEx *cl,int width,int height)
{
    WindowXdg *p;

    p = (WindowXdg *)calloc(1, sizeof(WindowXdg));
    if(!p) return NULL;

    p->client = cl;

    //wl_surface

    p->surface = wl_compositor_create_surface(cl->b.compositor);

    //xdg_surface

    p->xdg_surface = xdg_wm_base_get_xdg_surface(cl->xdg_wm_base, p->surface);

    xdg_surface_add_listener(p->xdg_surface, &g_xdg_surface_listener, p);

    //xdg_toplevel

    p->xdg_toplevel = xdg_surface_get_toplevel(p->xdg_surface);
    
    xdg_toplevel_add_listener(p->xdg_toplevel, &g_xdg_toplevel_listener, p);

    //適用

    wl_surface_commit(p->surface);

    //イメージ作成

    p->img = ImageBuf_create(cl->b.shm, width, height);

    return p;
}

/* ウィンドウ破棄 */

void WindowXdg_destroy(WindowXdg *p)
{
    if(p)
    {
        ImageBuf_destroy(p->img);
    
        xdg_toplevel_destroy(p->xdg_toplevel);
        xdg_surface_destroy(p->xdg_surface);
        wl_surface_destroy(p->surface);
    
        free(p);
    }
}

/* ウィンドウ更新 */

void WindowXdg_update(WindowXdg *p)
{
    wl_surface_attach(p->surface, p->img->buffer, 0, 0);
    wl_surface_damage(p->surface, 0, 0, p->img->width, p->img->height);
    wl_surface_commit(p->surface);
}


//-------------------------
// xdg_wm_base
//-------------------------


static void _xdg_wm_base_ping(void *data,
    struct xdg_wm_base *xdg_wm_base,uint32_t serial)
{
    //応答する
    xdg_wm_base_pong(xdg_wm_base, serial);
}

const struct xdg_wm_base_listener g_xdg_wm_base_listener = {
    _xdg_wm_base_ping
};


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

    if(strcmp(name, "xdg_wm_base") == 0)
    {
        p->xdg_wm_base = wl_registry_bind(reg, id, &xdg_wm_base_interface, 1);

        xdg_wm_base_add_listener(p->xdg_wm_base, &g_xdg_wm_base_listener, NULL);
    }
}

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


int main(void)
{
    ClientEx *client;
    WindowXdg *win;

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

    client->b.registry_global = _registry_global;

    Client_init(CLIENT(client));

    //サーバーでインターフェイスが存在しなかった

    if(!client->xdg_wm_base)
    {
        printf("not fount 'xdg_wm_base'\n");
        Client_destroy(CLIENT(client));
        return 1;
    }

    //ウィンドウ作成

    win = WindowXdg_create(client, 256, 256);

    //ここでウィンドウを更新しようとすると、エラーになる

#if 0
    ImageBuf_fill(win->img, 0xffff0000);

    WindowXdg_update(win);
#endif

    //イベントループ

    Client_loop_simple(CLIENT(client));

    //

    WindowXdg_destroy(win);

    xdg_wm_base_destroy(client->xdg_wm_base);

    Client_destroy(CLIENT(client));

    return 0;
}

xdg-shell を使って、赤いウィンドウを表示するだけです。

GNOME では、タスクバー上のメニューをクリックして出る「終了」のコマンドで終了できますが、weston 上ではアプリを終了する方法がないので、プログラムを起動した端末で Ctrl+C キーを押すなどして終了させてください。
xdg_wm_base
まずは、xdg-shell を使うのに必要なグローバルオブジェクトをバインドします。

インターフェイス名は "xdg_shell" ではなく、"xdg_wm_base" です。
この名前でバインドするので、注意してください。

unstable の xdg-shell-unstable-v5 で "xdg_shell" の名前が使われているため、それと区別するために名前が変更されています。

バインドしたら、同時にハンドラも設定します。

static void _xdg_wm_base_ping(void *data,
    struct xdg_wm_base *xdg_wm_base,uint32_t serial)
{
    xdg_wm_base_pong(xdg_wm_base, serial);
}

const struct xdg_wm_base_listener g_xdg_wm_base_listener = {
    _xdg_wm_base_ping
};

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

    if(strcmp(name, "xdg_wm_base") == 0)
    {
        p->xdg_wm_base = wl_registry_bind(reg, id, &xdg_wm_base_interface, 1);

        xdg_wm_base_add_listener(p->xdg_wm_base, &g_xdg_wm_base_listener, NULL);
    }
}
ping イベント
xdg_wm_base の ping イベントは、wl_shell_surface の ping イベントと同じです。

クライアントが応答可能な状態かどうかを通知してくるので、xdg_wm_base_pong() 関数で応答します。
ウィンドウの作成
//wl_surface

p->surface = wl_compositor_create_surface(cl->b.compositor);

//xdg_surface

p->xdg_surface = xdg_wm_base_get_xdg_surface(cl->xdg_wm_base, p->surface);

xdg_surface_add_listener(p->xdg_surface, &g_xdg_surface_listener, p);

//xdg_toplevel

p->xdg_toplevel = xdg_surface_get_toplevel(p->xdg_surface);

xdg_toplevel_add_listener(p->xdg_toplevel, &g_xdg_toplevel_listener, p);

//適用

wl_surface_commit(p->surface);

まずは wl_surface が必要です。

次に、wl_surface から xdg_surface を作成します。
xdg-shell 用のサーフェスです。

さらに、xdg_surface から、トップレベルウィンドウ用の xdg_toplevel を作成し、ハンドラを設定します。

最後に、これらの要求を wl_surface_commit() で適用させます。
xdg_surface のイベント
static void _xdg_surface_configure(
    void *data,struct xdg_surface *surface,uint32_t serial)
{
    WindowXdg *win = (WindowXdg *)data;

    printf("xdg_surface#configure | serial %u\n", serial);

    //commit の前に実行

    xdg_surface_ack_configure(surface, serial);

    //初期表示時、ウィンドウを更新

    if(win->flag_configure == 0)
    {
        win->flag_configure = 1;

        ImageBuf_fill(win->img, 0xffff0000);

        WindowXdg_update(win);
    }
}

xdg_surfaceconfigure イベントは、ウィンドウのサイズ変更や状態の変更が確定した時に呼ばれます。

このイベント内でウィンドウ内容を変更する場合、クライアントは、commit する前に xdg_surface_ack_configure() を実行する必要があります。

なお、最初の configure イベントが呼ばれる前にウィンドウ内容を更新しようとすると、以下のエラーが出て、強制終了します。

xdg_surface@8: error 3: xdg_surface has never been configured

一度 configure イベントが呼ばれた後でないと、ウィンドウ内容は更新できないようなので、最初に configure イベントが呼ばれた時に初期表示処理を行っています。
xdg_toplevel
configure イベント
/* サイズ変更時や状態変更時 */

static void _xdg_toplevel_configure(
    void *data, struct xdg_toplevel *toplevel,
    int32_t width, int32_t height, struct wl_array *states)
{
    uint32_t *ps;

    printf("xdg_toplevel#configure | w:%d, h:%d, states:", width, height);

    wl_array_for_each(ps, states)
    {
        switch(*ps)
        {
            case XDG_TOPLEVEL_STATE_MAXIMIZED:
                printf("MAXIMIZED ");
                break;
            case XDG_TOPLEVEL_STATE_FULLSCREEN:
                printf("FULLSCREEN ");
                break;
            case XDG_TOPLEVEL_STATE_RESIZING:
                printf("RESIZING ");
                break;
            case XDG_TOPLEVEL_STATE_ACTIVATED:
                printf("ACTIVATED ");
                break;
        }
    }

    printf("\n");
}

xdg_toplevel の configure イベントは、サーバーがクライアントに対して、ウィンドウのサイズ変更や状態変更を要求するときに通知されます。

ここで示された内容が実際に適用された時に、xdg_surface の configure イベントが呼ばれます。

width, height はサイズ変更後の幅と高さです。状態変化しかない場合は 0 となります。
states は、状態変化の配列データとなっています。

states 引数
states は、wl_array 構造体で、中身は uint32_t 型の数値の配列となっていて、現在の状態が ON なら、対応する値が入っています。

値は、enum xdg_toplevel_state の列挙型で定義されています。

XDG_TOPLEVEL_STATE_MAXIMIZED = 1
XDG_TOPLEVEL_STATE_FULLSCREEN = 2
XDG_TOPLEVEL_STATE_RESIZING = 3
XDG_TOPLEVEL_STATE_ACTIVATED = 4
# ver 2 以降
XDG_TOPLEVEL_STATE_TILED_LEFT = 5
XDG_TOPLEVEL_STATE_TILED_RIGHT = 6
XDG_TOPLEVEL_STATE_TILED_TOP = 7
XDG_TOPLEVEL_STATE_TILED_BOTTOM = 8

今回のプログラムの場合、他のウィンドウをアクティブにした後に、テストウィンドウ内をクリックしてアクティブにすると、states に XDG_TOPLEVEL_STATE_ACTIVATED の値が入ります。

wl_array
wl_array は「wayland-util.h」で定義されており、配列のデータを扱います。

struct wl_array {
    size_t size;  // 配列の全体バイト数
    size_t alloc; // 確保されているバイト数
    void *data;   // データの先頭位置
};

#define wl_array_for_each(pos, array) \
    for (pos = (array)->data; \
        (const char *) pos < ((const char *) (array)->data + (array)->size); \
        (pos)++)

data の位置から順に、連続してデータが並んでいるので、必要な型に応じて読み込んでいきます。

pos には、読み込むデータの型に合わせたポインタ変数を指定します。
ここでは、配列の各データを uint32_t として読み込むので、"uint32_t *" の型となります。

配列の先頭から順に、pos の変数にデータのポインタ位置が渡され、終端まで来たら終了します。
close イベント
close イベントは、サーバーがアプリの終了を要求する時に呼ばれます。

デスクトップの操作で、ユーザーがアプリの終了を選択した時などに呼ばれます。

必ず終了しなければいけないというわけではなく、データの保存が必要な場合は、ダイアログを表示したりします。