Writing Your Own X-11 Framework Basics (Part 3) Creating A Style Manager

In Part 3 We Create A Style Manager That will Work with many coming future widgets!

Writing Your Own X-11 Framework Basics (Part 3) Creating A Style Manager

This series builds on Part 1 - where we reviewed in detail how the X-11 system works, Part 2 where we made some buttons, with their own event handler system. Our buttons were basic, but they worked, and you now have full control over their size, no more wrangling with a layout manager deciding what's best for you! Why go to such extents to simple regain full control of your layouts?! - It's simple, suppose you wanted to make a instrumentation cluster for engineering, or a control interface for your raspberry pi? In the end this is worth doing!

Our buttons are pretty basic - but they work!

But why not have nice buttons - stuff that looks like this? Well first we need to define styles...

Before you can do any coding in this you will need the support library as in:

sudo apt install libx11-dev -y

And your C++ source code will typically need to reference the header as in:

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

If you are using a CMakeLists.txt here is a good example that will work with X-11

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)

Our original button structure was very simple as in:

struct SButton
{
    std::string name;
    int x1, y1, x2, y2;
};

Now we will want to effectively add our own style class, and since this button manager really manages a set of buttons we can simple just have a std::vector of <SStyles> which will apply to the button set.   Typically one style of buttons will be active for a session. Naturally this is extensible to other widgets, and really generic, so in reality a good plan - is to just make it - it's own tracking class.  Once we move onto more advanced and different types of widgets our style manager will be independently ready, so we are putting in the work now for it.

Hence we are looking at an basic starting information structure of the following:

  • Fill Color unsigned long - As set by X-11 (Internally it will be a Xcolor)
  • Outline Color unsigned long  - As set by X-11 (Internally it will be a XColor)
  • Corner Radius int  (X-11 does not draw with float precision unlike Qt
  • Line Width  int
  • Font Style XFontStruct

So from this it's a good start:

struct SStyle
{
    unsigned long fillColor_;
    unsigned long outlineColor_;
    unsigned long radius_;
    unsigned long lineWidth_;
    XFontStruct *font_;
};

Naturally we will need an vector array of these, and on top of this we will need a method to CRUD or create(add)  replace update and delete them, and really it would be nice for these setting to be able to save and load themselves at run-time. So really we will need a good class example.  However because we are using this as a demonstrator, and attempting to keep the tasks incrementally simple - we will not add object serialization at this time - that is where compound objects are either written to a binary format or to a json string.

Finally it can become problematic to have a index based list of styles without names so we update our structure as:

struct SStyle
{
  unsigned long fillColor_;
  unsigned long outlineColor_;
  unsigned long radius_;
  unsigned long lineWidth_;
  XFontStruct *font_;
  std::string style_name_;
};

struct Srgb
{
  int r;
  int g;
  int b;
};
  • Srgb is to allow for passing a r, g, b value as one set.

A Very Basic Style Manager

class SStyle_Manager
{
private:
  std::vector<SStyle> styles_;
  Display *display_;
  Window window_;
  int screen_;
  GC gc_;
  size_t active_style = 0; // just set the default to first passed.
public:
  SStyle_Manager(Display *display, Window window, int screen, GC gc)
  {
    display_ = display;
    window_ = window;
    screen_ = screen;
    gc_ = gc;
  }
  ~SStyle_Manager()
  {
    for (auto& style : styles_) 
    {
      if (style.font_) 
      {
        XFreeFont(display_, style.font_);  // Free the X11 font resource
      }
    }
    styles_.clear(); 
  }

  int add_style(std::string styleName, Srgb fillColor, Srgb outlineColor, int radius, int linewidth,  std::string fontName)
  {
    SStyle nstyle;
    nstyle.style_name_ = std::move(styleName);
    nstyle.fillColor_ = get_color_Srgb(fillColor);
    nstyle.outlineColor_ = get_color_Srgb(outlineColor);
    nstyle.radius_ = radius;
    nstyle.lineWidth_ = linewidth;
    nstyle.font_ = XLoadQueryFont(display_, fontName.c_str());
    // Define fallback fonts to try if the requested font fails
    const char* fallbackFonts[] = { "fixed", "helvetica" };
    size_t numFallbacks = sizeof(fallbackFonts) / sizeof(*fallbackFonts);

    nstyle.font_ = nullptr;

    // Attempt to load the primary font (if provided and not empty)
    if (!fontName.empty()) {
      nstyle.font_ = XLoadQueryFont(display_, fontName.c_str());
    }

    // If the primary failed, iterate through fallback fonts until one succeeds or all are exhausted
    if (!nstyle.font_) {
      for (size_t i = 0; i < numFallbacks && !nstyle.font_; ++i) {
        nstyle.font_ = XLoadQueryFont(display_, fallbackFonts[i]);
      }
    }

    // Final validation: if no font loaded, report which fonts were attempted and exit
    if (!nstyle.font_)
    {
      printf("Failed to load any font (tried: ");
      if (!fontName.empty()) printf("%s", fontName.c_str());
      for (size_t i = 0; i < numFallbacks; ++i) {
        if (!fontName.empty() || i > 0) printf(", ");
        printf("%s", fallbackFonts[i]);
      }
      printf(")\n");
      return -1;
    }
    styles_.push_back(nstyle);
    return 0;
  }
  unsigned long get_color_Srgb(Srgb color)
  {
    XColor xcolor;
    xcolor.red = color.r << 8;
    xcolor.green = color.g << 8;
    xcolor.blue = color.b << 8;
    if (XAllocColor(display_, DefaultColormap(display_, screen_), &xcolor))
    {
      return xcolor.pixel;
    } else {
      printf("Failed to allocate color! RGB(%d,%d,%d)\n", color.r, color.g, color.b);
      return BlackPixel(display_, screen_);  // Fallback
    }
  }
  unsigned long get_color_rgb(unsigned char r, unsigned char g, unsigned char b)
  {
    XColor xcolor;
    xcolor.red = r << 8;
    xcolor.green = g << 8;
    xcolor.blue = b << 8;
    if (XAllocColor(display_, DefaultColormap(display_, screen_), &xcolor))
    {
      return xcolor.pixel;
    } else {
      printf("Failed to allocate color! RGB(%d,%d,%d)\n", r, g, b);
      return BlackPixel(display_, screen_);  // Fallback
    }
  }

};

We can go over all the parts, it will need some more functions to be utility ready, but anyways.

  • std::vector<SStyle> styles_; will hold an array of different styles, enabling different theme layouts in a piece of software.
  • Constructor takes Display *display_; Window window_; int screen_; GC gc_; and finally int active_style; will track any system hooks, this is needed for calls like XAllocColor which requires a hook into display_ so we simply pass it at constructor time. When it is done it looks like:
  SStyle_Manager(Display *display, Window window, int screen, GC gc)
  {
    display_ = display;
    window_ = window;
    screen_ = screen;
    gc_ = gc;
  }
  • Destructor will make sure to free up the XFreeFont and it will look as:
  ~SStyle_Manager()
  {
    for (auto& style : styles_)
    {
      if (style.font_)
      {
        XFreeFont(display_, style.font_);  // Free the X11 font resource
      }
    }
    styles_.clear();
  }
  • Color Converter get_color_Srgb will look as follows:
  unsigned long get_color_Srgb(Srgb color)
  {
    XColor xcolor;
    xcolor.red = color.r << 8;
    xcolor.green = color.g << 8;
    xcolor.blue = color.b << 8;
    if (XAllocColor(display_, DefaultColormap(display_, screen_), &xcolor))
    {
      return xcolor.pixel;
    } else {
      printf("Failed to allocate color! RGB(%d,%d,%d)\n", color.r, color.g, color.b);
      return BlackPixel(display_, screen_);  // Fallback
    }
  }

Finally the major add_style class which importantly will take a set of parameters, and will build a style object and stack it. It is a little extensive because we have coded in some font forgiveness which is just apropos, as we want it to default back to a system font if the passed font is not installed.

  int add_style(std::string styleName, Srgb fillColor, Srgb outlineColor, int radius, int linewidth,  std::string fontName)
  {
    SStyle nstyle;
    nstyle.style_name_ = std::move(styleName);
    nstyle.fillColor_ = get_color_Srgb(fillColor);
    nstyle.outlineColor_ = get_color_Srgb(outlineColor);
    nstyle.radius_ = radius;
    nstyle.lineWidth_ = linewidth;
    nstyle.font_ = XLoadQueryFont(display_, fontName.c_str());
    // Define fallback fonts to try if the requested font fails
    const char* fallbackFonts[] = { "fixed", "helvetica" };
    size_t numFallbacks = sizeof(fallbackFonts) / sizeof(*fallbackFonts);

    nstyle.font_ = nullptr;

    // Attempt to load the primary font (if provided and not empty)
    if (!fontName.empty()) {
      nstyle.font_ = XLoadQueryFont(display_, fontName.c_str());
    }

    // If the primary failed, iterate through fallback fonts until one succeeds or all are exhausted
    if (!nstyle.font_) {
      for (size_t i = 0; i < numFallbacks && !nstyle.font_; ++i) {
        nstyle.font_ = XLoadQueryFont(display_, fallbackFonts[i]);
      }
    }

    // Final validation: if no font loaded, report which fonts were attempted and exit
    if (!nstyle.font_)
    {
      printf("Failed to load any font (tried: ");
      if (!fontName.empty()) printf("%s", fontName.c_str());
      for (size_t i = 0; i < numFallbacks; ++i) {
        if (!fontName.empty() || i > 0) printf(", ");
        printf("%s", fallbackFonts[i]);
      }
      printf(")\n");
      return -1;
    }
    styles_.push_back(nstyle);
    return 0;
  }

This is pretty close to complete - what is missing is get_active_style and set_active_style and since we have built our style tracker with a super nice style_name_ we get to use that!  This gives real flexibility to the developer because they can simply go set_active_style_byName('default') and if there is a registered style with that name it will automatically become the default.

Adding a set_active_style_byName then becomes:

  int set_active_style_byName(std::string styleName)
  {
    auto it = std::find_if(styles_.begin(), styles_.end(),
        [&](const SStyle& s) { return s.style_name_ == styleName; });

    if (it != styles_.end())
    {
      active_style = static_cast<int>(std::distance(styles_.begin(), it));
      return active_style;
    }
    return -1; // Not found
  }

And some get_active_style  functions thusly:

  SStyle get_active_style_byName(std::string styleName)
  {
    auto it = std::find_if(styles_.begin(), styles_.end(),
      [&](const SStyle& s) { return s.style_name_ == styleName; });
  }
  SStyle get_active_style()
  {
    return styles_[active_style];
  }
  SStyle get_index_style(int index)
  {
    if (index > 0 && index < (int)styles_.size())
    {
      return styles_[index];
    }
  }

The entire code block now looks as (I know there are some overloaded #includes but anyhow..)

//
// Created by c on 8/31/26.
//

#include <X11/Xlib.h>
#include <X11/Xutil.h>
#include <X11/fonts/font.h>
#include <algorithm>
#include <cstdint>
#include <cstdlib>
#include <utility>
#include <vector>
#include <algorithm>
#include <cstring>
#include <iostream>
#include <string>
#include <stdio.h>
#include <memory>


struct SStyle
{
  unsigned long fillColor_;
  unsigned long outlineColor_;
  unsigned long radius_;
  unsigned long lineWidth_;
  XFontStruct *font_;
  std::string style_name_;
};

struct Srgb
{
  int r;
  int g;
  int b;
};

class SStyle_Manager
{
private:
  std::vector<SStyle> styles_;
  Display *display_;
  Window window_;
  int screen_;
  GC gc_;
  int active_style = 0; // just set the default to first passed.
public:
  SStyle_Manager(Display *display, Window window, int screen, GC gc)
  {
    display_ = display;
    window_ = window;
    screen_ = screen;
    gc_ = gc;
  }
  ~SStyle_Manager()
  {
    for (auto& style : styles_)
    {
      if (style.font_)
      {
        XFreeFont(display_, style.font_);  // Free the X11 font resource
      }
    }
    styles_.clear();
  }
  int add_style(std::string styleName, Srgb fillColor, Srgb outlineColor, int radius, int linewidth,  std::string fontName)
  {
    SStyle nstyle;
    nstyle.style_name_ = std::move(styleName);
    nstyle.fillColor_ = get_color_Srgb(fillColor);
    nstyle.outlineColor_ = get_color_Srgb(outlineColor);
    nstyle.radius_ = radius;
    nstyle.lineWidth_ = linewidth;
    nstyle.font_ = XLoadQueryFont(display_, fontName.c_str());
    // Define fallback fonts to try if the requested font fails
    const char* fallbackFonts[] = { "fixed", "helvetica" };
    size_t numFallbacks = sizeof(fallbackFonts) / sizeof(*fallbackFonts);

    nstyle.font_ = nullptr;

    // Attempt to load the primary font (if provided and not empty)
    if (!fontName.empty()) {
      nstyle.font_ = XLoadQueryFont(display_, fontName.c_str());
    }

    // If the primary failed, iterate through fallback fonts until one succeeds or all are exhausted
    if (!nstyle.font_) {
      for (size_t i = 0; i < numFallbacks && !nstyle.font_; ++i) {
        nstyle.font_ = XLoadQueryFont(display_, fallbackFonts[i]);
      }
    }

    // Final validation: if no font loaded, report which fonts were attempted and exit
    if (!nstyle.font_)
    {
      printf("Failed to load any font (tried: ");
      if (!fontName.empty()) printf("%s", fontName.c_str());
      for (size_t i = 0; i < numFallbacks; ++i) {
        if (!fontName.empty() || i > 0) printf(", ");
        printf("%s", fallbackFonts[i]);
      }
      printf(")\n");
      return -1;
    }
    styles_.push_back(nstyle);
    return 0;
  }
  unsigned long get_color_Srgb(Srgb color)
  {
    XColor xcolor;
    xcolor.red = color.r << 8;
    xcolor.green = color.g << 8;
    xcolor.blue = color.b << 8;
    if (XAllocColor(display_, DefaultColormap(display_, screen_), &xcolor))
    {
      return xcolor.pixel;
    } else {
      printf("Failed to allocate color! RGB(%d,%d,%d)\n", color.r, color.g, color.b);
      return BlackPixel(display_, screen_);  // Fallback
    }
  }
  unsigned long get_color_rgb(unsigned char r, unsigned char g, unsigned char b)
  {
    XColor xcolor;
    xcolor.red = r << 8;
    xcolor.green = g << 8;
    xcolor.blue = b << 8;
    if (XAllocColor(display_, DefaultColormap(display_, screen_), &xcolor))
    {
      return xcolor.pixel;
    } else {
      printf("Failed to allocate color! RGB(%d,%d,%d)\n", r, g, b);
      return BlackPixel(display_, screen_);  // Fallback
    }
  }
  int set_active_style_byName(std::string styleName)
  {
    auto it = std::find_if(styles_.begin(), styles_.end(),
        [&](const SStyle& s) { return s.style_name_ == styleName; });

    if (it != styles_.end())
    {
      active_style = static_cast<int>(std::distance(styles_.begin(), it));
      return active_style;
    }
    return -1; // Not found
  }
  SStyle get_active_style_byName(std::string styleName)
  {
    auto it = std::find_if(styles_.begin(), styles_.end(),
      [&](const SStyle& s) { return s.style_name_ == styleName; });
  }
  SStyle get_active_style()
  {
    return styles_[active_style];
  }
  SStyle get_index_style(int index)
  {
    if (index > 0 && index < (int)styles_.size())
    {
      return styles_[index];
    }
  }

};

Conclusion

We are goingto stop there for today, we have a full basic style manager that will allow our future application to have various themes that we can allow the user to select, yes it's very basic, but you can see that it is very extensible.  In our next article we will look at integrating the style manager and making buttons a standalone class, not a button manager. The idea behind this is a small application does not need a giant widget tracker and can make one class instance for the widgets that it desires!  Notice no we have not coded up a delete function - there is a challenge for you to figure out!

Linux Rocks Every Day