Декларативное описание UI на C++20

Несколько лет назад у меня появилась идея как можно реализовать декларативное описание UI на современном C++. Наконец-то нашёл время и реализовал маленькую экспериментальную библиотеку.

Ключевая идея заключается в том, что с помощью Class Template Argument Deduction (CTAD), добавленного в C++17, и Variadic Templates, которые существуют ещё с C++11, можно в compile time описывать сложные иерархические структуры данных с помощью фигурных скобок, без явного указания параметров шаблонов.

Например, мы можем объявить класс widget для описания виджетов UI следующим образом:

template <typename ... Elements>
class widget {
public:
    explicit widget(Elements && ... elements) { /*...*/ }
};

Теперь этот класс можно использовать с другими классами, объявленными подобным же образом, для построения дерева разметки UI:

auto w = widget {
    title {"My Widget"},
    widget {
        /* ... */
    },
    other_widget {
        /* ... */
    }
};

Компилятор сам выведет параметры Elements шаблона widget для переменной w, в данном случае тип переменной w будет widget<title, widget<...>, other_widget<...>>. Имея типы дочерних элементов в виде параметров шаблона класса мы можем с помощью метапрограммирования делать много интересных вещей, например отделить дочерние виджеты (widget и other_widget) от свойств (title), сохранить их в переменную-член и организовать к ним доступ по индексу со статической типизацией:

// child1 type is widget<...> &
auto & child1 = w.child<0>();

// child2 type os other_widget<...> &
auto & child2 = w.child<1>();

В C++20 добавили возможность использования literal class type в качестве параметров шаблона, и с помощью этого можно реализовать параметризацию шаблонных классов и функций строковым литералом. Применив всё это вместе можно реализовать поиск дочерних виджетов в разметке по строковому ID со статическиой типизацией:

auto w = widget {
    some_widget {
        id<"my_widget">
    }
}

// child type is some_widget<...> &
auto & child = w.child<"my_widget">();

Отдельной проблемой была реализация возможности добавления обработчиков событий прямо в разметке. С помощью обобщённых лямбда-функций (из C++14) и небольших хитростей удалось реализовать возможность доступа из обработчиков к дочерним элементам разметки по ID или индексу:

auto w = root_widget {
    some_widget {
        id<"child_widget">
    }
    button {
        on_click { [](auto & root) {
            // child type is some_widget &
            auto & child = root.template child<"child_widget">();
        } }
    }
}

Идея заключается в том, чтобы добавить специальные типы виджетов (root_widget, window, dialog, и т. д.), которые будут корнями разметки. Конструкторы этих виджетов будут устанавливать себя в качестве параметра, передаваемого в обобщенные лямбда-функции всех обработчиков событий.

Если всё это сложить вместе, то реализация простого диалога с кнопкой и счётчиком выглядит примерно так:

auto dlg = ui::dialog {
    ui::vbox {
        ui::hbox {
            ui::label {
                ui::text("Counter:")
            },
            ui::line_edit {
                ui::id<"counter">,
                ui::text("0")
            }
        },
        ui::button {
            ui::text("Button"),
            ui::on_click { [](auto & root) {
                auto & edit = root.template child<"counter">();
                edit.set_text(std::to_wstring(std::stoi(edit.text()) + 1));
            } }
        }
    }
};

Реализация библиотеки для Qt с примером использования лежит здесь: https://github.com/cxx-ui/ull.

Это пока только экспериментальная версия, proof of concept. Осталось понять что с этим делать дальше. Я склоняюсь к тому, что не стоит пытаться делать отдельную новую библиотеку для UI на C++, а сделать это для начала в виде вспомогательной библиотеки, которую можно будет использовать для описания разметки виджетов в любом существующем коде UI, написанном с помощью разных библиотек: Qt, wxWidgets, Gtk.

comments powered by Disqus