Графични (desktop) интерфейси
04 януари 2022
Административни неща
- Домашно 3 тече с пълна сила: https://fmi.rust-lang.bg/tasks/3
- Мислете за проекти (https://fmi.rust-lang.bg/guides/projects)
GTK
- Главния сайт: https://gtk-rs.org/
- Добър, макар и недовършен guide за GTK4: https://gtk-rs.org/gtk4-rs/stable/latest/book/
- API документация за GTK4: https://docs.rs/gtk4/latest/gtk4/
- API документация за GTK3 (по-стабилна засега): https://docs.rs/gtk/latest/gtk/
Как да направим ООП-style наследяване?
Deref
: Ограничен, само за един тип
Как да направим ООП-style наследяване?
Deref
: Ограничен, само за един тип- Делегация, в някоя бъдеща версия на Rust (RFC)
Как да направим ООП-style наследяване?
Deref
: Ограничен, само за един тип- Делегация, в някоя бъдеща версия на Rust (RFC)
- Trait-ове
Наследяване в GTK-rs
Ext-traits
Типа gtk::MessageDialog
има само и единствено една асоциирана функция new
. (Други типове могат да имат и други асоциирани методи, но предимно само за конструиране).
Оттам нататък, всички собствени методи на този "клас" се намират в trait-а gtk::prelude::MessageDialogExt
.
Всички наследени методи се намират в trait-овете: DialogExt
, GtkWindowExt
, BinExt
, ContainerExt
, WidgetExt
, glib::object::ObjectExt
, BuildableExt
. Това са всички типове, за които имаме IsA
имплементация за MessageDialog
.
Това работи, когато повечето код е автоматично-генерирани binding-и, но би било доста тегаво да се поддържа ръчно.
ООП-style наследяване
IsA<T>
(Вижте и https://gtk-rs.org/gtk4-rs/stable/latest/book/gobject_subclassing.html)
Примерно, имаме
MessageDialog::new<T: IsA<Window>>(parent: Option<&T>, /* ... */)
Това ни позволява да cast-ваме неща напред-назад:
let button = gtk::Button::new();
let widget = button.upcast::<gtk::Widget>();
assert!(widget.downcast::<gtk::Button>().is_ok());
Забележете, че upcast
не връща резултат, а връща директно структура от правилния тип. Това се проверява compile-time, така че upcast
няма да се компилира, ако cast-а е несъвместим.
Downcast, от друга страна, няма как да се провери at compile-time, затова връща Result
.
Native rust-ки аналог (kind of): Any
Инсталация на GTK3
Външните библиотеки вероятно ще са най-досадната част, особено под Windows: https://www.gtk.org/docs/installations/
[dependencies]
gtk = { version = "0.14", features = ["v3_20"] }
В main файла:
// За да може всички trait-ове да се include-нат,
// иначе ще трябва да се изброяват *доста*:
use gtk::prelude::*;
use gio::prelude::*;
Инсталация на GTK4
Горе-долу същата, но книгата им изтъква и разни неща като flatpak: https://gtk-rs.org/gtk4-rs/stable/latest/book/installation.html
[dependencies]
gtk = { version = "0.3.1", package = "gtk4", features = ["v4_4"]}
# или:
gtk4 = { version = "0.3.1", features = ["v4_4"]}
В main файла:
use gtk::prelude::*;
use gio::prelude::*;
Версии
Ако намерите tutorial online, който не се компилира, добра идея е да пробвате да пуснете cargo update
-- това ще опита да инсталира по-нови версии на пакетите, които продължават да са съвместими с изискванията.
Примерно, проекта може да е фиксирал libc версия "0.2.33", но пакетите просто да търсят версия "0.2.x". Един cargo update
може да вдигне до версия "0.2.82", която е API-compatible, но просто оправя някакви вътрешни проблеми.
Размери на пакетите
Досадно големи. Моя "quickmd" пакет има 3.7GB "target" директория. Проекти, които сте пробвали да компилирате веднъж и сте ги изоставили, може да ги зачистите с cargo clean
.
Glade
При твърде сложен дизайн на интерфейса, може да си заслужава да минем на Glade: https://glade.gnome.org/. Може да заредите UI-а от gtk::Builder
и достъпвате компоненти с .object("<name>")
.
Само че builder-а генерира XML за GTK3, нищо че в книгата има пример с GTK4… 😅 https://gtk-rs.org/gtk4-rs/stable/latest/book/interface_builder.html
Отвъд това, "опаковането" на gui компоненти в наши си типове може да се окаже по-сложно… Но пък glade може и да ви даде идея какво имате като типове widget-и и какво може да използвате, дори да минете на ръка.
Комуникиране между нишки
В GTK, widget-ите трябва да им се викат методи в главния thread. Това означава, че ако искате да предавате ownership напред-назад, вероятно е добре да ги опаковате в клонируеми smart pointer-и, но дори тогава може да не сработят нещата.
Проблема е добре описан в този blog post: https://coaxion.net/blog/2019/02/mpsc-channel-api-for-painless-usage-of-threads-with-gtk-in-rust/
Решението на статията е доста добро -- използвайте канали! В quickmd, има две нишки, които си комуникират със съобщения с канали:
- UI thread
- Watcher loop
Когато watcher-а, докато си цикли безкрайно, намери промяна във файл, изпраща съобщение по канал, и това съобщение стига до UI нишката и предизвиква update.
В някои отношения, това усложнява нещата. В други, ги опростява значително. Няма нужда да си мислите как ще споделите някакво парче данни -- дръжте му ownership-а на едно-единствено място и просто изпращайте съобщения по канал. Това улеснява много и тестването на неща в изолация.
Дебъгване
GTK_DEBUG=help cargo run
за помощ,GTK_DEBUG=interactive cargo run
за да видите дървото от widget-и live и да го ръчкате
gtk::Application vs gtk::main
"Стария" стил на писане на GTK приложения (GTK 2.0):
gtk::Application vs gtk::main
"Стария" стил на писане на GTK приложения (GTK 2.0):
gtk::init()
gtk::Application vs gtk::main
"Стария" стил на писане на GTK приложения (GTK 2.0):
gtk::init()
- Правим каквото правим, създаваме си дървото от GUI елементи
gtk::Application vs gtk::main
"Стария" стил на писане на GTK приложения (GTK 2.0):
gtk::init()
- Правим каквото правим, създаваме си дървото от GUI елементи
- Викаме на най-главния прозорец
window.show_all()
gtk::Application vs gtk::main
"Стария" стил на писане на GTK приложения (GTK 2.0):
gtk::init()
- Правим каквото правим, създаваме си дървото от GUI елементи
- Викаме на най-главния прозорец
window.show_all()
gtk::main()
gtk::Application vs gtk::main
"Стария" стил на писане на GTK приложения (GTK 2.0):
gtk::init()
- Правим каквото правим, създаваме си дървото от GUI елементи
- Викаме на най-главния прозорец
window.show_all()
gtk::main()
- За да приключим, някъде викаме
gtk::main_quit()
gtk::Application vs gtk::main
"Новия" стил на писане на GTK приложения (GTK 3.0+):
gtk::Application vs gtk::main
"Новия" стил на писане на GTK приложения (GTK 3.0+):
let application = gtk::Application::new(<уникално име>, <флагове>)?;
gtk::Application vs gtk::main
"Новия" стил на писане на GTK приложения (GTK 3.0+):
let application = gtk::Application::new(<уникално име>, <флагове>)?;
application.connect_startup(|application| <инициализираме си всичко>);
gtk::Application vs gtk::main
"Новия" стил на писане на GTK приложения (GTK 3.0+):
let application = gtk::Application::new(<уникално име>, <флагове>)?;
application.connect_startup(|application| <инициализираме си всичко>);
- Стартираме приложението със
application.run(&args().collect::<Vec<_>>());
gtk::Application vs gtk::main
"Новия" стил на писане на GTK приложения (GTK 3.0+):
let application = gtk::Application::new(<уникално име>, <флагове>)?;
application.connect_startup(|application| <инициализираме си всичко>);
- Стартираме приложението със
application.run(&args().collect::<Vec<_>>());
- (или може би със
application.run(&[]);
)
gtk::Application vs gtk::main
"Новия" стил на писане на GTK приложения (GTK 3.0+):
let application = gtk::Application::new(<уникално име>, <флагове>)?;
application.connect_startup(|application| <инициализираме си всичко>);
- Стартираме приложението със
application.run(&args().collect::<Vec<_>>());
- (или може би със
application.run(&[]);
) - (Вместо
gtk::Window
, използвамеgtk::ApplicationWindow
)
gtk::Application vs gtk::main
"Новия" стил на писане на GTK приложения (GTK 3.0+):
let application = gtk::Application::new(<уникално име>, <флагове>)?;
application.connect_startup(|application| <инициализираме си всичко>);
- Стартираме приложението със
application.run(&args().collect::<Vec<_>>());
- (или може би със
application.run(&[]);
) - (Вместо
gtk::Window
, използвамеgtk::ApplicationWindow
) - За да приключим, някъде викаме на приложението
application.quit()
(или очакваме автоматично да приключи при затваряне на всички прозорци)
gtk::Application vs gtk::main
Двата модела не са съвсем еквивалентни -- стария стил означава, че приложението е self-contained -- стартира, прави нещо, приключва. Ако пуснете второ такова приложение, то ще е отделно.
С новия модел, ако пуснете второ приложение, то просто ще "активира" първото. Примерно, ако в командния ред пуснете firefox http://google.com
, това ще ви отвори firefox и ще чака в терминала. Ако след това в друг терминал пуснете firefox http://duckduckgo.com
, това веднага ще приключи, и ще отвори DuckDuckGo във вече отворения firefox.
Това усложнява малко логиката, но е нещо, което е oчаквано донякъде в повечето модерни GUI приложения. Един application, множество прозорци. Има и бонуси, като интеграция с DBUS и разни други неща.
Command-line handling
В rust-quickmd:
use gio::prelude::*;
use gio::ApplicationFlags;
fn main() {
let app = gtk::Application::new(
Some("com.andrewradev.quickmd"),
ApplicationFlags::HANDLES_OPEN | ApplicationFlags::HANDLES_COMMAND_LINE
).expect("GTK initialization failed");
app.connect_command_line(move |app, cmdline| {
if let Err(e) = run(&app, cmdline.arguments()) {
eprintln!("{}", e);
1 // неуспешен резултат
} else {
0 // успешен резултат
}
});
app.run(&env::args().collect::<Vec<_>>());
}
Тоест, вместо да чакаме connect_startup
, чакаме connect_command_line
, защото очакваме някакъв потенциално различен вход при всяко изпълнение.
EGUI
EGUI
- По-rusty, предвид че си е създадено за Rust, няма legacy design decisions както GTK.
EGUI
- По-rusty, предвид че си е създадено за Rust, няма legacy design decisions както GTK.
- Immediate-mode, а не "retained" -- няма widget-и, които си държат state, просто на всеки loop се рендерира всичко от нулата.
EGUI
- По-rusty, предвид че си е създадено за Rust, няма legacy design decisions както GTK.
- Immediate-mode, а не "retained" -- няма widget-и, които си държат state, просто на всеки loop се рендерира всичко от нулата.
- Има интересни features, може да се компилира за web, може да си persist-ва state-а на приложението
EGUI
- По-rusty, предвид че си е създадено за Rust, няма legacy design decisions както GTK.
- Immediate-mode, а не "retained" -- няма widget-и, които си държат state, просто на всеки loop се рендерира всичко от нулата.
- Има интересни features, може да се компилира за web, може да си persist-ва state-а на приложението
- НО: няма native look & feel, и си е work in progress. Някои неща стават доста тегаво, защото се мъчи да бъде гъвкаво и минималистично.
EGUI template -- хитри неща
#[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))]
: https://doc.rust-lang.org/reference/conditional-compilation.html#the-cfg_attr-attributeT.e. ако
feature = "persistence"
, все едно има#[derive(serde::Deserialize, serde::Serialize)]
Опционални пакети:
1 2 3 4 5 6serde = { version = "1", features = ["derive"], optional = true } [features] default = [] # Enable if you want to persist app state on shutdown: persistence = ["eframe/persistence", "serde"]
Зареждане на картинки
if let Some(image_tag) = tag.pictures().next() {
// Първо, обработваме PNG/JPEG/Whatever данните с "image" crate-а
if let Ok(image) = ::image::load_from_memory(&image_tag.data) {
let dimensions = image.dimensions();
let egui_image = epi::Image::from_rgba_unmultiplied(
[dimensions.0 as usize, dimensions.1 as usize],
// После, конвертираме до списък от RGBA8 стойности и до [u8] с deref:
image.to_rgba8().deref(),
);
// `frame` е `epi::Frame` и идва като параметър на `update` функцията:
self.selected_texture = Some(frame.alloc(egui_image));
// Вероятно е добра идея да викнем и:
// `self.selected_texture.take().map(|t| frame.free(t));` в един момент
}
}
Кое е по-харно?
Кое е по-харно?
GTK
- ✅ Тонове widget-и за каквото ви душа иска
- ✅
GTK_DEBUG=interactive
, има glade за интерфейс - ✅ Guide и обилна документация
Кое е по-харно?
GTK
- ✅ Тонове widget-и за каквото ви душа иска
- ✅
GTK_DEBUG=interactive
, има glade за интерфейс - ✅ Guide и обилна документация
- ❌ Особени абстракции като
*Ext
trait-овете - ❌ Голям surface area
- ❌ Вероятно малко досадно да се подкара под windows, macOS
Кое е по-харно?
GTK
- ✅ Тонове widget-и за каквото ви душа иска
- ✅
GTK_DEBUG=interactive
, има glade за интерфейс - ✅ Guide и обилна документация
- ❌ Особени абстракции като
*Ext
trait-овете - ❌ Голям surface area
- ❌ Вероятно малко досадно да се подкара под windows, macOS
Egui
- ✅ По-директно API, по-проста употреба
- ✅ Лесна компилация -- нулеви външни зависимости
Кое е по-харно?
GTK
- ✅ Тонове widget-и за каквото ви душа иска
- ✅
GTK_DEBUG=interactive
, има glade за интерфейс - ✅ Guide и обилна документация
- ❌ Особени абстракции като
*Ext
trait-овете - ❌ Голям surface area
- ❌ Вероятно малко досадно да се подкара под windows, macOS
Egui
- ✅ По-директно API, по-проста употреба
- ✅ Лесна компилация -- нулеви външни зависимости
- ❌ Някои неща все пак са досадни заради минимализъм и/или съвместимост между desktop/web
- ❌ Не толкова mature, примерно няма да намерите code editor widget, webview…
Други интересни ресурси
- Добра лекция, която прави един UI с GTK и също с терминален интерфейс: https://www.youtube.com/watch?v=dK9-oXptFcM
Други интересни ресурси
- Добра лекция, която прави един UI с GTK и също с терминален интерфейс: https://www.youtube.com/watch?v=dK9-oXptFcM
- fltk: Малко по-дървен интерфейс, но популярен за базови неща.
Други интересни ресурси
- Добра лекция, която прави един UI с GTK и също с терминален интерфейс: https://www.youtube.com/watch?v=dK9-oXptFcM
- fltk: Малко по-дървен интерфейс, но популярен за базови неща.
- relm: Библиотека, която седи отгоре на GTK и предоставя elm-подобен интерфейс.
- relm4: Същото като горното, ама различно? 😅
- vgtk: Също седи отгоре на GTK, прави интересни неща с макроси.
Други интересни ресурси
- Добра лекция, която прави един UI с GTK и също с терминален интерфейс: https://www.youtube.com/watch?v=dK9-oXptFcM
- fltk: Малко по-дървен интерфейс, но популярен за базови неща.
- relm: Библиотека, която седи отгоре на GTK и предоставя elm-подобен интерфейс.
- relm4: Същото като горното, ама различно? 😅
- vgtk: Също седи отгоре на GTK, прави интересни неща с макроси.
- tauri: Използва webview, като electron, но с доста по-малък overhead (твърдят).
Други интересни ресурси
- Добра лекция, която прави един UI с GTK и също с терминален интерфейс: https://www.youtube.com/watch?v=dK9-oXptFcM
- fltk: Малко по-дървен интерфейс, но популярен за базови неща.
- relm: Библиотека, която седи отгоре на GTK и предоставя elm-подобен интерфейс.
- relm4: Същото като горното, ама различно? 😅
- vgtk: Също седи отгоре на GTK, прави интересни неща с макроси.
- tauri: Използва webview, като electron, но с доста по-малък overhead (твърдят).
- sixtyfps: The new hotness, малко особено, цели да е cross-platform и cross-language даже.
Други интересни ресурси
- Добра лекция, която прави един UI с GTK и също с терминален интерфейс: https://www.youtube.com/watch?v=dK9-oXptFcM
- fltk: Малко по-дървен интерфейс, но популярен за базови неща.
- relm: Библиотека, която седи отгоре на GTK и предоставя elm-подобен интерфейс.
- relm4: Същото като горното, ама различно? 😅
- vgtk: Също седи отгоре на GTK, прави интересни неща с макроси.
- tauri: Използва webview, като electron, но с доста по-малък overhead (твърдят).
- sixtyfps: The new hotness, малко особено, цели да е cross-platform и cross-language даже.
- И всичко друго в секция "GUI" в "awesome-rust".
- Също, "Are we GUI Yet?"