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.