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.
 
Back
Top