My GUI Toolkit and Desktop Environment, Built from Scratch Specifically for FreeBSD

First of all, congratulations on the work — building both a GUI toolkit and a desktop environment is an impressive achievement.

I want to ask you : how would you describe the learning curve for someone who wants to:
  • use your toolkit in their own projects
  • contribute to the codebase
And what is the current state of the user and developer documentation?
 
First of all, congratulations on the work — building both a GUI toolkit and a desktop environment is an impressive achievement.
Thank you so much for the complement it means alot.

I want to ask you : how would you describe the learning curve for someone who wants to:
  • use your toolkit in their own projects

If you’re familiar with Qt and QML, development will feel very similar. The toolkit isn’t fully polished yet, but the following example shows what you can expect when creating an application.
hello.hml
Code:
Window {
    id: "main"
    title: "FreeBSD Toolkit Demo"
    width: 400
    height: 200

    signal buttonPressed()

    Layout {
        direction: "column"
        spacing: 16
        padding: 20

        Text {
            text: "Hello from HML"
            fontSize: 18
            align: "center"
        }

        Button {
            id: "btn"
            text: "Click me"
            onClicked: buttonPressed()
        }
    }
}


main.cc
C++:
#include <iostream>

#include <gui/application.h>
#include <gui/hml_loader.h>
#include <gui/window.h>

int main() {
    gui::Application app;
    if (!app.isValid()) {
        return 1;
    }

    // Load declarative UI
    auto doc = gui::HmlLoader::loadFromFile("hello.hml");
    if (!doc || !doc->isValid()) {
        return 1;
    }

    auto window = doc->rootAs<gui::Window>();
    if (!window || !window->isValid()) {
        return 1;
    }

    // Bind HML signal to C++
    doc->onSignal("buttonPressed", []() {
        std::cout << "Button was clicked!" << std::endl;
    });

    window->requestFrame();
    app.run();
    return 0;
}

  • contribute to the codebase


The SDK is organized into a set of modules: archive, audio, compression, cloud, core, cryptography, data, ecs, graphics2d, graphics3d, gui, network, physics2d, physics3d, scripting, system, math, widgets, and web.

The graphics2d module is a wrapper around Skia (see https://skia.org/docs/), the same 2D graphics engine used by Android, ChromeOS, Google Chrome, and Flutter. Skia is developed by Google and released under a BSD license. This module is just over 3,000 lines of code, and most semi-experienced C++ developers should be comfortable contributing to it. Below is an overview of what the module contains.

graphics2d/include/ this is the public api
Code:
alpha_type.h        mat3.h              rect.h
color.h             paint.h             shape.h
color_type.h        painter.h           surface.h
font.h              path.h              text_style.h
gpu_context.h       pattern.h           vulkan_context.h
image.h             pixmap.h            vulkan_image_info.h
image_info.h        point.h

graphics2d/private/ this is the private api (pimpl),
Code:
font_impl.h         image_info_skia.cpp path_impl.h
image_impl.h        jpeg_shim.cpp       pixmap_impl.h
image_info_impl.h   painter_impl.h      skia_support.cpp

graphics2d/src/ this is the implementation
Code:
font.cpp        image_svg.cpp   pixmap.cpp
gpu_context.cpp painter.cpp     shape.cpp
image.cpp       path.cpp        surface.cpp
image_info.cpp  pattern.cpp

Then we have the gui module, which includes the window system, widgets, layout engine, rendering engine, focus manager, and more, essentially the heart and soul of the GUI toolkit. It uses Wayland client, Vulkan (see https://www.vulkan.org/), and the Yoga layout engine (see https://www.yogalayout.dev/) library's. Yoga is developed by Facebook and released under the MIT license.

This is the most important part of the toolkit and is just over 10,000 lines of code, with much of it generated from Wayland protocol files. The layout class itself is a thin wrapper around Yoga. Anyone with working knowledge of Wayland and Vulkan should be comfortable contributing in this area. The layout wrapper requires just semi-experienced c++ developer.

gui/include/ this is the public api
Code:
application.h   layout.h        style.h
focus_manager.h layout_types.h  widget.h
input.h         paint_list.h    window.h

gui/private/ this is the private api (pimpl),
Code:
vulkan_swapchain.cpp
vulkan_swapchain.h
wayland_context.h
wlr-layer-shell-unstable-v1-protocol.c
wlr-layer-shell-unstable-v1-protocol.h
xdg-shell-client-protocol.c
xdg-shell-client-protocol.h

gui/src/ this is the implementation
Code:
application.cpp   paint_list.cpp    window.cpp
focus_manager.cpp style.cpp
layout.cpp        widget.cpp

Then we have widgets,

widgets/include this is the public api
Code:
button.h       progress_bar.h slider.h
checkbox.h     radio_button.h status_bar.h
container.h    scroll_area.h  text_edit.h
label.h        scroll_bar.h   title_bar.h

widgets/src/ this is the implementation
Code:
button.cpp       progress_bar.cpp slider.cpp
checkbox.cpp     radio_button.cpp status_bar.cpp
container.cpp    scroll_area.cpp  text_edit.cpp
label.cpp        scroll_bar.cpp   title_bar.cpp

Here is the implementation for a button widget if you can understand this code then you should be find developing widgets.

C++:
w#include "gui/widgets/button.h"

#include "graphics2d/text_style.h"

#include <algorithm>

namespace gui {

Button::Button(std::string text) : text_(std::move(text)) {}

void Button::setText(std::string text) {
    text_ = std::move(text);
    invalidate();
}

const std::string& Button::text() const {
    return text_;
}

void Button::setTextColor(graphics2d::Color color) {
    textColor_ = color;
    colorOverride_ = true;
    invalidate();
}

void Button::setBackgroundColor(graphics2d::Color color) {
    background_ = color;
    colorOverride_ = true;
    backgroundEnabled_ = true;
    invalidate();
}

void Button::setHoverColor(graphics2d::Color color) {
    hover_ = color;
    colorOverride_ = true;
    backgroundEnabled_ = true;
    invalidate();
}

void Button::setPressedColor(graphics2d::Color color) {
    pressed_ = color;
    colorOverride_ = true;
    backgroundEnabled_ = true;
    invalidate();
}

void Button::setCornerRadius(float radius) {
    cornerRadius_ = radius;
    invalidate();
}

void Button::setFont(std::shared_ptr<graphics2d::Font> font) {
    font_ = std::move(font);
    if (font_ && fontSize_ > 0.0f) {
        auto sized = font_->withSize(fontSize_);
        if (sized && sized->isValid()) {
            font_ = std::shared_ptr<graphics2d::Font>(std::move(sized));
        }
    }
    invalidate();
}

void Button::setFontSize(float size) {
    fontSize_ = size;
    if (font_ && fontSize_ > 0.0f) {
        auto sized = font_->withSize(fontSize_);
        if (sized && sized->isValid()) {
            font_ = std::shared_ptr<graphics2d::Font>(std::move(sized));
        }
    }
    invalidate();
}

void Button::setTextAlign(TextAlign align) {
    textAlign_ = align;
    invalidate();
}

void Button::setCircle(bool enabled) {
    circle_ = enabled;
    invalidate();
}

void Button::setBackgroundEnabled(bool enabled) {
    backgroundEnabled_ = enabled;
    invalidate();
}

void Button::setTextOffset(float dx, float dy) {
    textOffsetX_ = dx;
    textOffsetY_ = dy;
    invalidate();
}

void Button::setOnClick(ClickCallback callback) {
    onClick_ = std::move(callback);
}

void Button::onPaint(PaintList& list) {
    graphics2d::Rect rect = bounds();
    if (backgroundEnabled_) {
        graphics2d::Paint bgPaint;
        if (pressedState_) {
            bgPaint.setColor(pressed_);
        } else if (hovered_) {
            bgPaint.setColor(hover_);
        } else {
            bgPaint.setColor(background_);
        }

        if (circle_) {
            float radius = 0.5f * std::min(rect.w, rect.h);
            graphics2d::Point center{rect.x + rect.w * 0.5f, rect.y + rect.h * 0.5f};
            list.drawCircle(center, radius, bgPaint);
        } else if (cornerRadius_ > 0.0f) {
            list.drawRoundedRect(rect, cornerRadius_, cornerRadius_, bgPaint);
        } else {
            list.drawRect(rect, bgPaint);
        }
    }

    if (!text_.empty()) {
        graphics2d::Paint textPaint;
        textPaint.setColor(textColor_);
        float size = fontSize_;
        if (font_ && font_->isValid()) {
            size = font_->size();
        }
        if (textAlign_ == TextAlign::Center) {
            graphics2d::TextStyle style;
            style.setSize(size);
            style.setColor(textColor_);
            style.setAlign(graphics2d::TextStyle::Align::Center);
            style.setVerticalAlign(graphics2d::TextStyle::VerticalAlign::Middle);
            style.setWordWrap(false);
            if (font_ && font_->isValid()) {
                style.setFont(font_.get());
            }
            graphics2d::Rect box = rect;
            box.x += textOffsetX_;
            box.y += textOffsetY_;
            list.drawTextBox(text_, box, style);
        } else {
            auto countCodepoints = [](const std::string& text) -> std::size_t {
                std::size_t count = 0;
                for (unsigned char c : text) {
                    if ((c & 0xC0) != 0x80) {
                        ++count;
                    }
                }
                return count;
            };
            std::size_t glyphs = countCodepoints(text_);
            float approxWidth = size * 0.6f * static_cast<float>(glyphs == 0 ? 1 : glyphs);
            float textX = rect.x + 8.0f;
            float textY = rect.y + size + (rect.h - size) * 0.5f;
            if (font_ && font_->isValid()) {
                list.drawText(text_, {textX, textY}, *font_, textPaint);
            } else {
                list.drawText(text_, {textX, textY}, size, textPaint);
            }
        }
    }
}

bool Button::onPointerDown(const PointerEvent& event) {
    (void)event;
    pressedState_ = true;
    invalidate();
    return true;
}

bool Button::onPointerUp(const PointerEvent& event) {
    (void)event;
    bool wasPressed = pressedState_;
    pressedState_ = false;
    invalidate();
    if (wasPressed && onClick_) {
        onClick_();
    }
    return true;
}

bool Button::onPointerMove(const PointerEvent& event) {
    (void)event;
    return true;
}

void Button::onHoverEnter(const PointerEvent& event) {
    (void)event;
    hovered_ = true;button.h       progress_bar.h slider.h
checkbox.h     radio_button.h status_bar.h
container.h    scroll_area.h  text_edit.h
label.h        scroll_bar.h   title_bar.h

    invalidate();
}

void Button::onHoverLeave(const PointerEvent& event) {
    (void)event;
    hovered_ = false;
    invalidate();
}

void Button::onStyleChanged(const Style& style) {
    if (style.metrics.buttonFontSize > 0.0f) {
        fontSize_ = style.metrics.buttonFontSize;
    }
    if (style.metrics.buttonMinHeight > 0.0f) {
        layout().setMinHeight(style.metrics.buttonMinHeight);
    }
    if (style.cornerRadius > 0.0f) {
        cornerRadius_ = style.cornerRadius;
    }
    if (!colorOverride_) {
        background_ = style.accent.a == 0 ? style.background : style.accent;
        hover_ = applyAlpha(background_, 0.85f);
        pressed_ = applyAlpha(background_, 0.7f);
        textColor_ = style.accentForeground.a == 0 ? style.foreground : style.accentForeground;
        backgroundEnabled_ = true;
    }
    invalidate();
}

}  // namespace gui

And what is the current state of the user and developer documentation?

There is currently no documentation as the api isn't stable yet.
Let me know if you have anymore questions.
 
C++, HML, and inline JavaScript are alive! Take that Qt!! Going to go see if lexbor https://github.com/lexbor/lexbor is in the ports repo so I can add css styling support.

Holy cat crap!! Its actually in there! And it s recent version!!!
sh:
root@freebsd:~/Projects/sdk_freebsd # pkg search lexbor
lexbor-2.6.0                   Modular web engine (HTML/CSS parser, renderer, ...)
root@freebsd:~/Projects/sdk_freebsd #


C++:
#include <iostream>

#include <gui/application.h>
#include <gui/hml_loader.h>
#include <gui/hml_document.h>
#include <gui/window.h>
#include <gui/widget.h>

int main(int argc, char** argv) {

    gui::Application app;
    if (!app.isValid()) {
        std::cerr << "failed to initialize application" << std::endl;
        return 1;
    }

    const char* hmlPath = (argc > 1 && argv[1]) ? argv[1] : "hello.hml";
    auto doc = gui::HmlLoader::loadFromFile(hmlPath);
    if (!doc || !doc->isValid()) {
        std::cerr << "failed to load hml file: " << doc->errorMessage() << std::endl;
        return 1;
    }

    gui::Widget* rootWidget = doc->root();
    if (!rootWidget) {
        std::cerr << "no root widget" << std::endl;
        return 1;
    }

    gui::Window window(app, 400, 200, "FreeBSD Toolkit Demo");
    if (!window.isValid()) {
        std::cerr << "failed to create window" << std::endl;
        return 1;
    }

    doc->onSignal("buttonPressed", []() {
        std::cout << "you pressed a button" << std::endl;
    });


    auto rootShared = std::shared_ptr<gui::Widget>(rootWidget);
    window.setRootWidget(rootShared);

    window.requestFrame();

    app.run();

    return 0;
}

Code:
Window {
    id: "main"
    title: "FreeBSD Toolkit Demo"
    width: 400
    height: 200

    signal buttonPressed()

    Layout {
        direction: "column"
        spacing: 0
        padding: 0

        TitleBar {
            id: "titlebar"
            title: "FreeBSD Toolkit Demo"
            subtitle: "HML + JS"
            background: "#2b3139"
            height: 40

            Left {
                Button {
                    text: "Back"
                }
            }
        }

        Layout {
            direction: "column"
            spacing: 16
            padding: 20

            Text {
                text: "Hello from HML"
                fontSize: 18
                align: "center"
            }

            Button {
                id: "btn"
                text: "Click me"
                onClicked: buttonPressed()
            }
        }

        StatusBar {
            background: "#23272e"
            height: 28
            Left {
                Text {
                    text: "Ready"
                }
            }
            Right {
                Text {
                    text: "v0.1"
                }
            }
        }
    }
}

1770646412356.png
 
Note for future explorers: client-side window drop shadows are not the way!!! Seriously not the wayyyyyy. Drop shadows belong in the compositor. Doing them client-side leads to inconsistent geometry, broken hit-testing, and all kinds of subtle layout bugs.

What you’re seeing:
  1. Server-side decorations with server-side drop shadows
    It ensures applications like SDL3, Godot 4, SFML 3, Raylib, etc. look native and visually consistent with the rest of the system. The terminal shown here is foot.
  2. Client-side decorations with server-side drop shadows
    This application is very special, and I’ll go into more detail about it in the next post.
  3. Client-side decorations (GTK) with server-side drop shadows
    Even with GTK’s client-side decorations, shadows are now handled correctly and consistently by the compositor.
The compositor is getting very close to feature complete. Remaining work includes sub-windows, clipboard support, drag-and-drop, tiling mode, and a ton of edge cases, but the core architecture is now solid.


1770682281914.png
 
Once I figure out how to monetize this whether through donations or sponsorships, I’ll start releasing the code. The reality is that continuing this project will require full-time development. Feel free to share your thoughts in the comments.
Well first kudos, because writing something like that is a big project. And for sure a valuable lesson.

Monetising such a thing though sounds really hard to me, because there are so many GUI toolkits already out there. Beside GTK and Qt there's Tk, Athena Widgets, Fltk and many more.

So the question is: what are your goals with that project and what are you making different? What makes you unique?

You've got have some answers ready in order if you want people to support you with money.
 
Thank you for the compliment. It’s not really a learning curve so much as a huge amount of work and figuring out how to work around the limitations of GPU support and tooling that come with developing on FreeBSD. Fortunately, I do have some help.

Since this thread already goes into a lot of technical detail, I’ll summarize the core differences briefly. Tk, Athena Widgets, and FLTK aren’t comparable and I’d even go so far as to say GTK isn’t either. My stack is extremely modern: Wayland-only, GPU-accelerated via Vulkan or OpenGL with CPU fallback, and currently the fastest open-source 2D renderer available. It uses modern layout algorithms for responsive design, and all of the libraries I rely on have corporate backing from Google, Meta, Intel, AMD, and others so they’re virtually guaranteed not to be abandoned anytime soon, since those companies actively use them in their own products.

My SDK isn’t limited to 2D graphics, GUI, or widgets, it also includes audio and video, archiving and compression, HTML5, CSS3, XML, JSON, TOML, cryptography, cloud services, AI, networking, 3D graphics, and more.

It supports Lottie animations, SVG, scene graphs, and render lists in a retained-mode architecture. The stack is built specifically for FreeBSD rather than adapted from a cross-platform abstraction layer. The SDK and applications created with it are intended to remain FreeBSD-focused, giving developers targeting the platform a stronger opportunity to build and sustain a FreeBSD-centric user base.

It's also completely free of viral copyleft licenses so if someone want's to build closed source software they are welcome to do so and I will support them in doing that. Good luck finding that on Linux.

My goal is to build a FreeBSD development community that creates extremely high-quality commercial software that people can sell, prosper from, and ultimately build better lives with. That’s what I want for myself as well.
 
My stack is extremely modern: Wayland-only, GPU-accelerated via Vulkan or OpenGL with CPU fallback, and currently the fastest open-source 2D renderer available. It uses modern layout algorithms for responsive design, and all of the libraries I rely on have corporate backing from Google, Meta, Intel, AMD, and others so they’re virtually guaranteed not to be abandoned anytime soon, since those companies actively use them in their own products.
I'm interested in the fastest performance for fullscreen games and lowest-latency everywhere. I found GNOME 48+ slower with default GSK_RENDERER=vulkan (DE + GTK apps like Text Edit all GPU-rendering) vs cairo not GPU accelerating. I get better latency/perf Xfce disabling compositor. On Windows Steam and Bnet client have better perf in client windows with browser acceleration disabled. I have many cases where CPU-accelerating GUI windows was faster than trying to force it GPU.

Does this stack offer better performance and lower-latency than current solutions? I'm skeptical of mouse input (I use evdev on X but if I understand right libinput on Wayland does translation to evdev). GNOME on Wayland put cursors on timers or something that can make cursor movement inconsistent with high CPU/GPU load (I'm thinking it's a Wayland technical thing/how it has to be)
 
I'm interested in the fastest performance for fullscreen games and lowest-latency everywhere. I found GNOME 48+ slower with default GSK_RENDERER=vulkan (DE + GTK apps like Text Edit all GPU-rendering) vs cairo not GPU accelerating. I get better latency/perf Xfce disabling compositor. On Windows Steam and Bnet client have better perf in client windows with browser acceleration disabled. I have many cases where CPU-accelerating GUI windows was faster than trying to force it GPU.

Does this stack offer better performance and lower-latency than current solutions? I'm skeptical of mouse input (I use evdev on X but if I understand right libinput on Wayland does translation to evdev). GNOME on Wayland put cursors on timers or something that can make cursor movement inconsistent with high CPU/GPU load (I'm thinking it's a Wayland technical thing/how it has to be)
My renderer is hybrid: Vulkan, OpenGL, or CPU can be selected per configuration depending on workload and hardware.

The render path is retained mode with explicit control over frame submission, damage regions, and synchronization. For fullscreen workloads the compositor can run in a minimal pass-through mode, or be bypassed entirely if the game supports direct KMS/DRM.

I’ve also been exploring a console-style configuration: a stripped-down compositor, a dashboard similar to Xbox/PS5/Switch, and a system where the game is effectively the only active client so input, scheduling, and rendering are tuned purely for latency.

On input, we don’t use evdev or libinput we read devices directly via hidraw, keeping us close to the device driver.

I can’t give a definitive performance answer yet because I haven’t done any profiling. What I can say is that Qt’s published benchmarks show Skia performing significantly faster than their own renderer and other open-source options in several workloads.
 
For those of you who play games or are into vfx, I’d love to collaborate and help get your projects running as smoothly as possible. If you’re willing to support my efforts, I’m fully committed to doing everything I can to make that happen.

I also dabble a bit in the world of VFX 😉
You can check out my work here:

 
Ohhhhhh boys!!!!! yes, I am excited! Why? Because I just ripped out Flutter’s newest rendering engine, Impeller(BSD-3). It’s written in C++, and I also pulled their latest Render List, converted the entire build system to CMake, and wired it up to Vulkan then passed it some signed-distance-field GLSL examples from Inigo Quilez… https://iquilezles.org/articles/distfunctions2d/ and wow this is very very promising. SDF shapes are some of the nicest you can get they blend like metaballs. This could replace Skia and open up a ton of possibilities, think macOS-style liquid effects if you’re into that look. It also slashes an insane number of dependencies… easily 35+ gone.

Notice that insanely smooth anti-aliasing.

1770784109089.png


View: https://youtu.be/vd5NqS01rlA?si=BX0m04rDyJXumt-_
 
Back
Top