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

ウィンドウの移動とリサイズ
ここでは、ユーザーの操作による、ウィンドウの移動とリサイズを行います。

$ cc -o test 11_resize.c client.c imagebuf.c -lwayland-client -lwayland-cursor -lrt

ウィンドウの端にポインタを乗せると、位置に対応したカーソルに変わり、左ボタンのドラッグでリサイズが出来ます。
それ以外の部分でドラッグすると、ウィンドウ位置を移動できます。

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

<11_resize.c>
/******************************
 * ウィンドウの移動とリサイズ
 *
 * ウィンドウの端で左ボタンを押すと、リサイズ。
 * ウィンドウの真ん中で左ボタンを押すと、位置の移動。
 * 中ボタンで終了。
 ******************************/

#include <stdio.h>
#include <string.h>
#include <linux/input.h>

#include <wayland-client.h>
#include <wayland-cursor.h>

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


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

typedef struct
{
    Client b;

    Window *win;
    
    //ポインタデータ
    uint32_t serial_enter;
    int lastx,lasty,
        last_area;

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

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

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

//ポインタの現在位置がどのエリアか

enum
{
    AREA_MOVE = 0,

    //wl_shell_surface_resize() の edges に対応
    AREA_RESIZE_TOP = 1,
    AREA_RESIZE_BOTTOM = 2,
    AREA_RESIZE_LEFT = 4,
    AREA_RESIZE_RIGHT = 8,
    AREA_RESIZE_TOP_LEFT = AREA_RESIZE_TOP | AREA_RESIZE_LEFT, //5
    AREA_RESIZE_BOTTOM_LEFT = AREA_RESIZE_BOTTOM | AREA_RESIZE_LEFT, //6
    AREA_RESIZE_TOP_RIGHT = AREA_RESIZE_TOP | AREA_RESIZE_RIGHT, //9
    AREA_RESIZE_BOTTOM_RIGHT = AREA_RESIZE_BOTTOM | AREA_RESIZE_RIGHT //10
};

//カーソルの形状

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

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


//--------------------
// sub
//--------------------


/* カーソル変更 */

static void _set_cursor(ClientEx *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_area(Window *win,int x,int y)
{
    int f = 0;

    if(x < FRAME_WIDTH)
        f |= AREA_RESIZE_LEFT;
    else if(x >= win->width - FRAME_WIDTH)
        f |= AREA_RESIZE_RIGHT;
    
    if(y < FRAME_WIDTH)
        f |= AREA_RESIZE_TOP;
    else if(y >= win->height - FRAME_WIDTH)
        f |= AREA_RESIZE_BOTTOM;

    return f;
}

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

static void _enter_motion(ClientEx *p,wl_fixed_t x,wl_fixed_t y)
{
    int area;
    uint8_t area_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
    };

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

    area = _get_point_area(p->win, x, y);

    p->lastx = x;
    p->lasty = y;
    p->last_area = area;

    //カーソル変更

    _set_cursor(p, area_to_cursor[area]);
}


//---------------------
// Window
//---------------------


/* ウィンドウのサイズ変更が要求された時 */

static void _Window_configure(Window *p,int width,int height)
{
    printf("wl_shell_surface # configure | %dx%d\n", width, height);

    if(width < 50) width = 50;
    if(height < 50) height = 50;

    //サイズが変わったら更新

    if(width != p->width || height != p->height)
    {
        p->width = width;
        p->height = height;

        //イメージバッファを再作成

        ImageBuf_destroy(p->img);

        p->img = ImageBuf_create(p->client->shm, width, height);

        //更新

        ImageBuf_fill(p->img, 0xffff0000);
        Window_update(p);
    }
}


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

    printf("wl_pointer # enter\n");

    _enter_motion(p, x, y);

    p->serial_enter = serial;
}

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

    //次の enter 時、常にカーソルをセットさせる
    ((ClientEx *)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((ClientEx *)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)
{
    ClientEx *p = (ClientEx *)data;

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

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)
{
}

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

    wl_cursor_theme_destroy(p->cursor_theme);

    wl_surface_destroy(p->cursor_surface);
}


int main(void)
{
    ClientEx *p;
    Window *win;

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

    p->b.destroy = _clientex_destroy;
    p->b.init_flags = INIT_FLAGS_SEAT | 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 = Window_create(CLIENT(p), 256, 256, NULL);

    win->configure = _Window_configure;

    ImageBuf_fill(win->img, 0xffff0000);
    Window_update(win);

    //

    Client_loop_simple(CLIENT(p));

    //解放

    Window_destroy(win);

    Client_destroy(CLIENT(p));

    return 0;
}
解説
通常、ウィンドウには、タイトルバーやウィンドウ枠があり、タイトルバーをドラッグするとウィンドウ位置が移動できたり、ウィンドウ枠をドラッグするとウィンドウサイズが変更できますが、Wayland では基本的に、それらの処理はすべてクライアントが実装する必要があります。

今回の場合は、ウィンドウの端の部分がドラッグされたらリサイズ動作を行わせ、真ん中がドラッグされたら、ウィンドウ位置の移動を行わせます。
wl_pointer : enter/motion イベント
ボタンが押された時 (wl_pointer:button イベント時) は、現在のポインタ位置が送られてこないため、enter と motion イベント時に現在のカーソル位置を記録しておきます。

また、その時、現在のポインタ位置が、ウィンドウ上のどのエリアにあるかも判定して、記録しておきます。

エリアが変わった時は、動作に対応したカーソル形状に変更します。

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

## 移動を開始

void wl_shell_surface_move(
  struct wl_shell_surface *wl_shell_surface,
  struct wl_seat *seat, uint32_t serial);

## リサイズを開始

void wl_shell_surface_resize(
  struct wl_shell_surface *wl_shell_surface,
  struct wl_seat *seat, uint32_t serial, uint32_t edges);

これらの関数を呼ぶと、サーバー側が、渡された wl_seat を使って、ユーザーによる移動/リサイズの動作を行ってくれます。

これらの関数が実行され、ボタンが押されている間は、カーソル形状はサーバー側で自動的に各動作の形に変更されるので、クライアント側でカーソル形状を設定する必要はありません。

なお、これらの処理が開始された時は leave イベントが送信され、操作が終わった時に、ポインタがクライアントのウィンドウ上にあれば、enter イベントが送信されます。

wl_shell_surface_resize()
引数の edges は、enum wl_shell_surface_resize で定義された値を指定します。
どの位置でサイズを変更するかの値となります。

WL_SHELL_SURFACE_RESIZE_NONE = 0
WL_SHELL_SURFACE_RESIZE_TOP = 1
WL_SHELL_SURFACE_RESIZE_BOTTOM = 2
WL_SHELL_SURFACE_RESIZE_LEFT = 4
WL_SHELL_SURFACE_RESIZE_TOP_LEFT = 5
WL_SHELL_SURFACE_RESIZE_BOTTOM_LEFT = 6
WL_SHELL_SURFACE_RESIZE_RIGHT = 8
WL_SHELL_SURFACE_RESIZE_TOP_RIGHT = 9
WL_SHELL_SURFACE_RESIZE_BOTTOM_RIGHT = 10

値を見てみると、TOP, BOTTOM, LEFT, RIGHT がフラグの値になっていて、他は、「TOP + LEFT」というように、2つのフラグを組み合わせた値となっています。

なお、この関数だけでは、実際のウィンドウのリサイズ処理は行われないため、クライアントがイベントを受け取った時に、イメージバッファを指定サイズに変更して、ウィンドウのリサイズ処理を行う必要があります。
リサイズ処理
サイズを変えるタイミングが来た時には、wl_shell_surface の configure イベントが送信されるので、このイベント内で、ウィンドウのリサイズ処理を行います。

イメージバッファのサイズ変更 (再作成) をして、ウィンドウの再描画を行う必要があります。

サーバー側は、ウィンドウのサイズを、ドラッグ時のポインタ移動による変化を元に推奨値として提示しているだけなので、クライアント側では、それを元に、好きなサイズに調整することができます。

例えば、ウィンドウの最小サイズを決めて、それ以下のサイズにならないようにするといったことが出来ます。
今回の場合は、50 x 50 px を最小サイズとしています。

なお、ウィンドウの位置はサーバー側で調整してくれるので、クライアントはサイズを変更するだけで OK です。
リサイズの方向を意識する必要はありません。

weston 上で実行すると、最初の左 or 上方向のリサイズ時だけ動作がおかしくなりますが、おそらく weston 側のバグです。