- BrainTools - https://www.braintools.ru -
Продолжаю воплощать свои самые смелые идеи, на которые ранее не было ресурсов и которые теперь можно реализовать благодаря появлению LLM. Я верю, что сейчас ценность самой идеи стала выше и именно она всё решает.
Сегодня хочу представить лёгкий UI-фреймворк. Он написан на Rust, работает на Windows (x64, ARM), macOS, Linux, Android, iOS/iPadOS и даже в браузере через WASM (онлайн-демо на GitHub Pages). Снабжён примерами использования на Rust, C, C++ и даже ASM.
Отличительная особенность: фреймворк имеет два формата – либо отображает графический UI для людей, либо же рендерит UI в JSON так, чтобы с ним могли взаимодействовать AI-агенты без снятия скриншотов. Эта же фишка позволила добавить возможность перебросить UI по SSH (подробнее ниже).
Хотя перекладывание JSON-ов является притчей во языцех, в данном случае я посчитал его оправданным. Простота протокола, начиная с нижнего уровня, позволяет не тратить много времени на его изучение, и в данном случае это важнее скорости рендера (фреймворк предназначен для утилит, а не для сложной графики).
Нижний уровень фреймворка умеет рендерить только JSON-кадр из 9 примитивов. Вот они:
|
Примитив |
Геометрия |
Свойства |
Описание |
|---|---|---|---|
|
|
|
|
прямоугольник, в том числе со скруглёнными углами |
|
|
|
|
отрезок |
|
|
|
|
круг |
|
|
|
|
треугольник (стрелки, галочки) |
|
|
|
|
замкнутый многоугольник (секторы графиков, свои фигуры) |
|
|
|
|
строка текста; перенос делает слой экранов |
|
|
|
|
картинка |
|
|
|
|
группа, обрезанная по прямоугольнику (прокрутка) |
|
|
|
|
невидимая область для мыши и касаний (не рисуется) |
Уровнем выше в JSON добавляются привычные нам элементы управления, такие как Row, Text, IconButton и прочие. JSON со сложными элементами при спуске на нижний уровень будет преобразован так, что останутся только эти 9 примитивов.
Всё это позволило быстро адаптировать фреймворк под все 6 платформ человечества:
|
платформа |
бэкенд |
|---|---|
|
macOS |
AppKit, CoreGraphics, CoreText, ImageIO через рантайм Objective-C |
|
iOS |
UIKit, CADisplayLink, тот же код CoreGraphics/CoreText, что и на macOS |
|
Android |
JNI вручную, |
|
Windows |
Win32, Direct2D, DirectWrite и WIC, COM вручную; DPI для каждого монитора, тёмный заголовок |
|
Linux |
свой код протокола Wayland и X11 (xcb), Cairo + Pango, xkbcommon – всё через |
|
Браузер |
|
Для добавления платформы нужно реализовать 9 методов (по одному для каждого из примитивов) средствами платформы, а также добавить обработку событий.
На самом высоком уровне пользователи могут добавлять в JSON свои элементы управления вот так:
#include <uison_widgets.hpp>
using namespace uison::widgets;
template <class F>
uison::Node Pill(std::string text, F on_click) {
return Box({
.padding = {8, 22},
.fill = uison::json(R"({ state: {
normal: "$theme.primary", hover: "$theme.primaryHover", pressed: "$theme.primaryPressed" } })"),
.radius = 999, // more than half the height: the ends become semicircles
.align = "center",
.interactive = true,
.on_click = on_click,
.child = Text(text, {.bold = true, .color = "$theme.onPrimary"}),
});
}
int main() {
auto screen = Column({.padding = 24, .gap = 16, .cross = "start", .children = {
Text("Pressed {@n} times", {.size = 20}),
Pill("Press me", [](uison::Cx& cx) {
cx.data().set("n", cx.data().get<int>("n") + 1);
}),
}});
uison::Ui ui(screen, {.size = {360, 140}, .data = uison::obj{{"n", 0}}});
return ui.run("Pill");
}
Или прямо в JSON:
{
Pill: {
params: { text: "" },
interactive: true, // the node gets hover / pressed states and a click
body: {
Box: {
padding: [8, 22],
radius: 999, // more than half the height: the ends become semicircles
align: "center",
fill: { state: {
normal: "$theme.primary",
hover: "$theme.primaryHover",
pressed: "$theme.primaryPressed",
} },
child: { Text: { text: "$text", bold: true, color: "$theme.onPrimary" } },
},
},
},
}
Все эти новые элементы, дойдя до нижнего слоя, будут преобразованы в JSON из 9 тех самых примитивов, которые приведены в таблице выше.
Новый кадр сравнивается со старым поэлементно (выравнивание LCS, группы clip с одинаковым прямоугольником сравниваются внутри). Изменённые, добавленные и удалённые элементы дают области “повреждения”, и бэкенд перерисовывает только их.
К примеру, благодаря виртуализации списков кадр почты из 2000 писем содержит пару десятков строк.
На основе этой же идеи удалось реализовать даже простой браузер. HTML и CSS преобразуются в JSON, после чего результат просто скармливается нашему фреймворку.
Сам uison при этом ничего не знает про HTML и CSS. Он получает привычный JSON и отображает его точно так же, как интерфейс обычного приложения.
Таким образом, один и тот же механизм позволяет рисовать как обычные приложения с кнопками и таблицами, так и веб-страницы.
Поскольку в самом низу у нас JSON, его можно вообще не рендерить, а отдавать как есть – в консоль в голом виде. Это позволяет простым образом запустить программу на сервере и работать с ней по SSH так, как будто она работает на вашем компьютере.
Программа на сервере не знает про SSH. Она пишет кадры в stdout и читает события из stdin. Серверу нужен только sshd, ни X11, ни VNC, ни браузера.
Плеер умеет сам загрузить программу на сервер и запустить её с –console.
Плеер оформлен отдельным проектом [1]. Его можно собрать даже для Android и управлять программой через телефон. В отдельной вкладке можно посмотреть JSON-события и кадры рендера, которые передаются между клиентом и сервером:
Одно и то же приложение может работать в нескольких режимах без изменения исходного кода. Оно может открыть обычное графическое окно для человека, передавать JSON-кадры внешнему рендереру или предоставлять семантическое описание интерфейса AI-агенту.
|
Запуск |
Кто на том конце |
Что идёт в stdout |
|---|---|---|
|
|
Человек |
Ничего: открывается окно |
|
|
Плеер, который рисует |
JSON-кадры: прямоугольники, текст, координаты |
|
|
Скрипт или AI-агент |
JSON со смыслом: роли, имена, состояния |
|
|
Claude Code и другие MCP-клиенты |
Тот же смысл, но в протоколе MCP |
С флагом –mcp программа становится MCP-сервером, то есть набором инструментов для Claude Code, Cursor и других AI-клиентов. Подключается одной командой claude mcp add todo – ./todo –mcp, кода писать не нужно. Агент вызывает snapshot и получает экран текстом, а потом действует инструментами click, type, choose. В ответ приходит только то, что изменилось. Например, агент вписывает «Milk» в поле и жмёт Enter:
→ {"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"type","arguments":{"key":"draft/1/0","text":"Milk","submit":true}}}
← {"jsonrpc":"2.0","id":4,"result":{"content":[{"type":"text","text":"oknchanges (+ added, - removed, ~ changed):n+ checkbox "Milk" #4/0 uncheckedn+ button "trash" #del-4n+ text "3"n+ text "1 of 4 done"n+ text "Remaining: 3"n- text "2"n- text "1 of 3 done"n- text "Remaining: 2"n~ #draft/1/0 focusedn"}],"isError":false}}
В списке появились пункт «Milk» и его кнопка удаления, а счётчик сменился с «1 of 3» на «1 of 4». Нажать неактивную кнопку агент не сможет, как не смог бы человек: программа ответит, что кнопка неактивна.
Покажу на примере реальной программы с открытым кодом – Disk Usage на macOS. Она создана в двух вариантах: на egui [2] и на uison [3], оба на Rust. Вот результат тестов:
|
Показатель |
egui (оригинал) |
uison |
|---|---|---|
|
Размер программы, macOS arm64 |
5,1 МБ (5 126 464 Б) |
0,9 МБ (923 216 Б), −82 % |
|
Размер с |
4,5 МБ (4 450 560 Б) |
0,9 МБ, примерно в 5 раз меньше |
|
Зависимости в |
362 |
0 внешних (только крейты uison) |
|
Память [4] пустого окна, без сканирования |
122 МБ |
55 МБ |
|
Память после сканирования |
152 МБ |
97 МБ, −36 % |
|
Пик памяти во время сканирования |
167–176 МБ |
183–184 МБ |
|
Строк Rust |
3 198 |
3 547 + 479 строк JSON экрана |
Всё есть в уроках на русском языке. Уроки для Rust [5], C [6], C++ [7], а также краткая вводная для ASM [8].
Здесь приведу, как стартануть на Rust.
cargo new hello && cd hello
cargo add uison
use uison::prelude::*;
fn main() {
let screen = center(column().gap(12).cross("center").children([
text("Pressed {@n} times").size(20),
button("+1").key("inc").on_click(|cx| cx.data.edit("n", |n: &mut i32| *n += 1)),
]));
let mut ui = Ui::from_node(screen).with_size(320.0, 200.0);
ui.data.set("n", 0);
run(ui, "Hello");
}
cargo run --release
Откроется окно со счётчиком. {@n} – привязка к данным: обработчик меняет n, и текст обновляется сам, без ручной перерисовки.
Чтобы программа была маленькой, добавьте в конец Cargo.toml профиль сборки:
[profile.release]
opt-level = "s"
lto = true
codegen-units = 1
panic = "abort"
strip = true
Фреймворк не ставит своей целью заменить Qt или другие полноценные GUI-фреймворки. uison в первую очередь рассчитан на утилиты, инструменты разработчика и обычные приложения с кнопками, списками, таблицами и формами. Для игр и сложной графики лучше использовать другие решения.
Кому интересно, все нужное можно найти по ссылкам:
GitHub-репозиторий проекта: uisonlab/uison [9]
Автоматические сборки [10] (сделаны с помощью GitHub Actions, а это гарантия, что собраны именно исходники, а не загружено что-то левое).
Плеер SSH для запуска программы удалённо через SSH: uisonlab/uison-ssh-player [1]
WASM-демо [11] на GitHub Pages.
Автор: codecity
Источник [12]
Сайт-источник BrainTools: https://www.braintools.ru
Путь до страницы источника: https://www.braintools.ru/article/37689
URLs in this post:
[1] отдельным проектом: https://github.com/uisonlab/uison-ssh-player
[2] egui: https://github.com/MarketKernel/disk-usage
[3] uison: https://github.com/uisonlab/uison-disk-usage
[4] Память: http://www.braintools.ru/article/4140
[5] Rust: https://github.com/uisonlab/uison/tree/main/docs/ru/rust
[6] C: https://github.com/uisonlab/uison/tree/main/docs/ru/c
[7] C++: https://github.com/uisonlab/uison/tree/main/docs/ru/cpp
[8] ASM: https://github.com/uisonlab/uison/blob/main/docs/ru/asm.md
[9] uisonlab/uison: https://github.com/uisonlab/uison
[10] Автоматические сборки: https://github.com/uisonlab/uison/releases
[11] WASM-демо: https://uisonlab.github.io/uison/
[12] Источник: https://habr.com/ru/articles/1100946/?utm_source=habrahabr&utm_medium=rss&utm_campaign=1100946
Нажмите здесь для печати.