/* ========================================================================= */
/**
 * @file window.c
 *
 * @copyright
 * Copyright (c) 2026 Philipp Kaeser (kaeser@gubbe.ch)
 * Copyright 2025 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include "window.h"

#include <libbase/libbase.h>
#include <libbase/plist.h>
#include <linux/input-event-codes.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <wayland-server-core.h>
#define WLR_USE_UNSTABLE
#include <wlr/types/wlr_keyboard.h>
#include <wlr/types/wlr_output.h>
#include <wlr/types/wlr_output_layout.h>
#include <wlr/util/edges.h>
#undef WLR_USE_UNSTABLE

#include "bordered.h"
#include "box.h"
#include "container.h"
#include "input.h"
#include "menu_item.h"
#include "resizebar.h"
#include "test.h"  // IWYU pragma: keep
#include "tile.h"
#include "titlebar.h"

/* == Declarations ========================================================= */

/** Window handle. */
struct _wlmtk_window_t {
    /** Bordered, wraps around the box. */
    wlmtk_bordered_t          bordered;
    /** Original virtual method table of the window's element superclass. */
    wlmtk_element_vmt_t       orig_super_element_vmt;

    /** Composed of a box: Holds decoration, popup container and content. */
    wlmtk_box_t               box;

    /** Element in @ref wlmtk_workspace_t::windows, when mapped. */
    bs_dllist_node_t          dlnode;

    /** Events for this window. */
    wlmtk_window_events_t    events;
    /** Client information. See @ref wlmtk_window_set_client. */
    wlmtk_util_client_t       client;

    /** Container for the content. */
    wlmtk_container_t         content_container;
    /** The content. */
    wlmtk_element_t           *content_element_ptr;

    /** The workspace, when mapped to a workspace. NULL otherwise. */
    wlmtk_workspace_t         *workspace_ptr;

    /** Preferred output. See @ref wlmtk_window_set_wlr_output. */
    struct wlr_output         *wlr_output_ptr;

    /** Reference to the menu's style. */
    wlmtk_menu_style_ref_t    *menu_style_ref_ptr;
    /** Reference to the window's style. */
    wlmtk_window_style_ref_t  *style_ref_ptr;
    /** The window's style. Implies holding a reference. */
    const struct wlmtk_window_style *style_ptr;

    /** The titlebar, when server-side decorated. */
    wlmtk_titlebar_t          *titlebar_ptr;
    /** The resize-bar, when server-side decorated. */
    wlmtk_resizebar_t         *resizebar_ptr;

    /** The window's title. */
    char                      *title_ptr;

    /** Properties of the window. See @ref wlmtk_window_property_t. */
    uint32_t                  properties;
    /** Modifier to trigger moving the window, on pointer drag. */
    uint32_t                  move_modifier;

    /**
     * Position of the window, and size of the content window when not in
     * fullscreen or maximized state.
     */
    struct wlr_box            organic_bounding_box;
    /**
     * Committed content size. That may differ from the dimensions of
     * the content element. See @ref wlmtk_window_t::committed_size and
     * @ref wlmtk_window_get_size.
     */
    struct wlr_box            committed_size;

    /** Edges to anchor on when resizing. */
    uint32_t                  resize_edges;
    /** Current box size when resizing. */
    struct wlr_box            old_box;

    /** The window menu. */
    wlmtk_menu_t              *window_menu_ptr;
    /** Listener for then the popup menu requests to be closed. */
    struct wl_listener        menu_request_close_listener;

    /**
     * Whether an "inorganic" sizing operation is in progress, and thus size
     * changes should not record to @ref wlmtk_window_t::organic_bounding_box.
     *
     * This is eg. between @ref wlmtk_window_request_fullscreen and
     * @ref wlmtk_window_commit_fullscreen.
     */
    bool                      inorganic_sizing;
    /** Whether this window has server-side decorations. */
    bool                      server_side_decorated;
    /** Whether this windows is currently in fullscreen mode. */
    bool                      fullscreen;
    /** Whether this window is currently in maximized state. */
    bool                      maximized;
    /** Whether this window is currently shaded. */
    bool                      shaded;
    /** Whether this window is currently activated (has keyboard focus). */
    bool                      activated;
};

/** Type-safe holder for the window style's reference counter. */
struct _wlmtk_window_style_ref_t {
    /** Actual reference counter. */
    bs_ref_t                  reference;
};
/** Holds the reference and the style. */
struct wlmtk_window_style_holder {
    /** Reference counter */
    struct _wlmtk_window_style_ref_t wsr;
    /** Style */
    struct wlmtk_window_style style;
};

static bool _wlmtk_window_element_pointer_button(
    wlmtk_element_t *element_ptr,
    const wlmtk_button_event_t *button_event_ptr);
static void _wlmtk_window_element_layout(
    wlmtk_element_t *element_ptr);
static void _wlmtk_window_container_element_get_dimensions(
        wlmtk_element_t *element_ptr,
        int *x1_ptr,
        int *y1_ptr,
        int *x2_ptr,
        int *y2_ptr);

static void _wlmtk_window_apply_decoration(wlmtk_window_t *window_ptr);
static void _wlmtk_window_create_titlebar(wlmtk_window_t *window_ptr);
static void _wlmtk_window_create_resizebar(wlmtk_window_t *window_ptr);
static void _wlmtk_window_destroy_titlebar(wlmtk_window_t *window_ptr);
static void _wlmtk_window_destroy_resizebar(wlmtk_window_t *window_ptr);
static void _wlmtk_window_set_decoration_width(wlmtk_window_t *window_ptr);

static void _wlmtk_window_menu_request_close_handler(
    struct wl_listener *listener_ptr,
    void *data_ptr);

static void _wlmtk_window_style_destroy(bs_ref_t *ref_ptr);

static void _wlmtk_window_request_fullscreen_size(
    wlmtk_window_t *window_ptr,
    bool fullscreen);
static void _wlmtk_window_request_maximized_size(
    wlmtk_window_t *window_ptr,
    bool maximized);

/* == Data ================================================================= */

/** Virtual method table for the window's element superclass. */
static const wlmtk_element_vmt_t window_element_vmt = {
    .pointer_button = _wlmtk_window_element_pointer_button,
    .layout = _wlmtk_window_element_layout,
};

/** Virtual method table for the window's content superclass element. */
static const wlmtk_element_vmt_t _wlmtk_window_content_container_element_vmt = {
    .get_dimensions = _wlmtk_window_container_element_get_dimensions,
};

/** Descriptor for decoding the "Window" dictionary. */
static const bspl_desc_t _wlmtk_window_style_desc[] = {
    BSPL_DESC_DICT(
        "TitleBar", true, struct wlmtk_window_style, titlebar, titlebar,
        wlmtk_titlebar_style_desc),
    BSPL_DESC_DICT(
        "ResizeBar", true, struct wlmtk_window_style, resizebar, resizebar,
        wlmtk_resizebar_style_desc),
    BSPL_DESC_DICT(
        "Border", true, struct wlmtk_window_style, border, border,
        wlmtk_style_margin_desc),
    BSPL_DESC_DICT(
        "Margin", true, struct wlmtk_window_style, margin, margin,
        wlmtk_style_margin_desc),
    BSPL_DESC_SENTINEL()
};

/* == Exported methods ===================================================== */

/* ------------------------------------------------------------------------- */
wlmtk_window_t *wlmtk_window_create(
    wlmtk_element_t *content_element_ptr,
    wlmtk_window_style_ref_t *style_ref_ptr,
    wlmtk_menu_style_ref_t *menu_style_ref_ptr)
{
    wlmtk_window_t *window_ptr = logged_calloc(1, sizeof(wlmtk_window_t));
    if (NULL == window_ptr) return NULL;
    window_ptr->style_ref_ptr = style_ref_ptr;
    window_ptr->style_ptr = wlmtk_window_style_ref_retain(style_ref_ptr);
    window_ptr->menu_style_ref_ptr = menu_style_ref_ptr;
    wlmtk_menu_style_ref_retain(menu_style_ref_ptr);
    wl_signal_init(&window_ptr->events.state_changed);
    wl_signal_init(&window_ptr->events.request_close);
    wl_signal_init(&window_ptr->events.set_activated);
    wl_signal_init(&window_ptr->events.request_size);
    wl_signal_init(&window_ptr->events.request_fullscreen);
    wl_signal_init(&window_ptr->events.request_maximized);
    window_ptr->move_modifier = WLR_MODIFIER_ALT;

    if (!wlmtk_window_set_title(window_ptr, NULL)) goto error;

    if (!wlmtk_container_init(&window_ptr->content_container)) goto error;
    if (!wlmtk_box_init(
            &window_ptr->box,
            WLMTK_BOX_VERTICAL,
            &window_ptr->style_ptr->margin)) {
        goto error;
    }
    if (!wlmtk_bordered_init(
            &window_ptr->bordered,
            wlmtk_box_element(&window_ptr->box),
            &window_ptr->style_ptr->border)) {
        goto error;
    }
    window_ptr->orig_super_element_vmt = wlmtk_element_extend(
        &window_ptr->bordered.super_container.super_element,
        &window_element_vmt);

    wlmtk_element_extend(
        &window_ptr->content_container.super_element,
        &_wlmtk_window_content_container_element_vmt);
    wlmtk_element_set_visible(
        &window_ptr->content_container.super_element, true);
    if (NULL != content_element_ptr) {
        wlmtk_element_set_visible(content_element_ptr, true);
        wlmtk_container_add_element(
            &window_ptr->content_container,
            content_element_ptr);
        window_ptr->content_element_ptr = content_element_ptr;
    }

    wlmtk_box_add_element_front(
        &window_ptr->box,
        &window_ptr->content_container.super_element);
    wlmtk_element_set_visible(wlmtk_box_element(&window_ptr->box), true);

    // Create the window menu. It is kept hidden until invoked.
    window_ptr->window_menu_ptr = wlmtk_menu_create(menu_style_ref_ptr);
    if (NULL == window_ptr->window_menu_ptr) goto error;
    wlmtk_container_add_element(
        &window_ptr->content_container,
        wlmtk_menu_element(window_ptr->window_menu_ptr));
    wlmtk_util_connect_listener_signal(
        &wlmtk_menu_events(window_ptr->window_menu_ptr)->request_close,
        &window_ptr->menu_request_close_listener,
        _wlmtk_window_menu_request_close_handler);

    _wlmtk_window_apply_decoration(window_ptr);
    return window_ptr;

error:
    wlmtk_window_destroy(window_ptr);
    return NULL;
}

/* ------------------------------------------------------------------------- */
void wlmtk_window_destroy(wlmtk_window_t *window_ptr)
{
    wlmtk_window_set_server_side_decorated(window_ptr, false);

    if (NULL != window_ptr->window_menu_ptr) {
        wlmtk_util_disconnect_listener(
            &window_ptr->menu_request_close_listener);
        wlmtk_container_remove_element(
            &window_ptr->content_container,
            wlmtk_menu_element(window_ptr->window_menu_ptr));
        wlmtk_menu_destroy(window_ptr->window_menu_ptr);
        window_ptr->window_menu_ptr = NULL;
    }

    if (window_ptr->content_container.super_element.parent_container_ptr) {
        wlmtk_box_remove_element(
            &window_ptr->box,
            &window_ptr->content_container.super_element);
    }

    if (NULL != window_ptr->content_element_ptr) {
        wlmtk_container_remove_element(
            &window_ptr->content_container,
            window_ptr->content_element_ptr);
        window_ptr->content_element_ptr = NULL;
    }

    wlmtk_bordered_fini(&window_ptr->bordered);
    wlmtk_box_fini(&window_ptr->box);
    wlmtk_container_fini(&window_ptr->content_container);

    if (NULL != window_ptr->title_ptr) {
        free(window_ptr->title_ptr);
        window_ptr->title_ptr = NULL;
    }

    if (NULL != window_ptr->style_ref_ptr) {
        wlmtk_window_style_ref_release(window_ptr->style_ref_ptr);
        window_ptr->style_ref_ptr = NULL;
    }
    if (NULL != window_ptr->menu_style_ref_ptr) {
        wlmtk_menu_style_ref_release(window_ptr->menu_style_ref_ptr);
        window_ptr->menu_style_ref_ptr = NULL;
    }
    free(window_ptr);
}

/* ------------------------------------------------------------------------- */
wlmtk_window_events_t *wlmtk_window_events(wlmtk_window_t *window_ptr)
{
    return &window_ptr->events;
}

/* ------------------------------------------------------------------------- */
wlmtk_element_t *wlmtk_window_element(wlmtk_window_t *window_ptr)
{
    return &window_ptr->bordered.super_container.super_element;
}

/* ------------------------------------------------------------------------- */
wlmtk_window_t *wlmtk_window_from_element(wlmtk_element_t *element_ptr)
{
    return BS_CONTAINER_OF(
        element_ptr, wlmtk_window_t, bordered.super_container.super_element);
}

/* ------------------------------------------------------------------------- */
struct wlr_box wlmtk_window_get_bounding_box(wlmtk_window_t *window_ptr)
{
    struct wlr_box wbox = wlmtk_element_get_dimensions_box(
        wlmtk_window_element(window_ptr));
    int x, y;
    wlmtk_element_get_position(wlmtk_window_element(window_ptr), &x, &y);
    wbox.x += x;
    wbox.y += y;
    return wbox;
}

/* ------------------------------------------------------------------------- */
void wlmtk_window_position_changed(wlmtk_window_t *window_ptr)
{
    if (window_ptr->inorganic_sizing) return;
    wlmtk_element_get_position(
        wlmtk_window_element(window_ptr),
        &window_ptr->organic_bounding_box.x,
        &window_ptr->organic_bounding_box.y);
}

/* ------------------------------------------------------------------------- */
void wlmtk_window_set_properties(
    wlmtk_window_t *window_ptr,
    uint32_t properties)
{
    if (window_ptr->properties == properties) return;

    window_ptr->properties = properties;
    _wlmtk_window_apply_decoration(window_ptr);
}

/* ------------------------------------------------------------------------- */
void wlmtk_window_set_client(
    wlmtk_window_t *window_ptr,
    const wlmtk_util_client_t *client_ptr)
{
    window_ptr->client = *client_ptr;
}

/* ------------------------------------------------------------------------- */
const wlmtk_util_client_t *wlmtk_window_get_client_ptr(
    __UNUSED__ wlmtk_window_t *window_ptr)
{
    return &window_ptr->client;
}

/* ------------------------------------------------------------------------- */
void wlmtk_window_set_wlr_output(
    wlmtk_window_t *window_ptr,
    struct wlr_output *wlr_output_ptr)
{
    window_ptr->wlr_output_ptr = wlr_output_ptr;
}

/* ------------------------------------------------------------------------- */
struct wlr_output *wlmtk_window_get_wlr_output(wlmtk_window_t *window_ptr)
{
    if (NULL != window_ptr->wlr_output_ptr) return window_ptr->wlr_output_ptr;

    wlmtk_workspace_t *workspace_ptr = wlmtk_window_get_workspace(window_ptr);
    if (NULL == workspace_ptr) return NULL;
    struct wlr_output_layout *wlr_output_layout_ptr =
        wlmtk_workspace_get_wlr_output_layout(workspace_ptr);
    if (NULL == wlr_output_layout_ptr) return NULL;

    struct wlr_box wbox = wlmtk_window_get_bounding_box(window_ptr);
    double dest_x, dest_y;
    wlr_output_layout_closest_point(
        wlmtk_workspace_get_wlr_output_layout(workspace_ptr),
        NULL,  // struct wlr_output* reference. We don't need a reference.
        wbox.x + wbox.width / 2, wbox.y + wbox.height / 2,
        &dest_x, &dest_y);
    return wlr_output_layout_output_at(
        wlmtk_workspace_get_wlr_output_layout(workspace_ptr),
        dest_x, dest_y);
}

/* ------------------------------------------------------------------------- */
bool wlmtk_window_set_title(
    wlmtk_window_t *window_ptr,
    const char *title_ptr)
{
    char *new_title_ptr = NULL;
    if (NULL != title_ptr) {
        new_title_ptr = logged_strdup(title_ptr);
    } else {
        char buf[64];
        snprintf(buf, sizeof(buf), "Unnamed window %p", window_ptr);
        new_title_ptr = logged_strdup(buf);
    }
    if (NULL == new_title_ptr) return false;

    if (NULL != window_ptr->title_ptr) {
        if (0 == strcmp(window_ptr->title_ptr, new_title_ptr)) {
            free(new_title_ptr);
            return true;
        }
        free(window_ptr->title_ptr);
    }
    window_ptr->title_ptr = new_title_ptr;

    if (NULL != window_ptr->titlebar_ptr) {
        wlmtk_titlebar_set_title(window_ptr->titlebar_ptr,
                                 window_ptr->title_ptr);
    }
    return true;
}

/* ------------------------------------------------------------------------- */
const char *wlmtk_window_get_title(wlmtk_window_t *window_ptr)
{
    BS_ASSERT(NULL != window_ptr->title_ptr);
    return window_ptr->title_ptr;
}

/* ------------------------------------------------------------------------- */
void wlmtk_window_set_activated(
    wlmtk_window_t *window_ptr,
    bool activated)
{
    if (window_ptr->activated == activated) return;

    window_ptr->activated = activated;
    wl_signal_emit(&window_ptr->events.set_activated, NULL);

    if (NULL != window_ptr->titlebar_ptr) {
        wlmtk_titlebar_set_activated(window_ptr->titlebar_ptr, activated);
    }

    if (!activated) {
        wlmtk_window_menu_set_enabled(window_ptr, false);

        // TODO(kaeser@gubbe.ch): Should test this behaviour.
        if (NULL != window_ptr->workspace_ptr) {
            wlmtk_workspace_activate_window(window_ptr->workspace_ptr, NULL);
        }
    }
}

/* ------------------------------------------------------------------------- */
bool wlmtk_window_is_activated(wlmtk_window_t *window_ptr)
{
    return window_ptr->activated;
}

/* ------------------------------------------------------------------------- */
void wlmtk_window_request_size(
    wlmtk_window_t *window_ptr,
    const struct wlr_box *box_ptr)
{
    if (window_ptr->fullscreen) return;

    if (window_ptr->maximized) {
        static bool not_maximized = false;
        wl_signal_emit(&window_ptr->events.request_maximized, &not_maximized);
    }
    wl_signal_emit(&window_ptr->events.request_size, (void*)box_ptr);
}

/* ------------------------------------------------------------------------- */
void wlmtk_window_commit_size(
    wlmtk_window_t *window_ptr,
    int width, int height)
{
    window_ptr->committed_size = (struct wlr_box){
        .width = width, .height = height };

    // Store organic size. If we're in an organic mode.
    if (!window_ptr->inorganic_sizing &&
        !window_ptr->fullscreen &&
        !window_ptr->maximized) {
        window_ptr->organic_bounding_box.width = width;
        window_ptr->organic_bounding_box.height = height;
    }

    wlmtk_element_layout(wlmtk_window_element(window_ptr));
    wlmtk_element_invalidate_parent_layout(wlmtk_window_element(window_ptr));
}

/* ------------------------------------------------------------------------- */
struct wlr_box wlmtk_window_get_size(wlmtk_window_t *window_ptr)
{
    return window_ptr->committed_size;
}

/* ------------------------------------------------------------------------- */
void wlmtk_window_set_resize_edges(
    wlmtk_window_t *window_ptr,
    uint32_t edges)
{
    window_ptr->resize_edges = edges;
    window_ptr->old_box = wlmtk_element_get_dimensions_box(
        wlmtk_bordered_element(&window_ptr->bordered));
}

/* ------------------------------------------------------------------------- */
uint32_t wlmtk_window_get_resize_edges(wlmtk_window_t *window_ptr)
{
    return window_ptr->resize_edges;
}

/* ------------------------------------------------------------------------- */
void wlmtk_window_request_close(wlmtk_window_t *window_ptr)
{
    if (!(window_ptr->properties & WLMTK_WINDOW_PROPERTY_CLOSABLE)) return;
    wl_signal_emit(&window_ptr->events.request_close, NULL);
}

/* ------------------------------------------------------------------------- */
void wlmtk_window_request_minimize(wlmtk_window_t *window_ptr)
{
    bs_log(BS_ERROR, "TODO: Request minimize for window %p", window_ptr);
}

/* ------------------------------------------------------------------------- */
void wlmtk_window_request_fullscreen(
    wlmtk_window_t *window_ptr,
    bool fullscreen)
{
    window_ptr->inorganic_sizing = fullscreen;

    // If a window is not mapped, nor does have an output, we cannot make it
    // fullscreen. We wouldn't have the dimensions needed.
    if (NULL == window_ptr->workspace_ptr &&
        NULL == window_ptr->wlr_output_ptr) {
        static bool not_fullscreen = false;
        wl_signal_emit(&window_ptr->events.request_fullscreen,
                       &not_fullscreen);
        return;
    }
    wl_signal_emit(&window_ptr->events.request_fullscreen, &fullscreen);

    _wlmtk_window_request_fullscreen_size(window_ptr, fullscreen);
}

/* ------------------------------------------------------------------------- */
bool wlmtk_window_is_fullscreen(wlmtk_window_t *window_ptr)
{
    return window_ptr->fullscreen;
}

/* ------------------------------------------------------------------------- */
void wlmtk_window_commit_fullscreen(
    wlmtk_window_t *window_ptr,
    bool fullscreen)
{
    // Guard clause: Nothing to do if already in expected state.
    if (window_ptr->fullscreen == fullscreen ) return;

    window_ptr->fullscreen = fullscreen;
    _wlmtk_window_apply_decoration(window_ptr);

    if (NULL != window_ptr->workspace_ptr) {
        wlmtk_workspace_window_to_fullscreen(
            window_ptr->workspace_ptr, window_ptr, fullscreen);

        struct wlr_box fsbox = wlmtk_workspace_get_fullscreen_extents(
            window_ptr->workspace_ptr,
            wlmtk_window_get_wlr_output(window_ptr));
        wlmtk_workspace_set_window_position(
            window_ptr->workspace_ptr,
            window_ptr,
            fullscreen ? fsbox.x : window_ptr->organic_bounding_box.x,
            fullscreen ? fsbox.y : window_ptr->organic_bounding_box.y);
    }
    _wlmtk_window_request_fullscreen_size(window_ptr, fullscreen);

    window_ptr->inorganic_sizing = false;
    wl_signal_emit(&window_ptr->events.state_changed, window_ptr);
}

/* ------------------------------------------------------------------------- */
void wlmtk_window_request_maximized(
    wlmtk_window_t *window_ptr,
    bool maximized)
{
    // Guard clause: Maximizing is refused if on fullscreen or neither mapped
    // nor having an output. Signal the window that it shall not be maximized.
    if (window_ptr->fullscreen ||
        (NULL == window_ptr->workspace_ptr &&
         NULL == window_ptr->wlr_output_ptr)) {
        bool not_maximized = false;
        wl_signal_emit(&window_ptr->events.request_maximized, &not_maximized);
        return;
    }
    window_ptr->inorganic_sizing = maximized;
    wl_signal_emit(&window_ptr->events.request_maximized, &maximized);

    _wlmtk_window_request_maximized_size(window_ptr, maximized);
}

/* ------------------------------------------------------------------------- */
bool wlmtk_window_is_maximized(wlmtk_window_t *window_ptr)
{
    return window_ptr->maximized;
}

/* ------------------------------------------------------------------------- */
void wlmtk_window_commit_maximized(
    wlmtk_window_t *window_ptr,
    bool maximized)
{
    if (window_ptr->maximized == maximized ||
        NULL == window_ptr->workspace_ptr) return;
    window_ptr->maximized = maximized;

    struct wlr_box max_box = wlmtk_workspace_get_maximize_extents(
        window_ptr->workspace_ptr,
        wlmtk_window_get_wlr_output(window_ptr));
    wlmtk_workspace_set_window_position(
        window_ptr->workspace_ptr,
        window_ptr,
        maximized ? max_box.x : window_ptr->organic_bounding_box.x,
        maximized ? max_box.y : window_ptr->organic_bounding_box.y);

    _wlmtk_window_request_maximized_size(window_ptr, maximized);

    window_ptr->inorganic_sizing = false;
    wl_signal_emit(&window_ptr->events.state_changed, window_ptr);
}

/* ------------------------------------------------------------------------- */
void wlmtk_window_request_shaded(wlmtk_window_t *window_ptr, bool shaded)
{
    if (window_ptr->fullscreen ||
        !window_ptr->server_side_decorated ||
        window_ptr->shaded == shaded) return;

    wlmtk_element_set_visible(window_ptr->content_element_ptr, !shaded);
    if (NULL != window_ptr->resizebar_ptr) {
        wlmtk_element_set_visible(
            wlmtk_resizebar_element(window_ptr->resizebar_ptr), !shaded);
    }

    window_ptr->shaded = shaded;
    wl_signal_emit(&window_ptr->events.state_changed, window_ptr);

    wlmtk_element_layout(wlmtk_window_element(window_ptr));
    wlmtk_element_invalidate_parent_layout(wlmtk_window_element(window_ptr));
}

/* ------------------------------------------------------------------------- */
bool wlmtk_window_is_shaded(wlmtk_window_t *window_ptr)
{
    return window_ptr->shaded;
}

/* ------------------------------------------------------------------------- */
void wlmtk_window_menu_set_enabled(wlmtk_window_t *window_ptr, bool enabled)
{
    if (!window_ptr->activated) enabled = false;

    // For convenience: Get the menu's element. Note: It must have a parent,
    // since it's contained within the window.
    wlmtk_element_t *menu_element_ptr = wlmtk_menu_element(
        window_ptr->window_menu_ptr);
    BS_ASSERT(NULL != menu_element_ptr->parent_container_ptr);

    wlmtk_menu_set_open(window_ptr->window_menu_ptr, enabled);

    if (enabled) {
        int x_pos = 0;
        // Grabbing the element will clear the focus.
        if (wlmtk_window_element(window_ptr)->pointer_inside) {
            x_pos = wlmtk_window_element(
                window_ptr)->last_pointer_motion_event.x;
        }

        wlmtk_menu_set_mode(
            window_ptr->window_menu_ptr,
            WLMTK_MENU_MODE_RIGHTCLICK);
        wlmtk_container_raise_element_to_top(
            menu_element_ptr->parent_container_ptr,
            menu_element_ptr);
        wlmtk_container_pointer_grab(
            menu_element_ptr->parent_container_ptr,
            menu_element_ptr);

            wlmtk_element_set_position(menu_element_ptr, x_pos, 0);
    } else {
        wlmtk_container_pointer_grab_release(
            menu_element_ptr->parent_container_ptr,
            menu_element_ptr);
    }
}

/* ------------------------------------------------------------------------- */
wlmtk_menu_t *wlmtk_window_menu(wlmtk_window_t *window_ptr)
{
    return window_ptr->window_menu_ptr;
}

/* ------------------------------------------------------------------------- */
void wlmtk_window_set_server_side_decorated(
    wlmtk_window_t *window_ptr,
    bool decorated)
{
    if (window_ptr->server_side_decorated == decorated) return;

    window_ptr->server_side_decorated = decorated;
    _wlmtk_window_apply_decoration(window_ptr);
}

/* ------------------------------------------------------------------------- */
void wlmtk_window_set_options(
    wlmtk_window_t *window_ptr,
    uint32_t move_modifier)
{
    window_ptr->move_modifier = move_modifier;
}

/* ------------------------------------------------------------------------- */
void wlmtk_window_set_workspace(
    wlmtk_window_t *window_ptr,
    wlmtk_workspace_t *workspace_ptr)
{
    if (window_ptr->workspace_ptr == workspace_ptr) return;
    window_ptr->workspace_ptr = workspace_ptr;

    wl_signal_emit(&window_ptr->events.state_changed, window_ptr);
}

/* ------------------------------------------------------------------------- */
wlmtk_workspace_t *wlmtk_window_get_workspace(wlmtk_window_t *window_ptr)
{
    return window_ptr->workspace_ptr;
}

/* ------------------------------------------------------------------------- */
bs_dllist_node_t *wlmtk_dlnode_from_window(wlmtk_window_t *window_ptr)
{
    return &window_ptr->dlnode;
}

/* ------------------------------------------------------------------------- */
wlmtk_window_t *wlmtk_window_from_dlnode(bs_dllist_node_t *dlnode_ptr)
{
    return BS_CONTAINER_OF(dlnode_ptr, wlmtk_window_t, dlnode);
}

/* ------------------------------------------------------------------------- */
bool wlmtk_window_set_style(
    wlmtk_window_t *window_ptr,
    wlmtk_window_style_ref_t *style_ref_ptr,
    wlmtk_menu_style_ref_t *menu_style_ref_ptr)
{
    // Cautiously keep the reference until style update is done.
    wlmtk_window_style_ref_t *old_ref_ptr = window_ptr->style_ref_ptr;
    window_ptr->style_ref_ptr = style_ref_ptr;
    window_ptr->style_ptr = wlmtk_window_style_ref_retain(style_ref_ptr);

    wlmtk_menu_style_ref_t *old_menu_ref_ptr = window_ptr->menu_style_ref_ptr;
    window_ptr->menu_style_ref_ptr = menu_style_ref_ptr;
    wlmtk_menu_style_ref_retain(menu_style_ref_ptr);

    bool rv = true;
    wlmtk_bordered_set_style(
        &window_ptr->bordered,
        &window_ptr->style_ptr->border);
    wlmtk_box_set_style(
        &window_ptr->box,
        &window_ptr->style_ptr->margin);

    if (NULL != window_ptr->resizebar_ptr) {
        rv &= wlmtk_resizebar_set_style(window_ptr->resizebar_ptr,
                                        &window_ptr->style_ptr->resizebar);
    }

    if (NULL != window_ptr->titlebar_ptr) {
        rv &= wlmtk_titlebar_set_style(window_ptr->titlebar_ptr,
                                       &window_ptr->style_ptr->titlebar);
    }

    if (NULL != window_ptr->window_menu_ptr) {
        rv &= wlmtk_menu_set_style(window_ptr->window_menu_ptr,
                                   menu_style_ref_ptr);
    }

    wlmtk_menu_style_ref_release(old_menu_ref_ptr);
    wlmtk_window_style_ref_release(old_ref_ptr);
    return rv;
}

/* ------------------------------------------------------------------------- */
struct wlmtk_window_style *wlmtk_window_style_create(void)
{
    struct wlmtk_window_style_holder *sh = logged_calloc(1, sizeof(*sh));
    if (NULL == sh) return NULL;

    bs_ref_init(&sh->wsr.reference, _wlmtk_window_style_destroy);
    return &sh->style;
}

/* ------------------------------------------------------------------------- */
wlmtk_window_style_ref_t *wlmtk_window_style_to_ref(
    struct wlmtk_window_style *window_style_ptr)
{
    // Guard clause.
    if (NULL == window_style_ptr) return NULL;

    struct wlmtk_window_style_holder *sh = BS_CONTAINER_OF(
        window_style_ptr, struct wlmtk_window_style_holder, style);
    return &sh->wsr;
}

/* ------------------------------------------------------------------------- */
const struct wlmtk_window_style *wlmtk_window_style_ref_retain(
    wlmtk_window_style_ref_t *ref_ptr)
{
    bs_ref_retain(&ref_ptr->reference);
    struct wlmtk_window_style_holder *sh = BS_CONTAINER_OF(
        ref_ptr, struct wlmtk_window_style_holder, wsr.reference);
    return &sh->style;
}

/* ------------------------------------------------------------------------- */
void wlmtk_window_style_ref_release(wlmtk_window_style_ref_t *ref_ptr)
{
    bs_ref_release(&ref_ptr->reference);
}

/* ------------------------------------------------------------------------- */
bool wlmtk_window_style_decode(
    bspl_object_t *object_ptr,
    __UNUSED__ const union bspl_desc_value *desc_value_ptr,
    void *value_ptr)
{
    struct wlmtk_window_style **s = value_ptr;

    return bspl_decode_dict(
        bspl_dict_from_object(object_ptr),
        _wlmtk_window_style_desc,
        *s);
}

/* ------------------------------------------------------------------------- */
bool wlmtk_window_style_decode_init(void *dst_ptr)
{
    struct wlmtk_window_style **s = dst_ptr;
    *s = wlmtk_window_style_create();
    return *s != NULL;
}

/* ------------------------------------------------------------------------- */
void wlmtk_window_style_decode_fini(void *dst_ptr)
{
    struct wlmtk_window_style **s = dst_ptr;
    if (NULL != *s) {
        wlmtk_window_style_ref_release(wlmtk_window_style_to_ref(*s));
        *s = NULL;
    }
}

/* == Local (static) methods =============================================== */

/* ------------------------------------------------------------------------- */
/** Activates window on button press, and calls the parent's implementation. */
bool _wlmtk_window_element_pointer_button(
    wlmtk_element_t *element_ptr,
    const wlmtk_button_event_t *button_event_ptr)
{
    wlmtk_window_t *window_ptr = BS_CONTAINER_OF(
        element_ptr, wlmtk_window_t, bordered.super_container.super_element);

    // In right-click mode: Any out-of-window action will close it.
    // TODO(kaeser@gubbe.ch): This should be a specific window mode, and should
    // have a handler method when leaving that mode (eg. left release within
    // the window).
    // Also TODO(kaeser@gubbe.ch): Test this.
    if (window_ptr->properties & WLMTK_WINDOW_PROPERTY_RIGHTCLICK) {
        bool rv = window_ptr->orig_super_element_vmt.pointer_button(
            element_ptr, button_event_ptr);
        if (BTN_RIGHT == button_event_ptr->button &&
            WLMTK_BUTTON_UP == button_event_ptr->type &&
            !rv) {
            wlmtk_window_request_close(window_ptr);
            return true;
        }
        return rv;
    }

    // Modifier-click initiates move.
    if (0 != window_ptr->move_modifier &&
        BTN_LEFT == button_event_ptr->button &&
        WLMTK_BUTTON_DOWN == button_event_ptr->type &&
        window_ptr->move_modifier == button_event_ptr->keyboard_modifiers &&
        NULL != wlmtk_window_get_workspace(window_ptr)) {
        wlmtk_workspace_begin_window_move(
            wlmtk_window_get_workspace(window_ptr),
            window_ptr);
        return true;
    }

    // We shouldn't receive buttons when not mapped.
    wlmtk_workspace_t *workspace_ptr = wlmtk_window_get_workspace(window_ptr);
    wlmtk_workspace_activate_window(workspace_ptr, window_ptr);
    if (!window_ptr->fullscreen) {
        wlmtk_workspace_raise_window(workspace_ptr, window_ptr);
    }

    return window_ptr->orig_super_element_vmt.pointer_button(
        element_ptr, button_event_ptr);
}

/* ------------------------------------------------------------------------- */
/** Gets dimensions of the content container: Forward to only the content. */
void _wlmtk_window_container_element_get_dimensions(
        wlmtk_element_t *element_ptr,
        int *x1_ptr,
        int *y1_ptr,
        int *x2_ptr,
        int *y2_ptr)
{
    wlmtk_window_t *window_ptr = BS_CONTAINER_OF(
        element_ptr, wlmtk_window_t, content_container.super_element);

    if (NULL == window_ptr->content_element_ptr ||
        !window_ptr->content_element_ptr->visible) {
        if (NULL != x1_ptr) *x1_ptr = 0;
        if (NULL != y1_ptr) *y1_ptr = 0;
        if (NULL != x2_ptr) *x2_ptr = 0;
        if (NULL != y2_ptr) *y2_ptr = 0;
    } else {
        wlmtk_element_get_dimensions(
            window_ptr->content_element_ptr, x1_ptr, y1_ptr, x2_ptr, y2_ptr);
    }
}

/* ------------------------------------------------------------------------- */
/**
 * Implementation of @ref wlmtk_element_vmt_t::layout.
 *
 * Invoked when the window's contained elements triggered a layout update,
 * and will use this to trigger (potential) size updates to the window
 * decorations.
 *
 * @param element_ptr
 */
void _wlmtk_window_element_layout(
    wlmtk_element_t *element_ptr)
{
    wlmtk_window_t *window_ptr = BS_CONTAINER_OF(
        element_ptr, wlmtk_window_t, bordered.super_container.super_element);

    _wlmtk_window_set_decoration_width(window_ptr);

    window_ptr->orig_super_element_vmt.layout(element_ptr);

    // No updates if there is no content element (or not in the container).
    if (NULL == window_ptr->content_element_ptr ||
        NULL == window_ptr->content_element_ptr->parent_container_ptr) {
        return;
    }

    // new_box includes potential decorations.
    struct wlr_box new_box = wlmtk_element_get_dimensions_box(
        wlmtk_bordered_element(&window_ptr->bordered));
    int x, y;
    wlmtk_element_get_position(
        wlmtk_bordered_element(&window_ptr->bordered), &x, &y);
    if (window_ptr->resize_edges & WLR_EDGE_LEFT) {
        x += window_ptr->old_box.width - new_box.width;
    }
    if (window_ptr->resize_edges & WLR_EDGE_TOP) {
        y += window_ptr->old_box.height - new_box.height;
    }
    // TODO: Should we rather use wlmtk_workspace_set_window_position ?
    wlmtk_element_set_position(
        wlmtk_bordered_element(&window_ptr->bordered), x, y);
    window_ptr->old_box = new_box;
}

/* ------------------------------------------------------------------------- */
/** Applies window decoration depending on current state. */
void _wlmtk_window_apply_decoration(wlmtk_window_t *window_ptr)
{
    struct wlmtk_margin_style bstyle = window_ptr->style_ptr->border;

    if (window_ptr->server_side_decorated && !window_ptr->fullscreen) {
        _wlmtk_window_create_titlebar(window_ptr);
    } else {
        bstyle.width = 0;
        _wlmtk_window_destroy_titlebar(window_ptr);
    }

    if (NULL != window_ptr->titlebar_ptr) {
        uint32_t properties = 0;
        if (window_ptr->properties & WLMTK_WINDOW_PROPERTY_ICONIFIABLE) {
            properties |= WLMTK_TITLEBAR_PROPERTY_ICONIFY;
        }
        if (window_ptr->properties & WLMTK_WINDOW_PROPERTY_CLOSABLE) {
            properties |= WLMTK_TITLEBAR_PROPERTY_CLOSE;
        }
        wlmtk_titlebar_set_properties(window_ptr->titlebar_ptr, properties);
        wlmtk_titlebar_set_activated(
            window_ptr->titlebar_ptr, window_ptr->activated);
    }

    if (window_ptr->server_side_decorated &&
        !window_ptr->fullscreen &&
        (window_ptr->properties & WLMTK_WINDOW_PROPERTY_RESIZABLE)) {
        _wlmtk_window_create_resizebar(window_ptr);
    } else {
        _wlmtk_window_destroy_resizebar(window_ptr);
    }

    wlmtk_bordered_set_style(&window_ptr->bordered, &bstyle);

    _wlmtk_window_set_decoration_width(window_ptr);

    wlmtk_element_layout(wlmtk_window_element(window_ptr));
    wlmtk_element_invalidate_parent_layout(wlmtk_window_element(window_ptr));
}

/* ------------------------------------------------------------------------- */
/** Creates the titlebar. */
void _wlmtk_window_create_titlebar(wlmtk_window_t *window_ptr)
{
    BS_ASSERT(window_ptr->server_side_decorated && !window_ptr->fullscreen);

    // Guard clause: Don't add decoration.
    if (NULL != window_ptr->titlebar_ptr) return;

    // Create decoration.
    window_ptr->titlebar_ptr = wlmtk_titlebar_create(
        window_ptr, &window_ptr->style_ptr->titlebar);
    BS_ASSERT(NULL != window_ptr->titlebar_ptr);

    wlmtk_element_set_visible(
        wlmtk_titlebar_element(window_ptr->titlebar_ptr), true);
    wlmtk_titlebar_set_activated(
        window_ptr->titlebar_ptr,
        window_ptr->activated);

    // Hm, if the content has a popup that extends over the titlebar area,
    // it'll be partially obscured. That will look odd... Well, let's
    // address that problem once there's a situation.
    wlmtk_box_add_element_front(
        &window_ptr->box,
        wlmtk_titlebar_element(window_ptr->titlebar_ptr));
}

/* ------------------------------------------------------------------------- */
/** Destroys the titlebar. */
void _wlmtk_window_destroy_titlebar(wlmtk_window_t *window_ptr)
{
    BS_ASSERT(!window_ptr->server_side_decorated || window_ptr->fullscreen);

    if (NULL == window_ptr->titlebar_ptr) return;

    wlmtk_box_remove_element(
        &window_ptr->box,
        wlmtk_titlebar_element(window_ptr->titlebar_ptr));
    wlmtk_titlebar_destroy(window_ptr->titlebar_ptr);
    window_ptr->titlebar_ptr = NULL;
}

/* ------------------------------------------------------------------------- */
/** Creates the resizebar. */
void _wlmtk_window_create_resizebar(wlmtk_window_t *window_ptr)
{
    BS_ASSERT(window_ptr->server_side_decorated && !window_ptr->fullscreen);

    // Guard clause: Don't add decoration.
    if (NULL != window_ptr->resizebar_ptr) return;

    window_ptr->resizebar_ptr = wlmtk_resizebar_create(
        window_ptr, &window_ptr->style_ptr->resizebar);
    BS_ASSERT(NULL != window_ptr->resizebar_ptr);
    wlmtk_element_set_visible(
        wlmtk_resizebar_element(window_ptr->resizebar_ptr), true);
    wlmtk_box_add_element_back(
        &window_ptr->box,
        wlmtk_resizebar_element(window_ptr->resizebar_ptr));
}

/* ------------------------------------------------------------------------- */
/** Destroys the resizebar. */
void _wlmtk_window_destroy_resizebar(wlmtk_window_t *window_ptr)
{
    BS_ASSERT(!window_ptr->server_side_decorated ||
              window_ptr->fullscreen ||
              !(window_ptr->properties & WLMTK_WINDOW_PROPERTY_RESIZABLE));

    if (NULL == window_ptr->resizebar_ptr) return;

    wlmtk_box_remove_element(
        &window_ptr->box,
        wlmtk_resizebar_element(window_ptr->resizebar_ptr));
    wlmtk_resizebar_destroy(window_ptr->resizebar_ptr);
    window_ptr->resizebar_ptr = NULL;
}

/* ------------------------------------------------------------------------- */
/** Sets the decoration's width to match the content. */
void _wlmtk_window_set_decoration_width(wlmtk_window_t *window_ptr)
{
    if (NULL == window_ptr->content_element_ptr) return;

    struct wlr_box box = wlmtk_element_get_dimensions_box(
        window_ptr->content_element_ptr);
    if (NULL != window_ptr->titlebar_ptr) {
        wlmtk_titlebar_set_width(window_ptr->titlebar_ptr, box.width);
    }
    if (NULL != window_ptr->resizebar_ptr) {
        wlmtk_resizebar_set_width(window_ptr->resizebar_ptr, box.width);
    }
}

/* ------------------------------------------------------------------------- */
/** Handles @ref wlmtk_menu_events_t::request_close. */
void _wlmtk_window_menu_request_close_handler(
    struct wl_listener *listener_ptr,
    __UNUSED__ void *data_ptr)
{
    wlmtk_window_t *window_ptr = BS_CONTAINER_OF(
        listener_ptr, wlmtk_window_t, menu_request_close_listener);
    wlmtk_window_menu_set_enabled(window_ptr, false);
}

/* ------------------------------------------------------------------------- */
/** Destroys the window style. */
void _wlmtk_window_style_destroy(bs_ref_t *ref_ptr)
{
    struct wlmtk_window_style_holder *sh = BS_CONTAINER_OF(
        ref_ptr, struct wlmtk_window_style_holder, wsr.reference);
    free(sh);
}

/* ------------------------------------------------------------------------- */
/** Requests the size appropriate for the maximized state. */
void _wlmtk_window_request_maximized_size(
    wlmtk_window_t *window_ptr,
    bool maximized)
{
    struct wlr_box desired_size = window_ptr->organic_bounding_box;
    desired_size.x = 0;
    desired_size.y = 0;

    if (maximized) {
        // For decorated windows, we need to subtract the decorations size.
        // Since maximization will be requested from an organic size, we can
        // derive this from the delta of the "bordered" element, minus the
        // dimensions of the "content" element.
        // That will be zero for non-decorated windows.
        struct wlr_box wbox = wlmtk_element_get_dimensions_box(
            wlmtk_bordered_element(&window_ptr->bordered));
        struct wlr_box cbox = wlmtk_element_get_dimensions_box(
            window_ptr->content_element_ptr);

        struct wlr_output *wo_ptr = window_ptr->wlr_output_ptr;
        if (NULL != window_ptr->workspace_ptr) {
            desired_size = wlmtk_workspace_get_maximize_extents(
                window_ptr->workspace_ptr, wo_ptr);
        } else if (NULL != wo_ptr) {
            desired_size.width = wo_ptr->width;
            desired_size.height = wo_ptr->height;
        } else {
            // Not mappend nor a specified output -- no size to set.
            return;
        }
        desired_size.width -= wbox.width - cbox.width;
        desired_size.height -= wbox.height - cbox.height;
    }

    wl_signal_emit(&window_ptr->events.request_size, &desired_size);
}

/* ------------------------------------------------------------------------- */
/** Requests size appropriate for the fullscreen state. */
void _wlmtk_window_request_fullscreen_size(wlmtk_window_t *window_ptr,
                                           bool fullscreen)
{
    struct wlr_box desired_size = window_ptr->organic_bounding_box;
    desired_size.x = 0;
    desired_size.y = 0;

    if (fullscreen) {
        struct wlr_output *wo_ptr = window_ptr->wlr_output_ptr;
        if (NULL != window_ptr->workspace_ptr) {
            desired_size = wlmtk_workspace_get_fullscreen_extents(
                window_ptr->workspace_ptr, wo_ptr);
        } else if (NULL != window_ptr->wlr_output_ptr) {
            desired_size = (struct wlr_box){
                .width = wo_ptr->width,
                .height = wo_ptr->height
            };
        } else {
            // No output and not mapped? No size to set.
            return;
        }
    }

    wl_signal_emit(&window_ptr->events.request_size, &desired_size);
}

/* == Unit Tests =========================================================== */

static void test_create_destroy(bs_test_t *test_ptr);
static void test_decoration(bs_test_t *test_ptr);
static void test_events(bs_test_t *test_ptr);
static void test_set_activated(bs_test_t *test_ptr);
static void test_resize(bs_test_t *test_ptr);
static void test_fullscreen(bs_test_t *test_ptr);
static void test_fullscreen_unmap(bs_test_t *test_ptr);
static void test_maximized(bs_test_t *test_ptr);
static void test_shaded(bs_test_t *test_ptr);
static void test_modifier_move(bs_test_t *test_ptr);
static void test_menu(bs_test_t *test_ptr);
static void test_style(bs_test_t *test_ptr);
static void test_style_decode(bs_test_t *test_ptr);

// TODO(kaeser@gubbe.ch): Add tests for ..
// * storing organic_bounding_box

/** Test cases */
static const bs_test_case_t _wlmtk_window_test_cases[] = {
    { 1, "create_destroy", test_create_destroy },
    { 1, "decoration", test_decoration },
    { 1, "events", test_events },
    { 1, "set_activated", test_set_activated },
    { 1, "resize", test_resize },
    { 1, "fullscreen", test_fullscreen },
    { 1, "fullscreen_unmap", test_fullscreen_unmap },
    { 1, "maximized", test_maximized },
    { 1, "shaded", test_shaded },
    { 1, "modifier_move", test_modifier_move },
    { 1, "menu", test_menu },
    { 1, "style", test_style },
    { 1, "style_decode", test_style_decode },
    BS_TEST_CASE_SENTINEL()
};

const bs_test_set_t wlmtk_window_test_set = BS_TEST_SET(
    true, "window", _wlmtk_window_test_cases);

/** Window style used as default for testing, including a reference. */
static struct wlmtk_window_style_holder _wlmtk_window_test_style_holder = {
    .wsr = { .reference = { .count = 1 } },
    .style = {
        .titlebar = { .height = 10 },
        .resizebar = { .height = 5 },
        .margin = { .width = 1 },
        .border = { .width = 2 },
    }
};

/** Menu style used as default for testing windows. */
static const struct wlmtk_menu_style _wlmtk_window_test_menu_style = {
    .item = {
        .height = 20,
        .width = 100
    }
};

/* ------------------------------------------------------------------------- */
wlmtk_window_t *wlmtk_test_window_create(
    wlmtk_element_t *content_element_ptr)
{
    struct wlmtk_menu_style *ms = wlmtk_menu_style_create();
    if (NULL == ms) return NULL;
    *ms = _wlmtk_window_test_menu_style;

    wlmtk_window_t *window_ptr = wlmtk_window_create(
        content_element_ptr,
        &_wlmtk_window_test_style_holder.wsr,
        wlmtk_menu_style_to_ref(ms));
    wlmtk_menu_style_ref_release(wlmtk_menu_style_to_ref(ms));
    return window_ptr;
}

/* ------------------------------------------------------------------------- */
/** Exercises ctor and dtor and some accessors. */
void test_create_destroy(bs_test_t *test_ptr)
{
    wlmtk_fake_element_t *fe = wlmtk_fake_element_create();
    BS_TEST_VERIFY_NEQ_OR_RETURN(test_ptr, NULL, fe);

    wlmtk_window_t *w = wlmtk_test_window_create(&fe->element);
    BS_TEST_VERIFY_NEQ_OR_RETURN(test_ptr, NULL, w);

    // Title. Must be set when unnamed, and override otherwise.
    BS_TEST_VERIFY_STRMATCH(
        test_ptr,
        wlmtk_window_get_title(w),
        "Unnamed window .*");
    wlmtk_window_set_title(w, "Title");
    BS_TEST_VERIFY_STREQ(test_ptr, "Title", wlmtk_window_get_title(w));

    // Transform to element and dlnode and back.
    wlmtk_element_t *e = wlmtk_window_element(w);
    BS_TEST_VERIFY_EQ(test_ptr, w, wlmtk_window_from_element(e));
    bs_dllist_node_t *dlnode_ptr = wlmtk_dlnode_from_window(w);
    BS_TEST_VERIFY_EQ(test_ptr, w, wlmtk_window_from_dlnode(dlnode_ptr));

    // set_workspace and get_workspace.
    struct wl_display *display_ptr = wl_display_create();
    BS_TEST_VERIFY_NEQ_OR_RETURN(test_ptr, NULL, display_ptr);
    struct wlr_output_layout *wlr_output_layout_ptr =
        wlr_output_layout_create(display_ptr);
    BS_TEST_VERIFY_NEQ_OR_RETURN(test_ptr, NULL, wlr_output_layout_ptr);
    struct wlmtk_tile_style ts = {};
    wlmtk_workspace_t *workspace_ptr = wlmtk_workspace_create(
        wlr_output_layout_ptr, "t", &ts);
    BS_TEST_VERIFY_NEQ_OR_RETURN(test_ptr, NULL, workspace_ptr);

    wlmtk_util_test_listener_t l;
    wlmtk_util_connect_test_listener(
        &wlmtk_window_events(w)->state_changed, &l);

    wlmtk_window_set_workspace(w, workspace_ptr);
    BS_TEST_VERIFY_EQ(test_ptr, 1, l.calls);
    BS_TEST_VERIFY_EQ(test_ptr, w, l.last_data_ptr);
    BS_TEST_VERIFY_EQ(
        test_ptr,
        workspace_ptr,
        wlmtk_window_get_workspace(w));

    wlmtk_util_clear_test_listener(&l);
    wlmtk_window_set_workspace(w, workspace_ptr);
    BS_TEST_VERIFY_EQ(test_ptr, 0, l.calls);

    wlmtk_workspace_destroy(workspace_ptr);
    wl_display_destroy(display_ptr);

    wlmtk_window_destroy(w);
    wlmtk_element_destroy(&fe->element);
}

/* ------------------------------------------------------------------------- */
/** Tests server-side decoration. */
void test_decoration(bs_test_t *test_ptr)
{
    wlmtk_fake_element_t *fe = wlmtk_fake_element_create();
    BS_TEST_VERIFY_NEQ_OR_RETURN(test_ptr, NULL, fe);
    wlmtk_fake_element_set_dimensions(fe, 42, 20);

    wlmtk_window_t *w = wlmtk_test_window_create(&fe->element);
    BS_TEST_VERIFY_NEQ_OR_RETURN(test_ptr, NULL, w);

    // Initially: no decoration.
    BS_TEST_VERIFY_EQ(test_ptr, NULL, w->titlebar_ptr);
    BS_TEST_VERIFY_EQ(test_ptr, NULL, w->resizebar_ptr);
    WLMTK_TEST_VERIFY_WLRBOX_EQ(
        test_ptr, 0, 0, 42, 20,
        wlmtk_window_get_bounding_box(w));

    // Enable: Default property is not resizable, so we only get titlebar.
    wlmtk_window_set_server_side_decorated(w, true);
    BS_TEST_VERIFY_NEQ(test_ptr, NULL, w->titlebar_ptr);
    BS_TEST_VERIFY_EQ(test_ptr, NULL, w->resizebar_ptr);

    // Set property. Now we must have resize bar.
    wlmtk_window_set_properties(w, WLMTK_WINDOW_PROPERTY_RESIZABLE);
    BS_TEST_VERIFY_NEQ(test_ptr, NULL, w->resizebar_ptr);

    // Disable. Decoration vanishes.
    wlmtk_window_set_server_side_decorated(w, false);
    BS_TEST_VERIFY_EQ(test_ptr, NULL, w->titlebar_ptr);
    BS_TEST_VERIFY_EQ(test_ptr, NULL, w->resizebar_ptr);

    // Re-enable. Properties are sticky, so we want all.
    wlmtk_window_set_server_side_decorated(w, true);
    BS_TEST_VERIFY_NEQ(test_ptr, NULL, w->titlebar_ptr);
    BS_TEST_VERIFY_NEQ(test_ptr, NULL, w->resizebar_ptr);

    // Verify width: Must match the fake element's dimensions.
    wlmtk_element_t *te = wlmtk_titlebar_element(w->titlebar_ptr);
    WLMTK_TEST_VERIFY_WLRBOX_EQ(
        test_ptr, 0, 0, 42, 10,
        wlmtk_element_get_dimensions_box(te));
    wlmtk_element_t *re = wlmtk_resizebar_element(w->resizebar_ptr);
    WLMTK_TEST_VERIFY_WLRBOX_EQ(
        test_ptr, 0, 0, 42, 5,
        wlmtk_element_get_dimensions_box(re));
    WLMTK_TEST_VERIFY_WLRBOX_EQ(
        test_ptr, 0, 0, 46, 41,
        wlmtk_window_get_bounding_box(w));

    // An update in the element would call the parent's update_layout.
    wlmtk_fake_element_set_dimensions(fe, 52, 20);
    wlmtk_element_layout(wlmtk_window_element(w));
    WLMTK_TEST_VERIFY_WLRBOX_EQ(
        test_ptr, 0, 0, 52, 10,
        wlmtk_element_get_dimensions_box(te));
    WLMTK_TEST_VERIFY_WLRBOX_EQ(
        test_ptr, 0, 0, 52, 5,
        wlmtk_element_get_dimensions_box(re));

    // Update again, with top-left edges resizing.
    wlmtk_window_set_resize_edges(w, WLR_EDGE_TOP | WLR_EDGE_LEFT);
    wlmtk_fake_element_set_dimensions(fe, 32, 10);
    wlmtk_element_layout(wlmtk_window_element(w));
    BS_TEST_VERIFY_TRUE(
        test_ptr,
        w->bordered.super_container.invalidated_layout);
    WLMTK_TEST_VERIFY_WLRBOX_EQ(
        test_ptr, 0, 0, 32, 10,
        wlmtk_element_get_dimensions_box(te));
    WLMTK_TEST_VERIFY_WLRBOX_EQ(
        test_ptr, 0, 0, 32, 5,
        wlmtk_element_get_dimensions_box(re));
    WLMTK_TEST_VERIFY_WLRBOX_EQ(
        test_ptr, 20, 10, 36, 31,
        wlmtk_window_get_bounding_box(w));
    BS_TEST_VERIFY_EQ(
        test_ptr,
        WLR_EDGE_TOP | WLR_EDGE_LEFT,
        wlmtk_window_get_resize_edges(w));

    wlmtk_window_destroy(w);
    wlmtk_element_destroy(&fe->element);
}

/* ------------------------------------------------------------------------- */
/** Tests that desired events are firing. */
void test_events(bs_test_t *test_ptr)
{
    wlmtk_window_t *w = wlmtk_test_window_create(NULL);
    BS_TEST_VERIFY_NEQ_OR_RETURN(test_ptr, NULL, w);

    wlmtk_util_test_listener_t l;

    wlmtk_util_connect_test_listener(
        &wlmtk_window_events(w)->request_close, &l);
    wlmtk_window_request_close(w);
    BS_TEST_VERIFY_EQ(test_ptr, 0, l.calls);

    wlmtk_window_set_properties(w, WLMTK_WINDOW_PROPERTY_CLOSABLE);
    wlmtk_window_request_close(w);
    BS_TEST_VERIFY_EQ(test_ptr, 1, l.calls);

    wlmtk_window_destroy(w);
}

/* ------------------------------------------------------------------------- */
/** Tests that activation updates decoration and callback. */
void test_set_activated(bs_test_t *test_ptr)
{
    wlmtk_window_t *w = wlmtk_test_window_create(NULL);
    BS_TEST_VERIFY_NEQ_OR_RETURN(test_ptr, NULL, w);

    wlmtk_util_test_listener_t l;
    wlmtk_util_connect_test_listener(
        &wlmtk_window_events(w)->set_activated, &l);

    BS_TEST_VERIFY_FALSE(test_ptr, wlmtk_window_is_activated(w));

    wlmtk_window_set_activated(w, true);
    BS_TEST_VERIFY_TRUE(test_ptr, wlmtk_window_is_activated(w));
    BS_TEST_VERIFY_EQ(test_ptr, 1, l.calls);

    wlmtk_window_set_server_side_decorated(w, true);
    BS_TEST_VERIFY_TRUE(
        test_ptr,
        wlmtk_titlebar_is_activated(w->titlebar_ptr));

    wlmtk_util_clear_test_listener(&l);
    wlmtk_window_set_activated(w, false);
    BS_TEST_VERIFY_FALSE(test_ptr, wlmtk_window_is_activated(w));
    BS_TEST_VERIFY_EQ(test_ptr, 1, l.calls);
    BS_TEST_VERIFY_FALSE(
        test_ptr,
        wlmtk_titlebar_is_activated(w->titlebar_ptr));

    wlmtk_window_destroy(w);
}

/* ------------------------------------------------------------------------- */
/** Tests resize request: Ignore when fullscreen, unmaximize when maximized. */
void test_resize(bs_test_t *test_ptr)
{
    struct wl_display *display_ptr = wl_display_create();
    BS_TEST_VERIFY_NEQ_OR_RETURN(test_ptr, NULL, display_ptr);
    struct wlr_output_layout *wlr_output_layout_ptr =
        wlr_output_layout_create(display_ptr);
    BS_TEST_VERIFY_NEQ_OR_RETURN(test_ptr, NULL, wlr_output_layout_ptr);
    struct wlr_output output = { .width = 1024, .height = 768, .scale = 1 };
    wlmtk_test_wlr_output_init(&output);
    wlr_output_layout_add(wlr_output_layout_ptr, &output, 0, 0);

    struct wlmtk_tile_style ts = {};
    wlmtk_workspace_t *ws_ptr = wlmtk_workspace_create(
        wlr_output_layout_ptr, "t", &ts);
    BS_TEST_VERIFY_NEQ_OR_RETURN(test_ptr, NULL, ws_ptr);
    wlmtk_workspace_enable(ws_ptr, true);

    wlmtk_fake_element_t *fe_ptr = wlmtk_fake_element_create();
    BS_TEST_VERIFY_NEQ_OR_RETURN(test_ptr, NULL, fe_ptr);
    wlmtk_window_t *w = wlmtk_test_window_create(&fe_ptr->element);
    wlmtk_window_set_properties(w, WLMTK_WINDOW_PROPERTY_RESIZABLE);
    BS_TEST_VERIFY_NEQ_OR_RETURN(test_ptr, NULL, w);

    wlmtk_workspace_map_window(ws_ptr, w);

    wlmtk_util_test_wlr_box_listener_t l = {};
    wlmtk_util_connect_test_wlr_box_listener(
        &wlmtk_window_events(w)->request_size, &l);
    wlmtk_util_test_listener_t maxl = {};
    wlmtk_util_connect_test_listener(
        &wlmtk_window_events(w)->request_maximized, &maxl);

    // Request a size change on normal window: Accepted, forward to listener.
    struct wlr_box size = { .width = 100, .height = 50 };
    wlmtk_window_request_size(w, &size);
    BS_TEST_VERIFY_EQ(test_ptr, 1, l.calls);
    WLMTK_TEST_VERIFY_WLRBOX_EQ(test_ptr, 0, 0, 100, 50, l.box);

    // Request a size change on a fullscreen window: Not accepted.
    wlmtk_window_commit_fullscreen(w, true);
    wlmtk_util_clear_test_wlr_box_listener(&l);
    wlmtk_window_request_size(w, &size);
    BS_TEST_VERIFY_EQ(test_ptr, 0, l.calls);
    wlmtk_window_commit_fullscreen(w, false);

    // Request size change on maximized window: Accept, but request unmaximize.
    wlmtk_window_commit_maximized(w, true);
    wlmtk_util_clear_test_wlr_box_listener(&l);
    wlmtk_util_clear_test_listener(&maxl);
    wlmtk_window_request_size(w, &size);
    BS_TEST_VERIFY_EQ(test_ptr, 1, maxl.calls);
    BS_TEST_VERIFY_EQ(test_ptr, 1, l.calls);
    WLMTK_TEST_VERIFY_WLRBOX_EQ(test_ptr, 0, 0, 100, 50, l.box);

    wlmtk_workspace_unmap_window(ws_ptr, w);

    wlmtk_util_disconnect_listener(&l.listener);
    wlmtk_window_destroy(w);
    wlmtk_element_destroy(&fe_ptr->element);

    wlmtk_workspace_destroy(ws_ptr);
    wl_display_destroy(display_ptr);
}

/* ------------------------------------------------------------------------- */
/** Tests fullscreen mode of a window. */
void test_fullscreen(bs_test_t *test_ptr)
{
    struct wl_display *display_ptr = wl_display_create();
    BS_TEST_VERIFY_NEQ_OR_RETURN(test_ptr, NULL, display_ptr);
    struct wlr_output_layout *wlr_output_layout_ptr =
        wlr_output_layout_create(display_ptr);
    BS_TEST_VERIFY_NEQ_OR_RETURN(test_ptr, NULL, wlr_output_layout_ptr);
    struct wlr_output output = { .width = 1024, .height = 768, .scale = 1 };
    wlmtk_test_wlr_output_init(&output);
    wlr_output_layout_add(wlr_output_layout_ptr, &output, 0, 0);

    struct wlmtk_tile_style ts = {};
    wlmtk_workspace_t *ws_ptr = wlmtk_workspace_create(
        wlr_output_layout_ptr, "t", &ts);
    BS_TEST_VERIFY_NEQ_OR_RETURN(test_ptr, NULL, ws_ptr);
    wlmtk_workspace_enable(ws_ptr, true);

    wlmtk_fake_element_t *fe_ptr = wlmtk_fake_element_create();
    BS_TEST_VERIFY_NEQ_OR_RETURN(test_ptr, NULL, fe_ptr);
    wlmtk_window_t *w = wlmtk_test_window_create(&fe_ptr->element);
    wlmtk_window_set_properties(w, WLMTK_WINDOW_PROPERTY_RESIZABLE);
    BS_TEST_VERIFY_NEQ_OR_RETURN(test_ptr, NULL, w);

    wlmtk_util_test_listener_t state_changed_listener;
    wlmtk_util_connect_test_listener(
        &wlmtk_window_events(w)->state_changed,
        &state_changed_listener);

    wlmtk_util_test_listener_t request_fullscreen_listener;
    wlmtk_util_connect_test_listener(
        &wlmtk_window_events(w)->request_fullscreen,
        &request_fullscreen_listener);

    wlmtk_util_test_wlr_box_listener_t request_size_listener;
    wlmtk_util_connect_test_wlr_box_listener(
        &wlmtk_window_events(w)->request_size,
        &request_size_listener);

    wlmtk_util_test_listener_t request_maximized_listener;
    wlmtk_util_connect_test_listener(
        &wlmtk_window_events(w)->request_maximized,
        &request_maximized_listener);

    // Map the window. Must be activated.
    wlmtk_window_set_server_side_decorated(w, true);
    wlmtk_workspace_map_window(ws_ptr, w);
    BS_TEST_VERIFY_EQ(test_ptr, 1, state_changed_listener.calls);
    BS_TEST_VERIFY_EQ(test_ptr, w, state_changed_listener.last_data_ptr);
    wlmtk_util_clear_test_listener(&state_changed_listener);
    BS_TEST_VERIFY_TRUE(test_ptr, wlmtk_window_is_activated(w));

    // Setup initial size and position. Verify.
    wlmtk_fake_element_set_dimensions(fe_ptr, 200, 100);
    wlmtk_window_commit_size(w, 200, 100);
    wlmtk_workspace_set_window_position(ws_ptr, w, 20, 10);
    WLMTK_TEST_VERIFY_WLRBOX_EQ(
        test_ptr, 20, 10, 204, 121,
        wlmtk_window_get_bounding_box(w));
    WLMTK_TEST_VERIFY_WLRBOX_EQ(
        test_ptr, 0, 0, 200, 100,
        wlmtk_window_get_size(w));

    // Request fullscreen. Size yet unchanged, but requests issued.
    wlmtk_window_request_fullscreen(w, true);
    WLMTK_TEST_VERIFY_WLRBOX_EQ(
        test_ptr, 20, 10, 204, 121,
        wlmtk_window_get_bounding_box(w));
    BS_TEST_VERIFY_EQ(test_ptr, 1, request_fullscreen_listener.calls);
    wlmtk_util_clear_test_listener(&request_fullscreen_listener);
    BS_TEST_VERIFY_EQ(test_ptr, 1, request_size_listener.calls);
    WLMTK_TEST_VERIFY_WLRBOX_EQ(
        test_ptr, 0, 0, 1024, 768, request_size_listener.box);

    // Only after commit: Be fullscreen.
    wlmtk_fake_element_set_dimensions(fe_ptr, 1024, 768);
    wlmtk_window_commit_fullscreen(w, true);
    wlmtk_window_commit_size(w, 1024, 768);
    BS_TEST_VERIFY_EQ(test_ptr, 1, state_changed_listener.calls);
    BS_TEST_VERIFY_EQ(test_ptr, w, state_changed_listener.last_data_ptr);
    wlmtk_util_clear_test_listener(&state_changed_listener);
    WLMTK_TEST_VERIFY_WLRBOX_EQ(
        test_ptr, 0, 0, 1024, 768,
        wlmtk_window_get_bounding_box(w));
    WLMTK_TEST_VERIFY_WLRBOX_EQ(
        test_ptr, 0, 0, 1024, 768,
        wlmtk_window_get_size(w));
    BS_TEST_VERIFY_TRUE(test_ptr, w->server_side_decorated);
    BS_TEST_VERIFY_EQ(test_ptr, NULL, w->titlebar_ptr);
    BS_TEST_VERIFY_EQ(test_ptr, NULL, w->resizebar_ptr);

    // No maximization permitted.
    wlmtk_util_clear_test_wlr_box_listener(&request_size_listener);
    wlmtk_window_request_maximized(w, true);
    BS_TEST_VERIFY_EQ(test_ptr, 1, request_maximized_listener.calls);
    BS_TEST_VERIFY_FALSE(
        test_ptr,
        *((bool*)request_maximized_listener.last_data_ptr));
    BS_TEST_VERIFY_EQ(test_ptr, 0, request_size_listener.calls);

    // Request to end fullscreen. Also not taking immediate effect.
    wlmtk_window_request_fullscreen(w, false);
    WLMTK_TEST_VERIFY_WLRBOX_EQ(
        test_ptr, 0, 0, 1024, 768,
        wlmtk_window_get_bounding_box(w));
    WLMTK_TEST_VERIFY_WLRBOX_EQ(
        test_ptr, 0, 0, 1024, 768,
        wlmtk_window_get_size(w));
    BS_TEST_VERIFY_EQ(test_ptr, 1, request_fullscreen_listener.calls);
    wlmtk_util_clear_test_listener(&request_fullscreen_listener);
    BS_TEST_VERIFY_EQ(test_ptr, 1, request_size_listener.calls);
    WLMTK_TEST_VERIFY_WLRBOX_EQ(
        test_ptr, 0, 0, 200, 100, request_size_listener.box);
    wlmtk_util_clear_test_wlr_box_listener(&request_size_listener);

    // Only after commit: Be fullscreen.
    wlmtk_fake_element_set_dimensions(fe_ptr, 200, 100);
    wlmtk_window_commit_fullscreen(w, false);
    BS_TEST_VERIFY_EQ(test_ptr, 1, state_changed_listener.calls);
    BS_TEST_VERIFY_EQ(test_ptr, w, state_changed_listener.last_data_ptr);
    wlmtk_util_clear_test_listener(&state_changed_listener);
    WLMTK_TEST_VERIFY_WLRBOX_EQ(
        test_ptr, 20, 10, 204, 121,
        wlmtk_window_get_bounding_box(w));
    BS_TEST_VERIFY_TRUE(test_ptr, w->server_side_decorated);
    BS_TEST_VERIFY_NEQ(test_ptr, NULL, w->titlebar_ptr);
    BS_TEST_VERIFY_NEQ(test_ptr, NULL, w->resizebar_ptr);

    wlmtk_workspace_unmap_window(ws_ptr, w);

    wlmtk_util_disconnect_test_listener(&request_maximized_listener);
    wlmtk_util_disconnect_test_listener(&request_fullscreen_listener);
    wlmtk_util_disconnect_test_listener(&state_changed_listener);
    wlmtk_window_destroy(w);
    wlmtk_element_destroy(&fe_ptr->element);
    wlmtk_workspace_destroy(ws_ptr);
    wl_display_destroy(display_ptr);
}

/* ------------------------------------------------------------------------- */
/** Tests that unmapping a fullscreen window works. */
void test_fullscreen_unmap(bs_test_t *test_ptr)
{
    struct wl_display *display_ptr = wl_display_create();
    BS_TEST_VERIFY_NEQ_OR_RETURN(test_ptr, NULL, display_ptr);
    struct wlr_output_layout *wlr_output_layout_ptr =
        wlr_output_layout_create(display_ptr);
    BS_TEST_VERIFY_NEQ_OR_RETURN(test_ptr, NULL, wlr_output_layout_ptr);
    struct wlr_output output = { .width = 1024, .height = 768, .scale = 1 };
    wlmtk_test_wlr_output_init(&output);
    wlr_output_layout_add(wlr_output_layout_ptr, &output, 0, 0);

    struct wlmtk_tile_style ts = {};
    wlmtk_workspace_t *ws_ptr = wlmtk_workspace_create(
        wlr_output_layout_ptr, "t", &ts);
    BS_TEST_VERIFY_NEQ_OR_RETURN(test_ptr, NULL, ws_ptr);
    wlmtk_workspace_enable(ws_ptr, true);

    wlmtk_fake_element_t *fe_ptr = wlmtk_fake_element_create();
    BS_TEST_VERIFY_NEQ_OR_RETURN(test_ptr, NULL, fe_ptr);
    wlmtk_window_t *w = wlmtk_test_window_create(&fe_ptr->element);
    wlmtk_window_set_properties(w, WLMTK_WINDOW_PROPERTY_RESIZABLE);
    BS_TEST_VERIFY_NEQ_OR_RETURN(test_ptr, NULL, w);

    wlmtk_util_test_listener_t state_changed_listener;
    wlmtk_util_connect_test_listener(
        &wlmtk_window_events(w)->state_changed,
        &state_changed_listener);

    wlmtk_util_test_listener_t request_fullscreen_listener;
    wlmtk_util_connect_test_listener(
        &wlmtk_window_events(w)->request_fullscreen,
        &request_fullscreen_listener);

    // Map the window. Must be activated.
    wlmtk_window_set_server_side_decorated(w, true);
    wlmtk_workspace_map_window(ws_ptr, w);
    BS_TEST_VERIFY_EQ(test_ptr, 1, state_changed_listener.calls);
    wlmtk_util_clear_test_listener(&state_changed_listener);
    BS_TEST_VERIFY_TRUE(test_ptr, wlmtk_window_is_activated(w));

    // Setup initial size and position. Verify.
    wlmtk_fake_element_set_dimensions(fe_ptr, 200, 100);
    wlmtk_element_layout(wlmtk_window_element(w));
    wlmtk_workspace_set_window_position(ws_ptr, w, 20, 10);
    WLMTK_TEST_VERIFY_WLRBOX_EQ(
        test_ptr, 20, 10, 204, 121,
        wlmtk_window_get_bounding_box(w));

    // Request fullscreen. Verify the signals to request size and fullscreen.
    wlmtk_window_request_fullscreen(w, true);
    BS_TEST_VERIFY_EQ(test_ptr, 1, request_fullscreen_listener.calls);
    wlmtk_util_clear_test_listener(&request_fullscreen_listener);
    WLMTK_TEST_VERIFY_WLRBOX_EQ(
        test_ptr, 20, 10, 204, 121,
        wlmtk_window_get_bounding_box(w));

    // Only after commit: Be back at organic size.
    wlmtk_fake_element_set_dimensions(fe_ptr, 1024, 768);
    wlmtk_window_commit_fullscreen(w, true);
    BS_TEST_VERIFY_EQ(test_ptr, 1, state_changed_listener.calls);
    wlmtk_util_clear_test_listener(&state_changed_listener);
    WLMTK_TEST_VERIFY_WLRBOX_EQ(
        test_ptr, 0, 0, 1024, 768,
        wlmtk_window_get_bounding_box(w));
    BS_TEST_VERIFY_TRUE(test_ptr, w->server_side_decorated);
    BS_TEST_VERIFY_EQ(test_ptr, NULL, w->titlebar_ptr);
    BS_TEST_VERIFY_EQ(test_ptr, NULL, w->resizebar_ptr);

    wlmtk_workspace_unmap_window(ws_ptr, w);
    BS_TEST_VERIFY_FALSE(test_ptr, wlmtk_window_is_activated(w));

    wlmtk_util_disconnect_test_listener(&request_fullscreen_listener);
    wlmtk_util_disconnect_test_listener(&state_changed_listener);
    wlmtk_window_destroy(w);
    wlmtk_element_destroy(&fe_ptr->element);
    wlmtk_workspace_destroy(ws_ptr);
    wl_display_destroy(display_ptr);
}

/* ------------------------------------------------------------------------- */
/** Tests maximizing a window. */
void test_maximized(bs_test_t *test_ptr)
{
    struct wl_display *display_ptr = wl_display_create();
    BS_TEST_VERIFY_NEQ_OR_RETURN(test_ptr, NULL, display_ptr);
    struct wlr_output_layout *wlr_output_layout_ptr =
        wlr_output_layout_create(display_ptr);
    BS_TEST_VERIFY_NEQ_OR_RETURN(test_ptr, NULL, wlr_output_layout_ptr);
    struct wlr_output output = { .width = 1024, .height = 768, .scale = 1 };
    wlmtk_test_wlr_output_init(&output);
    wlr_output_layout_add(wlr_output_layout_ptr, &output, 0, 0);

    struct wlmtk_tile_style ts = {};
    wlmtk_workspace_t *ws_ptr = wlmtk_workspace_create(
        wlr_output_layout_ptr, "t", &ts);
    BS_TEST_VERIFY_NEQ_OR_RETURN(test_ptr, NULL, ws_ptr);
    wlmtk_workspace_enable(ws_ptr, true);

    wlmtk_fake_element_t *fe_ptr = wlmtk_fake_element_create();
    BS_TEST_VERIFY_NEQ_OR_RETURN(test_ptr, NULL, fe_ptr);
    wlmtk_window_t *w = wlmtk_test_window_create(&fe_ptr->element);
    wlmtk_window_set_properties(w, WLMTK_WINDOW_PROPERTY_RESIZABLE);
    BS_TEST_VERIFY_NEQ_OR_RETURN(test_ptr, NULL, w);

    wlmtk_util_test_listener_t state_changed_listener;
    wlmtk_util_connect_test_listener(
        &wlmtk_window_events(w)->state_changed,
        &state_changed_listener);

    wlmtk_util_test_listener_t request_maximized_listener;
    wlmtk_util_connect_test_listener(
        &wlmtk_window_events(w)->request_maximized,
        &request_maximized_listener);

    wlmtk_util_test_wlr_box_listener_t request_size_listener;
    wlmtk_util_connect_test_wlr_box_listener(
        &wlmtk_window_events(w)->request_size,
        &request_size_listener);

    // Before mapped: Refuses.
    wlmtk_window_request_maximized(w, false);
    wlmtk_window_request_maximized(w, true);
    BS_TEST_VERIFY_EQ(test_ptr, 2, request_maximized_listener.calls);
    wlmtk_util_clear_test_listener(&request_maximized_listener);

    // Map the window. Must be activated.
    wlmtk_window_set_server_side_decorated(w, true);
    wlmtk_workspace_map_window(ws_ptr, w);
    BS_TEST_VERIFY_EQ(test_ptr, 1, state_changed_listener.calls);
    BS_TEST_VERIFY_EQ(test_ptr, w, state_changed_listener.last_data_ptr);
    wlmtk_util_clear_test_listener(&state_changed_listener);
    BS_TEST_VERIFY_TRUE(test_ptr, wlmtk_window_is_activated(w));

    // Setup initial size and position. Verify.
    wlmtk_fake_element_set_dimensions(fe_ptr, 200, 100);
    wlmtk_window_commit_size(w, 150, 80);
    wlmtk_workspace_set_window_position(ws_ptr, w, 20, 10);
    WLMTK_TEST_VERIFY_WLRBOX_EQ(
        test_ptr, 20, 10, 204, 121,
        wlmtk_window_get_bounding_box(w));
    WLMTK_TEST_VERIFY_WLRBOX_EQ(
        test_ptr, 0, 0, 150, 80,
        wlmtk_window_get_size(w));

    wlmtk_util_clear_test_listener(&state_changed_listener);
    wlmtk_window_request_maximized(w, false);
    BS_TEST_VERIFY_EQ(test_ptr, 1, request_maximized_listener.calls);

    // Request maximized. Size yet unchanged, but requests issued.
    wlmtk_util_clear_test_listener(&request_maximized_listener);
    wlmtk_util_clear_test_wlr_box_listener(&request_size_listener);
    wlmtk_window_request_maximized(w, true);
    WLMTK_TEST_VERIFY_WLRBOX_EQ(
        test_ptr, 20, 10, 204, 121,
        wlmtk_window_get_bounding_box(w));
    BS_TEST_VERIFY_EQ(test_ptr, 1, request_maximized_listener.calls);
    wlmtk_util_clear_test_listener(&request_maximized_listener);
    BS_TEST_VERIFY_EQ(test_ptr, 1, request_size_listener.calls);
    WLMTK_TEST_VERIFY_WLRBOX_EQ(
        test_ptr, 0, 0, 1020, 747, request_size_listener.box);

    // Only after commit: Be maximized.
    wlmtk_fake_element_set_dimensions(fe_ptr, 1020, 747);
    wlmtk_window_commit_maximized(w, true);
    wlmtk_window_commit_size(w, 1010, 737);
    BS_TEST_VERIFY_EQ(test_ptr, 1, state_changed_listener.calls);
    BS_TEST_VERIFY_EQ(test_ptr, w, state_changed_listener.last_data_ptr);
    wlmtk_util_clear_test_listener(&state_changed_listener);
    WLMTK_TEST_VERIFY_WLRBOX_EQ(
        test_ptr, 0, 0, 1024, 768,
        wlmtk_window_get_bounding_box(w));
    WLMTK_TEST_VERIFY_WLRBOX_EQ(
        test_ptr, 0, 0, 1010, 737,
        wlmtk_window_get_size(w));
    BS_TEST_VERIFY_TRUE(test_ptr, w->server_side_decorated);
    BS_TEST_VERIFY_NEQ(test_ptr, NULL, w->titlebar_ptr);
    BS_TEST_VERIFY_NEQ(test_ptr, NULL, w->resizebar_ptr);

    // A further "request_maximized" call will be refued.
    wlmtk_window_request_maximized(w, true);
    BS_TEST_VERIFY_EQ(test_ptr, 1, request_maximized_listener.calls);

    // But, if the workspace extents are different, it will accept and request
    // resizing..
    wlmtk_util_clear_test_listener(&request_maximized_listener);
    wlmtk_util_clear_test_wlr_box_listener(&request_size_listener);
    output.width = 800; output.height = 600;
    wlmtk_window_request_maximized(w, true);
    BS_TEST_VERIFY_EQ(test_ptr, 1, request_maximized_listener.calls);
    BS_TEST_VERIFY_EQ(test_ptr, 1, request_size_listener.calls);
    WLMTK_TEST_VERIFY_WLRBOX_EQ(
        test_ptr, 0, 0, 796, 579, request_size_listener.box);

    // Request to end maximized. Also not taking immediate effect.
    wlmtk_util_clear_test_listener(&request_maximized_listener);
    wlmtk_util_clear_test_wlr_box_listener(&request_size_listener);
    wlmtk_window_request_maximized(w, false);
    WLMTK_TEST_VERIFY_WLRBOX_EQ(
        test_ptr, 0, 0, 1024, 768,
        wlmtk_window_get_bounding_box(w));
    WLMTK_TEST_VERIFY_WLRBOX_EQ(
        test_ptr, 0, 0, 1010, 737,
        wlmtk_window_get_size(w));
    BS_TEST_VERIFY_EQ(test_ptr, 1, request_maximized_listener.calls);
    BS_TEST_VERIFY_EQ(test_ptr, 1, request_size_listener.calls);
    WLMTK_TEST_VERIFY_WLRBOX_EQ(
        test_ptr, 0, 0, 150, 80, request_size_listener.box);

    // Only after commit: Be back at organic size.
    wlmtk_fake_element_set_dimensions(fe_ptr, 200, 100);
    wlmtk_window_commit_size(w, 160, 80);
    wlmtk_window_commit_maximized(w, false);
    BS_TEST_VERIFY_EQ(test_ptr, 1, state_changed_listener.calls);
    wlmtk_util_clear_test_listener(&state_changed_listener);
    WLMTK_TEST_VERIFY_WLRBOX_EQ(
        test_ptr, 20, 10, 204, 121,
        wlmtk_window_get_bounding_box(w));
    WLMTK_TEST_VERIFY_WLRBOX_EQ(
        test_ptr, 0, 0, 160, 80,
        wlmtk_window_get_size(w));
    BS_TEST_VERIFY_TRUE(test_ptr, w->server_side_decorated);
    BS_TEST_VERIFY_NEQ(test_ptr, NULL, w->titlebar_ptr);
    BS_TEST_VERIFY_NEQ(test_ptr, NULL, w->resizebar_ptr);

    wlmtk_workspace_unmap_window(ws_ptr, w);

    wlmtk_util_disconnect_test_listener(&request_maximized_listener);
    wlmtk_util_disconnect_test_listener(&state_changed_listener);
    wlmtk_window_destroy(w);
    wlmtk_element_destroy(&fe_ptr->element);
    wlmtk_workspace_destroy(ws_ptr);
    wl_display_destroy(display_ptr);
}

/* ------------------------------------------------------------------------- */
/** Tests shading of a window. */
void test_shaded(bs_test_t *test_ptr)
{
    wlmtk_fake_element_t *fe_ptr = wlmtk_fake_element_create();
    BS_TEST_VERIFY_NEQ_OR_RETURN(test_ptr, NULL, fe_ptr);
    wlmtk_fake_element_set_dimensions(fe_ptr, 42, 20);
    wlmtk_window_t *w = wlmtk_test_window_create(&fe_ptr->element);
    wlmtk_window_set_properties(w, WLMTK_WINDOW_PROPERTY_RESIZABLE);
    BS_TEST_VERIFY_NEQ_OR_RETURN(test_ptr, NULL, w);

    wlmtk_util_test_listener_t l;
    wlmtk_util_connect_test_listener(
        &wlmtk_window_events(w)->state_changed, &l);

    // Not decorated: Will not be shaded.
    wlmtk_window_set_server_side_decorated(w, false);
    wlmtk_window_request_shaded(w, true);
    BS_TEST_VERIFY_FALSE(test_ptr, wlmtk_window_is_shaded(w));
    wlmtk_window_set_server_side_decorated(w, true);
    BS_TEST_VERIFY_EQ(test_ptr, 0, l.calls);
    WLMTK_TEST_VERIFY_WLRBOX_EQ(
        test_ptr, 0, 0, 46, 41,
        wlmtk_window_get_bounding_box(w));

    // Fullscreen: Will not be shaded.
    wlmtk_window_commit_fullscreen(w, true);
    wlmtk_util_clear_test_listener(&l);
    wlmtk_window_request_shaded(w, true);
    BS_TEST_VERIFY_FALSE(test_ptr, wlmtk_window_is_shaded(w));
    BS_TEST_VERIFY_EQ(test_ptr, 0, l.calls);
    wlmtk_window_commit_fullscreen(w, false);

    // Now a regular, decorated window: Will be shaded.
    wlmtk_util_clear_test_listener(&l);
    wlmtk_window_request_shaded(w, true);
    BS_TEST_VERIFY_TRUE(test_ptr, wlmtk_window_is_shaded(w));
    BS_TEST_VERIFY_FALSE(test_ptr, fe_ptr->element.visible);
    BS_TEST_VERIFY_FALSE(
        test_ptr,
        wlmtk_resizebar_element(w->resizebar_ptr)->visible);
    WLMTK_TEST_VERIFY_WLRBOX_EQ(
        test_ptr, 0, 0, 46, 15,
        wlmtk_window_get_bounding_box(w));
    BS_TEST_VERIFY_EQ(test_ptr, 1, l.calls);
    BS_TEST_VERIFY_EQ(test_ptr, w, l.last_data_ptr);

    // And verify that unshading works.
    wlmtk_util_clear_test_listener(&l);
    wlmtk_window_request_shaded(w, false);
    BS_TEST_VERIFY_FALSE(test_ptr, wlmtk_window_is_shaded(w));
    BS_TEST_VERIFY_TRUE(test_ptr, fe_ptr->element.visible);
    BS_TEST_VERIFY_TRUE(
        test_ptr,
        wlmtk_resizebar_element(w->resizebar_ptr)->visible);
    WLMTK_TEST_VERIFY_WLRBOX_EQ(
        test_ptr, 0, 0, 46, 41,
        wlmtk_window_get_bounding_box(w));
    BS_TEST_VERIFY_EQ(test_ptr, 1, l.calls);
    BS_TEST_VERIFY_EQ(test_ptr, w, l.last_data_ptr);

    wlmtk_window_destroy(w);
    wlmtk_element_destroy(&fe_ptr->element);
}

/* ------------------------------------------------------------------------- */
/** Tests that modifer-motion moves the window. */
void test_modifier_move(bs_test_t *test_ptr)
{
    struct wl_display *display_ptr = wl_display_create();
    BS_TEST_VERIFY_NEQ_OR_RETURN(test_ptr, NULL, display_ptr);
    struct wlr_output_layout *wlr_output_layout_ptr =
        wlr_output_layout_create(display_ptr);
    BS_TEST_VERIFY_NEQ_OR_RETURN(test_ptr, NULL, wlr_output_layout_ptr);
    struct wlr_output output = { .width = 1024, .height = 768, .scale = 1 };
    wlmtk_test_wlr_output_init(&output);
    wlr_output_layout_add(wlr_output_layout_ptr, &output, 0, 0);
    struct wlmtk_tile_style ts = {};
    wlmtk_workspace_t *ws_ptr = wlmtk_workspace_create(
        wlr_output_layout_ptr, "t", &ts);
    BS_TEST_VERIFY_NEQ_OR_RETURN(test_ptr, NULL, ws_ptr);
    wlmtk_element_set_visible(wlmtk_workspace_element(ws_ptr), true);
    wlmtk_workspace_enable(ws_ptr, true);

    wlmtk_fake_element_t *fe_ptr = wlmtk_fake_element_create();
    BS_TEST_VERIFY_NEQ_OR_RETURN(test_ptr, NULL, fe_ptr);
    wlmtk_fake_element_set_dimensions(fe_ptr, 40, 20);
    wlmtk_window_t *w = wlmtk_test_window_create(&fe_ptr->element);
    BS_TEST_VERIFY_NEQ_OR_RETURN(test_ptr, NULL, w);
    wlmtk_workspace_map_window(ws_ptr, w);
    BS_TEST_VERIFY_EQ(test_ptr, 0, wlmtk_window_element(w)->x);
    BS_TEST_VERIFY_EQ(test_ptr, 0, wlmtk_window_element(w)->y);

    // Move pointer over the window.
    wlmtk_pointer_motion_event_t mev = { .x = 20, .y = 10 };
    BS_TEST_VERIFY_TRUE(
        test_ptr,
        wlmtk_element_pointer_motion(
            wlmtk_workspace_element(ws_ptr), &mev));

    // Drag: Button down, and have both modifiers pressed, then move.
    wlmtk_button_event_t bev = {
        .button = BTN_LEFT,
        .type = WLMTK_BUTTON_DOWN,
        .keyboard_modifiers = WLR_MODIFIER_ALT
    };
    BS_TEST_VERIFY_TRUE(
        test_ptr,
        wlmtk_element_pointer_button(wlmtk_workspace_element(ws_ptr), &bev));
    mev = (wlmtk_pointer_motion_event_t){ .x = 24, .y = 13 };
    BS_TEST_VERIFY_TRUE(
        test_ptr,
        wlmtk_element_pointer_motion(
            wlmtk_workspace_element(ws_ptr), &mev));

    // Now must be at a new position.
    BS_TEST_VERIFY_EQ(test_ptr, 4, wlmtk_window_element(w)->x);
    BS_TEST_VERIFY_EQ(test_ptr, 3, wlmtk_window_element(w)->y);

    // Change modifier. Move again, with ALT -- must not move the window.
    bev.type = WLMTK_BUTTON_UP;
    wlmtk_element_pointer_button(wlmtk_workspace_element(ws_ptr), &bev);
    bev.type = WLMTK_BUTTON_DOWN;
    wlmtk_window_set_options(w, WLR_MODIFIER_LOGO);
    BS_TEST_VERIFY_TRUE(
        test_ptr,
        wlmtk_element_pointer_button(wlmtk_workspace_element(ws_ptr), &bev));
    mev = (wlmtk_pointer_motion_event_t){ .x = 22, .y = 13 };
    BS_TEST_VERIFY_TRUE(
        test_ptr,
        wlmtk_element_pointer_motion(
            wlmtk_workspace_element(ws_ptr), &mev));
    BS_TEST_VERIFY_EQ(test_ptr, 4, wlmtk_window_element(w)->x);
    BS_TEST_VERIFY_EQ(test_ptr, 3, wlmtk_window_element(w)->y);

    // Now, move with the correct modifier. Moves by another bit to the right.
    bev.type = WLMTK_BUTTON_UP;
    wlmtk_element_pointer_button(wlmtk_workspace_element(ws_ptr), &bev);
    bev.keyboard_modifiers = WLR_MODIFIER_LOGO;
    bev.type = WLMTK_BUTTON_DOWN;
    wlmtk_window_set_options(w, WLR_MODIFIER_LOGO);
    BS_TEST_VERIFY_TRUE(
        test_ptr,
        wlmtk_element_pointer_button(wlmtk_workspace_element(ws_ptr), &bev));
    mev = (wlmtk_pointer_motion_event_t){ .x = 24, .y = 13 };
    BS_TEST_VERIFY_TRUE(
        test_ptr,
        wlmtk_element_pointer_motion(
            wlmtk_workspace_element(ws_ptr), &mev));
    BS_TEST_VERIFY_EQ(test_ptr, 6, wlmtk_window_element(w)->x);
    BS_TEST_VERIFY_EQ(test_ptr, 3, wlmtk_window_element(w)->y);


    wlmtk_workspace_unmap_window(ws_ptr, w);
    wlmtk_window_destroy(w);
    wlmtk_element_destroy(&fe_ptr->element);
    wlmtk_workspace_destroy(ws_ptr);
    wl_display_destroy(display_ptr);
}

/* ------------------------------------------------------------------------- */
/** Tests enabling and disabling the window menu. */
void test_menu(bs_test_t *test_ptr)
{
    wlmtk_window_t *w = wlmtk_test_window_create(NULL);
    BS_TEST_VERIFY_NEQ_OR_RETURN(test_ptr, NULL, w);

    wlmtk_menu_item_t *i = wlmtk_menu_item_create(w->menu_style_ref_ptr);
    BS_TEST_VERIFY_NEQ_OR_RETURN(test_ptr, NULL, i);
    wlmtk_menu_add_item(wlmtk_window_menu(w), i);

    // No menu shown when not activated.
    wlmtk_window_menu_set_enabled(w, true);
    BS_TEST_VERIFY_FALSE(
        test_ptr,
        wlmtk_menu_element(wlmtk_window_menu(w))->visible);

    // Show when activated.
    wlmtk_window_set_activated(w, true);
    wlmtk_window_menu_set_enabled(w, true);
    BS_TEST_VERIFY_TRUE(
        test_ptr,
        wlmtk_menu_element(wlmtk_window_menu(w))->visible);
    // Bounding box still none: The menu's dimensions not included.
    WLMTK_TEST_VERIFY_WLRBOX_EQ(
        test_ptr, 0, 0, 0, 0,
        wlmtk_window_get_bounding_box(w));

    // Hide.
    _wlmtk_window_menu_request_close_handler(
        &w->menu_request_close_listener, NULL);
    BS_TEST_VERIFY_FALSE(
        test_ptr,
        wlmtk_menu_element(wlmtk_window_menu(w))->visible);

    wlmtk_window_destroy(w);
}

/* ------------------------------------------------------------------------- */
/** Tests style reference counter. */
void test_style(bs_test_t *test_ptr)
{
    struct wlmtk_window_style *s = wlmtk_window_style_create();
    BS_TEST_VERIFY_NEQ_OR_RETURN(test_ptr, NULL, s);

    wlmtk_window_style_ref_t *r = wlmtk_window_style_to_ref(s);
    BS_TEST_VERIFY_EQ(test_ptr, s, wlmtk_window_style_ref_retain(r));
    wlmtk_window_style_ref_release(r);
    wlmtk_window_style_ref_release(r);

    BS_TEST_VERIFY_EQ(test_ptr, NULL, wlmtk_window_style_to_ref(NULL));
}

/* ------------------------------------------------------------------------- */
/** Tests decoding the window style from a plist dict. */
void test_style_decode(bs_test_t *test_ptr)
{
    static const char *s = ("{"
         "  TitleBar = {"
         "    FocussedFill = {"
         "      Color = \"argb32:ff000000\";"
         "      Type = SOLID;"
         "    };"
         "    FocussedTextColor = \"argb32:ffffffff\";"
         "    BlurredFill = {"
         "      Color = \"argb32:ff666666\";"
         "      Type = SOLID;"
         "    };"
         "    BlurredTextColor = \"argb32:ff000000\";"
         "    Height = 22;"
         "    BezelWidth = 1;"
         "    Margin = {"
         "      Width = 1;"
         "      Color = \"argb32:ff000000\";"
         "    };"
         "    Font = {"
         "      Face = Helvetica;"
         "      Weight = Bold;"
         "      Size = 15;"
         "    };"
         "  };"
         "  ResizeBar = {"
         "    Fill = { Color = \"argb32:ffaaaaaa\"; Type = SOLID; };"
         "    Height = 7; CornerWidth = 29; BezelWidth = 1;"
         "  };"
         "  Border = { Width = 1; Color = \"argb32:10203040\"; };"
         "  Margin = { Width = 2; Color = \"argb32:01020304\"; }"
         "}");

    struct wlmtk_window_style *ws = wlmtk_window_style_create();
    BS_TEST_VERIFY_NEQ_OR_RETURN(test_ptr, NULL, ws);

    bspl_dict_t *d = bspl_dict_from_object(
        bspl_create_object_from_plist_string(s));
    BS_TEST_VERIFY_NEQ_OR_RETURN(test_ptr, NULL, d);
    BS_TEST_VERIFY_TRUE(
        test_ptr,
        bspl_decode_dict(d, _wlmtk_window_style_desc, ws));

    BS_TEST_VERIFY_EQ(test_ptr, 0xffffffff, ws->titlebar.focussed_text_color);
    BS_TEST_VERIFY_EQ(test_ptr, 15, ws->titlebar.font.size);

    BS_TEST_VERIFY_EQ(test_ptr, WLMTK_STYLE_COLOR_SOLID, ws->resizebar.fill.type);
    BS_TEST_VERIFY_EQ(test_ptr, 0xffaaaaaa, ws->resizebar.fill.param.solid.color);
    BS_TEST_VERIFY_EQ(test_ptr, 7, ws->resizebar.height);
    BS_TEST_VERIFY_EQ(test_ptr, 29, ws->resizebar.corner_width);
    BS_TEST_VERIFY_EQ(test_ptr, 1, ws->resizebar.bezel_width);

    BS_TEST_VERIFY_EQ(test_ptr, 1, ws->border.width);
    BS_TEST_VERIFY_EQ(test_ptr, 0x10203040, ws->border.color);
    BS_TEST_VERIFY_EQ(test_ptr, 2, ws->margin.width);
    BS_TEST_VERIFY_EQ(test_ptr, 0x01020304, ws->margin.color);

    bspl_dict_unref(d);
    wlmtk_window_style_ref_release(wlmtk_window_style_to_ref(ws));
}

/* == End of window.c ===================================================== */
