Changing window sizes programmatically with XCB

Viewed 57

I'm working toward an XCB-controlled window which is able to respond to requested changes in position and resolution programmatically, rather than via the window manager. I'm successfully able to "fullscreen" the window via right alt and return. However, when I use that key combo again, it appears as if nothing happens.

Here's the code I'm working with:

// g++ -Wall -Wextra -std=c++20 -o xcb-resize -lxcb -lxcb-randr -lxcb-keysyms xcb-resize.cpp

#include <iostream>
#include <iomanip>
#include <string>
#include <cstring>
#include <chrono>
#include <ctime>

#include <xcb/xcb.h>
#include <xcb/xcb_keysyms.h>
#include <xcb/randr.h>

#define XK_MISCELLANY
#define XK_LATIN1
#include <X11/keysymdef.h>

using client_msg        = ::xcb_client_message_event_t *;
using config_notify     = ::xcb_configure_notify_event_t *;
using keypress_notify   = ::xcb_key_press_event_t *;
using keyrelease_notify = ::xcb_key_release_event_t *;
using property_notify   = ::xcb_property_notify_event_t *;

static constexpr int XCB_EVENT_RESPONSE_TYPE_MASK = ~0x80;
static constexpr ::xcb_mod_mask_t altmask = ::XCB_MOD_MASK_5;

//==============================================================================
auto timestamp() {
    auto now = std::time(nullptr);
    return std::put_time(std::gmtime(&now), "%H:%M:%S");
}

//==============================================================================
const std::string get_atom_name(xcb_connection_t *connection, xcb_atom_t atom) {
    auto cookie = xcb_get_atom_name(connection, atom);
    auto *reply = xcb_get_atom_name_reply(connection, cookie, nullptr);

    std::string name(xcb_get_atom_name_name(reply));

    free(reply);
    return name;
}

class X11Window {
public:
//==============================================================================
    bool message_loop() {
        ::xcb_generic_event_t *event = nullptr;

        while((event = ::xcb_poll_for_event(_connection))) {        
            switch(event->response_type & XCB_EVENT_RESPONSE_TYPE_MASK) {            
                case XCB_KEY_PRESS: std::cout << timestamp() << " " << "XCB_KEY_PRESS\n";
                {
                    auto *press = reinterpret_cast<keypress_notify>(event);
                    auto key = ::xcb_key_symbols_get_keysym(
                        _key_symbols,
                        press->detail,
                        0
                    );

                    switch(key) {
                        case XK_Escape:
                            std::cout << timestamp() << " " << "Exiting...\n";
                            _running = false;
                            break;
                    }
                    break;
                }

                case XCB_KEY_RELEASE: std::cout << timestamp() << " " << "XCB_KEY_RELEASE\n";
                {
                    auto *release = reinterpret_cast<keyrelease_notify>(event);
                    auto key = ::xcb_key_symbols_get_keysym(
                        _key_symbols,
                        release->detail,
                        0
                    );

                    switch(key) {
                        case XK_Return: {
                            if(release->state & altmask) {
                                static uint32_t prev_width;
                                static uint32_t prev_height;

                                if(_fullscreen) {
                                    _width  = prev_width;
                                    _height = prev_height;
                                }
                                else {
                                    prev_width  = _width;
                                    prev_height = _height;

                                    _width  = _display_xres;
                                    _height = _display_yres;
                                }

                                _fullscreen = !_fullscreen;
                                _size_and_place_window(_width, _height);
                            }
                            break;
                        }
                    }
                    break;
                }

                case XCB_BUTTON_PRESS:      std::cout << timestamp() << " " << "XCB_BUTTON_PRESS\n";      break;
                case XCB_BUTTON_RELEASE:    std::cout << timestamp() << " " << "XCB_BUTTON_RELEASE\n";    break;
                case XCB_MOTION_NOTIFY:     std::cout << timestamp() << " " << "XCB_MOTION_NOTIFY\n";     break;
                case XCB_ENTER_NOTIFY:      std::cout << timestamp() << " " << "XCB_ENTER_NOTIFY\n";      break;
                case XCB_LEAVE_NOTIFY:      std::cout << timestamp() << " " << "XCB_LEAVE_NOTIFY\n";      break;
                case XCB_FOCUS_IN:          std::cout << timestamp() << " " << "XCB_FOCUS_IN\n";          break;
                case XCB_FOCUS_OUT:         std::cout << timestamp() << " " << "XCB_FOCUS_OUT\n";         break;
                case XCB_KEYMAP_NOTIFY:     std::cout << timestamp() << " " << "XCB_KEYMAP_NOTIFY\n";     break;
                case XCB_EXPOSE:            std::cout << timestamp() << " " << "XCB_EXPOSE\n";            break;
                case XCB_GRAPHICS_EXPOSURE: std::cout << timestamp() << " " << "XCB_GRAPHICS_EXPOSURE\n"; break;
                case XCB_NO_EXPOSURE:       std::cout << timestamp() << " " << "XCB_NO_EXPOSURE\n";       break;
                case XCB_VISIBILITY_NOTIFY: std::cout << timestamp() << " " << "XCB_VISIBILITY_NOTIFY\n"; break;

                case XCB_CREATE_NOTIFY: std::cout << timestamp() << " " << "XCB_CREATE_NOTIFY\n";
                    _running = true;
                    break;

                case XCB_DESTROY_NOTIFY: std::cout << timestamp() << " " << "XCB_DESTROY_NOTIFY\n";
                    _running = false;
                    break;

                case XCB_UNMAP_NOTIFY:    std::cout << timestamp() << " " << "XCB_UNMAP_NOTIFY\n";    break;
                case XCB_MAP_NOTIFY:      std::cout << timestamp() << " " << "XCB_MAP_NOTIFY\n";      break;
                case XCB_MAP_REQUEST:     std::cout << timestamp() << " " << "XCB_MAP_REQUEST\n";     break;
                case XCB_REPARENT_NOTIFY: std::cout << timestamp() << " " << "XCB_REPARENT_NOTIFY\n"; break;

                case XCB_CONFIGURE_NOTIFY: std::cout << timestamp() << " " << "XCB_CONFIGURE_NOTIFY\n";
                {
                    auto *config = reinterpret_cast<config_notify>(event);
                    _width         = config->width;
                    _height        = config->height;
                    break;
                }

                case XCB_CONFIGURE_REQUEST: std::cout << timestamp() << " " << "XCB_CONFIGURE_REQUEST\n"; break;
                case XCB_GRAVITY_NOTIFY:    std::cout << timestamp() << " " << "XCB_GRAVITY_NOTIFY\n";    break;

                case XCB_RESIZE_REQUEST: std::cout << timestamp() << " " << "XCB_RESIZE_REQUEST\n";
                    // _resized = true;
                    break;

                case XCB_CIRCULATE_NOTIFY:  std::cout << timestamp() << " " << "XCB_CIRCULATE_NOTIFY\n";  break;
                
                case XCB_PROPERTY_NOTIFY: std::cout << timestamp() << " " << "XCB_PROPERTY_NOTIFY\n";
                {
                    auto *props = reinterpret_cast<property_notify>(event);
                    
                    printf(
                        "\tatom: %u (%s)\n",
                        props->atom,
                        get_atom_name(_connection, props->atom).c_str()
                    );
                    break;
                }

                case XCB_SELECTION_CLEAR:   std::cout << timestamp() << " " << "XCB_SELECTION_CLEAR\n";   break;
                case XCB_SELECTION_REQUEST: std::cout << timestamp() << " " << "XCB_SELECTION_REQUEST\n"; break;
                case XCB_SELECTION_NOTIFY:  std::cout << timestamp() << " " << "XCB_SELECTION_NOTIFY\n";  break;
                case XCB_COLORMAP_NOTIFY:   std::cout << timestamp() << " " << "XCB_COLORMAP_NOTIFY\n";   break;

                case XCB_CLIENT_MESSAGE: std::cout << timestamp() << " " << "XCB_CLIENT_MESSAGE\n";
                {
                    auto *msg = reinterpret_cast<client_msg>(event);
                    if(msg->data.data32[0] == _wm_delete) {
                        _running = false;
                    }
                    break;
                }
                
                case XCB_GE_GENERIC: std::cout << timestamp() << " " << "XCB_GE_GENERIC\n"; break;
                
                default:
                    std::cerr << "Unknown XCB message?\n";
                    break;
            }

            free(event);
        }

        return _running;
    }

//==============================================================================
    void init_window() {
        int screenp = 0;
        _connection = ::xcb_connect(nullptr, &screenp);

        if(::xcb_connection_has_error(_connection)) {
            std::cerr << "Could not connect to X server.\n";
        }

        _key_symbols = xcb_key_symbols_alloc(_connection);

        ::xcb_screen_iterator_t screen_iter =
            ::xcb_setup_roots_iterator(::xcb_get_setup(_connection));

        for(int screen = screenp; screen > 0; --screen) {
            ::xcb_screen_next(&screen_iter);
        }

        _screen = screen_iter.data;

        _window = ::xcb_generate_id(_connection);

        uint32_t vakue_mask = ::XCB_CW_BACK_PIXEL | ::XCB_CW_EVENT_MASK;
        uint32_t value_list[] {
            _screen->black_pixel,
            ::XCB_EVENT_MASK_KEY_PRESS |
            ::XCB_EVENT_MASK_KEY_RELEASE |
            ::XCB_EVENT_MASK_BUTTON_PRESS |
            ::XCB_EVENT_MASK_BUTTON_RELEASE |
            ::XCB_EVENT_MASK_ENTER_WINDOW |
            ::XCB_EVENT_MASK_LEAVE_WINDOW |
            ::XCB_EVENT_MASK_POINTER_MOTION |
            ::XCB_EVENT_MASK_POINTER_MOTION_HINT |
            ::XCB_EVENT_MASK_BUTTON_1_MOTION |
            ::XCB_EVENT_MASK_BUTTON_2_MOTION |
            ::XCB_EVENT_MASK_BUTTON_3_MOTION |
            ::XCB_EVENT_MASK_BUTTON_4_MOTION |
            ::XCB_EVENT_MASK_BUTTON_5_MOTION |
            ::XCB_EVENT_MASK_BUTTON_MOTION |
            ::XCB_EVENT_MASK_KEYMAP_STATE |
            ::XCB_EVENT_MASK_EXPOSURE |
            ::XCB_EVENT_MASK_VISIBILITY_CHANGE |
            ::XCB_EVENT_MASK_STRUCTURE_NOTIFY |
            ::XCB_EVENT_MASK_RESIZE_REDIRECT |
            ::XCB_EVENT_MASK_SUBSTRUCTURE_NOTIFY |
            ::XCB_EVENT_MASK_SUBSTRUCTURE_REDIRECT |
            ::XCB_EVENT_MASK_FOCUS_CHANGE |
            ::XCB_EVENT_MASK_PROPERTY_CHANGE |
            ::XCB_EVENT_MASK_COLOR_MAP_CHANGE |
            ::XCB_EVENT_MASK_OWNER_GRAB_BUTTON |
            ::XCB_EVENT_MASK_NO_EVENT
        };

        ::xcb_create_window(
            _connection,
            XCB_COPY_FROM_PARENT,
            _window,
            _screen->root,
            0, 0,
            static_cast<uint16_t>(_width),
            static_cast<uint16_t>(_height),
            0,
            ::XCB_WINDOW_CLASS_INPUT_OUTPUT,
            _screen->root_visual,
            vakue_mask,
            value_list
        );

        _set_delete_event();
        _no_decorations();

        ::xcb_map_window(_connection, _window);

        _query_randr();
        _size_and_place_window(_width, _height);

        _running = true;
    }

private:
    ::xcb_connection_t  *_connection;
    ::xcb_window_t       _window;
    ::xcb_screen_t      *_screen;
    ::xcb_atom_t         _wm_delete;
    ::xcb_atom_t         _wm_proto;
    ::xcb_key_symbols_t *_key_symbols;
    
    uint32_t _width;
    uint32_t _height;

    uint32_t _display_xres;
    uint32_t _display_yres;
    int32_t  _display_xoff;
    int32_t  _display_yoff;

    bool _running;
    bool _fullscreen;

//==============================================================================
    void _no_decorations() {
        auto motif_cookie = xcb_intern_atom(
            _connection,
            0,
            strlen("_MOTIF_WM_HINTS"),
            "_MOTIF_WM_HINTS"
        );
        auto *motif_reply = xcb_intern_atom_reply(
            _connection,
            motif_cookie,
            NULL
        );

        struct MotifHints {
            uint32_t   flags;
            uint32_t   functions;
            uint32_t   decorations;
            int32_t    input_mode;
            uint32_t   status;
        };

        MotifHints hints {
            .flags = 2u,
            .functions = 0u,
            .decorations = 0u,
            .input_mode = 0,
            .status = 0u,
        };

        ::xcb_change_property(
            _connection,
            XCB_PROP_MODE_REPLACE,
            _window,
            motif_reply->atom,
            XCB_ATOM_INTEGER,
            32,
            5,
            &hints
        );

        free(motif_reply);
    }

//==============================================================================
    void _set_delete_event() {
        ::xcb_intern_atom_cookie_t wm_delete_cookie = ::xcb_intern_atom(
            _connection,
            0,
            strlen("WM_DELETE_WINDOW"),
            "WM_DELETE_WINDOW"
        );

        ::xcb_intern_atom_cookie_t wm_protocols_cookie = ::xcb_intern_atom(
            _connection,
            0,
            strlen("WM_PROTOCOLS"),
            "WM_PROTOCOLS"
        );

        ::xcb_intern_atom_reply_t *delete_reply = ::xcb_intern_atom_reply(
            _connection,
            wm_delete_cookie,
            nullptr
        );

        ::xcb_intern_atom_reply_t *protocols_reply = ::xcb_intern_atom_reply(
            _connection,
            wm_protocols_cookie,
            nullptr
        );

        _wm_delete = delete_reply->atom;
        _wm_proto  = protocols_reply->atom;

        ::xcb_change_property(
            _connection,
            ::XCB_PROP_MODE_REPLACE,
            _window,
            _wm_proto,
            ::XCB_ATOM_ATOM,
            32u,    // data format
            1u,     // data length (?)
            &_wm_delete
        );

        free(delete_reply);
        free(protocols_reply);
    }

//==============================================================================
    void _size_and_place_window(const uint32_t width, const uint32_t height) {
        uint32_t pos_x = (_display_xres / 2 - (width  / 2));
        uint32_t pos_y = (_display_yres / 2 - (height / 2));
        
        uint32_t value_mask =
            ::XCB_CONFIG_WINDOW_X |
            ::XCB_CONFIG_WINDOW_Y |
            ::XCB_CONFIG_WINDOW_WIDTH |
            ::XCB_CONFIG_WINDOW_HEIGHT;

        int32_t value_list[] {
            static_cast<int32_t>(pos_x) + _display_xoff,
            static_cast<int32_t>(pos_y) + _display_yoff,
            static_cast<int32_t>(width),
            static_cast<int32_t>(height)
        };

        ::xcb_configure_window(
            _connection,
            _window,
            value_mask,
            value_list
        );

        ::xcb_flush(_connection);
        
        std::cout << timestamp() << " " << "Resized window " << width << "x" << height
                  << " and positioned window " << value_list[0] << "x"
                  << value_list[1] << std::endl;
    }

//==============================================================================
    void _query_randr() {
        auto *reply = xcb_randr_get_screen_resources_current_reply(
            _connection,
            xcb_randr_get_screen_resources_current(_connection, _window),
            nullptr
        );

        int count = xcb_randr_get_screen_resources_current_outputs_length(reply);

        if(count == 0) {
            std::cerr << "xrandr could not provide any outputs\n";
        }

        auto *outputs = xcb_randr_get_screen_resources_current_outputs(reply);
        auto *output = xcb_randr_get_output_info_reply(
            _connection,
            xcb_randr_get_output_info(
                _connection,
                outputs[0],
                reply->config_timestamp
            ),
            nullptr
        );

        if(output == nullptr) {
            std::cerr << "Primary xrandr output provided nullptr\n";
        }

        if(output->crtc == XCB_NONE ||
            output->connection == XCB_RANDR_CONNECTION_DISCONNECTED)
        {
            std::cerr << "Primary xrandr output is invalid or disconnected\n";
        }

        auto *crtc = xcb_randr_get_crtc_info_reply(
            _connection,
            xcb_randr_get_crtc_info(
                _connection,
                output->crtc,
                reply->config_timestamp
            ),
            nullptr
        );

        _display_xres = crtc->width;
        _display_yres = crtc->height;

        _display_xoff = crtc->x;
        _display_yoff = crtc->y;

        free(crtc);
        free(output);
        free(reply);
    }

public:
//==============================================================================
    X11Window(const uint32_t width, const uint32_t height) :
        _connection    { nullptr },
        _window        { 0u },
        _screen        { nullptr },
        _wm_delete     { 0u },
        _wm_proto      { 0u },
        _key_symbols   { nullptr },
        _width         { width  },
        _height        { height },
        _display_xres  { 0u },
        _display_yres  { 0u },
        _display_xoff  { 0u },
        _display_yoff  { 0u },
        _running       { false },
        _fullscreen    { false }
    { }

    ~X11Window() {
        ::xcb_key_symbols_free(_key_symbols);
        ::xcb_destroy_window(_connection, _window);
        ::xcb_disconnect(_connection);
    }
};


int main() {
    X11Window window(1024u, 768u);
    window.init_window();

    while(window.message_loop());

    return 0;
}

Clearly this is no production application, and it turns out my right alt key is mapped differently than my friend with (ostensibly) the same keyboard layout. That aside, right alt + return does work for me, at least in so far as transitioning from centered 1024x768 to borderless full screen.

What I see in the logs reflects this, of course.

This is all just setup, but:

03:37:00 Resized window 1024x768 and positioned window 2848x696
03:37:00 XCB_PROPERTY_NOTIFY
    atom: 335 (WM_PROTOCOLS)
03:37:00 XCB_PROPERTY_NOTIFY
    atom: 406 (_MOTIF_WM_HINTS)
03:37:00 XCB_PROPERTY_NOTIFY
    atom: 459 (_NET_WM_ALLOWED_ACTIONS)
03:37:00 XCB_CONFIGURE_NOTIFY
03:37:00 XCB_PROPERTY_NOTIFY
    atom: 420 (_GTK_EDGE_CONSTRAINTS)
03:37:00 XCB_PROPERTY_NOTIFY
    atom: 343 (_NET_WM_DESKTOP)
03:37:00 XCB_PROPERTY_NOTIFY
    atom: 405 (WM_STATE1)
03:37:00 XCB_PROPERTY_NOTIFY
    atom: 349 (_NET_WM_STATE)
03:37:00 XCB_PROPERTY_NOTIFY
    atom: 420 (_GTK_EDGE_CONSTRAINTS)
03:37:00 XCB_CONFIGURE_NOTIFY
03:37:00 XCB_CONFIGURE_NOTIFY
03:37:00 XCB_PROPERTY_NOTIFY
    atom: 420 (_GTK_EDGE_CONSTRAINTS)
03:37:00 XCB_PROPERTY_NOTIFY
    atom: 343 (_NET_WM_DESKTOP)
03:37:00 XCB_PROPERTY_NOTIFY
    atom: 420 (_GTK_EDGE_CONSTRAINTS)
03:37:00 XCB_PROPERTY_NOTIFY
    atom: 349 (_NET_WM_STATE)
03:37:00 XCB_PROPERTY_NOTIFY
    atom: 420 (_GTK_EDGE_CONSTRAINTS)
03:37:00 XCB_MAP_NOTIFY
03:37:00 XCB_VISIBILITY_NOTIFY
03:37:00 XCB_EXPOSE
03:37:01 XCB_CONFIGURE_NOTIFY
03:37:01 XCB_FOCUS_IN
03:37:01 XCB_KEYMAP_NOTIFY
03:37:01 XCB_FOCUS_IN
03:37:01 XCB_KEYMAP_NOTIFY
03:37:01 XCB_PROPERTY_NOTIFY
    atom: 349 (_NET_WM_STATE)
03:37:01 XCB_PROPERTY_NOTIFY
    atom: 420 (_GTK_EDGE_CONSTRAINTS)
03:37:01 XCB_PROPERTY_NOTIFY
    atom: 349 (_NET_WM_STATE)
03:37:01 XCB_PROPERTY_NOTIFY
    atom: 420 (_GTK_EDGE_CONSTRAINTS)
03:37:01 XCB_KEY_RELEASE

Here is where I hit the key combo:

03:37:03 XCB_KEY_PRESS
03:37:03 XCB_KEY_PRESS
03:37:03 XCB_KEY_RELEASE
03:37:03 Resized window 3840x2160 and positioned window 1440x0
03:37:03 XCB_PROPERTY_NOTIFY
    atom: 459 (_NET_WM_ALLOWED_ACTIONS)
03:37:03 XCB_PROPERTY_NOTIFY
    atom: 349 (_NET_WM_STATE)
03:37:03 XCB_PROPERTY_NOTIFY
    atom: 420 (_GTK_EDGE_CONSTRAINTS)
03:37:03 XCB_RESIZE_REQUEST
03:37:03 XCB_CONFIGURE_NOTIFY
03:37:03 XCB_PROPERTY_NOTIFY
    atom: 420 (_GTK_EDGE_CONSTRAINTS)
03:37:03 XCB_KEY_RELEASE

At this point, everything seems to be working just fine, but when I hit the key combo again:

03:37:08 XCB_KEY_PRESS
03:37:08 XCB_KEY_PRESS
03:37:08 XCB_KEY_RELEASE
03:37:08 Resized window 1024x768 and positioned window 2848x696
03:37:08 XCB_CONFIGURE_NOTIFY
03:37:08 XCB_PROPERTY_NOTIFY
    atom: 420 (_GTK_EDGE_CONSTRAINTS)
03:37:08 XCB_KEY_RELEASE

And closing down the program:

03:37:11 XCB_KEY_PRESS
03:37:11 Exiting...

This behavior is consistent, and my only "workaround" is simply restoring the window decorations and leaning on the window manager to make sure everything happens as it is supposed to.

You might notice, in the XCB_RESIZE_REQUEST block that I'm doing nothing and have commented out a nonexistent member variable. This comes from the actual purpose of this code - a Vulkan renderer. I have the render loop polling events much like main() does in this example code, but _resized is used to notify the Vulkan instance that it needs to recreate the swapchain. However, at least with my current setup, it doesn't appear as though that's necessary, since VK_ERROR_OUT_OF_DATE_KHR or VK_SUBOPTIMAL_KHR seem to beat XCB to the punch after calls to vkAcquireNextImageKHR and vkQueuePresentKHR respectively.

Regardless, I should be able to get this minimal example working, but here we are.

Any and all thoughts, critiques, and best practices would be most welcome!

0 Answers
Related