Wayland: xdg-shell

プログラム
実際に、xdg-shell を使って、ウィンドウを表示してみます。

※scanner で生成した「xdg-shell-client-protocol.h」と「xdg-shell-protocol.c」が必要になります。

なお、前回までにやった wl_buffer のイメージは、imagebuf.c にまとめてあるので、そちらを使います。
トップページから、ソースコードをダウンロードしてください。

$ cc -o test 06-xdgshell.c imagebuf.c xdg-shell-protocol.c -lwayland-client -lrt

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

ウィンドウに閉じるボタンなどは付いていないので、終了時は、各デスクトップ上のタスクバーやウィンドウ一覧から、ウィンドウを閉じてください。
閉じる方法がない場合は、Ctrl+C や kill コマンドなどで強制終了してください。

<06-xdgshell.c>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

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

#include "imagebuf.h"


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

/* クライアント */

typedef struct
{
    struct wl_display *display;
    struct wl_registry *registry;
    struct wl_compositor *compositor;
    struct wl_shm *shm;
    struct xdg_wm_base *xdg_wm_base;
    int finish_loop;
}client;

/* ウィンドウ */

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

    imagebuf *img;
}window_xdg;

void window_xdg_update(window_xdg *p);

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


//========================
// xdg_surface
//========================


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

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

    xdg_surface_ack_configure(surface, serial);

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

    //終了させる

    ((window_xdg *)data)->client->finish_loop = 1;
}

const struct xdg_toplevel_listener g_xdg_toplevel_listener = {
    _xdg_toplevel_configure, _xdg_toplevel_close
};


//========================
// window_xdg
//========================


/* ウィンドウ作成 */

window_xdg *window_xdg_create(client *cl,int width,int height)
{
    window_xdg *p;

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

    p->client = cl;

    //wl_surface

    p->surface = wl_compositor_create_surface(cl->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->shm, width, height);

    return p;
}

/* ウィンドウ破棄 */

void window_xdg_destroy(window_xdg *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 window_xdg_update(window_xdg *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
//========================

/* PING 応答 */

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);
    printf("xdg_wm_base#ping | serial:%u\n", serial);
}

const struct xdg_wm_base_listener g_xdg_wm_base_listener = {
    _xdg_wm_base_ping
};


//========================
// wl_registry
//========================


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

    if(strcmp(name, "wl_compositor") == 0)
    {
        p->compositor = wl_registry_bind(reg, id, &wl_compositor_interface, 1);
    }
    else if(strcmp(name, "wl_shm") == 0)
    {
        p->shm = wl_registry_bind(reg, id, &wl_shm_interface, 1);
    }
    else 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);
    }
}

static void _registry_global_remove(void *data,struct wl_registry *registry,uint32_t id)
{
}

static const struct wl_registry_listener g_registry_listener = {
    _registry_global, _registry_global_remove
};

    
//==========================
// main
//==========================


/* クライアント終了 */

static void _client_finish(client *p)
{
    if(p->xdg_wm_base)
        xdg_wm_base_destroy(p->xdg_wm_base);

    wl_shm_destroy(p->shm);
    wl_compositor_destroy(p->compositor);
    wl_registry_destroy(p->registry);

    wl_display_disconnect(p->display);
}

/* クライアント初期化 */

static void _client_init(client *p)
{
    struct wl_display *disp;

    //接続

    disp = p->display = wl_display_connect(NULL);

    if(!disp)
    {
        printf("failed wl_display_connect\n");
        exit(1);
    }

    //wl_registry

    p->registry = wl_display_get_registry(disp);

    wl_registry_add_listener(p->registry, &g_registry_listener, p);

    wl_display_roundtrip(disp);

    //オブジェクトがない

    if(!p->xdg_wm_base)
    {
        printf("unfound xdg-shell\n");
        _client_finish(p);
        exit(1);
    }
}

/* main */

int main(void)
{
    client cl;
    window_xdg *win;

    memset(&cl, 0, sizeof(client));

    _client_init(&cl);

    //ウィンドウ作成

    win = window_xdg_create(&cl, 256, 256);

    imagebuf_fill(win->img, 0xffff0000);

    //イベントループ

    while(wl_display_dispatch(cl.display) != -1 && cl.finish_loop == 0);

    //

    window_xdg_destroy(win);

    _client_finish(&cl);

    return 0;
}
ウィンドウ装飾について
Wayland におけるウィンドウは、基本的に、タイトルバーやウィンドウ枠などの装飾を含みません。

サーバー側でウィンドウ装飾を付けられるようにする拡張プロトコルとして、xdg-decoration が存在しますが、2025年1月現在では、需要がないためか、GNOME では実装されていません。

そのため、現状では、クライアント側が自分でウィンドウ装飾を実装する必要があります。
wl_compositor
まず、ウィンドウをデスクトップ画面に表示するために、wl_compositor が必要になるので、バインドします。

wl_compositor は、ウィンドウのイメージを、デスクトップの画面に合成する処理を行います。

if(strcmp(name, "wl_compositor") == 0)
    p->compositor = wl_registry_bind(reg, id, &wl_compositor_interface, 1);

wl_compositor の子オブジェクトに wl_surfacewl_region があり、wl_compositor をバインドすると、これらを作成することができます。

2025年1月時点で、wl_compositor の最大バージョンは「6」ですが、今回は ver 1 の機能しか使わないので、バージョンは 1 で固定しています。

終了時は、wl_compositor_destroy() で破棄します。
xdg_wm_base
ウィンドウの操作に xdg-shell (stable) を使うので、xdg_wm_base をバインドします。
※名前は、"xdg_shell" ではなく、"xdg_wm_base" となっているので、注意してください。

unstable 版の xdg-shell-unstable-v5 では、"xdg_shell" の名前が使われていましたが、以降のバージョンでは、それと区別するために、名前が変更されています。

2025年1月現在の xdg-shell の最大バージョンは「6」ですが、ここでは ver 1 の機能のみ使用するので、1 で固定しています。

バインドしたら、同時に xdg_wm_base_add_listener() でハンドラを設定します。
終了時は、xdg_wm_base_destroy() でオブジェクトを破棄します。

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

...
    else 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 xdg_wm_base_add_listener(struct xdg_wm_base *xdg_wm_base,
    const struct xdg_wm_base_listener *listener, void *data);

struct xdg_wm_base_listener {
  void (*ping)(void *data, struct xdg_wm_base *xdg_wm_base, uint32_t serial);
};

//破棄

void xdg_wm_base_destroy(struct xdg_wm_base *xdg_wm_base);

//ping 応答

void xdg_wm_base_pong(struct xdg_wm_base *xdg_wm_base, uint32_t serial);
ping イベント
xdg_wm_base では、ping イベントがあります。
アプリ単位で、クライアントが応答可能な状態であるかどうかを判断するために、サーバーから不定期に呼ばれます。

2025年時点の GNOME では、ウィンドウの上でカーソルが動くたびに呼ばれるようです。

ping 内で xdg_wm_base_pong() を実行すると、サーバーに対して、応答可能であることを通知します。
引数の serial は、ping に渡された serial 値をそのまま渡します。

ping で応答が返ってこなかった場合は、クライアントがフリーズしている状態と見なされ、サーバー側で自動的に「応答がありません」などのダイアログが出て、強制終了させられます。

何秒もかかるような重い処理をする場合は、スレッドを使うなどして、GUI イベントループで適時 ping に対応しながら、処理する必要があります。
ウィンドウの作成
1つのウィンドウを扱うためには、複数のオブジェクトが必要になります。

  • ウィンドウイメージ (wl_buffer)
  • イメージを画面に表示するための wl_surface
  • xdg-shell において、ウィンドウの基底となる xdg_surface
  • xdg-shell において、トップレベルウィンドウを扱うための xdg_toplevel

wl_buffer の作成方法は前回までにやったので、ウィンドウ用のイメージとして作成しておきます。
イメージのサイズが、ウィンドウのサイズになります。
wl_surface
一つのウィンドウを表示するためには、まず最初に wl_surface が必要になります。

これは、デスクトップ画面に表示するイメージ内容を管理するためのオブジェクトです。
主にウィンドウの表示用に使いますが、マウスカーソル画像などでも使用します。

wl_surface は wl_compositor の子オブジェクトなので、wl_compositor から作成します。
一つのウィンドウごとに、一つの wl_surface を作成します。

//作成

struct wl_surface *wl_compositor_create_surface(struct wl_compositor *wl_compositor);

//破棄

void wl_surface_destroy(struct wl_surface *wl_surface);
xdg_surface
wl_surface を作成したら、次に xdg_surface を作成します。

//xdg_surface 作成

struct xdg_surface *xdg_wm_base_get_xdg_surface(
    struct xdg_wm_base *xdg_wm_base, struct wl_surface *surface);

//破棄

void xdg_surface_destroy(struct xdg_surface *xdg_surface);

//ハンドラ設定

int xdg_surface_add_listener(struct xdg_surface *xdg_surface,
    const struct xdg_surface_listener *listener, void *data);

struct xdg_surface_listener {
  void (*configure)(void *data, struct xdg_surface *xdg_surface, uint32_t serial);
};

xdg_surface の configure イベントは、ウィンドウのサイズが変更された時や、ウィンドウ状態の変更が確定した時に呼ばれます。
xdg_toplevel
最後に、トップレベルウィンドウを扱うための xdg_toplevel を作成します。

//xdg_toplevel 作成

struct xdg_toplevel *xdg_surface_get_toplevel(struct xdg_surface *xdg_surface);

//破棄

void xdg_toplevel_destroy(struct xdg_toplevel *xdg_toplevel);

//ハンドラ設定

int xdg_toplevel_add_listener(struct xdg_toplevel *xdg_toplevel,
    const struct xdg_toplevel_listener *listener, void *data);

struct xdg_toplevel_listener {
  void (*configure)(void *data, struct xdg_toplevel *xdg_toplevel,
    int32_t width, int32_t height, struct wl_array *states);

  void (*close)(void *data, struct xdg_toplevel *xdg_toplevel);
};

イベントについては、後述します。
wl_surface に適用
一連のウィンドウ初期化が終了したら、最後に wl_surface_commit() を実行して、変更を適用します。

void wl_surface_commit(struct wl_surface *wl_surface);
ウィンドウ内容の適用
ウィンドウの中身を実際に表示するためには、まず、wl_buffer のバッファに直接イメージを書き込みます。

その後、その内容をウィンドウ画面として表示するよう、wl_surface に設定する必要があります。

//wl_buffer をイメージとしてセット

void wl_surface_attach(struct wl_surface *wl_surface,
  struct wl_buffer *buffer, int32_t x, int32_t y);

//指定範囲を更新させる

void wl_surface_damage(struct wl_surface *wl_surface,
  int32_t x, int32_t y, int32_t width, int32_t height);

//すべての変更を実際に適用させる

void wl_surface_commit(struct wl_surface *wl_surface);

まず、wl_surface_attach() で、指定した wl_buffer のイメージを、ウィンドウの内容としてセットします。
x, y は、イメージ上で基準となる左上位置です。

画面上に更新させたい範囲がある場合は、wl_surface_damage() で、その範囲を指定します。

wl_surface の情報を変更した場合、最後には毎回 wl_surface_commit() を実行する必要があります。
これにより、各設定が実際に適用されます。
保留中のデータについて
wl_surface には、commit 後の確定された内容とは別に、適用保留中のデータが存在します。

wl_surface_attach() を含む、wl_surface の変更関数は、保留中のデータを変更するだけで、現在の確定している情報とは別の場所にセットされます。

wl_surface_commit() を実行すると、保留中のすべてのデータが適用されて、実際に内容が更新されます。
ダブルバッファリング
ウィンドウのイメージや、その他の状態などのデータは、内部で「ダブルバッファリング」されています。

つまり、wl_surface で指定されたデータは、イメージも含めて、サーバー内部に現在の状態を保持する領域があって、commit 時には、そこにデータをコピーする形となります。

そのため、commit 後は、attach でセットした wl_buffer は不要になるため、その後は wl_buffer を削除したり、バッファの内容を変更したりしても、問題ありません。

より正確には、wl_buffer の release イベントが呼ばれた時点で、コンポーザーがそのバッファに対するアクセスを終えたということなので、その後は wl_buffer を自由に使えます。

※commit 直後の段階では、まだコンポーザーはバッファにアクセスしていません。
wl_surface_damage
wl_surface_damage() は、ウィンドウ上で画面を更新する範囲を指定します。

ウィンドウ全体を更新する時は、ウィンドウ全体の範囲を指定し、一部分だけイメージを書き換えた時は、その範囲だけ指定します。

最初のウィンドウ表示時は、常に全体を更新する必要があるため、この関数を使わなくても問題なく表示されますが、それ以降にウィンドウ内容を更新する場合は、毎回この関数を使って範囲を指定しないと、画面が更新されません。
xdg_surface イベント
static void _xdg_surface_configure(void *data,struct xdg_surface *surface,uint32_t serial)
{
    window_xdg *win = (window_xdg *)data;

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

    xdg_surface_ack_configure(surface, serial);

    window_xdg_update(win);
}

xdg_surfaceconfigure イベントは、xdg_toplevel の configure など、一連の configure イベントが終了した時に来ます。

複数の configure 関連のイベントがあった場合は、それらの後に来ますが、今回のトップレベルウィンドウの場合は、xdg_toplevel の configure イベントが1回来る度に来ます。

このイベント内では、基本的にウィンドウの更新処理を行います。

今回の場合、ウィンドウの内容は、初期表示の後全く変化せず、ウィンドウサイズも変更させないので、イメージの描画後に一度だけ更新すれば問題ありませんが、configure イベント時は一応、毎回更新させています。

なお、更新されたウィンドウのイメージは、サーバー側で常に保持されているので、X11 と違い、ウィンドウが隠れたり表示された時に、毎回更新処理を行うといったことは、必要ありません。
xdg_surface_ack_configure
このイベント内で、ウィンドウ内容を更新するために wl_surface_commit() を使用する場合、その前に xdg_surface_ack_configure() を実行する必要があります。

void xdg_surface_ack_configure(struct xdg_surface *xdg_surface, uint32_t serial);

これにより、サーバー側で、ウィンドウに対する何らかの処理が行われる場合があります。

例えば、最大化される場合、ウィンドウの位置を左上に移動するといったことが行われます。
xdg_toplevel のイベント
struct xdg_toplevel_listener {
    void (*configure)(void *data, struct xdg_toplevel *xdg_toplevel,
        int32_t width, int32_t height, struct wl_array *states);
    void (*close)(void *data, struct xdg_toplevel *xdg_toplevel);
    //ver 4 以降で追加があるが、今回は使用しない
};
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 イベントは、サーバーがクライアントに対して、ウィンドウのサイズや状態の変更を要求するときに、通知されます。
クライアントは、ウィンドウに対して、指定された状態を適用する必要があります。

デスクトップの操作などによって、ウィンドウサイズが変更される時や、ウィンドウが最大化/フルスクリーン化される時、ウィンドウのアクティブ状態が変化する時などに呼ばれます。
指定された状態は、実際にすぐに適用されるわけではありません。

width, height は、ウィンドウの新しい幅と高さです。サイズの変更がない場合は、0 となります。
states は、新しいウィンドウ状態の配列データです。

ウィンドウサイズは、width, height を超えるサイズには変更できませんが、それ以下のサイズであれば、クライアント側で任意のサイズに調整することができます。
ウィンドウサイズの変更は、wl_buffer のサイズ変更と、wl_surface の更新処理によって適用されます。

states 引数
states 引数は、wayland-util.h で定義されている、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,
  # ver 6
  XDG_TOPLEVEL_STATE_SUSPENDED = 9
};

今回のプログラムの場合は、ウィンドウがアクティブ状態であれば、XDG_TOPLEVEL_STATE_ACTIVATED の値が入り、非アクティブになると、一つも値がない状態になります。

wl_array
wl_array は、Wayland で任意型の配列を扱う時のデータです。

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 の位置から順に、連続して任意型のデータが並んでいるので、型変換して読み込んでいきます。

wl_array_for_each は、先頭から繰り返し値を読み込む時に使うマクロです。

pos には、読み込むデータの型に合わせた、ポインタ変数を指定します。
一つのデータを uint32_t として読み込みたい場合は、"uint32_t *p" などとして変数を用意し、その変数名を指定します。

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

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

このイベントが来たら、必ず終了しなければならないというわけではないので、実際に終了させるかどうかは、クライアントが判断します。

例えば、終了する前にデータの保存が必要な場合は、ダイアログを表示したりできます。
イベントループ
前回まではループが必要なかったので、wl_display_roundtrip() でイベントを処理していましたが、今回は、イベントループ内で wl_display_dispatch() を使用しています。

int wl_display_dispatch(struct wl_display *display);

wl_display_roundtrip
wl_display_roundtrip() は、すでに発生しているすべてのイベントの処理が終了するまで待つ関数です。

この関数は、現在発生しているイベントに対してのみ効果があるため、新しいイベントが来るまで待つといったことはしません。

明確にイベントがあるのがわかっていて、かつ、そのイベントのハンドラ関数がすべて呼び終わるまで待ちたい時に使います。
wl_registry で、すべてのグローバルオブジェクトを取得する時など、主に初期化時に使います。

wl_display_dispatch
wl_display_dispatch() は、デフォルトのイベントキューで、読み込まれているイベントを処理します。
キューが空の場合は、新しいイベントが来るまで待ちます。

通常のイベントループでは、基本的にこちらを使います。

イベントがすでに存在すれば、それらを処理して、その数を返し、イベントが存在しなければ、新しいイベントが来るまで待ち、新しく処理したイベントの数を返します。

エラーにならない限りは、常に1つ以上のイベントを処理することになり、イベントを一度処理するごとに関数は戻ってきます。

今回の場合は、xdg_toplevel の close イベントで、ウィンドウの終了が要求された時にフラグを ON にし、そのイベントが処理されて wl_display_dispatch() が戻った時に、フラグをチェックして、ON ならイベントループを終了させています。