By now, we've already learned many ways to glue our widgets together.
We can send messages through channels, emit signals, share reference-counted state and bind properties.
Now, we will complete our set by learning about actions.
An action is a piece of functionality bound to a certain GObject.
Let's check out the simplest case where we activate an action without a parameter.
use gio::ActionEntry;
use gtk::prelude::*;
use gtk::{gio, glib, Application, ApplicationWindow};
const APP_ID: &str = "org.gtk_rs.Actions1";
fnmain() -> glib::ExitCode {
// Create a new applicationlet app = Application::builder().application_id(APP_ID).build();
// Connect to "activate" signal of `app` app.connect_activate(build_ui);
// Set keyboard accelerator to trigger "win.close". app.set_accels_for_action("win.close", &["<Ctrl>W"]);
// Run the application app.run()
}
fnbuild_ui(app: &Application) {
// Create a window and set the titlelet window = ApplicationWindow::builder()
.application(app)
.title("My GTK App")
.width_request(360)
.build();
// Add action "close" to `window` taking no parameterlet action_close = ActionEntry::builder("close")
.activate(|window: &ApplicationWindow, _, _| {
window.close();
})
.build();
window.add_action_entries([action_close]);
// Present window
window.present();
}
First, we created a new gio::ActionEntry which is named "close" and takes no parameter.
We also connected a callback which closes the window when the action is activated.
Finally, we add the action entry to the window via add_action_entries.
use gio::ActionEntry;
use gtk::prelude::*;
use gtk::{gio, glib, Application, ApplicationWindow};
const APP_ID: &str = "org.gtk_rs.Actions1";
fnmain() -> glib::ExitCode {
// Create a new applicationlet app = Application::builder().application_id(APP_ID).build();
// Connect to "activate" signal of `app`
app.connect_activate(build_ui);
// Set keyboard accelerator to trigger "win.close".
app.set_accels_for_action("win.close", &["<Ctrl>W"]);
// Run the application
app.run()
}
fnbuild_ui(app: &Application) {
// Create a window and set the titlelet window = ApplicationWindow::builder()
.application(app)
.title("My GTK App")
.width_request(360)
.build();
// Add action "close" to `window` taking no parameterlet action_close = ActionEntry::builder("close")
.activate(|window: &ApplicationWindow, _, _| {
window.close();
})
.build();
window.add_action_entries([action_close]);
// Present window window.present();
}
One of the most popular reasons to use actions are keyboard accelerators, so we added one here.
With set_accels_for_action one can assign one or more accelerators to a certain action.
Check the documentation of accelerator_parse in order to learn more about its syntax.
Before we move on to other aspects of actions, let's appreciate a few things that are curious here.
The "win" part of "win.close" is the group of the action.
But how does GTK know that "win" is the action group of our window?
The answer is that it is so common to add actions to windows and applications that there are already two predefined groups available:
"app" for actions global to the application, and
"win" for actions tied to an application window.
We can add an action group to any widget via the method insert_action_group.
Let's add our action to the action group "custom-group" and add the group then to our window.
The action entry isn't specific to our window anymore, the first parameter of the "activate" callback is of type SimpleActionGroup instead of ApplicationWindow.
This means we have to clone window into the closure.
use gio::ActionEntry;
use glib::clone;
use gtk::gio::SimpleActionGroup;
use gtk::prelude::*;
use gtk::{gio, glib, Application, ApplicationWindow};
const APP_ID: &str = "org.gtk_rs.Actions2";
fnmain() -> glib::ExitCode {
// Create a new applicationlet app = Application::builder().application_id(APP_ID).build();
// Connect to "activate" signal of `app` app.connect_activate(build_ui);
// Set keyboard accelerator to trigger "custom-group.close". app.set_accels_for_action("custom-group.close", &["<Ctrl>W"]);
// Run the application app.run()
}
fnbuild_ui(app: &Application) {
// Create a window and set the titlelet window = ApplicationWindow::builder()
.application(app)
.title("My GTK App")
.width_request(360)
.build();
// Add action "close" to `window` taking no parameterlet action_close = ActionEntry::builder("close")
.activate(clone!(
#[weak]
window,
move |_, _, _| {
window.close();
}
))
.build();
// Create a new action group and add actions to itlet actions = SimpleActionGroup::new();
actions.add_action_entries([action_close]);
window.insert_action_group("custom-group", Some(&actions));
// Present window
window.present();
}
If we bind the accelerator to "custom-group.close", it works just as before.
use gio::ActionEntry;
use glib::clone;
use gtk::gio::SimpleActionGroup;
use gtk::prelude::*;
use gtk::{gio, glib, Application, ApplicationWindow};
const APP_ID: &str = "org.gtk_rs.Actions2";
fnmain() -> glib::ExitCode {
// Create a new applicationlet app = Application::builder().application_id(APP_ID).build();
// Connect to "activate" signal of `app` app.connect_activate(build_ui);
// Set keyboard accelerator to trigger "custom-group.close".
app.set_accels_for_action("custom-group.close", &["<Ctrl>W"]);
// Run the application app.run()
}
fnbuild_ui(app: &Application) {
// Create a window and set the titlelet window = ApplicationWindow::builder()
.application(app)
.title("My GTK App")
.width_request(360)
.build();
// Add action "close" to `window` taking no parameterlet action_close = ActionEntry::builder("close")
.activate(clone!(
#[weak] window,
move |_, _, _| {
window.close();
}
))
.build();
// Create a new action group and add actions to itlet actions = SimpleActionGroup::new();
actions.add_action_entries([action_close]);
window.insert_action_group("custom-group", Some(&actions));
// Present window window.present();
}
Also, if we had multiple instances of the same windows, we would expect that only the currently focused window will be closed when activating "win.close".
And indeed, the "win.close" will be dispatched to the currently focused window.
However, that also means that we actually define one action per window instance.
If we want to have a single globally accessible action instead, we call add_action_entries on our application instead.
Adding "win.close" was useful as a simple example.
However, in the future we will use the pre-defined "window.close" action which does exactly the same thing.
use gio::ActionEntry;
use gtk::prelude::*;
use gtk::{
gio, glib, Align, Application, ApplicationWindow, Button, Label, Orientation,
};
const APP_ID: &str = "org.gtk_rs.Actions3";
fnmain() -> glib::ExitCode {
// Create a new applicationlet app = Application::builder().application_id(APP_ID).build();
// Connect to "activate" signal of `app` app.connect_activate(build_ui);
// Run the application app.run()
}
fnbuild_ui(app: &Application) {
let original_state = 0;
let label = Label::builder()
.label(format!("Counter: {original_state}"))
.build();
// Create a button with labellet button = Button::builder().label("Press me!").build();
// Connect to "clicked" signal of `button`
button.connect_clicked(move |button| {
// Activate "win.count" and pass "1" as parameterlet parameter = 1;
button
.activate_action("win.count", Some(¶meter.to_variant()))
.expect("The action does not exist.");
});
// Create a `gtk::Box` and add `button` and `label` to itlet gtk_box = gtk::Box::builder()
.orientation(Orientation::Vertical)
.margin_top(12)
.margin_bottom(12)
.margin_start(12)
.margin_end(12)
.spacing(12)
.halign(Align::Center)
.build();
gtk_box.append(&button);
gtk_box.append(&label);
// Create a window, set the title and add `gtk_box` to itlet window = ApplicationWindow::builder()
.application(app)
.title("My GTK App")
.width_request(360)
.child(>k_box)
.build();
// Add action "count" to `window` taking an integer as parameterlet action_count = ActionEntry::builder("count")
.parameter_type(Some(&i32::static_variant_type()))
.state(original_state.to_variant())
.activate(move |_, action, parameter| {
// Get stateletmut state = action
.state()
.expect("Could not get state.")
.get::<i32>()
.expect("The variant needs to be of type `i32`.");
// Get parameterlet parameter = parameter
.expect("Could not get parameter.")
.get::<i32>()
.expect("The variant needs to be of type `i32`.");
// Increase state by parameter and store state
state += parameter;
action.set_state(&state.to_variant());
// Update label with new state
label.set_label(&format!("Counter: {state}"));
})
.build();
window.add_action_entries([action_count]);
// Present window
window.present();
}
Here, we created a "win.count" action that increases its state by the given parameter every time it is activated.
It also takes care of updating the label with the current state.
The button activates the action with each click while passing "1" as parameter.
This is how our app works:
Connecting actions to the "clicked" signal of buttons is a typical use case, which is why all buttons implement the Actionable interface.
This way, the action can be specified by setting the "action-name" property.
If the action accepts a parameter, it can be set via the "action-target" property.
With ButtonBuilder, we can set everything up by calling its methods.
use gio::ActionEntry;
use gtk::prelude::*;
use gtk::{
gio, glib, Align, Application, ApplicationWindow, Button, Label, Orientation,
};
const APP_ID: &str = "org.gtk_rs.Actions4";
fnmain() -> glib::ExitCode {
// Create a new applicationlet app = Application::builder().application_id(APP_ID).build();
// Connect to "activate" signal of `app` app.connect_activate(build_ui);
// Run the application app.run()
}
fnbuild_ui(app: &Application) {
let original_state = 0;
let label = Label::builder()
.label(format!("Counter: {original_state}"))
.build();
// Create a button with label and actionlet button = Button::builder()
.label("Press me!")
.action_name("win.count")
.action_target(&1.to_variant())
.build();
// Create `gtk_box` and add `button` and `label` to itlet gtk_box = gtk::Box::builder()
.orientation(Orientation::Vertical)
.margin_top(12)
.margin_bottom(12)
.margin_start(12)
.margin_end(12)
.spacing(12)
.halign(Align::Center)
.build();
gtk_box.append(&button);
gtk_box.append(&label);
// Create a window and set the titlelet window = ApplicationWindow::builder()
.application(app)
.title("My GTK App")
.width_request(360)
.child(>k_box)
.build();
// Add action "count" to `window` taking an integer as parameterlet action_count = ActionEntry::builder("count")
.parameter_type(Some(&i32::static_variant_type()))
.state(original_state.to_variant())
.activate(move |_, action, parameter| {
// Get stateletmut state = action
.state()
.expect("Could not get state.")
.get::<i32>()
.expect("The variant needs to be of type `i32`.");
// Get parameterlet parameter = parameter
.expect("Could not get parameter.")
.get::<i32>()
.expect("The variant needs to be of type `i32`.");
// Increase state by parameter and store state state += parameter;
action.set_state(&state.to_variant());
// Update label with new state label.set_label(&format!("Counter: {state}"));
})
.build();
window.add_action_entries([action_count]);
// Present window window.present();
}
Actionable widgets are also easily accessible through the interface builder.
As usual, we build up the window via a composite template.
Within the template we can then set the "action-name" and "action-target" properties.
mod imp;
use gio::ActionEntry;
use glib::Object;
use gtk::prelude::*;
use gtk::subclass::prelude::*;
use gtk::{gio, glib, Application};
glib::wrapper! {
pubstructWindow(ObjectSubclass<imp::Window>)
@extends gtk::ApplicationWindow, gtk::Window, gtk::Widget,
@implements gio::ActionGroup, gio::ActionMap, gtk::Accessible, gtk::Buildable,
gtk::ConstraintTarget, gtk::Native, gtk::Root, gtk::ShortcutManager;
}
impl Window {
pubfnnew(app: &Application) -> Self {
// Create new window
Object::builder().property("application", app).build()
}
fnsetup_actions(&self) {
// Add stateful action "count" to `window` taking an integer as parameterlet original_state = 0;
let action_count = ActionEntry::builder("count")
.parameter_type(Some(&i32::static_variant_type()))
.state(original_state.to_variant())
.activate(move |window: &Self, action, parameter| {
// Get stateletmut state = action
.state()
.expect("Could not get state.")
.get::<i32>()
.expect("The variant needs to be of type `i32`.");
// Get parameterlet parameter = parameter
.expect("Could not get parameter.")
.get::<i32>()
.expect("The variant needs to be of type `i32`.");
// Increase state by parameter and store state
state += parameter;
action.set_state(&state.to_variant());
// Update label with new state
window.imp().label.set_label(&format!("Counter: {state}"));
})
.build();
self.add_action_entries([action_count]);
}
}
Finally, setup_actions will be called within constructed.
use glib::subclass::InitializingObject;
use gtk::subclass::prelude::*;
use gtk::{glib, CompositeTemplate, Label};
// Object holding the state#[derive(CompositeTemplate, Default)]#[template(resource = "/org/gtk_rs/example/window.ui")]pubstructWindow {
#[template_child]pub label: TemplateChild<Label>,
}
// The central trait for subclassing a GObject#[glib::object_subclass]impl ObjectSubclass for Window {
// `NAME` needs to match `class` attribute of templateconst NAME: &'staticstr = "MyGtkAppWindow";
typeType = super::Window;
typeParentType = gtk::ApplicationWindow;
fnclass_init(klass: &mut Self::Class) {
klass.bind_template();
}
fninstance_init(obj: &InitializingObject<Self>) {
obj.init_template();
}
}
// Trait shared by all GObjectsimpl ObjectImpl for Window {
fnconstructed(&self) {
// Call "constructed" on parentself.parent_constructed();
// Add actionsself.obj().setup_actions();
}
}
// Trait shared by all widgetsimpl WidgetImpl for Window {}
// Trait shared by all windowsimpl WindowImpl for Window {}
// Trait shared by all application windowsimpl ApplicationWindowImpl for Window {}
This app behaves the same as our previous example, but it will make it simpler for us to add a menu in the following section.
If you want to create a menu, you have to use actions, and you will want to use the interface builder.
Typically, a menu entry has an action fitting one of these three descriptions:
no parameter and no state, or
no parameter and boolean state, or
string parameter and string state.
Let's modify our small app to demonstrate these cases.
First, we extend setup_actions.
For the action without parameter or state, we can use the pre-defined "window.close" action.
Therefore, we don't have to add anything here.
With the action "button-frame", we manipulate the "has-frame" property of button.
Here, the convention is that actions with no parameter and boolean state should behave like toggle actions.
This means that the caller can expect the boolean state to toggle after activating the action. Luckily for us, that is the default behavior for gio::PropertyAction with a boolean property.
mod imp;
use gio::{ActionEntry, PropertyAction};
use glib::Object;
use gtk::prelude::*;
use gtk::subclass::prelude::*;
use gtk::{gio, glib, Application, Orientation};
glib::wrapper! {
pubstructWindow(ObjectSubclass<imp::Window>)
@extends gtk::ApplicationWindow, gtk::Window, gtk::Widget,
@implements gio::ActionGroup, gio::ActionMap, gtk::Accessible, gtk::Buildable,
gtk::ConstraintTarget, gtk::Native, gtk::Root, gtk::ShortcutManager;
}
impl Window {
pubfnnew(app: &Application) -> Self {
// Create new window Object::builder().property("application", app).build()
}
fnsetup_actions(&self) {
// Add stateful action "count" to `window` taking an integer as parameterlet original_state = 0;
let action_count = ActionEntry::builder("count")
.parameter_type(Some(&i32::static_variant_type()))
.state(original_state.to_variant())
.activate(move |window: &Self, action, parameter| {
// Get stateletmut state = action
.state()
.expect("Could not get state.")
.get::<i32>()
.expect("The variant needs to be of type `i32`.");
// Get parameterlet parameter = parameter
.expect("Could not get parameter.")
.get::<i32>()
.expect("The variant needs to be of type `i32`.");
// Increase state by parameter and store state state += parameter;
action.set_state(&state.to_variant());
// Update label with new state window.imp().label.set_label(&format!("Counter: {state}"));
})
.build();
// Add property action "button-frame" to `window`let button = self.imp().button.get();
let action_button_frame =
PropertyAction::new("button-frame", &button, "has-frame");
self.add_action(&action_button_frame);
// Add stateful action "orientation" to `window` taking a string as parameterlet action_orientation = ActionEntry::builder("orientation")
.parameter_type(Some(&String::static_variant_type()))
.state("Vertical".to_variant())
.activate(move |window: &Self, action, parameter| {
// Get parameterlet parameter = parameter
.expect("Could not get parameter.")
.get::<String>()
.expect("The value needs to be of type `String`.");
let orientation = match parameter.as_str() {
"Horizontal" => Orientation::Horizontal,
"Vertical" => Orientation::Vertical,
_ => unreachable!(),
};
// Set orientation and save state window.imp().gtk_box.set_orientation(orientation);
action.set_state(¶meter.to_variant());
})
.build();
self.add_action_entries([action_count, action_orientation]);
}
}
A PropertyAction is useful when you need an action that manipulates the property of a GObject.
The property then acts as the state of the action.
As mentioned above, if the property is a boolean the action has no parameter and toggles the property on activation.
In all other cases, the action has a parameter of the same type as the property.
When activating the action, the property gets set to the same value as the parameter of the action.
Finally, we add "win.orientation", an action with string parameter and string state.
This action can be used to change the orientation of gtk_box.
Here the convention is that the state should be set to the given parameter.
We don't need the action state to implement orientation switching, however it is useful for making the menu display the current orientation.
mod imp;
use gio::{ActionEntry, PropertyAction};
use glib::Object;
use gtk::prelude::*;
use gtk::subclass::prelude::*;
use gtk::{gio, glib, Application, Orientation};
glib::wrapper! {
pubstructWindow(ObjectSubclass<imp::Window>)
@extends gtk::ApplicationWindow, gtk::Window, gtk::Widget,
@implements gio::ActionGroup, gio::ActionMap, gtk::Accessible, gtk::Buildable,
gtk::ConstraintTarget, gtk::Native, gtk::Root, gtk::ShortcutManager;
}
impl Window {
pubfnnew(app: &Application) -> Self {
// Create new window Object::builder().property("application", app).build()
}
fnsetup_actions(&self) {
// Add stateful action "count" to `window` taking an integer as parameterlet original_state = 0;
let action_count = ActionEntry::builder("count")
.parameter_type(Some(&i32::static_variant_type()))
.state(original_state.to_variant())
.activate(move |window: &Self, action, parameter| {
// Get stateletmut state = action
.state()
.expect("Could not get state.")
.get::<i32>()
.expect("The variant needs to be of type `i32`.");
// Get parameterlet parameter = parameter
.expect("Could not get parameter.")
.get::<i32>()
.expect("The variant needs to be of type `i32`.");
// Increase state by parameter and store state state += parameter;
action.set_state(&state.to_variant());
// Update label with new state window.imp().label.set_label(&format!("Counter: {state}"));
})
.build();
// Add property action "button-frame" to `window`let button = self.imp().button.get();
let action_button_frame =
PropertyAction::new("button-frame", &button, "has-frame");
self.add_action(&action_button_frame);
// Add stateful action "orientation" to `window` taking a string as parameterlet action_orientation = ActionEntry::builder("orientation")
.parameter_type(Some(&String::static_variant_type()))
.state("Vertical".to_variant())
.activate(move |window: &Self, action, parameter| {
// Get parameterlet parameter = parameter
.expect("Could not get parameter.")
.get::<String>()
.expect("The value needs to be of type `String`.");
let orientation = match parameter.as_str() {
"Horizontal" => Orientation::Horizontal,
"Vertical" => Orientation::Vertical,
_ => unreachable!(),
};
// Set orientation and save state
window.imp().gtk_box.set_orientation(orientation);
action.set_state(¶meter.to_variant());
})
.build();
self.add_action_entries([action_count, action_orientation]);
}
}
Even though gio::Menu can also be created with the bindings, the most convenient way is to use the interface builder for that.
We do that by adding the menu in front of the template.
String is the default type of the target which is why we did not have to specify a type.
With targets of other types you need to manually specify the correct GVariant format string.
For example, an i32 variable with value "5" would correspond to this:
<attributename="target"type="i">5</attribute>
This is how the app looks in action:
We changed the icon of the MenuButton by setting its property "icon-name" to "open-menu-symbolic".
You can find more icons with the Icon Library.
They can be embedded with gio::Resource and then be referenced within the composite templates (or other places).
The menu entries nicely display the state of our stateful actions, but after the app is closed, all changes to that state are lost.
As usual, we solve this problem with gio::Settings.
First we create a schema with settings corresponding to the stateful actions we created before.
<?xml version="1.0" encoding="utf-8"?><schemalist><schemaid="org.gtk_rs.Actions7"path="/org/gtk_rs/Actions7/"><keyname="button-frame"type="b"><default>true</default><summary>Whether the button has a frame</summary></key><keyname="orientation"type="s"><choices><choicevalue='Horizontal'/><choicevalue='Vertical'/></choices><default>'Vertical'</default><summary>Orientation of GtkBox</summary></key></schema></schemalist>
Again, we install the schema as described in the settings chapter.
Then we add the settings to imp::Window.
Since gio::Settings does not implement Default, we wrap it in a std::cell::OnceCell.
mod imp;
use gio::{ActionEntry, Settings};
use glib::Object;
use gtk::prelude::*;
use gtk::subclass::prelude::*;
use gtk::{gio, glib, Application, Orientation};
use crate::APP_ID;
glib::wrapper! {
pubstructWindow(ObjectSubclass<imp::Window>)
@extends gtk::ApplicationWindow, gtk::Window, gtk::Widget,
@implements gio::ActionGroup, gio::ActionMap, gtk::Accessible, gtk::Buildable,
gtk::ConstraintTarget, gtk::Native, gtk::Root, gtk::ShortcutManager;
}
impl Window {
pubfnnew(app: &Application) -> Self {
// Create new window Object::builder().property("application", app).build()
}
fnsetup_settings(&self) {
let settings = Settings::new(APP_ID);
self.imp()
.settings
.set(settings)
.expect("`settings` should not be set before calling `setup_settings`.");
}
fnsettings(&self) -> &Settings {
self.imp()
.settings
.get()
.expect("`settings` should be set in `setup_settings`.")
}
fnsetup_actions(&self) {
// Add stateful action "count" to `window` taking an integer as parameterlet original_state = 0;
let action_count = ActionEntry::builder("count")
.parameter_type(Some(&i32::static_variant_type()))
.state(original_state.to_variant())
.activate(move |window: &Self, action, parameter| {
// Get stateletmut state = action
.state()
.expect("Could not get state.")
.get::<i32>()
.expect("The variant needs to be of type `i32`.");
// Get parameterlet parameter = parameter
.expect("Could not get parameter.")
.get::<i32>()
.expect("The variant needs to be of type `i32`.");
// Increase state by parameter and store state state += parameter;
action.set_state(&state.to_variant());
// Update label with new state window.imp().label.set_label(&format!("Counter: {state}"));
})
.build();
self.add_action_entries([action_count]);
// Create action from key "button-frame" and add to action group "win"let action_button_frame = self.settings().create_action("button-frame");
self.add_action(&action_button_frame);
// Create action from key "orientation" and add to action group "win"let action_orientation = self.settings().create_action("orientation");
self.add_action(&action_orientation);
}
fnbind_settings(&self) {
// Bind setting "button-frame" to "has-frame" property of `button`let button = self.imp().button.get();
self.settings()
.bind("button-frame", &button, "has-frame")
.build();
// Bind setting "orientation" to "orientation" property of `button`let gtk_box = self.imp().gtk_box.get();
self.settings()
.bind("orientation", >k_box, "orientation")
.mapping(|variant, _| {
let orientation = variant
.get::<String>()
.expect("The variant needs to be of type `String`.");
let orientation = match orientation.as_str() {
"Horizontal" => Orientation::Horizontal,
"Vertical" => Orientation::Vertical,
_ => unreachable!(),
};
Some(orientation.to_value())
})
.build();
}
}
Creating stateful actions from setting entries is so common that Settings provides a method for that exact purpose.
We create actions with thecreate_action method and then add them to the action group of our window.
mod imp;
use gio::{ActionEntry, Settings};
use glib::Object;
use gtk::prelude::*;
use gtk::subclass::prelude::*;
use gtk::{gio, glib, Application, Orientation};
use crate::APP_ID;
glib::wrapper! {
pubstructWindow(ObjectSubclass<imp::Window>)
@extends gtk::ApplicationWindow, gtk::Window, gtk::Widget,
@implements gio::ActionGroup, gio::ActionMap, gtk::Accessible, gtk::Buildable,
gtk::ConstraintTarget, gtk::Native, gtk::Root, gtk::ShortcutManager;
}
impl Window {
pubfnnew(app: &Application) -> Self {
// Create new window Object::builder().property("application", app).build()
}
fnsetup_settings(&self) {
let settings = Settings::new(APP_ID);
self.imp()
.settings
.set(settings)
.expect("`settings` should not be set before calling `setup_settings`.");
}
fnsettings(&self) -> &Settings {
self.imp()
.settings
.get()
.expect("`settings` should be set in `setup_settings`.")
}
fnsetup_actions(&self) {
// Add stateful action "count" to `window` taking an integer as parameterlet original_state = 0;
let action_count = ActionEntry::builder("count")
.parameter_type(Some(&i32::static_variant_type()))
.state(original_state.to_variant())
.activate(move |window: &Self, action, parameter| {
// Get stateletmut state = action
.state()
.expect("Could not get state.")
.get::<i32>()
.expect("The variant needs to be of type `i32`.");
// Get parameterlet parameter = parameter
.expect("Could not get parameter.")
.get::<i32>()
.expect("The variant needs to be of type `i32`.");
// Increase state by parameter and store state state += parameter;
action.set_state(&state.to_variant());
// Update label with new state window.imp().label.set_label(&format!("Counter: {state}"));
})
.build();
self.add_action_entries([action_count]);
// Create action from key "button-frame" and add to action group "win"let action_button_frame = self.settings().create_action("button-frame");
self.add_action(&action_button_frame);
// Create action from key "orientation" and add to action group "win"let action_orientation = self.settings().create_action("orientation");
self.add_action(&action_orientation);
}
fnbind_settings(&self) {
// Bind setting "button-frame" to "has-frame" property of `button`let button = self.imp().button.get();
self.settings()
.bind("button-frame", &button, "has-frame")
.build();
// Bind setting "orientation" to "orientation" property of `button`let gtk_box = self.imp().gtk_box.get();
self.settings()
.bind("orientation", >k_box, "orientation")
.mapping(|variant, _| {
let orientation = variant
.get::<String>()
.expect("The variant needs to be of type `String`.");
let orientation = match orientation.as_str() {
"Horizontal" => Orientation::Horizontal,
"Vertical" => Orientation::Vertical,
_ => unreachable!(),
};
Some(orientation.to_value())
})
.build();
}
}
Since actions from create_action follow the aforementioned conventions, we can keep further changes to a minimum.
The action "win.button-frame" toggles its state with each activation and the state of the "win.orientation" action follows the given parameter.
We still have to specify what should happen when the actions are activated though.
For the stateful actions, instead of adding callbacks to their "activate" signals, we bind the settings to properties we want to manipulate.
mod imp;
use gio::{ActionEntry, Settings};
use glib::Object;
use gtk::prelude::*;
use gtk::subclass::prelude::*;
use gtk::{gio, glib, Application, Orientation};
use crate::APP_ID;
glib::wrapper! {
pubstructWindow(ObjectSubclass<imp::Window>)
@extends gtk::ApplicationWindow, gtk::Window, gtk::Widget,
@implements gio::ActionGroup, gio::ActionMap, gtk::Accessible, gtk::Buildable,
gtk::ConstraintTarget, gtk::Native, gtk::Root, gtk::ShortcutManager;
}
impl Window {
pubfnnew(app: &Application) -> Self {
// Create new window Object::builder().property("application", app).build()
}
fnsetup_settings(&self) {
let settings = Settings::new(APP_ID);
self.imp()
.settings
.set(settings)
.expect("`settings` should not be set before calling `setup_settings`.");
}
fnsettings(&self) -> &Settings {
self.imp()
.settings
.get()
.expect("`settings` should be set in `setup_settings`.")
}
fnsetup_actions(&self) {
// Add stateful action "count" to `window` taking an integer as parameterlet original_state = 0;
let action_count = ActionEntry::builder("count")
.parameter_type(Some(&i32::static_variant_type()))
.state(original_state.to_variant())
.activate(move |window: &Self, action, parameter| {
// Get stateletmut state = action
.state()
.expect("Could not get state.")
.get::<i32>()
.expect("The variant needs to be of type `i32`.");
// Get parameterlet parameter = parameter
.expect("Could not get parameter.")
.get::<i32>()
.expect("The variant needs to be of type `i32`.");
// Increase state by parameter and store state state += parameter;
action.set_state(&state.to_variant());
// Update label with new state window.imp().label.set_label(&format!("Counter: {state}"));
})
.build();
self.add_action_entries([action_count]);
// Create action from key "button-frame" and add to action group "win"let action_button_frame = self.settings().create_action("button-frame");
self.add_action(&action_button_frame);
// Create action from key "orientation" and add to action group "win"let action_orientation = self.settings().create_action("orientation");
self.add_action(&action_orientation);
}
fnbind_settings(&self) {
// Bind setting "button-frame" to "has-frame" property of `button`let button = self.imp().button.get();
self.settings()
.bind("button-frame", &button, "has-frame")
.build();
// Bind setting "orientation" to "orientation" property of `button`let gtk_box = self.imp().gtk_box.get();
self.settings()
.bind("orientation", >k_box, "orientation")
.mapping(|variant, _| {
let orientation = variant
.get::<String>()
.expect("The variant needs to be of type `String`.");
let orientation = match orientation.as_str() {
"Horizontal" => Orientation::Horizontal,
"Vertical" => Orientation::Vertical,
_ => unreachable!(),
};
Some(orientation.to_value())
})
.build();
}
}
Finally, we make sure that bind_settings is called within constructed.
use gio::Settings;
use glib::subclass::InitializingObject;
use gtk::subclass::prelude::*;
use gtk::{gio, glib, Button, CompositeTemplate, Label};
use std::cell::OnceCell;
// Object holding the state#[derive(CompositeTemplate, Default)]#[template(resource = "/org/gtk_rs/example/window.ui")]pubstructWindow {
#[template_child]pub gtk_box: TemplateChild<gtk::Box>,
#[template_child]pub button: TemplateChild<Button>,
#[template_child]pub label: TemplateChild<Label>,
pub settings: OnceCell<Settings>,
}
// The central trait for subclassing a GObject#[glib::object_subclass]impl ObjectSubclass for Window {
// `NAME` needs to match `class` attribute of templateconst NAME: &'staticstr = "MyGtkAppWindow";
typeType = super::Window;
typeParentType = gtk::ApplicationWindow;
fnclass_init(klass: &mut Self::Class) {
klass.bind_template();
}
fninstance_init(obj: &InitializingObject<Self>) {
obj.init_template();
}
}
// Trait shared by all GObjectsimpl ObjectImpl for Window {
fnconstructed(&self) {
// Call "constructed" on parentself.parent_constructed();
// Setuplet obj = self.obj();
obj.setup_settings();
obj.setup_actions();
obj.bind_settings();
}
}
// Trait shared by all widgetsimpl WidgetImpl for Window {}
// Trait shared by all windowsimpl WindowImpl for Window {}
// Trait shared by all application windowsimpl ApplicationWindowImpl for Window {}
Actions are extremely powerful, and we are only scratching the surface here.
If you want to learn more about them, the GNOME developer documentation is a good place to start.