Wayland: xdg_toplevel

プログラム
今回は、xdg_toplevel ウィンドウで、もう少しウィンドウらしい処理を追加します。

$ cc -o test 11-toplevel.c client.c imagebuf.c xdg-shell-protocol.c -lwayland-client -lrt

ウィンドウにタイトル文字列をセットしています。
ウィンドウがアクティブ状態の場合は緑色、非アクティブ状態の場合は赤色で塗りつぶします。

左クリックで、最大化または元に戻すの切り替え。
右クリックで、クリックした位置に、サーバー独自のウィンドウ操作メニューを表示。
中ボタンクリックで終了。

<11-toplevel.c>
#include <stdio.h>
#include <linux/input-event-codes.h>

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

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


typedef struct
{
    client b;
    
    Toplevel *win;
    int pt_x, pt_y;
}client_ex;

#define WINDOW_COLOR_ACTIVE     0xff00ff00
#define WINDOW_COLOR_NON_ACTIVE 0xffff0000


//=======================
// xdg_toplevel
//=======================


static void _xdg_toplevel_configure(void *data, struct xdg_toplevel *xdg_toplevel,
    int32_t width, int32_t height, struct wl_array *states)
{
    Toplevel *p = (Toplevel *)data;
    uint32_t *ps,new_state;
    int redraw = 0;

    printf("xdg_toplevel # configure | %d x %d\n", width, height);

    printf("  <states> ");
    put_xdg_toplevel_state(states);

    //サイズ変更

    if(width && height)
    {
        if(toplevel_resize(p, width, height))
            redraw = 1;
    }

    //状態

    new_state = 0;

    wl_array_for_each(ps, states)
    {
        new_state |= 1 << (*ps);
    }

    if( (p->state_flags & (1 << XDG_TOPLEVEL_STATE_ACTIVATED))
        != (new_state & (1 << XDG_TOPLEVEL_STATE_ACTIVATED)) )
    {
        redraw = 1;
    }

    p->state_flags = new_state;

    //内容更新
    // アクティブ時は緑、非アクティブ時は赤

    if(redraw)
    {
        imagebuf_fill(p->sf.img,
            (p->state_flags & (1 << XDG_TOPLEVEL_STATE_ACTIVATED))
                ? WINDOW_COLOR_ACTIVE: WINDOW_COLOR_NON_ACTIVE);
    }
}

/* 閉じる */

static void _xdg_toplevel_close(void *data,struct xdg_toplevel *toplevel)
{
    TOPLEVEL(data)->cl->finish_loop = 1;
}

/* サイズ制限の境界 */

static void _xdg_toplevel_configure_bounds(void *data,
    struct xdg_toplevel *xdg_toplevel, int32_t width, int32_t height)
{
    printf("xdg_toplevel # configure_bounds | width:%d, height:%d\n",
        width, height);
}

/* サポートしている機能 */

static void _xdg_toplevel_wm_capabilities(void *data,
    struct xdg_toplevel *xdg_toplevel, struct wl_array *capabilities)
{
    printf("xdg_toplevel # wm_capabilities\n  ");

    put_xdg_toplevel_state(capabilities);
}

static const struct xdg_toplevel_listener g_xdg_toplevel_listener = {
    _xdg_toplevel_configure, _xdg_toplevel_close,
    _xdg_toplevel_configure_bounds, _xdg_toplevel_wm_capabilities
};


//=======================
// wl_pointer
//=======================


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->pt_x = x >> 8;
    p->pt_y = y >> 8;

    //カーソルセット

    client_set_cursor(CLIENT(p), serial);
}

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

    p->pt_x = x >> 8;
    p->pt_y = y >> 8;
}

/* ボタン */

static void _pointer_button(void *data, struct wl_pointer *pointer,
    uint32_t serial, uint32_t time, uint32_t button, uint32_t state)
{
    client_ex *p = (client_ex *)data;

    //押した時のみ
    if(state != WL_POINTER_BUTTON_STATE_PRESSED)
        return;

    switch(button)
    {
        //最大化/戻す
        case BTN_LEFT:
            if(p->win->state_flags & (1 << XDG_TOPLEVEL_STATE_MAXIMIZED))
                xdg_toplevel_unset_maximized(p->win->xdg_toplevel);
            else
                xdg_toplevel_set_maximized(p->win->xdg_toplevel);
            break;
        case BTN_RIGHT:
            //ウィンドウメニュー
            xdg_toplevel_show_window_menu(p->win->xdg_toplevel,
                p->b.seat, serial, p->pt_x, p->pt_y);
            break;
        case BTN_MIDDLE:
            //中ボタンで終了
            p->b.finish_loop = 1;
            break;
    }
}

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


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


/* main */

int main(void)
{
    client_ex *p;
    Toplevel *win;

    p = (client_ex *)client_new(sizeof(client_ex));

    p->b.init_flags = CLIENT_INIT_FLAGS_SEAT | CLIENT_INIT_FLAGS_POINTER
        | CLIENT_INIT_FLAGS_CREATE_CURSOR;

    p->b.pointer_listener = &g_pointer_listener;
    p->b.xdg_shell_need_ver = 6;

    client_init(CLIENT(p));

    //ウィンドウ

    p->win = win = toplevel_create(CLIENT(p), 256, 256, &g_xdg_toplevel_listener);

    xdg_toplevel_set_title(win->xdg_toplevel, "test_wayland");

    //

    client_loop_simple(CLIENT(p));

    //解放

    toplevel_destroy(win);

    client_destroy(CLIENT(p));

    return 0;
}
カーソル形状
今回は、きちんとしたカーソル形状がないと動作がわかりにくいので、ビットイメージを元に描画した、カーソル形状を用意しています。

wl_pointer の enter イベント時に、作成したカーソル形状をセットしています。
常に同じカーソル形状を使う場合でも、enter イベント時に毎回変更する必要があります。
xdg_toplevel イベント
今回は xdg-shell の ver 6 まで対応しているので、xdg_toplevel のイベントがいくつか増えています。
configure_bounds イベント (ver 4)
void (*configure_bounds)(void *data, struct xdg_toplevel *xdg_toplevel,
    int32_t width, int32_t height);

configure_bounds イベントでは、画面のサイズから、タスクバーなどの領域を除いたサイズが通知されます。
この値は基本的に、ウィンドウを最大化した時のウィンドウサイズと同じです。

xdg_toplevel の configure イベントの前に送信される場合があります。
width, height が 0 の場合は、サイズが不明という意味であり、イベントが送信されなかった場合と同じになります。
wm_capabilities イベント (ver 5)
void (*wm_capabilities)(void *data,
    struct xdg_toplevel *xdg_toplevel, struct wl_array *capabilities);

wm_capabilities イベントは、実際にサポートされている機能を通知します。

capabilities は、xdg_toplevel の configure イベントの states 引数と同じ値 (enum xdg_toplevel_state の配列) です。
configure イベントの処理
ウィンドウサイズ
最大化や、ウィンドウサイズの変更などの操作が行われた場合、xdg_toplevel の configure イベントで、新しいウィンドウサイズ (width, height) が送られてくるので、この値が 0 でなく、また、現在のウィンドウサイズと値が異なる場合は、ウィンドウイメージ用の wl_buffer を、新しいサイズで再作成する必要があります。

また、イメージの中身が破棄されるため、ウィンドウ内容全体も再描画する必要があります。

この処理を行わない場合は、当然ウィンドウサイズは変更されないままになります。

状態
states 引数では、ウィンドウが現在最大化されているかなどの状態が渡されます。

ウィンドウの現在の状態を取得する方法は他にないので、任意時にウィンドウが最大化されているかどうかなどを知りたい場合は、configure イベント時に、現在のウィンドウ状態を記録しておく必要があります。

enum xdg_toplevel_state は 1〜の値なので、今回の場合は、この値をビット位置として使い、フラグとして複数の状態を記憶しています。

ACTIVATED
今回の場合は、ウィンドウがアクティブ状態であれば、ウィンドウの内容を緑色にし、非アクティブ状態であれば、赤色にしています。

configure イベントが来た時に、前回のアクティブ状態と現在のアクティブ状態が異なる場合は、イメージを更新しています。

※configure イベント内では、wl_surface の更新処理を行っていませんが、client.c 内のデフォルトの xdg_surface:configure イベントで、常にウィンドウ全体の更新処理を行っているので、問題ありません。

注意点
(2025年2月時点) xdg_surface の configure イベント内で、リサイズの処理と更新を行うと、GNOME では、最大化から戻る時に、最大化前のウィンドウ位置に移動せず、左上位置のままになります。

xdg_toplevel の configure 内でリサイズを行うと、問題なく最大化前の位置に戻ります。
xdg_toplevel 操作
ウィンドウメニュー
void xdg_toplevel_show_window_menu(struct xdg_toplevel *xdg_toplevel,
    struct wl_seat *seat, uint32_t serial, int32_t x, int32_t y);

サーバー側で実装されている、ウィンドウ操作のメニューを表示します。
ボタン押し、またはキー押し時に実行します。

ここでは、右ボタンがクリックされた時に表示しています。

serial は、ボタン押し時のイベントなどで渡される serial 引数の値を、そのまま渡します。

x, y は、メニューを表示する位置 (ウィンドウ座標) です。

wl_pointer の button イベントでは、ボタンが押された時の座標は送られてこないため、enter, motion イベント時に、現在のポインタの位置を記録しておき、最後のポインタ位置を使います。

表示されるメニューの内容はデスクトップによりますが、最大化したり、ウィンドウ位置を移動したり、ウィンドウを閉じたりすることができます。
最大化
void xdg_toplevel_set_maximized(struct xdg_toplevel *xdg_toplevel);
void xdg_toplevel_unset_maximized(struct xdg_toplevel *xdg_toplevel);

ウィンドウの最大化、または、最大化を元に戻します。

今回の場合は、左クリックされた時、現在最大化されていなければ最大化を行い、最大化されていれば元に戻します。

この後に xdg_toplevel:configure イベントが送信されるので、実際の最大化に関する処理はそちらで行います。

すでに最大化されている状態で、set_maximized を行った場合も、configure イベントは来ます (MAXIMIZED が ON の状態)。

カーソル位置の問題点
左クリックでウィンドウを最大化した直後に、カーソルを全く動かさずに右クリックすると、クリックした位置にウィンドウメニューが表示されません。
(現状の GNOME 上では起こります。ウィンドウメニューで、ウィンドウの位置を画面の中央あたりに移動させてから行うと、わかりやすいです)

最大化により、ウィンドウ位置とサイズが変化したことで、カーソル位置のウィンドウ座標は変化していますが、カーソル自体は全く移動していないので、motion イベントは来ません。

そのため、記録しているカーソル位置は、最大化前の位置と変わっていないので、メニューを表示する位置がずれてしまいます。

位置を調整しようにも、ウィンドウ位置がどれだけ移動したかがわからないので、何かしらのイベントが来ない限り、現状では正確に調整することはできません。
他の Wayland プログラムでも、この問題は同様に起こります。