Actions

By now, we have 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 us check out the simplest case where we activate an action without a parameter.

Filename: listings/actions/1/main.rs

use gio::SimpleAction;
use glib::clone;
use gtk::prelude::*;
use gtk::{gio, glib};
use gtk::{Application, ApplicationWindow};

fn main() {
    // Create a new application
    let app = Application::builder()
        .application_id("org.gtk-rs.example")
        .build();

    // Connect to "activate" signal of `app`
    app.connect_activate(build_ui);

    // Set keyboard accelerator to trigger "win.quit".
    app.set_accels_for_action("win.quit", &["<primary>W"]);

    // Run the application
    app.run();
}

fn build_ui(app: &Application) {
    // Create a window and set the title
    let window = ApplicationWindow::builder()
        .application(app)
        .title("My GTK App")
        .width_request(360)
        .build();

    // Add action "quit" to `window` taking no parameter
    let action_quit = SimpleAction::new("quit", None);
    action_quit.connect_activate(clone!(@weak window => move |_, _| {
        window.close();
    }));
    window.add_action(&action_quit);

    // Present window to the user
    window.present();
}

First, we created a new gio::SimpleAction which is named "quit" and takes no parameter. We also connected a callback which closes the window.

Filename: listings/actions/1/main.rs

use gio::SimpleAction;
use glib::clone;
use gtk::prelude::*;
use gtk::{gio, glib};
use gtk::{Application, ApplicationWindow};

fn main() {
    // Create a new application
    let app = Application::builder()
        .application_id("org.gtk-rs.example")
        .build();

    // Connect to "activate" signal of `app`
    app.connect_activate(build_ui);

    // Set keyboard accelerator to trigger "win.quit".
    app.set_accels_for_action("win.quit", &["<primary>W"]);

    // Run the application
    app.run();
}

fn build_ui(app: &Application) {
    // Create a window and set the title
    let window = ApplicationWindow::builder()
        .application(app)
        .title("My GTK App")
        .width_request(360)
        .build();

    // Add action "quit" to `window` taking no parameter
    let action_quit = SimpleAction::new("quit", None);
    action_quit.connect_activate(clone!(@weak window => move |_, _| {
        window.close();
    }));
    window.add_action(&action_quit);

    // Present window to the user
    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. Here we assigned <primary>W which translates to Ctrl + W on Linux and Windows and ⌘ + W on macOS.

Before we move on to other aspects of actions, let us appreciate a few things that are curious here. The "win" part of "win.quit" 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.

If that was not the case, we would have to add the action group manually via gio::SimpleActionGroup.

Filename: listings/actions/2/main.rs

use gio::SimpleAction;
use glib::clone;
use gtk::gio::SimpleActionGroup;
use gtk::prelude::*;
use gtk::{gio, glib};
use gtk::{Application, ApplicationWindow};

fn main() {
    // Create a new application
    let app = Application::builder()
        .application_id("org.gtk-rs.example")
        .build();

    // Connect to "activate" signal of `app`
    app.connect_activate(build_ui);

    // Set keyboard accelerator to trigger "win.quit".
    app.set_accels_for_action("win.quit", &["<primary>W"]);

    // Run the application
    app.run();
}

fn build_ui(app: &Application) {
    // Create a window and set the title
    let window = ApplicationWindow::builder()
        .application(app)
        .title("My GTK App")
        .width_request(360)
        .build();

    // Add action "quit" to `window` taking no parameter
    let action_quit = SimpleAction::new("quit", None);
    action_quit.connect_activate(clone!(@weak window => move |_, _| {
        window.close();
    }));
    window.add_action(&action_quit);

    // Create a new action group and add actions to it
    let actions = SimpleActionGroup::new();
    window.insert_action_group("win", Some(&actions));
    actions.add_action(&action_quit);

    // Present window to the user
    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.quit". And indeed, the "win.quit" 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 on our application instead.

Parameter and State

An action, like most functions, can take a parameter. However, unlike most functions it can also be stateful. Let us see how this works.

Filename: listings/actions/3/main.rs

use gio::SimpleAction;
use glib::clone;
use gtk::{gio, glib};
use gtk::{prelude::*, Align};
use gtk::{Application, ApplicationWindow, Button, Label, Orientation};

fn main() {
    // Create a new application
    let app = Application::builder()
        .application_id("org.gtk-rs.example")
        .build();

    // Connect to "activate" signal of `app`
    app.connect_activate(build_ui);

    // Run the application
    app.run();
}

fn build_ui(app: &Application) {
    // Create a window and set the title
    let window = ApplicationWindow::builder()
        .application(app)
        .title("My GTK App")
        .width_request(360)
        .build();

    let original_state = 0;
    let label = Label::builder()
        .label(&format!("Counter: {}", original_state))
        .build();

    // Add action "count" to `window` taking an integer as parameter
    let action_count = SimpleAction::new_stateful(
        "count",
        Some(&i32::static_variant_type()),
        &original_state.to_variant(),
    );
    action_count.connect_activate(clone!(@weak label => move |action, parameter| {
        // Get state
        let mut state = action
            .state()
            .expect("Could not get state.")
            .get::<i32>()
            .expect("The variant needs to be of type `i32`.");

        // Get parameter
        let 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));
    }));
    window.add_action(&action_count);

    // Create a button with label
    let 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 parameter
        let parameter = 1;
        button.activate_action("win.count", Some(&parameter.to_variant()));
    });

    // Create a `gtk::Box` and add `button` and `label` to it
    let 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);

    // Add `gtk_box` to the window
    window.set_child(Some(&gtk_box));

    // Present window to the user
    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:

Actionable

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.

Filename: listings/actions/4/main.rs

use gio::SimpleAction;
use glib::clone;
use gtk::{gio, glib};
use gtk::{prelude::*, Align};
use gtk::{Application, ApplicationWindow, Button, Label, Orientation};

fn main() {
    // Create a new application
    let app = Application::builder()
        .application_id("org.gtk-rs.example")
        .build();

    // Connect to "activate" signal of `app`
    app.connect_activate(build_ui);

    // Run the application
    app.run();
}

fn build_ui(app: &Application) {
    // Create a window and set the title
    let window = ApplicationWindow::builder()
        .application(app)
        .title("My GTK App")
        .width_request(360)
        .build();

    let original_state = 0;
    let label = Label::builder()
        .label(&format!("Counter: {}", original_state))
        .build();

    // Add action "count" to `window` taking an integer as parameter
    let action_count = SimpleAction::new_stateful(
        "count",
        Some(&i32::static_variant_type()),
        &original_state.to_variant(),
    );
    action_count.connect_activate(clone!(@weak label => move |action, parameter| {
        // Get state
        let mut state = action
        .state()
        .expect("Could not get state.")
        .get::<i32>()
        .expect("The value needs to be of type `i32`.");

        // Get parameter
        let parameter = parameter
            .expect("Could not get parameter.")
            .get::<i32>()
            .expect("The value needs to be of type `i32`.");

        // Increase state by parameter and save state
        state += parameter;
        action.set_state(&state.to_variant());

        // Update label with new state
        label.set_label(&format!("Counter: {}", state));
    }));

    window.add_action(&action_count);

    // Create a button with label and action
    let 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 it
    let 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);

    // Add button
    window.set_child(Some(&gtk_box));

    // Present window to the user
    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.

Filename: listings/actions/5/window/window.ui

<?xml version="1.0" encoding="UTF-8"?>
<interface>
  <template class="MyGtkAppWindow" parent="GtkApplicationWindow">
    <property name="title">My GTK App</property>
    <child>
      <object class="GtkBox" id="gtk_box">
        <property name="orientation">vertical</property>
        <property name="margin-top">12</property>
        <property name="margin-bottom">12</property>
        <property name="margin-start">12</property>
        <property name="margin-end">12</property>
        <property name="spacing">12</property>
        <property name="halign">center</property>
        <child>
          <object class="GtkButton" id="button">
            <property name="label">Press me!</property>
            <property name="action-name">win.count</property>
            <property name="action-target">1</property>
          </object>
        </child>
        <child>
          <object class="GtkLabel" id="label">
            <property name="label">Counter: 0</property>
          </object>
        </child>
      </object>
    </child>
  </template>
</interface>

We will connect the actions and add them to the window in the Window::add_actions method.

Filename: listings/actions/5/window/mod.rs

mod imp;

use gio::SimpleAction;
use glib::{clone, Object};
use gtk::prelude::*;
use gtk::subclass::prelude::*;
use gtk::Application;
use gtk::{gio, glib};

glib::wrapper! {
    pub struct Window(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 {
    pub fn new(app: &Application) -> Self {
        // Create new window
        Object::new(&[("application", app)]).expect("Failed to create Window")
    }

    fn add_actions(&self) {
        let imp = imp::Window::from_instance(self);
        let label = imp.label.get();

        // Add stateful action "count" to `window` taking an integer as parameter
        let original_state = 0;
        let action_count = SimpleAction::new_stateful(
            "count",
            Some(&i32::static_variant_type()),
            &original_state.to_variant(),
        );

        action_count.connect_activate(clone!(@weak label => move |action, parameter| {
            // Get state
            let mut state = action
                .state()
                .expect("Could not get state.")
                .get::<i32>()
                .expect("The value needs to be of type `i32`.");

            // Get parameter
            let parameter = parameter
                .expect("Could not get parameter.")
                .get::<i32>()
                .expect("The value needs to be of type `i32`.");

            // Increase state by parameter and save state
            state += parameter;
            action.set_state(&state.to_variant());

            // Update label with new state
            label.set_label(&format!("Counter: {}", state));
        }));
        self.add_action(&action_count);
    }
}

Finally, add_actions will be called within constructed.

Filename: listings/actions/5/window/imp.rs

use glib::subclass::InitializingObject;
use gtk::prelude::*;

use gtk::glib;
use gtk::subclass::prelude::*;
use gtk::{CompositeTemplate, Label};

// Object holding the state
#[derive(CompositeTemplate, Default)]
#[template(file = "window.ui")]
pub struct Window {
    #[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 template
    const NAME: &'static str = "MyGtkAppWindow";
    type Type = super::Window;
    type ParentType = gtk::ApplicationWindow;

    fn class_init(klass: &mut Self::Class) {
        Self::bind_template(klass);
    }

    fn instance_init(obj: &InitializingObject<Self>) {
        obj.init_template();
    }
}

// Trait shared by all GObjects
impl ObjectImpl for Window {
    fn constructed(&self, obj: &Self::Type) {
        // Call "constructed" on parent
        self.parent_constructed(obj);

        // Add actions
        obj.add_actions();
    }
}

// Trait shared by all widgets
impl WidgetImpl for Window {}

// Trait shared by all windows
impl WindowImpl for Window {}

// Trait shared by all application
impl ApplicationWindowImpl for Window {}

This app behaves the same as our previous example, but it leads us to menu creation.

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 us modify our small app to demonstrate these cases. First we extend add_actions. For the action without parameter or state, we choose "win.quit" which we are already familiar with.

Filename: listings/actions/6/window/mod.rs

mod imp;

use gio::SimpleAction;
use glib::{clone, Object};
use gtk::prelude::*;
use gtk::subclass::prelude::*;
use gtk::{gio, glib};
use gtk::{Application, Orientation};

glib::wrapper! {
    pub struct Window(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 {
    pub fn new(app: &Application) -> Self {
        // Create new window
        Object::new(&[("application", app)]).expect("Failed to create Window")
    }

    fn add_actions(&self) {
        // Get state
        let imp = imp::Window::from_instance(self);
        let label = imp.label.get();

        // Add stateful action "count" to `window` taking an integer as parameter
        let original_state = 0;
        let action_count = SimpleAction::new_stateful(
            "count",
            Some(&i32::static_variant_type()),
            &original_state.to_variant(),
        );

        action_count.connect_activate(clone!(@weak label => move |action, parameter| {
            // Get state
            let mut state = action
                .state()
                .expect("Could not get state.")
                .get::<i32>()
                .expect("The value needs to be of type `i32`.");

            // Get parameter
            let parameter = parameter
                .expect("Could not get parameter.")
                .get::<i32>()
                .expect("The value needs to be of type `i32`.");

            // Increase state by parameter and save state
            state += parameter;
            action.set_state(&state.to_variant());

            // Update label with new state
            label.set_label(&format!("Counter: {}", state));
        }));
        self.add_action(&action_count);

        // Add action "quit" to `window` taking no parameter
        let action_quit = SimpleAction::new("quit", None);

        action_quit.connect_activate(clone!(@weak self as window => move |_, _| {
            window.close();
        }));
        self.add_action(&action_quit);

        // Add stateful action "sensitive-button" to `window` taking no parameter
        let button = imp.button.get();
        let action_sensitive_button =
            SimpleAction::new_stateful("sensitive-button", None, &true.to_variant());

        action_sensitive_button.connect_activate(clone!(@weak button =>
            move |action, _| {
                // Get state
                let mut state = action
                    .state()
                    .expect("Could not get state.")
                    .get::<bool>()
                    .expect("The value needs to be of type `bool`.");

                // Toggle and save state
                state = !state;
                action.set_state(&state.to_variant());

                // Set the button's sensitivity in accordance with the current state
                button.set_sensitive(state);
        }));
        self.add_action(&action_sensitive_button);


        // Add stateful action "orientation" to `window` taking a string as parameter
        let gtk_box = imp.gtk_box.get();
        let action_orientation = SimpleAction::new_stateful(
            "orientation",
            Some(&String::static_variant_type()),
            &"Vertical".to_variant(),
        );

        action_orientation.connect_activate(clone!(@weak gtk_box =>
            move |action, parameter| {
                // Get parameter
                let 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
                gtk_box.set_orientation(orientation);
                action.set_state(&parameter.to_variant());
        }));
        self.add_action(&action_orientation);
    }
}

With the action "sensitive-button", we manipulate the "sensitive" property of button. We need to be careful to follow the established conventions: 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.

Filename: listings/actions/6/window/mod.rs

mod imp;

use gio::SimpleAction;
use glib::{clone, Object};
use gtk::prelude::*;
use gtk::subclass::prelude::*;
use gtk::{gio, glib};
use gtk::{Application, Orientation};

glib::wrapper! {
    pub struct Window(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 {
    pub fn new(app: &Application) -> Self {
        // Create new window
        Object::new(&[("application", app)]).expect("Failed to create Window")
    }

    fn add_actions(&self) {
        // Get state
        let imp = imp::Window::from_instance(self);
        let label = imp.label.get();

        // Add stateful action "count" to `window` taking an integer as parameter
        let original_state = 0;
        let action_count = SimpleAction::new_stateful(
            "count",
            Some(&i32::static_variant_type()),
            &original_state.to_variant(),
        );

        action_count.connect_activate(clone!(@weak label => move |action, parameter| {
            // Get state
            let mut state = action
                .state()
                .expect("Could not get state.")
                .get::<i32>()
                .expect("The value needs to be of type `i32`.");

            // Get parameter
            let parameter = parameter
                .expect("Could not get parameter.")
                .get::<i32>()
                .expect("The value needs to be of type `i32`.");

            // Increase state by parameter and save state
            state += parameter;
            action.set_state(&state.to_variant());

            // Update label with new state
            label.set_label(&format!("Counter: {}", state));
        }));
        self.add_action(&action_count);

        // Add action "quit" to `window` taking no parameter
        let action_quit = SimpleAction::new("quit", None);

        action_quit.connect_activate(clone!(@weak self as window => move |_, _| {
            window.close();
        }));
        self.add_action(&action_quit);

        // Add stateful action "sensitive-button" to `window` taking no parameter
        let button = imp.button.get();
        let action_sensitive_button =
            SimpleAction::new_stateful("sensitive-button", None, &true.to_variant());

        action_sensitive_button.connect_activate(clone!(@weak button =>
            move |action, _| {
                // Get state
                let mut state = action
                    .state()
                    .expect("Could not get state.")
                    .get::<bool>()
                    .expect("The value needs to be of type `bool`.");

                // Toggle and save state
                state = !state;
                action.set_state(&state.to_variant());

                // Set the button's sensitivity in accordance with the current state
                button.set_sensitive(state);
        }));
        self.add_action(&action_sensitive_button);


        // Add stateful action "orientation" to `window` taking a string as parameter
        let gtk_box = imp.gtk_box.get();
        let action_orientation = SimpleAction::new_stateful(
            "orientation",
            Some(&String::static_variant_type()),
            &"Vertical".to_variant(),
        );

        action_orientation.connect_activate(clone!(@weak gtk_box =>
            move |action, parameter| {
                // Get parameter
                let 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
                gtk_box.set_orientation(orientation);
                action.set_state(&parameter.to_variant());
        }));
        self.add_action(&action_orientation);
    }
}

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 do not need the action state to implement orientation switching, however it is useful for making the menu display the current orientation.

Filename: listings/actions/6/window/mod.rs

mod imp;

use gio::SimpleAction;
use glib::{clone, Object};
use gtk::prelude::*;
use gtk::subclass::prelude::*;
use gtk::{gio, glib};
use gtk::{Application, Orientation};

glib::wrapper! {
    pub struct Window(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 {
    pub fn new(app: &Application) -> Self {
        // Create new window
        Object::new(&[("application", app)]).expect("Failed to create Window")
    }

    fn add_actions(&self) {
        // Get state
        let imp = imp::Window::from_instance(self);
        let label = imp.label.get();

        // Add stateful action "count" to `window` taking an integer as parameter
        let original_state = 0;
        let action_count = SimpleAction::new_stateful(
            "count",
            Some(&i32::static_variant_type()),
            &original_state.to_variant(),
        );

        action_count.connect_activate(clone!(@weak label => move |action, parameter| {
            // Get state
            let mut state = action
                .state()
                .expect("Could not get state.")
                .get::<i32>()
                .expect("The value needs to be of type `i32`.");

            // Get parameter
            let parameter = parameter
                .expect("Could not get parameter.")
                .get::<i32>()
                .expect("The value needs to be of type `i32`.");

            // Increase state by parameter and save state
            state += parameter;
            action.set_state(&state.to_variant());

            // Update label with new state
            label.set_label(&format!("Counter: {}", state));
        }));
        self.add_action(&action_count);

        // Add action "quit" to `window` taking no parameter
        let action_quit = SimpleAction::new("quit", None);

        action_quit.connect_activate(clone!(@weak self as window => move |_, _| {
            window.close();
        }));
        self.add_action(&action_quit);

        // Add stateful action "sensitive-button" to `window` taking no parameter
        let button = imp.button.get();
        let action_sensitive_button =
            SimpleAction::new_stateful("sensitive-button", None, &true.to_variant());

        action_sensitive_button.connect_activate(clone!(@weak button =>
            move |action, _| {
                // Get state
                let mut state = action
                    .state()
                    .expect("Could not get state.")
                    .get::<bool>()
                    .expect("The value needs to be of type `bool`.");

                // Toggle and save state
                state = !state;
                action.set_state(&state.to_variant());

                // Set the button's sensitivity in accordance with the current state
                button.set_sensitive(state);
        }));
        self.add_action(&action_sensitive_button);


        // Add stateful action "orientation" to `window` taking a string as parameter
        let gtk_box = imp.gtk_box.get();
        let action_orientation = SimpleAction::new_stateful(
            "orientation",
            Some(&String::static_variant_type()),
            &"Vertical".to_variant(),
        );

        action_orientation.connect_activate(clone!(@weak gtk_box =>
            move |action, parameter| {
                // Get parameter
                let 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
                gtk_box.set_orientation(orientation);
                action.set_state(&parameter.to_variant());
        }));
        self.add_action(&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.

Filename: listings/actions/6/window/window.ui

--- /var/home/julian/Projekte/gnome/libraries/gtk4-rs/book/listings/actions/5/window/window.ui
+++ /var/home/julian/Projekte/gnome/libraries/gtk4-rs/book/listings/actions/6/window/window.ui
@@ -1,7 +1,41 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <interface>
+  <menu id="main-menu">
+    <item>
+      <attribute name="label" translatable="yes">_Close window</attribute>
+      <attribute name="action">win.quit</attribute>
+    </item>
+    <item>
+      <attribute name="label" translatable="yes">_Sensitive button</attribute>
+      <attribute name="action">win.sensitive-button</attribute>
+    </item>
+    <section>
+      <attribute name="label" translatable="yes">Orientation</attribute>
+      <item>
+        <attribute name="label" translatable="yes">_Horizontal</attribute>
+        <attribute name="action">win.orientation</attribute>
+        <attribute name="target">Horizontal</attribute>
+      </item>
+      <item>
+        <attribute name="label" translatable="yes">_Vertical</attribute>
+        <attribute name="action">win.orientation</attribute>
+        <attribute name="target">Vertical</attribute>
+      </item>
+    </section>
+  </menu>
   <template class="MyGtkAppWindow" parent="GtkApplicationWindow">
     <property name="title">My GTK App</property>
+    <property name="width_request">360</property>
+    <child type="titlebar">
+      <object class="GtkHeaderBar">
+        <child type ="end">
+          <object class="GtkMenuButton">
+            <property name="icon_name">open-menu-symbolic</property>
+            <property name="menu_model">main-menu</property>
+          </object>
+        </child>
+      </object>
+    </child>
     <child>
       <object class="GtkBox" id="gtk_box">
         <property name="orientation">vertical</property>

Note how we specified the target:

<attribute name="target">Horizontal</attribute>

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:

<attribute name="target" type="i">5</attribute>

This is how the app looks in action:

Settings

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 add the settings to imp::Window and manually implement the Default trait.

Filename: listings/actions/7/window/imp.rs

use gio::Settings;
use glib::subclass::InitializingObject;
use gtk::prelude::*;
use gtk::subclass::prelude::*;
use gtk::{gio, glib};
use gtk::{Button, CompositeTemplate, Label};

// Object holding the state
#[derive(CompositeTemplate)]
#[template(file = "window.ui")]
pub struct Window {
    #[template_child]
    pub gtk_box: TemplateChild<gtk::Box>,
    #[template_child]
    pub button: TemplateChild<Button>,
    #[template_child]
    pub label: TemplateChild<Label>,
    pub settings: Settings,
}

impl Default for Window {
    fn default() -> Self {
        Window {
            gtk_box: TemplateChild::default(),
            button: TemplateChild::default(),
            label: TemplateChild::default(),
            settings: Settings::new("org.gtk-rs.example"),
        }
    }
}

// The central trait for subclassing a GObject
#[glib::object_subclass]
impl ObjectSubclass for Window {
    // `NAME` needs to match `class` attribute of template
    const NAME: &'static str = "MyGtkAppWindow";
    type Type = super::Window;
    type ParentType = gtk::ApplicationWindow;

    fn class_init(klass: &mut Self::Class) {
        Self::bind_template(klass);
    }

    fn instance_init(obj: &InitializingObject<Self>) {
        obj.init_template();
    }
}

// Trait shared by all GObjects
impl ObjectImpl for Window {
    fn constructed(&self, obj: &Self::Type) {
        // Call "constructed" on parent
        self.parent_constructed(obj);

        // Setup
        obj.add_actions();
        obj.bind_settings();
    }
}

// Trait shared by all widgets
impl WidgetImpl for Window {}

// Trait shared by all windows
impl WindowImpl for Window {}

// Trait shared by all application
impl ApplicationWindowImpl for Window {}

Creating stateful actions from setting entries is so common that Settings provides a method for that exact purpose. We create actions with create_actions and then add them to the action group of our window.

Filename: listings/actions/7/window/mod.rs

mod imp;

use gio::SimpleAction;
use glib::{clone, Object};
use gtk::prelude::*;
use gtk::subclass::prelude::*;
use gtk::{gio, glib};
use gtk::{Application, Orientation};

glib::wrapper! {
    pub struct Window(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 {
    pub fn new(app: &Application) -> Self {
        // Create new window
        Object::new(&[("application", app)]).expect("Failed to create Window")
    }

    fn add_actions(&self) {
        // Get state
        let imp = imp::Window::from_instance(self);
        let label = imp.label.get();

        // Add stateful action "count" to `window` taking an integer as parameter
        let original_state = 0;
        let action_count = SimpleAction::new_stateful(
            "count",
            Some(&i32::static_variant_type()),
            &original_state.to_variant(),
        );

        action_count.connect_activate(clone!(@weak label => move |action, parameter| {
            // Get state
            let mut state = action
            .state()
            .expect("Could not get state.")
            .get::<i32>()
            .expect("The variant needs to be of type `i32`.");

            // Get parameter
            let parameter = parameter
                .expect("Could not get parameter.")
                .get::<i32>()
                .expect("The variant needs to be of type `i32`.");

            // Increase state by parameter and save state
            state += parameter;
            action.set_state(&state.to_variant());

            // Update label with new state
            label.set_label(&format!("Counter: {}", state));
        }));
        self.add_action(&action_count);

        // Add action "quit" to `window` taking no parameter
        let action_quit = SimpleAction::new("quit", None);

        action_quit.connect_activate(clone!(@weak self as window => move |_, _| {
            window.close();
        }));
        self.add_action(&action_quit);

        // Create action from key "sensitive-button" and add to action group "win"
        let action_sensitive_button = imp.settings.create_action("sensitive-button");
        self.add_action(&action_sensitive_button);

        // Create action from key "orientation" and add to action group "win"
        let action_orientation = imp.settings.create_action("orientation");
        self.add_action(&action_orientation);
    }

    fn bind_settings(&self) {
        // Get state
        let imp = imp::Window::from_instance(self);

        // Bind setting "sensitive-button" to "sensitive" property of `button`
        let button = imp.button.get();
        imp.settings
            .bind("sensitive-button", &button, "sensitive")
            .build();

        // Bind setting "orientation" to "orientation" property of `button`
        let gtk_box = imp.gtk_box.get();
        imp.settings
            .bind("orientation", &gtk_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_actions follow the aforementioned conventions, we can keep further changes to a minimum. The action "win.sensitive-button" 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.

Filename: listings/actions/7/window/mod.rs

mod imp;

use gio::SimpleAction;
use glib::{clone, Object};
use gtk::prelude::*;
use gtk::subclass::prelude::*;
use gtk::{gio, glib};
use gtk::{Application, Orientation};

glib::wrapper! {
    pub struct Window(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 {
    pub fn new(app: &Application) -> Self {
        // Create new window
        Object::new(&[("application", app)]).expect("Failed to create Window")
    }

    fn add_actions(&self) {
        // Get state
        let imp = imp::Window::from_instance(self);
        let label = imp.label.get();

        // Add stateful action "count" to `window` taking an integer as parameter
        let original_state = 0;
        let action_count = SimpleAction::new_stateful(
            "count",
            Some(&i32::static_variant_type()),
            &original_state.to_variant(),
        );

        action_count.connect_activate(clone!(@weak label => move |action, parameter| {
            // Get state
            let mut state = action
            .state()
            .expect("Could not get state.")
            .get::<i32>()
            .expect("The variant needs to be of type `i32`.");

            // Get parameter
            let parameter = parameter
                .expect("Could not get parameter.")
                .get::<i32>()
                .expect("The variant needs to be of type `i32`.");

            // Increase state by parameter and save state
            state += parameter;
            action.set_state(&state.to_variant());

            // Update label with new state
            label.set_label(&format!("Counter: {}", state));
        }));
        self.add_action(&action_count);

        // Add action "quit" to `window` taking no parameter
        let action_quit = SimpleAction::new("quit", None);

        action_quit.connect_activate(clone!(@weak self as window => move |_, _| {
            window.close();
        }));
        self.add_action(&action_quit);

        // Create action from key "sensitive-button" and add to action group "win"
        let action_sensitive_button = imp.settings.create_action("sensitive-button");
        self.add_action(&action_sensitive_button);

        // Create action from key "orientation" and add to action group "win"
        let action_orientation = imp.settings.create_action("orientation");
        self.add_action(&action_orientation);
    }

    fn bind_settings(&self) {
        // Get state
        let imp = imp::Window::from_instance(self);

        // Bind setting "sensitive-button" to "sensitive" property of `button`
        let button = imp.button.get();
        imp.settings
            .bind("sensitive-button", &button, "sensitive")
            .build();

        // Bind setting "orientation" to "orientation" property of `button`
        let gtk_box = imp.gtk_box.get();
        imp.settings
            .bind("orientation", &gtk_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.

Filename: listings/actions/7/window/imp.rs

use gio::Settings;
use glib::subclass::InitializingObject;
use gtk::prelude::*;
use gtk::subclass::prelude::*;
use gtk::{gio, glib};
use gtk::{Button, CompositeTemplate, Label};

// Object holding the state
#[derive(CompositeTemplate)]
#[template(file = "window.ui")]
pub struct Window {
    #[template_child]
    pub gtk_box: TemplateChild<gtk::Box>,
    #[template_child]
    pub button: TemplateChild<Button>,
    #[template_child]
    pub label: TemplateChild<Label>,
    pub settings: Settings,
}

impl Default for Window {
    fn default() -> Self {
        Window {
            gtk_box: TemplateChild::default(),
            button: TemplateChild::default(),
            label: TemplateChild::default(),
            settings: Settings::new("org.gtk-rs.example"),
        }
    }
}

// The central trait for subclassing a GObject
#[glib::object_subclass]
impl ObjectSubclass for Window {
    // `NAME` needs to match `class` attribute of template
    const NAME: &'static str = "MyGtkAppWindow";
    type Type = super::Window;
    type ParentType = gtk::ApplicationWindow;

    fn class_init(klass: &mut Self::Class) {
        Self::bind_template(klass);
    }

    fn instance_init(obj: &InitializingObject<Self>) {
        obj.init_template();
    }
}

// Trait shared by all GObjects
impl ObjectImpl for Window {
    fn constructed(&self, obj: &Self::Type) {
        // Call "constructed" on parent
        self.parent_constructed(obj);

        // Setup
        obj.add_actions();
        obj.bind_settings();
    }
}

// Trait shared by all widgets
impl WidgetImpl for Window {}

// Trait shared by all windows
impl WindowImpl for Window {}

// Trait shared by all application
impl 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.