Wayland: ポップアップウィンドウ (xdg_popup)

ポップアップウィンドウ
xdg-shell で、xdg_popup を使って、ポップアップウィンドウを表示してみます。

$ cc -o test 14-popup.c client.c imagebuf.c xdg-shell-protocol.c -lwayland-client -lrt

  • 左ドラッグで、ウィンドウ位置を移動します。
  • 右クリックで、青いポップアップウィンドウを表示します。
  • ポップアップウィンドウ内で右クリックするか、ポップアップウィンドウ外をクリックすると、ポップアップウィンドウを閉じます。
  • トップレベルウィンドウを中ボタン押しで、終了します。
  • Space キー押しで、次にポップアップを表示する時の位置を、以下の2通りから切り替えます。
    [1] ウィンドウ内の黒い矩形の右上位置を基準にする。
    [2] (0, 0) - (1 x 1) の左上位置を基準にする。

ウィンドウを画面の端に移動させた後、ポップアップウィンドウを表示してみると、サーバーによるウィンドウ位置の調整が確認できます。

<14-popup.c>
#include <stdio.h>
#include <stdlib.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
{
    Surface sf;
    client *cl;
    struct xdg_surface *xdg_surface;
    struct xdg_popup *xdg_popup;
    Toplevel *parent;
}popupwindow;

void popupwindow_destroy(popupwindow *p);


typedef struct
{
    client b;
    
    Toplevel *win;
    popupwindow *popup;
    struct wl_surface *enter_surface;
    int popup_pos;
}client_ex;

#define ANCHOR_X  100
#define ANCHOR_Y  100
#define ANCHOR_W  100
#define ANCHOR_H  80

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


static void _clientex_popup_finish(client_ex *p)
{
    popupwindow_destroy(p->popup);
    p->popup = NULL;
}


//========================
// xdg_surface (popup)
//========================


static void _popup_xdg_surface_configure(
    void *data,struct xdg_surface *surface,uint32_t serial)
{
    printf("[popup] xdg_surface # configure\n");

    xdg_surface_ack_configure(surface, serial);

    surface_update(&((popupwindow *)data)->sf);
}

static const struct xdg_surface_listener g_popup_xdg_surface_listener = {
    _popup_xdg_surface_configure
};


//========================
// xdg_popup
//========================


/* 位置やサイズ変更時
 *
 * サーバーによって位置やサイズが調整される場合がある */

static void _popup_configure(void *data, struct xdg_popup *popup,
    int32_t x, int32_t y, int32_t width, int32_t height)
{
    popupwindow *p = (popupwindow *)data;

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

    if(surface_resize(&p->sf, p->cl, width, height))
        imagebuf_fill(p->sf.img, 0xff0000ff);
}

/* ポップアップ終了時 */

static void _popup_done(void *data, struct xdg_popup *popup)
{
    popupwindow *p = (popupwindow *)data;

    printf("xdg_popup # done\n");

    _clientex_popup_finish((client_ex *)p->cl);
}

static const struct xdg_popup_listener g_popup_listener = {
    _popup_configure, _popup_done
};


//========================
// popupwindow
//========================


/* ポップアップ用 xdg_positioner 作成 */

static struct xdg_positioner *_create_popup_positioner(
    client *cl,int w,int h,int pos_point)
{
    struct xdg_positioner *xdgpos;

    //xdg_positioner 作成

    xdgpos = xdg_wm_base_create_positioner(cl->xdg_wm_base);

    //ウィンドウサイズ

    xdg_positioner_set_size(xdgpos, w, h);

    //

    if(pos_point)
    {
        //(0, 0) 左上
        
        xdg_positioner_set_anchor_rect(xdgpos, 0, 0, 1, 1);

        xdg_positioner_set_anchor(xdgpos, XDG_POSITIONER_ANCHOR_TOP_LEFT);
    }
    else
    {
        //黒い矩形の右上
        
        xdg_positioner_set_anchor_rect(xdgpos,
            ANCHOR_X, ANCHOR_Y, ANCHOR_W, ANCHOR_H);

        xdg_positioner_set_anchor(xdgpos, XDG_POSITIONER_ANCHOR_TOP_RIGHT);
    }

    xdg_positioner_set_gravity(xdgpos, XDG_POSITIONER_GRAVITY_BOTTOM_RIGHT);

    //サーバー側で位置やサイズを調整できるようにする

    xdg_positioner_set_constraint_adjustment(xdgpos,
        XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_FLIP_X
        | XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_SLIDE_Y
        | XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_RESIZE_X
        | XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_RESIZE_Y);

    return xdgpos;
}

/* ポップアップウィンドウ作成 */

popupwindow *popupwindow_create(client *cl,Toplevel *parent,
    int width,int height,uint32_t serial,int pos_point)
{
    popupwindow *p;
    struct xdg_positioner *xdgpos;

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

    p->cl = cl;
    p->parent = parent;

    surface_create(&p->sf, cl, width, height);

    imagebuf_fill(p->sf.img, 0xff0000ff);

    //xdg_surface

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

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

    //xdg_positioner 作成

    xdgpos = _create_popup_positioner(cl, width, height, pos_point);

    //xdg_popup 作成

    p->xdg_popup = xdg_surface_get_popup(p->xdg_surface,
        parent->xdg_surface, xdgpos);

    xdg_positioner_destroy(xdgpos);

    xdg_popup_add_listener(p->xdg_popup, &g_popup_listener, p);

    //グラブ

    xdg_popup_grab(p->xdg_popup, cl->seat, serial);

    //適用

    wl_surface_commit(p->sf.surface);

    return p;
}

/* 破棄 */

void popupwindow_destroy(popupwindow *p)
{
    if(p)
    {
        xdg_popup_destroy(p->xdg_popup);
        xdg_surface_destroy(p->xdg_surface);

        surface_destroy(&p->sf);
    
        free(p);
    }
}


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


static const char *_get_surface_name(client_ex *p,struct wl_surface *surface)
{
    return (surface == p->win->sf.surface)? "toplevel": "popup";
}


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;

    printf("wl_pointer # enter | %s\n", _get_surface_name(p, surface));

    p->enter_surface = surface;

    client_set_cursor(CLIENT(p), serial);
}

static void _pointer_leave(void *data, struct wl_pointer *pointer,
    uint32_t serial, struct wl_surface *surface)
{
    client_ex *p = (client_ex *)data;

    printf("wl_pointer # leave | %s\n", _get_surface_name(p, surface));

    p->enter_surface = NULL;
}

static void _pointer_motion(void *data, struct wl_pointer *pointer,
    uint32_t time, wl_fixed_t x, wl_fixed_t y)
{
    printf("wl_pointer # motion | (%d, %d)\n", x >> 8, 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;
    Toplevel *win;

    printf("wl_pointer # button | %s, serial:%u\n",
        (state == WL_POINTER_BUTTON_STATE_PRESSED)? "press":"release",
        serial);

    if(state != WL_POINTER_BUTTON_STATE_PRESSED)
        return;

    win = p->win;

    //

    if(p->enter_surface == win->sf.surface)
    {
        //トップレベル

        if(p->popup)
            _clientex_popup_finish(p);
        else
        {
            switch(button)
            {
                //左ボタン:ウィンドウ位置移動
                case BTN_LEFT:
                    xdg_toplevel_move(win->xdg_toplevel, p->b.seat, serial);
                    break;
                //右ボタン:ポップアップ表示
                case BTN_RIGHT:
                    printf("-- create popup\n");

                    p->popup = popupwindow_create(CLIENT(p), win, 200, 300,
                        serial, p->popup_pos);
                    break;
                //中ボタン:終了
                case BTN_MIDDLE:
                    p->b.finish_loop = 1;
                    break;
            }
        }
    }
    else
    {
        //ポップアップ
        
        if(button == BTN_RIGHT)
            _clientex_popup_finish(p);
    }
}

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

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

static void _keyboard_press(client *cl,uint32_t key,uint32_t serial)
{
    client_ex *p = (client_ex *)cl;

    //Space キーで表示位置を切り替え
    
    if(key == KEY_SPACE)
    {
        p->popup_pos ^= 1;
        printf("# popup pos: %s\n", (p->popup_pos)? "(0,0)": "anchor");
    }
}

/* 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_KEYBOARD | CLIENT_INIT_FLAGS_CREATE_CURSOR;

    p->b.pointer_listener = &g_pointer_listener;
    p->b.xdg_shell_need_ver = 3;
    p->b.func_keyboard_key_press = _keyboard_press;

    client_init(CLIENT(p));

    //ウィンドウ

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

    imagebuf_fill(win->sf.img, 0xffff0000);
    imagebuf_box(win->sf.img, 0, 0, 256, 256, 0xff00ff00);
    imagebuf_box(win->sf.img, ANCHOR_X, ANCHOR_Y, ANCHOR_W, ANCHOR_H, 0xff000000);

    //イベントループ

    client_loop_simple(CLIENT(p));

    //解放

    toplevel_destroy(win);

    client_destroy(CLIENT(p));

    return 0;
}
ポップアップについて
xdg_popup を使うと、ポップアップウィンドウや、ツールチップなどを表示できます。

xdg_popup が xdg_toplevel と異なる点は、親ウィンドウ内の座標を基準にして、表示位置を指定できることと、一度表示した後は、ウィンドウを削除する以外の機能はないということです。
作成手順
ポップアップの作成手順は以下の通りです。

  1. wl_surface を作成。
  2. xdg_surface を作成。
  3. 親ウィンドウに配置するための xdg_positioner を作成。
  4. xdg_positioner を指定して、xdg_popup を作成。
  5. xdg_positioner を破棄。
  6. xdg_popup にハンドラを設定。
  7. ポインタ操作が必要な場合は、xdg_popup_grab() を実行する。
    (単に表示するだけのツールチップなどでは必要ない)
  8. wl_surface_commit() で、以上の内容を適用する。
処理
ポップアップの作成後は、以下のように処理を行っていきます。

  • ポップアップウィンドウが表示される前に、xdg_popup:configure イベントが来る。
    ここでは、サーバーによって調整された、最終的なウィンドウの位置やサイズが渡される。
    ここで wl_buffer イメージを作成しても良いし、サイズが変更された場合はイメージの再作成などを行う。

  • その後 xdg_surface:configure イベントが来るので、xdg_surface_ack_configure() を実行した後、更新処理を行う。

  • サーバー側の判断でポップアップが終了する時、xdg_popup:popup_done イベントが来るので、ポップアップの破棄などの処理を行う。
    任意のタイミングで終了したい時は、xdg_popup_destroy() で破棄する。
xdg_positioner
xdg_popup を作成する前に、xdg_positioner が必要になるので、まずはそちらを作成しておきます。

xdg_positioner は、親ウィンドウの任意の位置にウィンドウを表示する時、表示するウィンドウの位置やサイズ、配置などの設定を指定するためのものです。
作成
//作成

struct xdg_positioner *xdg_wm_base_create_positioner(
    struct xdg_wm_base *xdg_wm_base);

//破棄

void xdg_positioner_destroy(struct xdg_positioner *xdg_positioner);

xdg_wm_base から、xdg_positioner を作成します。
サイズのセット
void xdg_positioner_set_size(struct xdg_positioner *xdg_positioner,
    int32_t width, int32_t height);

表示するウィンドウのサイズを設定します。

※実際には、サーバー側でサイズが調整される場合があります。
アンカー矩形の設定
void xdg_positioner_set_anchor_rect(
    struct xdg_positioner *xdg_positioner,
    int32_t x, int32_t y, int32_t width, int32_t height);

アンカー矩形を、親の座標で指定します。

アンカー矩形は、ウィンドウの位置を決める時に基準となる、四角形の範囲です。
実際の表示位置は、この矩形の、4隅の点や4辺を基準にして調整されます。

例えば、親ウィンドウ内にボタンがあって、そのボタンの位置を基準にして、周囲にポップアップウィンドウを表示したい場合、親ウィンドウ内でのボタンの位置とサイズを指定します。

任意の点の位置に表示したい場合は、(x, y) - (1 x 1) とします。

※xdg_popup では、デスクトップ画面の任意の位置 (クライアントのウィンドウ外) にポップアップを表示することはできません。
アンカー矩形の辺を設定
void xdg_positioner_set_anchor(
    struct xdg_positioner *xdg_positioner, uint32_t anchor);

子の表示位置を決める時に、アンカー矩形のどの位置・辺を基準にするかを指定します。

以下の値から、一つ指定します。

enum xdg_positioner_anchor {
    XDG_POSITIONER_ANCHOR_NONE = 0,
    XDG_POSITIONER_ANCHOR_TOP = 1,
    XDG_POSITIONER_ANCHOR_BOTTOM = 2,
    XDG_POSITIONER_ANCHOR_LEFT = 3,
    XDG_POSITIONER_ANCHOR_RIGHT = 4,
    XDG_POSITIONER_ANCHOR_TOP_LEFT = 5,
    XDG_POSITIONER_ANCHOR_BOTTOM_LEFT = 6,
    XDG_POSITIONER_ANCHOR_TOP_RIGHT = 7,
    XDG_POSITIONER_ANCHOR_BOTTOM_RIGHT = 8,
};

NONE の場合は、矩形内の中央に配置されます。
TOP または BOTTOM の場合、X は中央、Y は上下の端に配置されます。
LEFT または RIGHT の場合、X は左右の端、Y は中央に配置されます。

TOP_RIGHT などの場合、矩形の指定隅が基準になります。左上や右下などです。

後述する設定で、位置の反転を許可した場合、サーバー側の調整によって、反対側の辺に配置される場合があります。
配置方向を設定
void xdg_positioner_set_gravity(
    struct xdg_positioner *xdg_positioner, uint32_t gravity);

子の配置方向を指定します。

enum xdg_positioner_gravity {
    XDG_POSITIONER_GRAVITY_NONE = 0,
    XDG_POSITIONER_GRAVITY_TOP = 1,
    XDG_POSITIONER_GRAVITY_BOTTOM = 2,
    XDG_POSITIONER_GRAVITY_LEFT = 3,
    XDG_POSITIONER_GRAVITY_RIGHT = 4,
    XDG_POSITIONER_GRAVITY_TOP_LEFT = 5,
    XDG_POSITIONER_GRAVITY_BOTTOM_LEFT = 6,
    XDG_POSITIONER_GRAVITY_TOP_RIGHT = 7,
    XDG_POSITIONER_GRAVITY_BOTTOM_RIGHT = 8,
};

指定されていない軸は、中央揃えにされます。

BOTTOM_RIGHT の場合は、アンカーの基準位置がウィンドウの左上として、右下方向に配置します。
TOP_LEFT の場合は、アンカーの基準位置がウィンドウの右下として、左上方向に配置します。
サーバーによる調整を設定
void xdg_positioner_set_constraint_adjustment(
    struct xdg_positioner *xdg_positioner, uint32_t constraint_adjustment);

設定された位置やサイズで、実際に子を配置した時に、子が画面からはみ出す場合、サーバー側で位置やサイズを調整させるようにすることができます。

指定する値はフラグになっているので、必要であれば複数指定してください。

enum xdg_positioner_constraint_adjustment {
    XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_NONE = 0,
    XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_SLIDE_X = 1,
    XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_SLIDE_Y = 2,
    XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_FLIP_X = 4,
    XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_FLIP_Y = 8,
    XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_RESIZE_X = 16,
    XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_RESIZE_Y = 32
};

デフォルトは NONE です。
指定されていない軸は、調整を許可しないという意味になります。
同じ軸で複数の値が指定されている場合は、優先順によって、適切なものが一つ使われます。

SLIDEウィンドウ位置をスライドして移動します。
画面の端からはみ出している場合は、画面内に収まるように移動します。
FLIPウィンドウ位置を反転します。
各軸ごとに、アンカー矩形の反対側の辺と、反対側の配置方向で調整します。
RESIZEウィンドウサイズを変更します。
幅や高さが画面サイズを超える場合、画面サイズに合わせます。
位置の調整がなく、サイズ変更のみ許可している場合は、位置はそのままで、画面内に収まるようにサイズを小さくします。
オフセット位置を設定
void xdg_positioner_set_offset(
    struct xdg_positioner *xdg_positioner, int32_t x, int32_t y);

子の表示位置を、実際に表示される位置から、指定 px 分ずらします。
xdg_popup
xdg_positioner が作成できたら、xdg_popup を作成します。
作成
//作成

struct xdg_popup *xdg_surface_get_popup(struct xdg_surface *xdg_surface,
    struct xdg_surface *parent, struct xdg_positioner *positioner);

//破棄

void xdg_popup_destroy(struct xdg_popup *xdg_popup);

//ハンドラ設定

int xdg_popup_add_listener(struct xdg_popup *xdg_popup,
    const struct xdg_popup_listener *listener, void *data);

struct xdg_popup_listener {
    void (*configure)(void *data, struct xdg_popup *xdg_popup,
        int32_t x, int32_t y, int32_t width, int32_t height);
    void (*popup_done)(void *data, struct xdg_popup *xdg_popup);
    //ver 3
    void (*repositioned)(void *data, struct xdg_popup *xdg_popup, uint32_t token);
};

parent には、親サーフェスを指定します。
xdg_toplevel か xdg_popup のサーフェスを指定できます。

任意のタイミングでポップアップを閉じたい場合は、xdg_popup_destroy() で破棄してください。

xdg_positioner は、xdg_popup を作成する時のみ必要になるので、作成後はすぐに破棄して構いません。
グラブ
ポップアップの範囲外がクリックされたら、ポップアップを終了させるようにしたい場合、ポインタのグラブを行う必要があります。

ツールチップウィンドウなど、単純に一定期間表示するだけのポップアップであれば、グラブは必要ありませんが、ポインタ操作によって何らかの処理を行うポップアップの場合は、ポップアップの開始時に、以下の関数でグラブを行う必要があります。

void xdg_popup_grab(struct xdg_popup *xdg_popup, struct wl_seat *seat, uint32_t serial);

serial には、ポインタのボタン押しや、キー押しイベント時に渡されたシリアル値を指定します。

何らかの理由でグラブが行えなかった場合は、すぐに xdg_popup:popup_done イベントが来て、ポップアップはキャンセル扱いになります。

階層メニューなど、ポップアップの実行中に、さらにポップアップを表示したい場合は、1つのポップアップごとに1回実行することができます。
その場合は、最後に作成したポップアップから順に、ポップアップを破棄する必要があります。

終了処理
グラブ中に、クライアントのウィンドウ内以外の位置でボタンが押された場合、サーバー側の処理によって xdg_popup:popup_done イベントが来るので、そこでポップアップを終了させます。

クライアントの他のウィンドウ上のポインタイベントは、通常通り送られてくるので、注意してください。
(ポップアップのサーフェスに対して送られてくるのではなく、それぞれのサーフェスに対して送られてきます)

そのため、ポップアップ以外のウィンドウ上でボタンが押された時は、クライアント側で、ポップアップを閉じる処理を行う必要があります。
xdg_popup のイベント
struct xdg_popup_listener {
    void (*configure)(void *data, struct xdg_popup *xdg_popup,
        int32_t x, int32_t y, int32_t width, int32_t height);
    void (*popup_done)(void *data, struct xdg_popup *xdg_popup);
    //ver 3
    void (*repositioned)(void *data, struct xdg_popup *xdg_popup, uint32_t token);
};
configure
サーバーによって、ポップアップの位置やサイズが確定した時に来ます。
最初にポップアップが表示される時は、常に送信されます。

サーバーによる位置やサイズの調整を許可した場合は、ここで、調整された値が送られてきます。

このイベントの後に、xdg_surface:configure イベントが来ます。
popup_done
ポップアップの表示後、サーバー側の判断で、ポップアップを終了するタイミングが来た時に送られてきます。
ポップアップを実際に終了させる場合は、xdg_popup_destroy() で破棄します。

クライアントが任意のタイミングで xdg_popup_destroy() を実行した場合、このイベントは来ません。
repositioned (ver 3)
xdg_popup_reposition() によって、ポップアップの表示中に位置を変更した時、その応答として送信されます。