Пишем простой калькулятор используя фреймворк eframe (egui)
Всем здравствуйте. Ниже будет приведен пример написания PWA приложения готового для использования как в браузере, так и на компьютере с ОС Windows. Использовать будем язык программирования Rust и фреймворк eframe (egui). Готовое приложение будет доступно как исполняемый файл для ОС Windows, и как файл Webassembly. В процессе работы мы будем использовать GitHub Action для отслеживания корректности написания нашего кода и сборки исполняемых файлов программы, а также для развертывания (версию программы с использованием Webassembly) как страницы в интернете (GitHub Pages).
Мотивация автора
В свободное от работы время, иногда, я занимаюсь тем, что смотрю в сторону разных языков программирования и их экосистем. Последнее время, в мой фокус внимания попал язык программирования Rust. И продолжая знакомиться с экосистемой RUST, я подошел к изучению инструментов для построения пользовательских графических интерфейсов. Нужно сказать, что я был очень сильно удивлен. Удивлен как в положительном ключе, так и в отрицательном.
Что порадовало:
доступно очень много библиотек и фреймворков для создания GUI, как полностью написанных на rust, так и оберток вокруг других инструментов, написанных на других языках;
многие из них доступны для кроссплатформенной разработки, включая возможность компилировать свое приложение в WASM и опубликовать его как веб-страницу.
Что расстроило:
опять же, доступно очень много библиотек и фреймворков для создания GUI и почти все они говорят, что не готовы для использования и носят экспериментальных характер (пример является проект Druid, основная команда которого прекратила работу над ним и сосредоточилась над новым проектом Xilem). Да, последний проект тоже еще не стабилен.
Самое ключевое, что привлекло мое внимание — это возможность компилировать приложение в WASM и опубликовать его как веб-страницу. «Тренд завтрашнего или вчерашнего дня» — подумал я, так или иначе нужно посмотреть в эту сторону (немного лукавлю, так как год или два назад я трогал rust + Yew, чтобы попробовать сделать игру морской бой на Webassembly).
Несмотря на то, что инструментов для разработки пользовательских инструментов очень много, мой выбор остановился на EGUI. Нет не потому, что я провел исследование, составил табличку, расставил плюсы и минусы, а потому, что я купился на этот пример.
План работ
Определиться с минимальным набором требований (что мы хотим).
Подготовить и разобрать шаблон для приложения.
Подготовить github actions, чтобы автоматически собирать и публиковать наш результат.
Разработать простой интерфейс.
Разработать простой решатель для нашей задачи.
Требования к результату
По окончанию всех работ хочется получить рабочий интерфейс калькулятора, который доступен в виде web страницы (PWA) в интернете, а также в виде исполняемых файлов для ОС Windows.
Почему калькулятор?
Случайно наткнулся на статью об обратной польской записи, после прочтения которой решил попробовать реализовать ее. И раз, худо-бедно, 2 + 2 вычислять я уже умею, то почему бы и не сделать графический интерфейс на туже тему.
В процессе хочется сосредоточиться на самом главном — разработке, и не тратить время на развертывание приложения в интернете и публикации собранных исполняемых файлов в публичный доступ. Поэтому, для начала, создадим простой шаблон проекта и настроим (примитивно) CI/CD с использованием GitHub Action и GitHub Pages.
1 Подготавливаем шаблон
1.1 Инициируем проект
Коммит содержащий код раздела.
Перед началом нужно убедиться, что у нас установлен rust.
$ rustup --version
rustup 1.25.2 (17db695f1 2023-02-01)
info: This is the version for the rustup toolchain manager, not the rustc compiler.
info: The currently active `rustc` version is `rustc 1.67.1 (d5a82bbd2 2023-02-07)`
Создадим новый проект выполнив команду cargo new <имя-проекта>
:
$ cargo new calculator-wasm-rust-pwa --bin
Created binary (application) `calculator-wasm-rust-pwa` package
Так как мы хотим получить исполняемый файл, а не библиотеку, мы указали ключ --bin
.
Давайте еще добавим в наш проект файл src/lib.rs
:
$ touch calculator-wasm-rust-pwa/src/lib.rs
Давайте посмотрим, что Cargo сгенерировал для нас:
$ cd calculator-wasm-rust-pwa/
$ tree .
H:.
│ .gitignore
│ Cargo.toml
└───src\
main.rs
В файле Cargo.toml
будет содержаться вся информация о нашем проекте, включая перечень всех пакетов (зависимостей) для нашего проекта. В папке src\
будем размещать наш код.
1.2 Добавляем необходимые зависимости в проект
Коммит содержащий код раздела.
Так как наш выбор остановился на на EGUI и мы хотим написать приложение для «Интернета» и нативное приложение, то добавим в первую очередь eframe
— это официальная библиотека фреймворка для написания приложений с использованием egui.
$ cargo add eframe --no-default-features -F default_fonts -F glow -F persistence
Примечание к добавлению eframe:
— default_fonts
— Позволяет добавить в проект шрифты egui по умолчанию.
— glow
— Указываем egui, что необходимо использовать glow как бэкэнд рендеринга. Альтернатива: «wgpu».
— persistence
— Позволит нам реализовать восстановление состояние приложения при перезапуске приложения.
Пока этого нам будет достаточно. В итоге мы получили следующие содержание файла Cargo.toml:
# ./Cargo.toml
[package]
version = "0.1.0"
name = "calculator-wasm-rust-pwa"
description = "A simple calculator created to demonstrate how to work with web gui on rust."
authors = ["Matkin Alexandr "]
edition = "2021"
rust-version = "1.67.1"
[dependencies]
eframe = { version = "0.21.3", default-features = false, features = ["default_fonts", "glow", "persistence"] }
[profile.release]
opt-level = 2 # fast and small wasm
codegen-units = 1
lto = true
panic = "abort"
# Optimize all dependencies even in debug builds:
[profile.dev.package."*"]
opt-level = 2
Вы можете заметить, что у меня есть дополнительные разделы, которых может не быть у вас. Обязательно, если вы не знаете на что они влияют и нужны ли они вам, прочитайте об их значении в The Manifest Format — The Cargo Book.
1.3 Напишем код нашего первого интерфейса
Коммит содержащий код раздела.
Пользуясь примером из документации eframe, немного изменив его, попробуем написать код с нашим первым интерфейсом.
Для этого давайте отредактируем файл ./src/main.rs
следующим образом:
// ./src/main.rs
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release
use eframe::egui;
fn main() -> eframe::Result<()> {
eframe::run_native(
"calculator-wasm-rust-pwa",
eframe::NativeOptions::default(),
Box::new(|cc| Box::new(CalcApp::new(cc))),
)
}
struct CalcApp {}
impl CalcApp {
fn new(_cc: &eframe::CreationContext<'_>) -> Self {
CalcApp {}
}
}
impl eframe::App for CalcApp {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
egui::CentralPanel::default().show(ctx, |ui| {
ui.label(r#"
Это вымышленный калькулятов.
Чтобы воспользоваться калькулятором вам необходимо воспользоваться воображением. Представте себе любой интерфейс, наберите в нем математическое выражение и нажмите '='.
Увидели результат, Да? - Поздравляю, ваш калькулятор работает хорошо.
"#);
});
}
}
Прежде чем двинуться дальше, давайте зафиксируемся на том, что здесь написано.
Стартовой точкой нашего приложения является функция main()
.
// ./src/main.rs
// ...
fn main() -> eframe::Result<()> {
eframe::run_native(
"calculator-wasm-rust-pwa",
eframe::NativeOptions::default(),
Box::new(|cc| Box::new(CalcApp::new(cc))),
)
}
// ...
Используя функцию eframe::run_native
мы будем запускать собственное (настольное) приложение. Функция принимает следующие аргументы:
app_name
(Первая строка) — это имя нашего приложения. Оно будет использоваться для строки заголовка нативного окна.native_options
(Вторая строка) — это структура NativeOptions, которая отвечает за управление поведением собственного окна. Далее мы еще прикоснемся к настройки нашего приложения, но пока мы будем использовать конфигурацию по умолчанию, которую нем реализуетeframe::NativeOptions::default()
.app_creator
(Третья стока) — здесь вы создаете свое приложение.
Мы уже указали в качестве app_creator написанную нами структуру CalcApp::new(cc)
, которая реализует типаж (трейт/черта) eframe::App
.
// ./src/main.rs
// ...
struct CalcApp {}
impl CalcApp {
fn new(_cc: &eframe::CreationContext<'_>) -> Self {
CalcApp {}
}
}
impl eframe::App for CalcApp {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
egui::CentralPanel::default().show(ctx, |ui| {
ui.label(r#"
Это вымышленный калькулятор.
Чтобы воспользоваться калькулятором вам необходимо воспользоваться воображением. Представите себе любой интерфейс, наберите в нем математическое выражение и нажмите '='.
Увидели результат, да? - Поздравляю, ваш калькулятор работает хорошо.
"#);
});
}
}
Мы создали пустую структуру CalcApp
и имплементировали к ней функцию new()
, которая будет вызываться один раз, перед первым кадром. Далее мы будем инициировать параметры нашего приложения.
Самая важная функция, с которой мы начинаем работу над нашим интерфейсом, является функция update
из типажа (трейта/черты) eframe::App
. Данная функция будет вызываться каждый раз, когда пользовательский интерфейс нуждается в перерисовке, что может происходить много раз в секунду. Внутри данной функции мы будем размещать наши виджеты в SidePanel
, TopPanel
, CentralPanel
, Window
или Area
.
Пробуем запустить наше приложение выполнив команду:
$ cargo run
Готово. Калькулятор готов. Расходимся.
Теперь у нас собирается и запускается наше приложение и мы можем получить исполняемый файл если выполним:
$ cargo build --release
Однако, получив исполняемый файл, мы достигли только половину цели. Мы хотим иметь возможность собирать наше приложение как набор файлов готовых для развертывания в интернете. Нужно заняться этим вопросом.
1.4 Изменим код, добавив возможность компилироваться в WASM
Коммит содержащий код раздела.
1.4.1 Устанавливаем Trunk
Собирать наше приложение в WASM файл, мы будем при помощи Trunk.
Устанавливаем Trunk с помощью
$ cargo install --locked trunk
Однако, если вы попробуете собрать приложение и запустив его с использованием dev сервера:
$ trunk serve
Вы столкнетесь с ошибками, которые будут говорить нам, что наш проект еще не готов. Давайте исправим это.
1.4.2 Создаем файл index.html и другие
Для начала нам необходимо создать файл index.html
. Давайте воспользуемся файлом, который подготовлен в этом примере и изменим его под себя.
Скопируем index.html
и папку assets\
, так, что бы у нас получилась следующая структура проекта :
$ cd calculator-wasm-rust-pwa/
$ tree .
H:.
│ .gitignore
│ Cargo.toml
│ index.html
├───assets\
│ favicon.ico
│ icon-1024.png
│ icon-256.png
│ icon_ios_touch_192.png
│ manifest.json
│ maskable_icon_x512.png
│ sw.js
└───src\
main.rs
Комментировать особенности файлов index.html
и sw.js
я не буду, но прошу, прежде чем двинуться дальше, пожалуйста ознакомьтесь с документацией Trunk и с использованием Service Workers.
1.4.3 Генерируем favicon.ico
Тут все просто. Бежим в stable diffusion web и генерируем картинку из которой мы сделаем иконку — favicon.ico.
1.4.4 Меняем код в main.rs, чтобы наладить компиляцию в wasm
Если мы попробуем выполнить сборку нашего приложения в WASM используя команду
$ trunk build
то мы столкнёмся с ошибкой компиляции:
error[E0433]: failed to resolve: could not find `NativeOptions` in `eframe`
--> src\main.rs:8:17
|
8 | eframe::NativeOptions::default(),
| ^^^^^^^^^^^^^ could not find `NativeOptions` in `eframe`
error[E0425]: cannot find function `run_native` in crate `eframe`
--> src\main.rs:6:13
|
6 | eframe::run_native(
| ^^^^^^^^^^ not found in `eframe`
Some errors have detailed explanations: E0425, E0433.
Мы видим как компилятор сообщает нам, что в eframe
отсутствует run_native()
. Но такого не может быть, мы уже пробовали собрать этот код, когда использовали команду cargo run
и в тот момент все работало.
Все дело в этой строчке:
#[cfg(not(target_arch = "wasm32"))]
расположенной перед аннотацией функции run_native()
. Более детально со смыслом данной строчки, как и с особенностями компиляции в WASM с использованием в rust вы можете ознакомиться в документации Rust