Пишем простой калькулятор используя фреймворк eframe (egui)

Всем здравствуйте. Ниже будет приведен пример написания PWA приложения готового для использования как в браузере, так и на компьютере с ОС Windows. Использовать будем язык программирования Rust и фреймворк eframe (egui). Готовое приложение будет доступно как исполняемый файл для ОС Windows, и как файл Webassembly. В процессе работы мы будем использовать GitHub Action для отслеживания корректности написания нашего кода и сборки исполняемых файлов программы, а также для развертывания (версию программы с использованием Webassembly) как страницы в интернете (GitHub Pages).

72dd5d5be08f0220faf17fb7cf54d7be.pngМотивация автора

В свободное от работы время, иногда, я занимаюсь тем, что смотрю в сторону разных языков программирования и их экосистем. Последнее время, в мой фокус внимания попал язык программирования Rust. И продолжая знакомиться с экосистемой RUST, я подошел к изучению инструментов для построения пользовательских графических интерфейсов. Нужно сказать, что я был очень сильно удивлен. Удивлен как в положительном ключе, так и в отрицательном.

Что порадовало:

  • доступно очень много библиотек и фреймворков для создания GUI, как полностью написанных на rust, так и оберток вокруг других инструментов, написанных на других языках;

  • многие из них доступны для кроссплатформенной разработки, включая возможность компилировать свое приложение в WASM и опубликовать его как веб-страницу.

Что расстроило:

  • опять же, доступно очень много библиотек и фреймворков для создания GUI и почти все они говорят, что не готовы для использования и носят экспериментальных характер (пример является проект Druid, основная команда которого прекратила работу над ним и сосредоточилась над новым проектом Xilem). Да, последний проект тоже еще не стабилен.

Самое ключевое, что привлекло мое внимание — это возможность компилировать приложение в WASM и опубликовать его как веб-страницу. «Тренд завтрашнего или вчерашнего дня» — подумал я, так или иначе нужно посмотреть в эту сторону (немного лукавлю, так как год или два назад я трогал rust + Yew, чтобы попробовать сделать игру морской бой на Webassembly).

Несмотря на то, что инструментов для разработки пользовательских инструментов очень много, мой выбор остановился на EGUI. Нет не потому, что я провел исследование, составил табличку, расставил плюсы и минусы, а потому, что я купился на этот пример.

План работ

  1. Определиться с минимальным набором требований (что мы хотим).

  2. Подготовить и разобрать шаблон для приложения.

  3. Подготовить github actions, чтобы автоматически собирать и публиковать наш результат.

  4. Разработать простой интерфейс.

  5. Разработать простой решатель для нашей задачи.

Требования к результату

По окончанию всех работ хочется получить рабочий интерфейс калькулятора, который доступен в виде 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

Готово. Калькулятор готов. Расходимся.

bd5bb2c13f6edb4092a75f00027d58e7.png

Теперь у нас собирается и запускается наше приложение и мы можем получить исполняемый файл если выполним:

$ 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.

ea556d1e6890c21a69a9fb50473f1c6c.png

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

© Habrahabr.ru