Skip to content

Instantly share code, notes, and snippets.

@bahrom04
Created December 27, 2025 19:03
Show Gist options
  • Select an option

  • Save bahrom04/2535e0b4c73e835d66af96213b6c7e98 to your computer and use it in GitHub Desktop.

Select an option

Save bahrom04/2535e0b4c73e835d66af96213b6c7e98 to your computer and use it in GitHub Desktop.
Relm4 stack sidebar text with icons
use crate::RelmActionGroup;
use relm4::*;
use relm4::actions::RelmAction;
use adw::prelude::*;
use gtk::{gio, glib};
use crate::config::{APP_ID, PROFILE};
use crate::modals::{
about::AboutDialog, bluetooth::BluetoothModel, network::NetworkModel, wifi::WifiModel,
};
use std::convert::identity;
pub(super) struct App {
_wifi: Controller<WifiModel>,
_network: Controller<NetworkModel>,
_bluetooth: Controller<BluetoothModel>,
}
#[derive(Debug)]
pub enum AppMsg {
Quit,
}
relm4::new_action_group!(pub(super) WindowActionGroup, "win");
relm4::new_stateless_action!(PreferencesAction, WindowActionGroup, "preferences");
relm4::new_stateless_action!(pub(super) ShortcutsAction, WindowActionGroup, "show-help-overlay");
relm4::new_stateless_action!(AboutAction, WindowActionGroup, "about");
#[relm4::component(pub)]
impl SimpleComponent for App {
type Init = ();
type Input = AppMsg;
type Output = ();
type Widgets = AppWidgets;
menu! {
primary_menu: {
section! {
"_Preferences" => PreferencesAction,
"_Keyboard" => ShortcutsAction,
"_About GTK Rust Template" => AboutAction,
}
}
}
view! {
#[root]
main_window = adw::ApplicationWindow::new(&main_application()) {
set_visible: true,
connect_close_request[sender] => move |_| {
sender.input(AppMsg::Quit);
glib::Propagation::Stop
},
#[wrap(Some)]
set_help_overlay: shortcuts = &gtk::Builder::from_resource(
"/net/bleur/GtkRustTemplate/gtk/help-overlay.ui"
)
.object::<gtk::ShortcutsWindow>("help_overlay")
.unwrap() -> gtk::ShortcutsWindow {
set_transient_for: Some(&main_window),
set_application: Some(&main_application()),
},
add_css_class?: if PROFILE == "Devel" {
Some("devel")
} else {
None
},
#[name(split_view)]
adw::NavigationSplitView {
#[wrap(Some)]
set_sidebar = &adw::NavigationPage {
set_title: "Settings",
#[wrap(Some)]
set_child = &adw::ToolbarView {
add_top_bar = &adw::HeaderBar {
pack_end = &gtk::MenuButton {
set_icon_name: "open-menu-symbolic",
set_menu_model: Some(&primary_menu),
}
},
#[wrap(Some)]
set_content = &gtk::StackSidebar {
set_stack: &stack,
},
},
},
#[wrap(Some)]
set_content = &adw::NavigationPage {
// set_title: "Content",
#[wrap(Some)]
set_child = &adw::ToolbarView {
// add_top_bar = &adw::HeaderBar {},
set_content: Some(&stack),
}
},
},
add_breakpoint = bp_with_setters(
adw::Breakpoint::new(
adw::BreakpointCondition::new_length(
adw::BreakpointConditionLengthType::MaxWidth,
400.0,
adw::LengthUnit::Sp,
)
),
&[(&split_view, "collapsed", true)]
),
},
stack = &gtk::Stack {
add_titled: (wifi.widget(), Some("wifi"), "Wi-Fi"),
add_titled: (network.widget(), Some("network"), "Network"),
add_titled: (bluetooth.widget(), Some("bluetooth"), "Bluetooth"),
set_vhomogeneous: false,
}
}
fn init(
_init: Self::Init,
root: Self::Root,
sender: ComponentSender<Self>,
) -> ComponentParts<Self> {
let wifi = WifiModel::builder()
.launch(())
.forward(sender.input_sender(), identity);
let network = NetworkModel::builder()
.launch(())
.forward(sender.input_sender(), identity);
let bluetooth = BluetoothModel::builder()
.launch(())
.forward(sender.input_sender(), identity);
let widgets = view_output!();
// Set icon names for stack pages
widgets
.stack
.page(wifi.widget())
.set_property("icon-name", "network-wireless-symbolic");
widgets
.stack
.page(network.widget())
.set_property("icon-name", "network-wired-symbolic");
widgets
.stack
.page(bluetooth.widget())
.set_property("icon-name", "bluetooth-symbolic");
let model = App {
_wifi: wifi,
_network: network,
_bluetooth: bluetooth,
};
widgets.stack.connect_visible_child_notify({
let split_view = widgets.split_view.clone();
move |_| {
split_view.set_show_content(true);
}
});
let mut actions = RelmActionGroup::<WindowActionGroup>::new();
let shortcuts_action = {
let shortcuts = widgets.shortcuts.clone();
RelmAction::<ShortcutsAction>::new_stateless(move |_| {
shortcuts.present();
})
};
let about_action = {
RelmAction::<AboutAction>::new_stateless(move |_| {
AboutDialog::builder().launch(()).detach();
})
};
actions.add_action(shortcuts_action);
actions.add_action(about_action);
actions.register_for_widget(&widgets.main_window);
widgets.load_window_size();
ComponentParts { model, widgets }
}
fn update(&mut self, message: Self::Input, _sender: ComponentSender<Self>) {
match message {
AppMsg::Quit => main_application().quit(),
}
}
fn shutdown(&mut self, widgets: &mut Self::Widgets, _output: relm4::Sender<Self::Output>) {
widgets.save_window_size().unwrap();
}
}
fn bp_with_setters(
bp: adw::Breakpoint,
additions: &[(&impl IsA<glib::Object>, &str, impl ToValue)],
) -> adw::Breakpoint {
bp.add_setters(additions);
bp
}
impl AppWidgets {
fn save_window_size(&self) -> Result<(), glib::BoolError> {
let settings = gio::Settings::new(APP_ID);
let (width, height) = self.main_window.default_size();
settings.set_int("window-width", width)?;
settings.set_int("window-height", height)?;
settings.set_boolean("is-maximized", self.main_window.is_maximized())?;
Ok(())
}
fn load_window_size(&self) {
let settings = gio::Settings::new(APP_ID);
let width = settings.int("window-width");
let height = settings.int("window-height");
let is_maximized = settings.boolean("is-maximized");
self.main_window.set_default_size(width, height);
if is_maximized {
self.main_window.maximize();
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment