Writing Your Own X-11 Framework Basics (Part 1)

We go in depth into a X-11 foundational primer.

Writing Your Own X-11 Framework Basics (Part 1)
We go into writing our own framework.

Many many moons ago a wonderful piece of software came out for Window 95, namely Delphi 2.0.  It was amazing, and  let you literally draw your buttons and applications, connect them to event handlers, and then insert some boilerplate Turbo Pascal code.  Today you would realize this was 25 years ahead of it's time, and basically long buried and gone after many attempts by Windows to bury it as it competed with their myriad of various IDE produtcs. It survived to a basic fashion, and  you can still pay huge dollars and buy Embarcadero products that have bought and traded hands many times, or the free Lazarus IDE, which was an opensource clone of the powerful Delphi products.

But as for a C / C++ based type GUI (in Linux) if you are not going the Embarcadero route,  you are pretty much stuck mostly with Qt.  Qt is really nice, however if you want total control like say a custom sized button that acts like a instrument you end up fighting the layout manager which will decide for you that your button needs to be a minimum of X pixels, or the font needs to be N size. Then the restrictive licensing agreements will helm you in.  In the end you waste days getting a generic 'toy' layout you never wanted.

So what if we just wrote  one from scratch! Secondly why even write this article when the AI's do it so well..  The answer is really simple to really learn something you need to spend some time in it, not just glancing through code but documenting each part one bit at a time.  It could be noted that 'vibe-coding' something to come back to it even a week later and you have no understanding or recollection of how it works, or what it does is just powerless - and dangerous.  AI's will get 98% of stuff right, but the 2% that they get wrong will trap you.  If you don't have a good understanding and can work close to the level that the AI is working at you will be trapped. That is the goal of the series of these articles to slowly build a strong foundational knowledge of the X-11 X-Window System, and eventually build an entire RAD (Rapid Application Developer) or IDE.

Supports:

Before you can write any X-11 application make sure to reference it in your source, and to also install the support library, so:

sudo apt install libx11-dev -y
#include <X11/Xlib.h>
#include <X11/Xutil.h>

Here additionally is a working CMakeLists.txt if it helps your project.

cmake_minimum_required(VERSION 3.20)
project(XFrame LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
find_package(X11 REQUIRED)
add_executable(XFrame main.cpp)
target_link_libraries(XFrame PRIVATE X11::X11)

BoilerPlate Introduction

int main()
{
    Display* display = XOpenDisplay(nullptr);
    if (!display) {return 1; }
    int screen = DefaultScreen(display);
    XID Xid = RootWindow(display, screen);
    Window window = XCreateSimpleWindow(display, Xid,100,100,800,600,1, BlackPixel(display, screen), WhitePixel(display, screen));
    XStoreName(display, window, "My C++ Canvas Framework");
    XSelectInput(display, window, ExposureMask | KeyPressMask | StructureNotifyMask );
    Atom deleteWindowMessage = XInternAtom(display, "WM_DELETE_WINDOW", False);
    XSetWMProtocols(display,window,&deleteWindowMessage,1);
    XMapWindow(display, window);
    GC gc = DefaultGC(display, screen);
    Canvas canvas(display, window, gc, 800, 600);
    bool running = true;
    while (running)
    {
        XEvent event;
        XNextEvent(display, &event);
        switch (event.type)
        {
        case Expose:
            canvas.present();
            break;

Going over this snippet of code let's just study the different parts:

  • Display* display = XOpenDisplay(nullptr); This will get the current display.  Please note - this is not necessarily the screen as X-11 can have virtual displays, but will get the active one that the current session is calling to use.  So if you open an application inside your Linux Gui, this is the display.  if (!display) {return 1;} is simply saying if no display is returned exit with error code  1 versus 0 (no error code.)
  • int screen = DefaultScreen(display); If you have a 3-head monitor setup this will get the default monitor of the group. Typically 0
  • XID Xid = RootWindow(display, screen) Effectively this is simply the resource identifier that the curent display / sreen combination is using.  When we step-debugged our code we received Xid: 883
  • Window window = XCreateSimpleWindow(display, Xid,100,100,800,600,1, BlackPixel(display, screen), WhitePixel(display, screen)); This actually creates a window, however it will not be displayed just yet. It exists virtually. display the current display, Xid - the resource identifier of the display, screen combination, 100,100 is x,y coordinates from top-left, 800,600 is the width, height.  1 is the border width, and BlackPixel(display,screen) border, and a WhitePixel(display,screen) background.
  • XStoreName(display, window, "My C++ Canvas Framework"); Will give this (still virtual) window a name for it's title bar.

Event Signal Handling

  • XSelectInput(display, window, ExposureMask | KeyPressMask | StructureNotifyMask ); will determine which events will be passed to the window for handling. An event is - did your mouse move? Does your window need to know about it, then we add a corresponding mask.

Here is a list of possible masks:

The standard event masks available to XSelectInput are:

MaskReports this event
NoEventMaskNo events
KeyPressMaskA keyboard key is pressed
KeyReleaseMaskA keyboard key is released
ButtonPressMaskA mouse button is pressed
ButtonReleaseMaskA mouse button is released
EnterWindowMaskThe pointer enters the window
LeaveWindowMaskThe pointer leaves the window
PointerMotionMaskThe pointer moves inside the window
PointerMotionHintMaskPointer-motion hints instead of every motion event
Button1MotionMaskPointer moves while button 1 is held
Button2MotionMaskPointer moves while button 2 is held
Button3MotionMaskPointer moves while button 3 is held
Button4MotionMaskPointer moves while button 4 is held
Button5MotionMaskPointer moves while button 5 is held
ButtonMotionMaskPointer moves while any mouse button is held
KeymapStateMaskKeyboard mapping state notification
ExposureMaskPart or all of the window needs repainting
VisibilityChangeMaskWindow visibility changes
StructureNotifyMaskThis window is moved, resized, mapped, unmapped, or destroyed
ResizeRedirectMaskRequests notification before this window is resized
SubstructureNotifyMaskA child window is created, moved, resized, mapped, unmapped, or destroyed
SubstructureRedirectMaskRequests control over changes to child windows
FocusChangeMaskKeyboard focus enters or leaves the window
PropertyChangeMaskA window property changes
ColormapChangeMaskThe window’s colormap changes
OwnerGrabButtonMaskThe window receives button events even when another window has a pointer grab

Specifically  if we want button based event handlers it becomes:

The mouse and pointer-related masks available to XSelectInput are:

MaskEvent typeMeaning
ButtonPressMaskButtonPressA mouse button was pressed
ButtonReleaseMaskButtonReleaseA mouse button was released
EnterWindowMaskEnterNotifyThe pointer entered the window
LeaveWindowMaskLeaveNotifyThe pointer left the window
PointerMotionMaskMotionNotifyThe pointer moved
PointerMotionHintMaskMotionNotifyRequests motion hints instead of every motion event
Button1MotionMaskMotionNotifyThe pointer moved while button 1 was held
Button2MotionMaskMotionNotifyThe pointer moved while button 2 was held
Button3MotionMaskMotionNotifyThe pointer moved while button 3 was held
Button4MotionMaskMotionNotifyThe pointer moved while button 4 was held
Button5MotionMaskMotionNotifyThe pointer moved while button 5 was held
ButtonMotionMaskMotionNotifyThe pointer moved while any button was held
OwnerGrabButtonMaskAffects pointer grabsAllows the window to receive button events when another client has a pointer grab

Typically a normal mouse interaction type mask setting would look like:

XSelectInput(display, window, ExposureMask | KeyPressMask |    StructureNotifyMask | ButtonPressMask | ButtonReleaseMask | PointerMotionMask | EnterWindowMask |  LeaveWindowMask
);

From this you would then have the following code block to handle these events, namely:

switch (event.type) {
case ButtonPress: {
    int x = event.xbutton.x;
    int y = event.xbutton.y;
    unsigned int button = event.xbutton.button;

    // Handle mouse press.
    break;
}

case ButtonRelease: {
    int x = event.xbutton.x;
    int y = event.xbutton.y;
    unsigned int button = event.xbutton.button;

    // Handle mouse release.
    break;
}

case MotionNotify: {
    int x = event.xmotion.x;
    int y = event.xmotion.y;

    // Handle pointer movement.
    break;
}

case EnterNotify:
    // Pointer entered the window.
    break;

case LeaveNotify:
    // Pointer left the window.
    break;
}

Finishing up this block of code we still have:

Atom deleteWindowMessage = XInternAtom(display, "WM_DELETE_WINDOW", False);
XSetWMProtocols(display,window,&deleteWindowMessage,1);
XMapWindow(display, window);
GC gc = DefaultGC(display, screen);
Canvas canvas(display, window, gc, 800, 600);
  • Atom deleteWindowMessage = XInteralAtom(display, "WM_DELETE_WINDOW", False); This is telling the system to give us it's internal Atom reference of a WM_DELETE_WINDOW message as we want to intercept it and handle it manually.
  • XSetWMProtocols(display, window, &deleteWindowMessage, 1);  Once we have the referenced Atom we then use that address to reference back that our application supports the WM_DELETE_WINDOW protocol.
  • XMapWindow(display, window) will make your application visible.

Picking your Drawing Context (Picking your Pen to Draw With)

Finally - yep I know this is a LOT, but were almost there, we need to get first the drawing context,

  • A drawing context is only settings - it describes the pen you are holding in your hand at the time you used it to write.  
  • For instance you might have one drawing context that uses line width 5 and a second that uses red.  Again think of them as pens.  
  • The default drawing context is obtained with :
GC gc = DefaultGC(display, screen);

Drawing Commands

XDrawPoint

XDrawPoint(display, window, gc, 100, 80);
  • XDrawPoint Of the current display for the current window using the drawing context of gc create a single point at 100, 80. This command queues until xflush(display) is called.

XDrawPoints

XPoint points[] = {{100, 80}, {110, 90}, {120, 100}, {130, 110}};
XDrawPoints(display, window, gc,points, 4, CoordModeOrigin);
  • XDrawPoints Of the current display for the current window using the drawing context of gc here are 4 coordinate points. CoordModeOrigin specifies that each point is absolute, while CoordModePrevious indicates the first point is absolute, and each point thereafter is relative. This command queues until xflush(display) is called.

XDrawLine

XDrawLine(display, window, gc,
    50, 150,       // starting x, y
    300, 200       // ending x, y
);
  • XDrawLine of the current display for the current window using the drawing context of gc draw a line from 50,150 to 300,120 This command queues until xflush(display) is called.

XDrawLines

XPoint linePoints[] = {{50, 250}, {150, 200}, {250, 250}, {350, 200}};

XDrawLines(display, window, gc, linePoints, 4, CoordModeOrigin);
  • XDrawLines Of the current display of the current window using the drawing context of gc here are 4 consecutive points that will will draw contiguously. This command queues until xflush(display) is called.

XDrawSegments

XSegment segments[] = {
    {50, 300, 150, 350},
    {200, 300, 300, 350},
    {350, 300, 450, 350}
};

XDrawSegments(display, window, gc, segments, 3);
  • XDrawSegments Of the current display of the current window using the drawing context of gc  here are 3 line segments that can each be drawn. This command queues until xflush(display) is called.

XDrawRectangle

XDrawRectangle(display, window, gc, 
    50, 400,       // x, y
    150, 80        // width, height
);
  • XDrawRectangle Of the current display of the current window using the drawing context of gc draw a rectangle starting at 50,400 with a width,height of 150,80 This command queues until xflush(display) is called.

XDrawRectangles

XRectangle outlinedRectangles[] = {
    {250, 400, 100, 60},
    {400, 400, 100, 80},
    {550, 400, 150, 50}
};

XDrawRectangles(display, window, gc, outlinedRectangles,3);
  • XDrawRectangles Of the current display of the current window using the drawing context of gc draw the following 3 rectangles as passed in outlinedRectangles This command queues until xflush(display) is called.

XFillRectangle

XFillRectangle(
    display,
    window,
    gc,
    50, 500,       // x, y
    150, 60        // width, height
);
  • XFillRectangle Of the current display of the current window using the drawing context of gc draw the following filled rectangle This command queues until xflush(display) is called.  
  • Does not put a line around the rectangle, you must make a second call to XDrawRectangle

One may set the fill with:

XSetFillStyle(display, gc, FillSolid);
XSetForeground(display, gc, 0xFF0000);
// Four fill styles are offered: 
// FillSolid
// FillTiled
// FillStippled
// FillOpaqueStippled

XFillRectangles

XRectangle filledRectangles[] = {
    {250, 500, 100, 50},
    {400, 500, 100, 70},
    {550, 500, 150, 40}
};

XFillRectangles(display, window, gc, filledRectangles, 3);
  • XFillRectangles Of the current display of the current window using the drawing context of gc draw the following set of filled rectangles This command queues until xflush(display) is called.

XDrawArc

  • Angles are measured in arc seconds each second is 1/64th of a degree, thus to draw a 1/2 circle becomes 180 * 64
XDrawArc(
    display,
    window,
    gc,
    50, 50,             // bounding box x, y
    150, 100,           // bounding box width, height
    0,                  // starting angle
    180 * 64            // angle extent
);
  • XDrawArc Of the current display of the current window using the drawing context of gc draw the following arc starting at reference point 50, 50 and bounded by 150,100 with a starting angle of 0 degrees. This command queues until xflush(display) is called.

XDrawArcs

  • Angles are measured in arc seconds each second is 1/64th of a degree, thus to draw a 1/2 circle becomes 180 * 64
XArc outlinedArcs[] = {
    {250, 50, 100, 100, 0,       360 * 64}, // circle
    {400, 50, 100, 100, 0,       180 * 64}, // half-circle
    {550, 50, 150, 100, 90 * 64, 180 * 64}
};

XDrawArcs(display, window, gc,outlinedArcs, 3);
  • XDrawArcs Of the current display of the current window using the drawing context  of gc draw the following 3 arcs each referenced by x, y, width, height, starting angle, angle extent  Thus each arc is described by 6 points of data. This command queues until xflush(display) is called.

XFillArc

  • Angles are measured in arc seconds each second is 1/64th of a degree, thus to draw a 1/2 circle becomes 180 * 64
XFillArc(
    display,
    window,
    gc,
    50, 180,             // bounding box x, y
    150, 150,            // width, height
    0,                   // starting angle
    90 * 64              // angle extent
);
  • XFillArc Of the current display of the current window using the drawing context  of gc draw and fill in the following arc referenced by x, y, width, height, starting angle, angle extent  Thus each arc is described by 6 points of data. This command queues until xflush(display) is called.

XFillArcs

XArc filledArcs[] = {
    {250, 180, 100, 100, 0,        360 * 64}, // filled circle
    {400, 180, 100, 100, 0,        180 * 64}, // filled half-circle
    {550, 180, 150, 100, 90 * 64,  180 * 64}
};

XFillArcs(
    display,
    window,
    gc,
    filledArcs,
    3
);
  • XFillArcs Of the current display of the current window using the drawing context  of gc draw the following 3 arcs each referenced by x, y, width, height, starting angle, angle extent  Thus each arc is described by 6 points of data. This command queues until xflush(display) is called.

XDrawString


XDrawString(
    display,
    window,
    gc,
    50, 600,             // x and baseline y
    text,
    std::strlen(text)
);
  • XDrawString Of the current display of the current window using the drawing context of gc at x,y point 50,600 write the following text that has a length of std::strlen(text). This command queues until xflush(display) is called.

XDrawImageString

XDrawImageString(
    display,
    window,
    gc,
    250, 600,
    text,
    std::strlen(text)
);
  • XDrawImageString Of the current display of the current window using the drawing context of gc at x,y point 250,600 write the following text that has a length of std::strlen(text) and give it a rectangular background. This command queues until xflush(display) is called.

XDrawString16

XChar2b text16[] = {
    {0, 'H'},
    {0, 'i'},
    {0, '!'}
};

// Draw 16-bit text
XDrawString16(
    display,
    window,
    gc,
    400, 600,
    text16,
    3
);
  • XDrawString16 Of the current display of the current window using the drawing context of gc at x,y point 50,600 write the following 16-bit text that has a length of 3. This command queues until xflush(display) is called.

XDrawText

XTextItem textItems[] = {
    {(char*)"Hello ", 6, 0, None},
    {(char*)"X11",    3, 0, None}
};

XDrawText(
    display,
    window,
    gc,
    50, 580,             // x and baseline y
    textItems,
    2                    // number of text items
);
  • XDrawText Of the current display of the current window using the drawing context of gc at x,y points 50, 580 write out the following textItems of count 2. This command queues until xflush(display) is called.

Finishing the Frame.

Once you have buffered up all your XDraw commands you  can send them for processing with

XFlush(display);

Conclusion

This is a good primer. We can see in the documentation that the raw X-11 does not natively handle buttons etc.  Below is a entire working X-11 with a Canvas class to to various updates etc:

#include <X11/Xlib.h>
#include <X11/Xutil.h>

#include <algorithm>
#include <cstdint>
#include <cstdlib>
#include <vector>

//region Region Canvas
class Canvas
{
private:
    Display* display = nullptr;
    Window window = 0;
    GC gc = nullptr;

    XImage* image = nullptr;

    int width = 0;
    int height = 0;

    std::vector<uint32_t> pixels;


public:
    Canvas(Display* display, Window window, GC graphicsContext,int width, int height): display(display), window(window), gc(graphicsContext)
    {
        resize(width, height);
    }
    ~Canvas() { destroyImage(); }
    void resize(int newWidth, int newHeight)
    {
        width = std::max(1, newWidth);
        height = std::max(1, newHeight);
        pixels.resize(static_cast<size_t>(width) * height);
        clear(0x202020);
        destroyImage();
        Visual* visual = DefaultVisual(display, DefaultScreen(display));
        int depth = DefaultDepth(display, DefaultScreen(display));

        /*
         * XCreateImage calculates bytes_per_line for us when the
         * data pointer is initially null.
         */
        image = XCreateImage(display, visual, depth, ZPixmap, 0, nullptr, width, height, 32, 0);

        if (!image)
        {
            std::exit(1);
        }
        image->data = static_cast<char*>(std::malloc(image->bytes_per_line * height));
        if (!image->data)
        {
            std::exit(1);
        }
    }
    int getWidth() const
    {
        return width;
    }
    int getHeight() const
    {
        return height;
    }
    // Direct access to the raw framebuffer.
    uint32_t* data()
    {
        return pixels.data();
    }
    const uint32_t* data() const
    {
        return pixels.data();
    }
    void clear(uint32_t rgb)
    {
        std::fill(pixels.begin(), pixels.end(), rgb);
    }
    void setPixel(int x, int y, uint32_t rgb)
    {
        if (x < 0 || x >= width || y < 0 || y >= height)
        {
            return;
        }
        pixels[static_cast<size_t>(y) * width + x] = rgb;
    }
    uint32_t getPixel(int x, int y) const
    {
        if (x < 0 || x >= width || y < 0 || y >= height)
        {
            return 0;
        }
        return pixels[static_cast<size_t>(y) * width + x];
    }
    void fillRect(int x, int y, int w, int h, uint32_t rgb)
    {
        int x0 = std::max(0, x);
        int y0 = std::max(0, y);
        int x1 = std::min(width, x + w);
        int y1 = std::min(height, y + h);
        for (int py = y0; py < y1; ++py)
        {
            for (int px = x0; px < x1; ++px)
            {
                setPixel(px, py, rgb);
            }
        }
    }
    void drawGradient()
    {
        for (int y = 0; y < height; ++y)
        {
            for (int x = 0; x < width; ++x)
            {
                uint8_t red = static_cast<uint8_t>(255.0 * x / std::max(1, width - 1));
                uint8_t green = static_cast<uint8_t>(255.0 * y / std::max(1, height - 1));
                uint8_t blue = 100;
                uint32_t color = (static_cast<uint32_t>(red) << 16) |  (static_cast<uint32_t>(green) << 8) | blue;
                setPixel(x, y, color);
            }
        }
    }
    void present()
    {
        /*
         * Convert our RGB framebuffer into the format expected by X11.
         *
         * XPutPixel is intentionally used here because it handles the
         * current X11 visual correctly. It is simple but not especially
         * fast. Later, you could optimize this using XShm or OpenGL.
         */
        for (int y = 0; y < height; ++y)
        {
            for (int x = 0; x < width; ++x)
            {

                uint32_t rgb = getPixel(x, y);
                unsigned long x11Pixel = ((rgb >> 16) & 0xff) << 16 | ((rgb >> 8)  & 0xff) << 8  | (rgb & 0xff);
                XPutPixel(image, x, y, x11Pixel);
            }
        }
        XPutImage(display,window,gc,image, 0, 0, 0, 0, width, height);
        XFlush(display);
    }

private:
    void destroyImage()
    {
        if (image)
        {
            XDestroyImage(image);
            image = nullptr;
        }
    }
};
//endregion

int main()
{
    Display* display = XOpenDisplay(nullptr);
    if (!display) {return 1; }
    int screen = DefaultScreen(display);
    XID Xid = RootWindow(display, screen);
    Window window = XCreateSimpleWindow(display, Xid,100,100,800,600,1, BlackPixel(display, screen), WhitePixel(display, screen));
    XStoreName(display, window, "My C++ Canvas Framework");
    XSelectInput(display, window, ExposureMask | KeyPressMask | StructureNotifyMask );
    Atom deleteWindowMessage = XInternAtom(display, "WM_DELETE_WINDOW", False);
    XSetWMProtocols(display,window,&deleteWindowMessage,1);
    XMapWindow(display, window);
    GC gc = DefaultGC(display, screen);
    Canvas canvas(display, window, gc, 800, 600);
    bool running = true;
    while (running)
    {
        XEvent event;
        XNextEvent(display, &event);
        switch (event.type)
        {
            case Expose:
            {
                canvas.present();
                break;
            }
            case ConfigureNotify:
            {
                int newWidth = event.xconfigure.width;
                int newHeight = event.xconfigure.height;
                if (newWidth != canvas.getWidth() || newHeight != canvas.getHeight())
                {
                    canvas.resize(newWidth, newHeight);
                    // Redraw after resizing.
                    canvas.drawGradient();
                    canvas.fillRect(100,100,250,150,0xff0000);
                    canvas.present();
                }
                break;
            }
            case KeyPress:
            {
                KeySym key = XLookupKeysym(&event.xkey, 0);
                if (key == XK_Escape) {  running = false;  }
                break;
            }
            case ClientMessage:
            {
                if (static_cast<Atom>(event.xclient.data.l[0]) == deleteWindowMessage)
                {
                    running = false;
                }
                break;
            }
        }
    }
    XCloseDisplay(display);
    return 0;
}
Linux Rocks Every Day