X11: XGetWindowAttributes: window position has offset

After rummaging around in the heap of stuff that is my ~/misc directory I actually found a WIP of that X11 window tree traversal program I did mention in the comments. I originally wrote this to visualize the different ways in which window managers create decorations and place their clients within. Essentially it’s a screenshot-all-the-things (except the root window) tool writing out a SVGs with an <image> object for each window and a red border around it (so kind of like how you’d debug CSS with border; solid red 1px;), showing the underlying X11 drawable’s contents.

Here’s the source code. You’ll need stb_image (for PNG compression), cppcodec (for base64 encoding) and xcb headers to build:

#include <stdlib.h>
#include <unistd.h>
#include <stdint.h>

#include <sstream>
#include <iostream>
#include <type_traits>

#include <xcb/xcb.h>
#include <xcb/xcb_image.h>
#include <cppcodec/base64_url.hpp>
#include <cppcodec/base64_rfc4648.hpp>

#define STB_IMAGE_WRITE_IMPLEMENTATION
#include "stb_image_write.h"

static
void stringstream_write_b64(void *buf, void *data, int size)
{
    auto v = reinterpret_cast<std::vector<uint8_t>*>(buf);
    v->insert(v->end(), (uint8_t*)data, (uint8_t*)data+size);
}

std::string x11drawable_to_b64png(
    xcb_connection_t *const conn,
    xcb_drawable_t drawable,
    xcb_translate_coordinates_cookie_t const *const tcc )
{
    std::ostringstream s;

    xcb_generic_error_t *err_gg = nullptr, *err_tc = nullptr;
    auto const r_gg = xcb_get_geometry_reply(conn, xcb_get_geometry(conn, drawable), &err_gg);
    auto const r_tc = tcc ? xcb_translate_coordinates_reply(conn, *tcc, &err_tc) : nullptr;
    if( r_gg ){
        int ox = 0, oy = 0;
        if( r_tc ){
            ox = r_tc->dst_x; oy = r_tc->dst_y;
        } else {
            ox += r_gg->x; oy += r_gg->y;
        }

        xcb_image_t * const image =
            xcb_image_get(conn, drawable,
                0, 0, r_gg->width, r_gg->height,
                0xFFFFFFFF, XCB_IMAGE_FORMAT_Z_PIXMAP );
        if( image ){
            s << "<g>";
            s << "<image "
                << "x="" << ox << "" "
                << "y="" << oy << "" "
                << "width="" << r_gg->width << "" "
                << "height="" << r_gg->height << "" "
                << "xlink:href="data:image/png;base64,";
            std::ostringstream fn;
            fn << drawable << ".png";
            auto str = fn.str();

            for(size_t y = 0; y < image->height; ++y){
                auto r = (uint32_t*)(
                    (uint8_t*)image->data + y*image->stride );
                for(size_t x = 0; x < image->width; ++x ){
                    r[x] |= 0xFF000000;
                }
            }

            std::vector<uint8_t> buf;
            stbi_write_png_to_func(
                stringstream_write_b64, reinterpret_cast<void*>(&buf),
                image->width, image->height, 4,
                image->data, image->stride );
            s << cppcodec::base64_rfc4648::encode(buf);
            s << ""/>n";

            s << "<rect "
                << "x="" << ox << "" "
                << "y="" << oy << "" "
                << "width="" << r_gg->width << "" "
                << "height="" << r_gg->height << "" "
                << "style="fill:none;fill-opacity:1;stroke:#f00;stroke-width:1;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;"/>";
            s << "</g>n";
        }
    }

    free(err_gg); free(err_tc); free(r_gg); free(r_tc);

    return s.str();
}

static void traverse_window_tree(
    xcb_connection_t *conn,
    xcb_window_t const win,
    int const level )
{

    xcb_generic_error_t *err_qt = nullptr;
    auto const r_qt = xcb_query_tree_reply(conn, xcb_query_tree(conn, win), &err_qt);
    if( r_qt ){
        auto const root = r_qt->root;
        auto const children = xcb_query_tree_children(r_qt);
        auto const n = xcb_query_tree_children_length(r_qt);
        for( std::remove_const<decltype(n)>::type i = 0; i < n; ++i ){
            auto const child = children[i];

            for(unsigned l=0; l<level;++l){ std::cerr << " "; }
            std::cerr << child << std::endl;

            int c_ox = 0, c_oy = 0;
            auto const xtcc = xcb_translate_coordinates(conn, child, root, 0, 0);
            std::cout << x11drawable_to_b64png(conn, child, &xtcc);

            traverse_window_tree(conn, child, level+1);
        }
    }

    free(r_qt);
    free(err_qt);
}

static xcb_generic_event_t *xcb_poll_for_event_timeout(
    xcb_connection_t *const conn,
    unsigned timeout_us )
{
    unsigned i = 0;
    while( timeout_us >> (2+i) ){ ++i; };
    i /= 2;

    xcb_generic_event_t *ev;
    while( !(ev = xcb_poll_for_event(conn)) && i ){
        unsigned const t = timeout_us >> (1 + (i--));
        usleep( t );
    }
    return ev;
}

int main(int argc, char *argv[])
{
    int rc = 0;

    int screen;
    auto const conn = xcb_connect(NULL, &screen);
    if( !conn ){ rc = -1; }

    auto const xsetup = xcb_get_setup(conn);
    if( !xsetup ){ rc = -2; }

    auto xroots_iter = xcb_setup_roots_iterator(xsetup);
    for(int i = 0; i < screen; ++i){ xcb_screen_next(&xroots_iter); }
    auto const xscreen = reinterpret_cast<xcb_screen_t*>(xroots_iter.data);
    if( !xscreen ){ rc = -4; }

    std::cout << "<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">" << std::endl;
    traverse_window_tree(conn, xscreen->root, 0);
    std::cout << "</svg>" << std::endl;

    if( conn ){ xcb_disconnect(conn); }
    return rc;
}

Dumping a Gnome Shell session yields the following: On the left column are gtk-demo and gtk3-demo showing the Dialogs example. The center top is gtk4-demo. In the upper right is gnome-control-centerblow that urxvt. In the middle-center and right is a very old version of CadSoft EAGLE running, which uses IIRC Qt2 or Qt3 (don’t know which exactly). Below that in the center is an instance of KDE Dolphin and in the bottom right runs an instance of DDD which uses Motif. (find the original SVG at https://dl.datenwolf.net/xsvgtree/gnome-shell.svg – save it locally since my server’s Content-Security-Policy disallows browsers from directly showing inline data).

What you can immediately see is, how differently the various toolkits go about structuring their windows and mapping them to the screen. The old school toolkits will create a lot of small, tiny subwindows within their client area, usually one for each user interactable control. Modern GTK and Qt on the other hand opt to just create a single large surface to draw to, with their own methods, and then transfer that over to the X11 server. The latest iterations of GTK in fact opted to completely bypass the decoration facilities of the window manager and just draw everything in the client. And in the case of gnome-control-center the window wherein everything happens is far larger than the extents of the visible region and “decoration” would suggest, and also far larger than the shadow margin. My suspicion is, that this is to avoid changing the size of the actual window in response to altering the extents of the “visible” area, for the simple reason that altering a windows size in a composited environment comes as the considerable cost of having to re-/initialize new GPU resources which can lead to stuttering and visual artifacts. In order to keep things running smoothly – quite literally – Gnome apparently applies a conservative resource reallocation strategy, which however on the X11 side may lead to unintuitive values ​​when querying window properties.

Now to juxtapose this, I ran an instance each of urxvt and gnome-control-center under management by FVWM and dumped it. Things looks like this there (original SVG found here: https://dl.datenwolf.net/xsvgtree/fvwm-gnome-shell.svg ):

gnome-control-center under management by FVWM

As you can see, in the case of being reparented, gnome-control-center will actually “behave” itself. Also it’s not worthy that all the little decoration elements FVWM creates each consist its own little window.

Leave a Comment