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, который печатает текст вместо того, чтобы рисовать окно, — их можно запустить прямо в браузере. Ищи кнопки «Попробуй сам».
Честно о состоянии GUI в Rust
Заголовок раздела «Честно о состоянии 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 зависят от системных библиотек. См. раздел Что нужно на разных платформах ниже.
Immediate mode и retained mode
Заголовок раздела «Immediate mode и retained mode»Инструменты расходятся в одном большом вопросе: кто помнит виджеты?
В 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 решает это за тебя: любой щелчок или нажатие клавиши заставляет его сразу нарисовать следующий кадр, так что старое число ты не увидишь.
Первое приложение на egui: счётчик
Заголовок раздела «Первое приложение на egui: счётчик»eframe — официальный «фреймворк приложений» для egui: он открывает окно и запускает в нём egui. Создай проект и добавь его:
cargo new countercd 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: Rust за веб-страницей
Заголовок раздела «Команда Tauri: Rust за веб-страницей»Приложение на Tauri состоит из двух частей. Фронтенд — обычная веб-страница (HTML, CSS и JavaScript или фреймворк вроде React или Svelte), которая работает в системном webview. Бэкенд — твой код на Rust. Фронтенд вызывает функции Rust, которые называются командами (commands).
Проще всего начать с официального генератора проектов. Он спросит имя и шаблон фронтенда (шаблон «vanilla», без фреймворка, — хороший первый выбор):
cargo install create-tauri-app --lockedcargo 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: параметр Rustinvoke_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
Заголовок раздела «Iced и архитектура Elm»Iced следует архитектуре Elm — она названа в честь языка Elm. У каждого приложения четыре части:
- Состояние (state): структура со всем, что помнит приложение.
- Сообщения (messages): перечисление всего, что может произойти («нажата кнопка +»).
- update: берёт состояние и одно сообщение и меняет состояние. Это единственное место, где происходят изменения.
- 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.
Попробуй архитектуру Elm в браузере
Заголовок раздела «Попробуй архитектуру Elm в браузере»Чтобы потренироваться, 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самого крейта для своей версии.
Практика
Заголовок раздела «Практика»1. Счётчик, который не уходит ниже нуля
Заголовок раздела «1. Счётчик, который не уходит ниже нуля»Напиши счётчик в стиле 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 -> Счёт: 1Increment -> Счёт: 2Decrement -> Счёт: 1Decrement -> Счёт: 0Decrement -> Счёт: 0Increment -> Счёт: 1Reset -> Счёт: 0Правило «никогда ниже нуля» живёт ровно в одном месте — в update. Имя сообщения мы форматируем до вызова update, потому что update принимает сообщение по значению и перемещает его.
2. Добавь кнопку «Отменить»
Заголовок раздела «2. Добавь кнопку «Отменить»»Добавь в счётчик сообщение 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.