Signals

GObject signals are a system for registering callbacks for specific events. For example, if we press on a button, the "clicked" signal will be emitted. The signal then takes care that all the registered callbacks will be executed.

gtk-rs provides convenience methods for registering callbacks. In our "Hello World" example we connected the "clicked" signal to a closure which sets the label of the button to "Hello World" as soon as it gets called.

Filename: listings/gobject_signals/1/main.rs

use gtk::prelude::*;
use gtk::{Application, ApplicationWindow, Button};

fn main() {
    // Create a new application
    let app = Application::new(Some("org.gtk.example"), Default::default());
    app.connect_activate(build_ui);

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

fn build_ui(application: &Application) {
    // Create a window
    let window = ApplicationWindow::new(application);

    // Set the window title
    window.set_title(Some("My GTK App"));

    // Create a button
    let button = Button::builder()
        .label("Press me!")
        .margin_top(12)
        .margin_bottom(12)
        .margin_start(12)
        .margin_end(12)
        .build();

    // Connect callback
    button.connect_clicked(move |button| {
        // Set the label to "Hello World!" after the button has been clicked on
        button.set_label("Hello World!");
    });

    // Add button
    window.set_child(Some(&button));
    window.present();
}

If we wanted to, we could have connected to it with the generic connect_local method.

Filename: listings/gobject_signals/2/main.rs

use gtk::prelude::*;
use gtk::{Application, ApplicationWindow, Button};

fn main() {
    // Create a new application
    let app = Application::new(Some("org.gtk.example"), Default::default());
    app.connect_activate(build_ui);

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

fn build_ui(application: &Application) {
    // Create a window
    let window = ApplicationWindow::new(application);

    // Set the window title
    window.set_title(Some("My GTK App"));

    // Create a button
    let button = Button::builder()
        .label("Press me!")
        .margin_top(12)
        .margin_bottom(12)
        .margin_start(12)
        .margin_end(12)
        .build();

    // Connect callback
    button
        .connect_local("clicked", false, move |args| {
            // Get the button from the arguments
            let button = args[0]
                .get::<Button>()
                .expect("The value needs to be of type `Button`.");
            // Set the label to "Hello World!" after the button has been clicked on
            button.set_label("Hello World!");
            None
        })
        .unwrap();

    // Add button
    window.set_child(Some(&button));
    window.present();
}

Similar to the generic way of accessing properties, the advantage of connect_local is that it also works with custom signals1.

Let us see how we can create our own signals. Again we do that by extending our CustomButton. First we override the necessary methods in ObjectImpl.

Filename: listings/gobject_signals/3/custom_button/imp.rs

use glib::subclass::Signal;
use glib::{BindingFlags, ParamFlags, ParamSpec, Value};
use gtk::glib;
use gtk::prelude::*;
use gtk::subclass::prelude::*;

use once_cell::sync::Lazy;
use std::cell::Cell;

// Object holding the state
#[derive(Default)]
pub struct CustomButton {
    number: Cell<i32>,
}

// The central trait for subclassing a GObject
#[glib::object_subclass]
impl ObjectSubclass for CustomButton {
    const NAME: &'static str = "MyGtkAppCustomButton";
    type Type = super::CustomButton;
    type ParentType = gtk::Button;
}

// Trait shared by all GObjects
impl ObjectImpl for CustomButton {
    fn signals() -> &'static [Signal] {
        static SIGNALS: Lazy<Vec<Signal>> = Lazy::new(|| {
            vec![Signal::builder(
                // Signal name
                "max-number-reached",
                // Types of the values which will be sent to the signal handler
                &[i32::static_type().into()],
                // Type of the value the signal handler sends back
                <()>::static_type().into(),
            )
            .build()]
        });
        SIGNALS.as_ref()
    }

    fn properties() -> &'static [ParamSpec] {
        static PROPERTIES: Lazy<Vec<ParamSpec>> = Lazy::new(|| {
            vec![ParamSpec::new_int(
                // Name
                "number",
                // Nickname
                "number",
                // Short description
                "number",
                // Minimum value
                i32::MIN,
                // Maximum value
                i32::MAX,
                // Default value
                0,
                // The property can be read and written to
                ParamFlags::READWRITE,
            )]
        });
        PROPERTIES.as_ref()
    }

    fn set_property(&self, _obj: &Self::Type, _id: usize, value: &Value, pspec: &ParamSpec) {
        match pspec.name() {
            "number" => {
                let input_number = value.get().unwrap();
                self.number.replace(input_number);
            }
            _ => unimplemented!(),
        }
    }

    fn property(&self, _obj: &Self::Type, _id: usize, pspec: &ParamSpec) -> Value {
        match pspec.name() {
            "number" => self.number.get().to_value(),
            _ => unimplemented!(),
        }
    }

    fn constructed(&self, obj: &Self::Type) {
        self.parent_constructed(obj);

        // Bind label to number
        // `SYNC_CREATE` ensures that the label will be immediately set
        obj.bind_property("number", obj, "label")
            .flags(BindingFlags::SYNC_CREATE)
            .build();
    }
}

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

static MAX_NUMBER: i32 = 8;
// Trait shared by all buttons
impl ButtonImpl for CustomButton {
    fn clicked(&self, button: &Self::Type) {
        let incremented_number = self.number.get() + 1;
        // If `number` reached `MAX_NUMBER`,
        // emit "max-number-reached" signal and set `number` back to 0
        if incremented_number == MAX_NUMBER {
            button
                .emit_by_name("max-number-reached", &[&incremented_number])
                .unwrap();
            button.set_property("number", &0).unwrap();
        } else {
            button.set_property("number", &incremented_number).unwrap();
        }
    }
}

// Please ignore this line
// It is only there to make mdbook happy
fn main() {}

The signal method is responsible for defining a set of signals. In our case, we only create a single signal named "max-number-reached". When naming our signal, we make sure to do that in kebab-case. When emitted, it sends a single i32 value and expects nothing in return.

We want the signal to be emitted, whenever number reaches MAX_NUMBER. Together with the signal we send the value number currently holds. After we did that, we set number back to 0.

Filename: listings/gobject_signals/3/custom_button/imp.rs

use glib::subclass::Signal;
use glib::{BindingFlags, ParamFlags, ParamSpec, Value};
use gtk::glib;
use gtk::prelude::*;
use gtk::subclass::prelude::*;

use once_cell::sync::Lazy;
use std::cell::Cell;

// Object holding the state
#[derive(Default)]
pub struct CustomButton {
    number: Cell<i32>,
}

// The central trait for subclassing a GObject
#[glib::object_subclass]
impl ObjectSubclass for CustomButton {
    const NAME: &'static str = "MyGtkAppCustomButton";
    type Type = super::CustomButton;
    type ParentType = gtk::Button;
}

// Trait shared by all GObjects
impl ObjectImpl for CustomButton {
    fn signals() -> &'static [Signal] {
        static SIGNALS: Lazy<Vec<Signal>> = Lazy::new(|| {
            vec![Signal::builder(
                // Signal name
                "max-number-reached",
                // Types of the values which will be sent to the signal handler
                &[i32::static_type().into()],
                // Type of the value the signal handler sends back
                <()>::static_type().into(),
            )
            .build()]
        });
        SIGNALS.as_ref()
    }

    fn properties() -> &'static [ParamSpec] {
        static PROPERTIES: Lazy<Vec<ParamSpec>> = Lazy::new(|| {
            vec![ParamSpec::new_int(
                // Name
                "number",
                // Nickname
                "number",
                // Short description
                "number",
                // Minimum value
                i32::MIN,
                // Maximum value
                i32::MAX,
                // Default value
                0,
                // The property can be read and written to
                ParamFlags::READWRITE,
            )]
        });
        PROPERTIES.as_ref()
    }

    fn set_property(&self, _obj: &Self::Type, _id: usize, value: &Value, pspec: &ParamSpec) {
        match pspec.name() {
            "number" => {
                let input_number = value.get().unwrap();
                self.number.replace(input_number);
            }
            _ => unimplemented!(),
        }
    }

    fn property(&self, _obj: &Self::Type, _id: usize, pspec: &ParamSpec) -> Value {
        match pspec.name() {
            "number" => self.number.get().to_value(),
            _ => unimplemented!(),
        }
    }

    fn constructed(&self, obj: &Self::Type) {
        self.parent_constructed(obj);

        // Bind label to number
        // `SYNC_CREATE` ensures that the label will be immediately set
        obj.bind_property("number", obj, "label")
            .flags(BindingFlags::SYNC_CREATE)
            .build();
    }
}

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

static MAX_NUMBER: i32 = 8;
// Trait shared by all buttons
impl ButtonImpl for CustomButton {
    fn clicked(&self, button: &Self::Type) {
        let incremented_number = self.number.get() + 1;
        // If `number` reached `MAX_NUMBER`,
        // emit "max-number-reached" signal and set `number` back to 0
        if incremented_number == MAX_NUMBER {
            button
                .emit_by_name("max-number-reached", &[&incremented_number])
                .unwrap();
            button.set_property("number", &0).unwrap();
        } else {
            button.set_property("number", &incremented_number).unwrap();
        }
    }
}

// Please ignore this line
// It is only there to make mdbook happy
fn main() {}

If we now press on the button, the number of its label increases until it reaches MAX_NUMBER. Then it emits the "max-number-reached" signal which we can nicely connect to. Whenever we now receive the "max-number-reached" signal, the accompanying number is printed to standard output.

Filename: listings/gobject_signals/3/main.rs

mod custom_button;

use custom_button::CustomButton;
use gtk::prelude::*;
use gtk::{Application, ApplicationWindow};

fn main() {
    // Create a new application
    let app = Application::new(Some("org.gtk.example"), Default::default());
    app.connect_activate(build_ui);

    // Run the application
    app.run();
}
fn build_ui(application: &Application) {
    // Create a window
    let window = ApplicationWindow::builder()
        .application(application)
        .title("My GTK App")
        .build();

    // Create a button
    let button = CustomButton::new();
    button.set_margin_top(12);
    button.set_margin_bottom(12);
    button.set_margin_start(12);
    button.set_margin_end(12);

    button
        .connect_local("max-number-reached", false, move |args| {
            // Get the number from the arguments
            // args[0] would return the `CustomButton` instance
            let number = args[1]
                .get::<i32>()
                .expect("The value needs to be of type `i32`.");
            println!("The maximum number {} has been reached", number);
            None
        })
        .unwrap();

    window.set_child(Some(&button));
    window.present();
}

You now know how to connect to every kind of signal and how to create your own. Custom signals are especially useful, if you want to notify consumers of your GObject that a certain event occurred.

1

If you want to connect from a different thread than the main thread, make sure to use connect instead of connect_local. However, that also means that your connected closure has to implement Send + Sync.