rust-gtk4
This skill should be used when the user asks to "build a GTK4 app in Rust", "create a libadwaita UI", "use relm4", "build a Linux desktop app with Rust", "implement a GTK4 widget", "create a preferenc
Files3
View on GitHubSKILL.mdpkg/skills/lang/rust-gtk4/SKILL.md
GitHubRole
Expert Rust/GTK4 developer specializing in libadwaita HIG-compliant desktop applications using the relm4 Elm-style architecture. Produces structured, composable, message-driven UIs with clean separation between model state and view rendering.
Core Architecture: relm4 Elm Pattern
relm4 implements an Elm-like model-update-view loop:
Component::init()— construct initial model state + build widget treeComponent::update()— receive a typed message, mutate modelview!macro — declarative widget tree; auto-syncs to model on#[watch]fields
The #[relm4::component] macro generates the boilerplate; the view! macro is the widget DSL.
Key Concepts
Sync vs Async Components
| Trait | When to use |
|---|---|
SimpleComponent |
No async work; instant model updates |
Component |
Background commands needed (type CommandOutput) |
AsyncComponent |
update() is async; background tasks via update_cmd() |
Message Flow
User action (signal) → sender.input(Msg::X) → update() → model mutates → view re-renders
Background task → sender.command(...) → update_cmd() → sender.input(...)
Background Work Pattern
Never block update(). Use sender.command() to dispatch a CommandOutput message, then handle the result in update_cmd():
async fn update(&mut self, msg: Self::Input, sender: ComponentSender<Self>) {
match msg {
Msg::FetchData => {
self.loading = true;
sender.command(|_, _| CmdOut::DoFetch);
}
}
}
async fn update_cmd(&mut self, cmd: Self::CommandOutput, sender: ComponentSender<Self>) {
match cmd {
CmdOut::DoFetch => match fetch().await {
Ok(data) => sender.input(Msg::SetData(data)),
Err(e) => sender.input(Msg::SetError(e.to_string())),
}
}
}
libadwaita Widget Hierarchy
AdwApplicationWindow
└── AdwToolbarView
├── add_top_bar: AdwHeaderBar
└── content: [primary content]
Preferences Dialog Structure
AdwPreferencesWindow
└── AdwPreferencesPage (tab)
└── AdwPreferencesGroup (section)
├── AdwActionRow (entry, switch suffix)
└── AdwSwitchRow
Empty + Loading States
// Empty state
adw::StatusPage {
set_icon_name: Some("document-open-symbolic"),
set_title: "No Items",
set_description: Some("Add an item to get started"),
}
// Inline alert
adw::Banner {
set_title: "Connection failed",
set_revealed: model.has_error,
set_button_label: Some("Retry"),
connect_button_clicked[sender] => move |_| sender.input(Msg::Retry),
}
Toast Notifications
adw::ToastOverlay {
// wraps main content; show toasts programmatically:
}
// In update():
self.toast_overlay.add_toast(
adw::Toast::new("Settings saved")
);
Signal → Message Pattern
All signals close over sender and call sender.input():
gtk::Button {
set_label: "Connect",
connect_clicked[sender] => move |_| {
sender.input(AppMsg::Connect);
}
}
gtk::Entry {
connect_changed[sender] => move |entry| {
sender.input(AppMsg::SetUrl(entry.text().to_string()));
}
}
adw::SwitchRow {
set_title: "Auto-restart",
set_active: model.auto_restart,
connect_active_notify[sender] => move |row| {
sender.input(AppMsg::SetAutoRestart(row.is_active()));
}
}
System Tray Integration
Use libayatana-appindicator (or ksni) for system tray. Build a dedicated TrayModel component, spawn it alongside the main app:
let mut indicator = AppIndicator::new("myapp", "myapp-symbolic");
indicator.set_status(AppIndicatorStatus::Active);
let menu = gio::Menu::new();
menu.append(Some("Show"), Some("app.show-window"));
menu.append(Some("Quit"), Some("app.quit"));
indicator.set_menu_model(Some(&menu));
Cargo.toml Dependencies
[dependencies]
relm4 = { version = "0.10", features = ["libadwaita"] }
gtk4 = { version = "0.9", features = ["v4_10"] }
libadwaita = { version = "0.7", features = ["v1_4"] }
glib = "0.20"
gio = { version = "0.20", features = ["v2_76"] }
tokio = { version = "1", features = ["full"] }
Edge Cases
If a component needs to talk to a sibling: use ComponentSender::output() to emit upward; parent routes messages downward.
If the view needs to reference a named widget outside the macro: use #[name = "my_widget"] annotation inside view!.
If GTK panics with "assertion failed: is_main_thread": ensure all GTK calls happen on the GLib main thread — never from tokio::spawn.
If libadwaita features are version-gated: add the feature flag to the libadwaita dep (e.g., features = ["v1_4"]).
For deep widget patterns, component composition, and real-world relm4 structure: see the reference files below.