Wayland: ウィンドウの移動とリサイズ

ウィンドウの移動とリサイズ
今回は、ユーザーのポインタ操作による、ウィンドウの移動とリサイズをやってみます。

$ cc -o test 13-resize.c client.c imagebuf.c xdg-shell-protocol.c -lwayland-client -lwayland-cursor -lrt

ウィンドウの上下左右の端でリサイズ操作、それ以外の範囲内で、ウィンドウ位置の移動を行います。
対応する位置によってカーソル形状が変わるので、左ボタンのドラッグで、移動またはリサイズの操作を行います。

中ボタン押しで終了します。

<13-resize.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;

    Toplevel *win;
    
    uint32_t serial_enter;
    int last_edge;

    //カーソルデータ
    struct wl_surface *cursor_surface;
    struct wl_cursor_theme *cursor_theme;
    int cursor_current;    //現在のカーソル形状
}client_ex;

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

//サイズ変更の枠の幅
#define FRAME_WIDTH  5

//カーソルの形状
enum
{
    CURSOR_MOVE,
    CURSOR_RESIZE_LEFT,
    CURSOR_RESIZE_RIGHT,
    CURSOR_RESIZE_TOP,
    CURSOR_RESIZE_BOTTOM,
    CURSOR_RESIZE_TOP_LEFT,
    CURSOR_RESIZE_TOP_RIGHT,
    CURSOR_RESIZE_BOTTOM_LEFT,
    CURSOR_RESIZE_BOTTOM_RIGHT
};

//形状に対応するカーソル名
static const char *g_cursor_name[] = {
    "move",
    "left_side", "right_side", "top_side", "bottom_side",
    "top_left_corner", "top_right_corner",
    "bottom_left_corner", "bottom_right_corner"
};

static const uint8_t g_edge_to_cursor[] = {
    CURSOR_MOVE, CURSOR_RESIZE_TOP, CURSOR_RESIZE_BOTTOM, 0,
    CURSOR_RESIZE_LEFT, CURSOR_RESIZE_TOP_LEFT,
    CURSOR_RESIZE_BOTTOM_LEFT, 0, CURSOR_RESIZE_RIGHT,
    CURSOR_RESIZE_TOP_RIGHT, CURSOR_RESIZE_BOTTOM_RIGHT
};


//=====================
// sub
//=====================


/* カーソル変更 */

static void _set_cursor(client_ex *p,int no)
{
    struct wl_cursor *cursor;
    struct wl_cursor_image *img;
    struct wl_buffer *buffer;

    //現在と同じ画像ならそのまま

    if(p->cursor_current == no) return;

    p->cursor_current = no;

    printf("* change cursor: '%s'\n", g_cursor_name[no]);

    //カーソル変更

    cursor = wl_cursor_theme_get_cursor(p->cursor_theme, g_cursor_name[no]);
    if(!cursor) return;

    img = cursor->images[0];

    buffer = wl_cursor_image_get_buffer(img);
    if(!buffer) return;

    wl_surface_attach(p->cursor_surface, buffer, 0, 0);
    wl_surface_damage(p->cursor_surface, 0, 0, img->width, img->height);
    wl_surface_commit(p->cursor_surface);

    wl_pointer_set_cursor(p->b.pointer, p->serial_enter, p->cursor_surface,
        img->hotspot_x, img->hotspot_y);
}

/* サーフェス内の位置がどのエリア内か */

static int _get_point_edge(Toplevel *win,int x,int y)
{
    int f = 0,w,h;

    w = win->sf.img->width;
    h = win->sf.img->height;

    if(x < FRAME_WIDTH)
        f |= XDG_TOPLEVEL_RESIZE_EDGE_LEFT;
    else if(x >= w - FRAME_WIDTH)
        f |= XDG_TOPLEVEL_RESIZE_EDGE_RIGHT;
    
    if(y < FRAME_WIDTH)
        f |= XDG_TOPLEVEL_RESIZE_EDGE_TOP;
    else if(y >= h - FRAME_WIDTH)
        f |= XDG_TOPLEVEL_RESIZE_EDGE_BOTTOM;

    return f;
}

/* ポインタの enter/motion 時 */

static void _enter_motion(client_ex *p,wl_fixed_t x,wl_fixed_t y)
{
    int edge;

    x >>= 8;
    y >>= 8;

    edge = _get_point_edge(p->win, x, y);

    p->last_edge = edge;

    //カーソル変更

    _set_cursor(p, g_edge_to_cursor[edge]);
}


//=====================
// toplevel
//=====================


/* xdg_toplevel:configure */

static void _toplevel_configure(Toplevel *p,int width,int height)
{
    printf("xdg_toplevel # configure | %d x %d\n", width, height);

    if(width && height)
    {
        if(toplevel_resize(p, width, height))
            imagebuf_fill(p->sf.img, 0xffff0000);
    }
}


//=====================
// 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;

    printf("wl_pointer # enter\n");

    p->serial_enter = serial;

    _enter_motion(p, x, y);
}

static void _pointer_leave(void *data, struct wl_pointer *pointer,
    uint32_t serial, struct wl_surface *surface)
{
    printf("wl_pointer # leave\n");

    //次の enter 時、常にカーソルをセットさせる
    ((client_ex *)data)->cursor_current = -1;
}

static void _pointer_motion(void *data, struct wl_pointer *pointer,
    uint32_t time, wl_fixed_t x, wl_fixed_t y)
{
    _enter_motion((client_ex *)data, x, y);
}

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;

    if(button == BTN_MIDDLE)
        //中ボタンで終了
        p->b.finish_loop = 1;
    else if(button == BTN_LEFT)
    {
        //左ボタン
        
        if(p->last_edge == XDG_TOPLEVEL_RESIZE_EDGE_NONE)
            //ユーザーによるウィンドウ位置の移動を要求
            xdg_toplevel_move(p->win->xdg_toplevel, p->b.seat, serial);
        else
        {
            //ユーザーによるリサイズを要求
            xdg_toplevel_resize(p->win->xdg_toplevel, p->b.seat,
                serial, p->last_edge);
        }
    }
}

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

    wl_cursor_theme_destroy(p->cursor_theme);

    wl_surface_destroy(p->cursor_surface);
}

/* 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));

    //カーソル

    p->cursor_current = -1;

    p->cursor_surface = wl_compositor_create_surface(p->b.compositor);

    p->cursor_theme = wl_cursor_theme_load(NULL, 32, p->b.shm);

    //ウィンドウ

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

    win->func_toplevel_configure = _toplevel_configure;

    imagebuf_fill(win->sf.img, 0xffff0000);

    xdg_toplevel_set_min_size(win->xdg_toplevel, 50, 50);

    //

    client_loop_simple(CLIENT(p));

    //解放

    toplevel_destroy(win);

    client_destroy(CLIENT(p));

    return 0;
}
解説
通常のウィンドウには、タイトルバーやウィンドウ枠といった、ウィンドウ装飾が付属しています。

タイトルバーをドラッグすると、ウィンドウ位置が移動できたり、ウィンドウ枠をドラッグすると、ウィンドウサイズが変更できたりしますが、Wayland では、それらの処理はすべてクライアントが実装する必要があります。

今回の場合は、ウィンドウの端の部分が左ドラッグされたら、リサイズ操作を行い、それ以外の内側が左ドラッグされたら、ウィンドウ位置の移動を行います。
wl_pointer : enter/motion イベント
enter と motion 時に、現在のポインタ位置から、その位置がリサイズのエリアなのか、移動のエリアなのかを判定します。

リサイズの場合は、上下左右4方向に加えて、斜めの4方向にも対応しています。

xdg_toplevel でリサイズの操作を行わせる場合は、以下の値で、どの端をリサイズするかを指定できます。

enum xdg_toplevel_resize_edge {
    XDG_TOPLEVEL_RESIZE_EDGE_NONE = 0,
    XDG_TOPLEVEL_RESIZE_EDGE_TOP = 1,
    XDG_TOPLEVEL_RESIZE_EDGE_BOTTOM = 2,
    XDG_TOPLEVEL_RESIZE_EDGE_LEFT = 4,
    XDG_TOPLEVEL_RESIZE_EDGE_TOP_LEFT = 5,
    XDG_TOPLEVEL_RESIZE_EDGE_BOTTOM_LEFT = 6,
    XDG_TOPLEVEL_RESIZE_EDGE_RIGHT = 8,
    XDG_TOPLEVEL_RESIZE_EDGE_TOP_RIGHT = 9,
    XDG_TOPLEVEL_RESIZE_EDGE_BOTTOM_RIGHT = 10,
};

TOP, BOTTOM, LEFT, RIGHT がぞれぞれフラグになっており、TOP_LEFT は (TOP | LEFT) という形で指定できるので、この値を、そのままエリアの値として使います。
NONE の場合は、移動のエリアになります。

ポインタの移動時にエリアが変わった場合は、それに対応したカーソル形状に変更します。
ただし、今回は、カーソルのアニメーションには対応していません。
wl_pointer : button イベント
左ボタンが押された時、現在のポインタのエリア位置によって、ウィンドウの端ならリサイズ、それ以外はウィンドウ位置の移動を行います。

//移動の操作を開始

void xdg_toplevel_move(struct xdg_toplevel *xdg_toplevel, struct wl_seat *seat, uint32_t serial);

//リサイズの操作を開始

void xdg_toplevel_resize(struct xdg_toplevel *xdg_toplevel,
    struct wl_seat *seat, uint32_t serial, uint32_t edges);

これらの関数を実行した場合、サーバー側が、渡された wl_seat を使って、ユーザーによるウィンドウ位置の移動・リサイズの動作を開始します。

edges は、サイズを変更する端の位置で、enum xdg_toplevel_resize_edge の値です。

ボタンが押されている間、カーソル形状は、サーバー側で自動的に変更されます。

なお、これらの処理が開始された時は、元のサーフェスに leave イベントが送信され、ボタンが離されて操作が終わった時に、ポインタがクライアントのウィンドウ上にあれば、ポインタの下にあるサーフェスで enter イベントが送信されます。
リサイズ処理
リサイズ動作中、またはリサイズが確定した時に、ウィンドウのサイズを変更する必要がある場合は、xdg_toplevel の configure イベントが送信されるので、width, height 引数を元に、ウィンドウのイメージをリサイズして、再描画を行う必要があります。
(width, height が 0 の場合、状態の変更だけで、サイズの変更はありません)

リサイズ動作中は、states 引数に XDG_TOPLEVEL_STATE_RESIZING の値が入るので、リサイズ中に独自の処理を行いたい場合は、この値で判断します。

クライアント側は、ウィンドウサイズを、渡された width, height より大きいサイズには変更できませんが、これより小さいサイズにすることはできます。

今回は xdg_toplevel_set_min_size() を使って、ウィンドウの最小サイズを 50 x 50 にしているので、リサイズ中は 50 x 50 より小さいサイズが来ることはありません。
(ただし、サーバーの実装にもよる)