Перейти к содержимому

GUI-приложения на Rust: Tauri, egui, Slint и Iced

Рано или поздно захочется настоящего окна с кнопками, а не только текста в терминале. Можно ли сделать такое на Rust? Да. GUI-приложения на Rust быстрые, маленькие и надёжные, и хороших инструментов для них сегодня несколько. Но мир GUI в Rust моложе, чем, скажем, C# с WPF или JavaScript с Electron, поэтому полезно осмотреться, прежде чем выбирать.

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

Где запускать код? GUI нужно настоящее окно, поэтому программы на egui, Tauri, Slint и Iced рассчитаны на твой компьютер и cargo run. Программы на egui и Iced с этой страницы скомпилированы через cargo check с текущими версиями. Два раздела моделируют GUI на чистом Rust, который печатает текст вместо того, чтобы рисовать окно, — их можно запустить прямо в браузере. Ищи кнопки «Попробуй сам».

Сайт сообщества Are we GUI yet? (на английском) подводит итог одной строкой: «Корни пока неглубокие, но семена посажены». Это по-прежнему справедливо:

  • Отлично: скорость, маленькие приложения и надёжность Rust. egui, Iced и Slint рисуют всё сами, поэтому один и тот же код работает в Windows, macOS и Linux. Tauri позволяет применить навыки веб-разработки, а приложение получается намного меньше, чем на Electron, потому что использует webview, который уже есть в системе.
  • Ещё растёт: нет единого «стандартного» инструмента вроде Qt в C++ или WPF в C#. Несколько популярных крейтов всё ещё на версии 0.x, так что обновления могут ломать твой код. Доступность, текст справа налево и «родной» внешний вид сильно зависят от инструмента.
  • Хорошая новость: идеальный инструмент искать не нужно. Для первого приложения хорошо подойдёт любой из таблицы ниже.

Более подробное сравнение — в обзоре GUI-библиотек Rust за 2025 год (на английском), где многие инструменты проверены по одному.

Настоящие приложения, на которые можно посмотреть уже сегодня:

  • Rerun Viewer — инструмент для просмотра данных от роботов и систем компьютерного зрения — написан на egui. (Rerun ещё и спонсирует разработку egui.)
  • COSMIC, рабочее окружение для Linux от System76, написано на Iced. Как и IRC-клиент Halloy.
  • Настольные утилиты с веб-интерфейсом: приложения для заметок, дашборды и небольшие бизнес-инструменты часто делают на Tauri.
  • Экраны на устройствах: Slint работает и на микроконтроллерах, поэтому хорошо подходит для сенсорных экранов станков и бытовой техники.

Версии — последние выпуски на октябрь 2026 года. Документация у всех на английском.

Крейт Версия Как пишется интерфейс Где работает Когда выбирать
Tauri 2.12 (3.0 в альфе) HTML, CSS и JavaScript для экрана; Rust для логики Windows, macOS, Linux, Android, iOS Ты знаешь веб-разработку или нужен богатый, стильный интерфейс
egui + eframe 0.37 Обычный Rust, immediate mode Windows, macOS, Linux, веб (Wasm) Утилиты, отладочные панели, редакторы и самый быстрый старт на чистом Rust
Slint 1.18 Собственная разметка .slint плюс Rust для логики Десктоп, встраиваемые системы, мобильные, веб Отполированное приложение или экран на встраиваемом устройстве
Iced 0.14 Обычный Rust, архитектура Elm (состояние, сообщения, update, view) Windows, macOS, Linux, веб Тебе нравится чистый предсказуемый код, где все изменения в одном месте
gtk-rs (gtk4) 0.11 Привязки Rust к GTK 4, retained mode Прежде всего Linux; Windows и macOS — с дополнительной настройкой Приложения с «родным» видом для рабочего стола GNOME в Linux
Dioxus 0.7 Компоненты в духе React с макросом rsx! Веб, десктоп (webview), мобильные Ты знаешь React и хочешь один код для веба и десктопа

Несколько честных замечаний о лицензиях и зрелости:

  • Slint бесплатен под GPLv3 или бесплатен для закрытых настольных, мобильных и веб-приложений по своей лицензии без отчислений (нужно указывать, что ты используешь Slint). Для закрытых встраиваемых продуктов нужна платная лицензия. Прочитай slint.dev/pricing (на английском), прежде чем выпускать продукт.
  • egui, Iced и Dioxus всё ещё на версии 0.x. Жди небольших несовместимых изменений в каждом выпуске и проверяй, что руководство подходит к твоей версии. Например, старые руководства по egui используют fn update(&mut self, ctx: &egui::Context, ...), а в eframe 0.37 это fn ui(&mut self, ui: &mut egui::Ui, ...).
  • Tauri, Dioxus для десктопа и gtk-rs зависят от системных библиотек. См. раздел Что нужно на разных платформах ниже.

Инструменты расходятся в одном большом вопросе: кто помнит виджеты?

В retained mode («сохраняемый» режим: gtk-rs, Slint, веб-страница Tauri и большинство инструментов в других языках) ты создаёшь кнопку один раз. Инструмент хранит её в дереве виджетов. Ты подключаешь колбэк (callback), и когда пользователь нажимает кнопку, инструмент его вызывает. Чтобы потом поменять текст кнопки, нужен дескриптор (handle) этой кнопки.

В immediate mode («немедленный» режим: egui) ничего не хранится. Твоя функция интерфейса запускается заново на каждом кадре и просто описывает, что показать прямо сейчас. Кнопка рисуется и проверяется в одной строке: if ui.button("+").clicked() { ... }. Ни колбэков, ни дескрипторов. Состояние приложения живёт в твоей собственной структуре, и интерфейс всегда рисуется из неё, так что экран никогда не расходится с данными.

Цена: инструмент раскладывает весь интерфейс на каждом кадре, так что огромному интерфейсу нужно больше времени процессора, а некоторые раскладки (например, отцентрировать окно, размер которого ещё неизвестен) сделать сложнее. egui рисует новый кадр, только когда что-то происходит, например двигается мышь, так что приложение в покое почти не тратит процессор.

Вот крошечный игрушечный инструмент в стиле immediate mode на чистом Rust. Каждый шаг цикла — один кадр. Массив clicks играет роль мыши:

// Игрушечный «Ui», который запоминает, что нарисовано в этом кадре.
struct Ui {
clicked: Option<&'static str>, // кнопка, по которой щёлкнули в этом кадре
drawn: Vec<String>,
}
impl Ui {
fn label(&mut self, text: &str) {
self.drawn.push(text.to_string());
}
// Рисует кнопку И сразу говорит, нажали ли её.
fn button(&mut self, text: &'static str) -> bool {
self.drawn.push(format!("[{text}]"));
self.clicked == Some(text)
}
}
// Твой код интерфейса: вызывается заново на каждом кадре.
fn draw(count: &mut i32, ui: &mut Ui) {
ui.label(&format!("Счёт: {count}"));
if ui.button("+") {
*count += 1;
}
if ui.button("Сброс") {
*count = 0;
}
}
fn main() {
let mut count = 0; // состояние приложения живёт вне кода интерфейса
let clicks = [None, Some("+"), Some("+"), None, Some("Сброс"), None];
for (frame, click) in clicks.into_iter().enumerate() {
let mut ui = Ui { clicked: click, drawn: Vec::new() };
draw(&mut count, &mut ui);
println!("кадр {frame}: {}", ui.drawn.join(" "));
}
}

Вывод:

кадр 0: Счёт: 0 [+] [Сброс]
кадр 1: Счёт: 0 [+] [Сброс]
кадр 2: Счёт: 1 [+] [Сброс]
кадр 3: Счёт: 2 [+] [Сброс]
кадр 4: Счёт: 2 [+] [Сброс]
кадр 5: Счёт: 0 [+] [Сброс]
▶ Попробуй сам

Посмотри на кадр 1: ты нажимаешь «+», а надпись всё ещё показывает 0. Надпись нарисовалась раньше, чем кнопка заметила нажатие. Новое значение появляется на кадр позже. Настоящий egui решает это за тебя: любой щелчок или нажатие клавиши заставляет его сразу нарисовать следующий кадр, так что старое число ты не увидишь.

eframe — официальный «фреймворк приложений» для egui: он открывает окно и запускает в нём egui. Создай проект и добавь его:

Terminal window
cargo new counter
cd counter
[dependencies]
eframe = "0.37"

Замени src/main.rs на это:

use eframe::egui;
fn main() -> eframe::Result {
let options = eframe::NativeOptions {
viewport: egui::ViewportBuilder::default().with_inner_size([300.0, 160.0]),
..Default::default()
};
eframe::run_native(
"Counter",
options,
Box::new(|_cc| Ok(Box::<CounterApp>::default())),
)
}
#[derive(Default)]
struct CounterApp {
count: i32,
}
impl eframe::App for CounterApp {
fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
egui::CentralPanel::default().show(ui, |ui| {
ui.heading(format!("Count: {}", self.count));
ui.horizontal(|ui| {
if ui.button("-").clicked() {
self.count -= 1;
}
if ui.button("+").clicked() {
self.count += 1;
}
if ui.button("Reset").clicked() {
self.count = 0;
}
});
});
}
}

Запусти через cargo run. Первая сборка скачивает и компилирует несколько сотен крейтов, так что подожди несколько минут. Следующие сборки быстрые. Откроется маленькое окно с надписью «Count: 0» и тремя кнопками. (Код мы оставили таким, каким проверяли его; надписи на кнопках, конечно, можно сделать русскими.)

Как это устроено:

  • CounterApp — это твоё состояние. #[derive(Default)] начинает count с 0.
  • run_native открывает окно. Замыкание один раз создаёт твоё приложение внутри Box (см. Умные указатели).
  • eframe вызывает ui на каждом кадре. CentralPanel заполняет окно, а ui.horizontal ставит три кнопки в ряд.
  • Каждое if ui.button(..).clicked() — это шаблон immediate mode из раздела выше: нарисовать кнопку, проверить её, изменить состояние.

Поэтому же состояние нужно хранить в self, а не в локальной переменной внутри ui: let mut count = 0; там начиналась бы с 0 заново на каждом кадре.

Приложение на Tauri состоит из двух частей. Фронтенд — обычная веб-страница (HTML, CSS и JavaScript или фреймворк вроде React или Svelte), которая работает в системном webview. Бэкенд — твой код на Rust. Фронтенд вызывает функции Rust, которые называются командами (commands).

Проще всего начать с официального генератора проектов. Он спросит имя и шаблон фронтенда (шаблон «vanilla», без фреймворка, — хороший первый выбор):

Terminal window
cargo install create-tauri-app --locked
cargo create-tauri-app

Если ты уже пользуешься Node.js, есть и npm create tauri-app@latest. Потом перейди в новую папку и запусти приложение командами npm install и npm run tauri dev (для шаблонов с npm) или cargo install tauri-cli --version "^2.0.0" --locked и cargo tauri dev (для шаблонов только на Rust). Инструмент сам напечатает нужные команды для твоего выбора.

Сгенерированный src-tauri/src/lib.rs содержит твою первую команду:

// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
#[tauri::command]
fn greet(name: &str) -> String {
format!("Hello, {}! You've been greeted from Rust!", name)
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_opener::init())
.invoke_handler(tauri::generate_handler![greet])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}

А фронтенд (src/main.js в шаблоне vanilla, здесь в сокращении) вызывает её так:

const { invoke } = window.__TAURI__.core;
greetMsgEl.textContent = await invoke("greet", { name: greetInputEl.value });

Что здесь происходит:

  • #[tauri::command] — это атрибутный макрос (см. Процедурные макросы). Он пишет связующий код, который превращает вызов из JavaScript в обычный вызов Rust.
  • Аргументы приходят в виде JSON, поэтому тип каждого аргумента должен реализовывать Deserialize из serde, а возвращаемый тип — Serialize (см. JSON и Serde). В JavaScript пиши имена аргументов в camelCase: параметр Rust invoke_message превращается в { invokeMessage: "..." }.
  • generate_handler![greet] регистрирует команду. Если забыть добавить сюда новую команду, вызов из JavaScript не сработает.
  • Возвращай Result, чтобы сообщать об ошибках: Ok выполняет promise в JavaScript, а Err отклоняет его, так что ошибку можно обработать через .catch(). Например, fn load(path: String) -> Result<String, String>.
  • main.rs только вызывает run(). Код живёт в lib.rs, чтобы то же приложение можно было собрать и для Android и iOS.

Slint: интерфейс на собственном языке разметки

Заголовок раздела «Slint: интерфейс на собственном языке разметки»

Slint описывает экран на маленьком декларативном языке, чем-то похожем на HTML и CSS вместе. Логикой занимается твой код на Rust. Вот официальный hello world. Добавь slint = "1.18" в раздел [dependencies] и положи это в src/main.rs:

slint::slint!{
export component HelloWorld {
Text {
text: "hello world";
color: green;
}
}
}
fn main() {
HelloWorld::new().unwrap().run().unwrap();
}

Макрос slint! компилирует разметку во время сборки и создаёт для тебя тип Rust HelloWorld. В настоящем приложении разметка лежит в отдельном файле .slint. Этот счётчик взят из официального шаблона Slint для Rust (на английском). Файл ui/app-window.slint:

import { Button, VerticalBox } from "std-widgets.slint";
export component AppWindow inherits Window {
in-out property <int> counter: 42;
callback request-increase-value();
VerticalBox {
Text {
text: "Counter: \{root.counter}";
}
Button {
text: "Increase value";
clicked => {
root.request-increase-value();
}
}
}
}

А сторона Rust в src/main.rs:

slint::include_modules!();
fn main() -> Result<(), Box<dyn std::error::Error>> {
let ui = AppWindow::new()?;
let ui_handle = ui.as_weak();
ui.on_request_increase_value(move || {
let ui = ui_handle.unwrap();
ui.set_counter(ui.get_counter() + 1);
});
ui.run()?;
Ok(())
}

Slint работает в retained mode: свойство counter живёт в интерфейсе, а кнопка вызывает колбэк. Slint генерирует по разметке get_counter, set_counter и on_request_increase_value (дефисы превращаются в подчёркивания). Замыкание держит слабый дескриптор (as_weak()), чтобы окно и его собственный колбэк не удерживали друг друга вечно (см. Weak и Cow). Для файла .slint нужны ещё slint-build = "1.18" в разделе [build-dependencies] и build.rs из одной строки: slint_build::compile("ui/app-window.slint").expect("Slint build failed");.

Iced следует архитектуре Elm — она названа в честь языка Elm. У каждого приложения четыре части:

  1. Состояние (state): структура со всем, что помнит приложение.
  2. Сообщения (messages): перечисление всего, что может произойти («нажата кнопка +»).
  3. update: берёт состояние и одно сообщение и меняет состояние. Это единственное место, где происходят изменения.
  4. view: строит виджеты по состоянию. Сами виджеты ничего не меняют — они только отправляют сообщения.

Вот официальный счётчик на Iced (добавь iced = "0.14" в [dependencies]):

use iced::widget::{button, column, text, Column};
use iced::Center;
fn main() -> iced::Result {
iced::run(Counter::update, Counter::view)
}
#[derive(Default)]
struct Counter {
value: i64,
}
#[derive(Debug, Clone, Copy)]
enum Message {
Increment,
Decrement,
}
impl Counter {
fn update(&mut self, message: Message) {
match message {
Message::Increment => self.value += 1,
Message::Decrement => self.value -= 1,
}
}
fn view(&self) -> Column<'_, Message> {
column![
button("Increment").on_press(Message::Increment),
text(self.value).size(50),
button("Decrement").on_press(Message::Decrement),
]
.padding(20)
.align_x(Center)
}
}

on_press(Message::Increment) не выполняет никакого кода. Он только говорит: «при нажатии отправь это сообщение». Потом Iced вызывает с ним update и снова вызывает view, чтобы показать новое состояние. Поскольку каждое изменение проходит через один match, легко увидеть (и протестировать) всё, что умеет приложение. Это хорошее применение перечислений и match.

Чтобы потренироваться, Iced не нужен. У этой модели списка дел те же четыре части. view возвращает текст вместо виджетов, а список сообщений заменяет ввод с клавиатуры и щелчки пользователя:

// 1. Состояние: всё, что приложению нужно помнить.
#[derive(Default)]
struct App {
draft: String,
todos: Vec<Todo>,
}
struct Todo {
title: String,
done: bool,
}
// 2. Сообщения: всё, что может произойти.
#[derive(Debug)]
enum Message {
DraftChanged(String),
AddPressed,
Toggle(usize),
ClearDone,
}
impl App {
// 3. update: ЕДИНСТВЕННОЕ место, где меняется состояние.
fn update(&mut self, message: Message) {
match message {
Message::DraftChanged(text) => self.draft = text,
Message::AddPressed => {
let title = self.draft.trim();
if !title.is_empty() {
self.todos.push(Todo { title: title.to_string(), done: false });
}
self.draft.clear();
}
Message::Toggle(index) => {
if let Some(todo) = self.todos.get_mut(index) {
todo.done = !todo.done;
}
}
Message::ClearDone => self.todos.retain(|todo| !todo.done),
}
}
// 4. view: превращает состояние в то, что видит пользователь (здесь — в текст).
fn view(&self) -> String {
let mut screen = format!("Новая: [{}]\n", self.draft);
for (index, todo) in self.todos.iter().enumerate() {
let mark = if todo.done { "x" } else { " " };
screen.push_str(&format!("{index}. [{mark}] {}\n", todo.title));
}
screen
}
}
fn main() {
let mut app = App::default();
// Представь, что это пришло от нажатий клавиш и кнопок.
let messages = vec![
Message::DraftChanged("Купить молоко".to_string()),
Message::AddPressed,
Message::DraftChanged("Позвонить в банк".to_string()),
Message::AddPressed,
Message::DraftChanged("Полить цветы".to_string()),
Message::Toggle(0),
];
for message in messages {
println!("> {message:?}");
app.update(message);
}
print!("{}", app.view());
app.update(Message::ClearDone);
println!("> после ClearDone:");
print!("{}", app.view());
}

Вывод:

> DraftChanged("Купить молоко")
> AddPressed
> DraftChanged("Позвонить в банк")
> AddPressed
> DraftChanged("Полить цветы")
> Toggle(0)
Новая: [Полить цветы]
0. [x] Купить молоко
1. [ ] Позвонить в банк
> после ClearDone:
Новая: [Полить цветы]
0. [ ] Позвонить в банк
▶ Попробуй сам

Заметь: view никогда ничего не меняет, а update никогда ничего не печатает. В этом разделении весь фокус. Чтобы превратить это в настоящее приложение на Iced, достаточно переписать view, чтобы он возвращал виджеты (text_input, который отправляет DraftChanged, и button, который отправляет AddPressed). Состояние, сообщения и update остаются в точности прежними.

GUI-крейты общаются с операционной системой, поэтому нужно чуть больше, чем просто Rust. Сначала установи основное по уроку Инструменты Rust. Затем:

ОС egui/eframe, Iced, Slint Tauri дополнительно нужно
Windows Ничего сверх инструментов MSVC C++ WebView2. Он уже входит в Windows 10 (версии 1803 и новее) и Windows 11
macOS Инструменты командной строки Xcode (xcode-select --install) Для настольных приложений больше ничего; для iOS — полный Xcode
Linux (Debian/Ubuntu) README eframe перечисляет libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev libxkbcommon-dev libssl-dev webkit2gtk: libwebkit2gtk-4.1-dev и ещё несколько пакетов со страницы требований Tauri (на английском)

Если сборка в Linux падает с сообщением от pkg-config о недостающей библиотеке, установи -dev-пакет этой библиотеки и собери снова. Для gtk-rs нужны файлы разработки GTK 4 (libgtk-4-dev в Ubuntu).

  • Блокировать поток интерфейса. Загрузка или долгое вычисление внутри функции ui в egui или update в Iced замораживает всё окно, пока не закончится. Делай медленную работу в другом потоке или в async-задаче (см. Параллельность и Async/Await) и отправляй результат в интерфейс сообщением.
  • Хранить состояние egui в локальных переменных. ui запускается заново на каждом кадре, так что let mut внутри него каждый раз начинается с нуля. Храни состояние в структуре приложения.
  • Команда Tauri, которую так и не зарегистрировали. Каждая команда должна быть в tauri::generate_handler![...]. А ещё в lib.rs не помечай команды как pub: документация Tauri объясняет, что это вызывает ошибку E0255 «defined multiple times» (определено несколько раз).
  • Следовать старому руководству. С крейтами версии 0.x прошлогодний пример может не скомпилироваться. Сверяй версию в руководстве со своим Cargo.toml и заглядывай в папку examples самого крейта для своей версии.

Напиши счётчик в стиле Elm с сообщениями Increment, Decrement и Reset. Значение никогда не должно опускаться ниже 0, даже если пользователь продолжает жать «-». Пропусти список сообщений через update и печатай view() после каждого.

Подсказка: возьми u32 и saturating_sub(1) — он останавливается на 0, а не переполняется (см. Переполнение целых чисел).

Показать решение
#[derive(Default)]
struct Counter {
value: u32,
}
#[derive(Debug)]
enum Message {
Increment,
Decrement,
Reset,
}
impl Counter {
fn update(&mut self, message: Message) {
match message {
Message::Increment => self.value += 1,
Message::Decrement => self.value = self.value.saturating_sub(1),
Message::Reset => self.value = 0,
}
}
fn view(&self) -> String {
format!("Счёт: {}", self.value)
}
}
fn main() {
let mut counter = Counter::default();
let messages = [
Message::Increment,
Message::Increment,
Message::Decrement,
Message::Decrement,
Message::Decrement,
Message::Increment,
Message::Reset,
];
for message in messages {
let name = format!("{message:?}");
counter.update(message);
println!("{name:<9} -> {}", counter.view());
}
}

Вывод:

Increment -> Счёт: 1
Increment -> Счёт: 2
Decrement -> Счёт: 1
Decrement -> Счёт: 0
Decrement -> Счёт: 0
Increment -> Счёт: 1
Reset -> Счёт: 0
▶ Попробуй сам

Правило «никогда ниже нуля» живёт ровно в одном месте — в update. Имя сообщения мы форматируем до вызова update, потому что update принимает сообщение по значению и перемещает его.

Добавь в счётчик сообщение Undo. Каждое изменение запоминает старое значение, а Undo возвращает на шаг назад. Undo без истории ничего не делает. Показывай в view, сколько шагов отмены осталось.

Подсказка: храни в состоянии history: Vec<u32>. Перед каждым изменением делай push старого значения, а при Undo — pop.

Показать решение
#[derive(Default)]
struct Counter {
value: u32,
history: Vec<u32>, // прежние значения, самое новое — последнее
}
#[derive(Debug)]
enum Message {
Increment,
Decrement,
Reset,
Undo,
}
impl Counter {
fn update(&mut self, message: Message) {
let new_value = match message {
Message::Increment => self.value + 1,
Message::Decrement => self.value.saturating_sub(1),
Message::Reset => 0,
Message::Undo => {
if let Some(previous) = self.history.pop() {
self.value = previous;
}
return;
}
};
self.history.push(self.value); // сначала запоминаем старое значение
self.value = new_value;
}
fn view(&self) -> String {
format!("Счёт: {} (шагов отмены: {})", self.value, self.history.len())
}
}
fn main() {
let mut counter = Counter::default();
let messages = [
Message::Increment,
Message::Increment,
Message::Decrement,
Message::Increment,
Message::Reset,
Message::Undo,
Message::Undo,
Message::Undo,
];
for message in messages {
let name = format!("{message:?}");
counter.update(message);
println!("{name:<9} -> {}", counter.view());
}
}

Вывод:

Increment -> Счёт: 1 (шагов отмены: 1)
Increment -> Счёт: 2 (шагов отмены: 2)
Decrement -> Счёт: 1 (шагов отмены: 3)
Increment -> Счёт: 2 (шагов отмены: 4)
Reset -> Счёт: 0 (шагов отмены: 5)
Undo -> Счёт: 2 (шагов отмены: 4)
Undo -> Счёт: 1 (шагов отмены: 3)
Undo -> Счёт: 2 (шагов отмены: 2)
▶ Попробуй сам

Добавить отмену было легко, потому что каждое изменение и так проходит через update. В настоящем приложении на Iced понадобится только ещё одна кнопка, которая отправляет Message::Undo.

  • GUI на Rust сегодня работает хорошо, но единого стандартного инструмента пока нет, и несколько популярных крейтов всё ещё на версии 0.x.
  • Tauri соединяет веб-фронтенд с бэкендом на Rust; egui — это immediate mode на чистом Rust; Slint использует собственную разметку и работает даже на встраиваемых устройствах; Iced использует архитектуру Elm.
  • Retained mode хранит дерево виджетов и вызывает твои колбэки. Immediate mode перерисовывает интерфейс по твоему состоянию на каждом кадре, без колбэков.
  • Архитектура Elm делит приложение на состояние, сообщения, update и view, поэтому за изменениями легко следить и их легко тестировать.
  • Tauri нужен WebView2 в Windows и webkit2gtk в Linux. Основы — в уроке Инструменты Rust.

Проверь себя

1. Почему в egui значение счётчика должно жить в структуре приложения, а не в локальной переменной внутри ui?

2. Что делает #[tauri::command] в приложении на Tauri?

3. Где меняется состояние приложения в архитектуре Elm, которую использует Iced?

4. Что нужно Tauri в Linux, чего не нужно egui?

Результат: 0 / 4

  • Are we GUI yet? (на английском): обзор GUI-крейтов Rust от сообщества, с метками вроде «immediate mode» и «reactive», и свежие новости.
  • Understanding immediate mode — документация egui (на английском): как работает egui, его плюсы и минусы — от автора.
  • Calling Rust from the Frontend — Tauri (на английском): всё о командах — аргументы, возвращаемые значения, ошибки, асинхронные команды и состояние.
  • Документация Slint для Rust (на английском): API для Rust, макрос slint! и как использовать файлы .slint со slint-build.
  • The Iced book: Architecture (на английском): понятное объяснение состояния, сообщений, update и view.