Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions book/listings/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,10 @@ path = "g_object_signals/1/main.rs"
name = "g_object_signals_2"
path = "g_object_signals/2/main.rs"

[[bin]]
name = "g_object_signals_3"
path = "g_object_signals/3/main.rs"

# g_object_subclassing
[[bin]]
name = "g_object_subclassing_1"
Expand Down
47 changes: 47 additions & 0 deletions book/listings/g_object_signals/3/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
mod tracked_button;

use glib::closure_local;
use gtk::prelude::*;
use gtk::{Application, ApplicationWindow, glib};
use tracked_button::TrackedButton;

const APP_ID: &str = "org.gtk_rs.GObjectSignals3";

fn main() -> glib::ExitCode {
let app = Application::builder().application_id(APP_ID).build();
app.connect_activate(build_ui);
app.run()
}

// ANCHOR: handler
fn build_ui(app: &Application) {
let button = TrackedButton::new();
button.set_text("press me");
button.set_margin_top(12);
button.set_margin_bottom(12);
button.set_margin_start(12);
button.set_margin_end(12);

// The handler reacts to "text-changed" by setting a *new* text value.
// `set_text` borrows the same `RefCell` mutably. This only works because
// `clicked()` dropped its borrow before emitting the signal - if it had
// kept the borrow alive we would panic here at runtime with
// `BorrowMutError: already borrowed`.
button.connect_closure(
"text-changed",
false,
closure_local!(move |button: TrackedButton, current: &str| {
println!("saw text-changed: {current}");
button.set_text(&format!("{current}!"));
}),
);
// ANCHOR_END: handler

let window = ApplicationWindow::builder()
.application(app)
.title("My GTK App")
.child(&button)
.build();

window.present();
}
67 changes: 67 additions & 0 deletions book/listings/g_object_signals/3/tracked_button/imp.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
use std::cell::RefCell;
use std::sync::OnceLock;

use glib::Properties;
use glib::subclass::Signal;
use gtk::glib;
use gtk::prelude::*;
use gtk::subclass::prelude::*;

// Object holding the state
#[derive(Properties, Default)]
#[properties(wrapper_type = super::TrackedButton)]
pub struct TrackedButton {
#[property(get, set)]
text: RefCell<String>,
}

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

// Trait shared by all GObjects
#[glib::derived_properties]
impl ObjectImpl for TrackedButton {
fn signals() -> &'static [Signal] {
static SIGNALS: OnceLock<Vec<Signal>> = OnceLock::new();
SIGNALS.get_or_init(|| {
vec![
Signal::builder("text-changed")
.param_types([str::static_type()])
.build(),
]
})
}

fn constructed(&self) {
self.parent_constructed();

// Bind label to text so the visible text reflects the property.
let obj = self.obj();
obj.bind_property("text", obj.as_ref(), "label")
.sync_create()
.build();
}
}

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

// ANCHOR: clicked_good
// Trait shared by all buttons
impl ButtonImpl for TrackedButton {
fn clicked(&self) {
// GOOD: we read the value, drop the borrow, then emit.
// If a "text-changed" handler calls back into this object - for
// example by calling `set_text` - it can borrow `text` mutably
// because we are no longer holding the borrow.
let snapshot: String = self.text.borrow().clone();
self.obj()
.emit_by_name::<()>("text-changed", &[&snapshot]);
}
}
// ANCHOR_END: clicked_good
22 changes: 22 additions & 0 deletions book/listings/g_object_signals/3/tracked_button/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
mod imp;

use glib::Object;
use gtk::glib;

glib::wrapper! {
pub struct TrackedButton(ObjectSubclass<imp::TrackedButton>)
@extends gtk::Button, gtk::Widget,
@implements gtk::Accessible, gtk::Actionable, gtk::Buildable, gtk::ConstraintTarget;
}

impl TrackedButton {
pub fn new() -> Self {
Object::builder().build()
}
}

impl Default for TrackedButton {
fn default() -> Self {
Self::new()
}
}
28 changes: 28 additions & 0 deletions book/src/g_object_signals.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,5 +65,33 @@ Filename: <a class=file-link href="https://github.com/gtk-rs/gtk4-rs/blob/main/b
{{#rustdoc_include ../listings/g_object_signals/2/main.rs:signal_handling}}
```

## Don't keep locks while emitting signals

There is one rule that is easy to overlook when you start emitting signals from a custom GObject:
**never hold a `RefCell` borrow or a `Mutex` guard while emitting a signal, notifying a property change, or otherwise calling into code that might call back into the same object.**
The same rule applies to `Cell` whenever it is wrapping a non-`Copy` type that exposes a borrow API.
Signal handlers run synchronously inside the call to `emit_by_name`, and a handler is free to call any method on your object - including methods that need to take a borrow on the same `RefCell`. If the borrow you took before emitting is still alive, the second borrow will panic with `BorrowError` (or `BorrowMutError`), and a `Mutex` would deadlock.

The fix is to take only the data you need out of the cell or lock, drop the borrow, and only then emit:

Filename: <a class=file-link href="https://github.com/gtk-rs/gtk4-rs/blob/main/book/listings/g_object_signals/3/tracked_button/imp.rs">listings/g_object_signals/3/tracked_button/imp.rs</a>

```rust
{{#rustdoc_include ../listings/g_object_signals/3/tracked_button/imp.rs:clicked_good}}
```

The handler that comes with this listing intentionally calls back into the button by setting `text` from inside the `text-changed` handler:

Filename: <a class=file-link href="https://github.com/gtk-rs/gtk4-rs/blob/main/book/listings/g_object_signals/3/main.rs">listings/g_object_signals/3/main.rs</a>

```rust
{{#rustdoc_include ../listings/g_object_signals/3/main.rs:handler}}
```

The fact that this works without panicking is the load-bearing detail.
If `clicked` had emitted while still holding `self.text.borrow()`, the call to `set_text` inside the handler would have aborted the program with `BorrowMutError: already borrowed`.

Property notifications go through the same dispatch as signals, so the same rule applies to property change handlers and to anything else that ends up calling `notify` on the object.

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.
Loading