gtk4/
builder_rust_scope.rs

1// Take a look at the license at the top of the repository in the LICENSE file.
2
3use std::rc::Rc;
4
5use crate::{subclass::prelude::*, BuilderCScope, BuilderScope};
6
7glib::wrapper! {
8    // rustdoc-stripper-ignore-next
9    /// An implementation of [`BuilderScope`](crate::BuilderScope) that can bind Rust callbacks.
10    ///
11    /// ```no_run
12    /// # use gtk4 as gtk;
13    /// use gtk::prelude::*;
14    /// use gtk::subclass::prelude::*;
15    ///
16    /// # fn main() {
17    /// let builder = gtk::Builder::new();
18    /// let scope = gtk::BuilderRustScope::new();
19    /// scope.add_callback("print_label", |values| {
20    ///     let button = values[1].get::<gtk::Button>().unwrap();
21    ///     println!("{}", button.label().unwrap().as_str());
22    ///     None
23    /// });
24    /// builder.set_scope(Some(&scope));
25    ///
26    /// // can also be used with template_callbacks
27    /// pub struct Callbacks {}
28    /// #[gtk::template_callbacks]
29    /// impl Callbacks {
30    ///     #[template_callback]
31    ///     fn button_clicked(button: &gtk::Button) {
32    ///         button.set_label("Clicked");
33    ///     }
34    /// }
35    /// Callbacks::add_callbacks_to_scope(&scope);
36    /// # }
37    /// ```
38    pub struct BuilderRustScope(ObjectSubclass<imp::BuilderRustScope>)
39        @extends BuilderCScope,
40        @implements BuilderScope;
41}
42
43impl Default for BuilderRustScope {
44    fn default() -> Self {
45        Self::new()
46    }
47}
48
49impl BuilderRustScope {
50    pub fn new() -> Self {
51        glib::Object::new()
52    }
53    // rustdoc-stripper-ignore-next
54    /// Adds a Rust callback to the scope with the given `name`. The callback
55    /// can then be accessed from a [`Builder`](crate::Builder) by referring
56    /// to it in the builder XML, or by using
57    /// [`Builder::create_closure`](crate::Builder::create_closure).
58    pub fn add_callback<N: Into<String>, F: Fn(&[glib::Value]) -> Option<glib::Value> + 'static>(
59        &self,
60        name: N,
61        callback: F,
62    ) {
63        self.imp()
64            .callbacks
65            .borrow_mut()
66            .insert(name.into(), Rc::new(callback));
67    }
68}
69
70mod imp {
71    use std::{cell::RefCell, collections::HashMap};
72
73    use glib::{translate::*, Closure, RustClosure};
74
75    use super::*;
76    use crate::{prelude::*, Builder, BuilderClosureFlags, BuilderError};
77
78    type Callback = dyn Fn(&[glib::Value]) -> Option<glib::Value>;
79
80    #[derive(Default)]
81    pub struct BuilderRustScope {
82        pub callbacks: RefCell<HashMap<String, Rc<Callback>>>,
83    }
84
85    #[glib::object_subclass]
86    impl ObjectSubclass for BuilderRustScope {
87        const NAME: &'static str = "GtkBuilderRustScope";
88        type Type = super::BuilderRustScope;
89        type ParentType = BuilderCScope;
90        type Interfaces = (BuilderScope,);
91    }
92
93    impl ObjectImpl for BuilderRustScope {}
94    impl BuilderScopeImpl for BuilderRustScope {
95        fn type_from_function(&self, _builder: &Builder, _function_name: &str) -> glib::Type {
96            // Override the implementation provided by BuilderCScope and default to the
97            // interface default implementation
98            glib::Type::INVALID
99        }
100
101        fn create_closure(
102            &self,
103            builder: &Builder,
104            function_name: &str,
105            flags: BuilderClosureFlags,
106            object: Option<&glib::Object>,
107        ) -> Result<Closure, glib::Error> {
108            self.callbacks
109                .borrow()
110                .get(function_name)
111                .ok_or_else(|| {
112                    glib::Error::new(
113                        BuilderError::InvalidFunction,
114                        &format!("No function named `{function_name}`"),
115                    )
116                })
117                .map(|callback| {
118                    let callback = callback.clone();
119                    let swapped = flags.contains(BuilderClosureFlags::SWAPPED);
120                    let object = object.cloned().or_else(|| builder.current_object());
121                    if let Some(object) = object {
122                        // passing a pointer here is safe as `watch_closure` ensures we have a ref
123                        let object_ptr = object.as_ptr();
124                        let closure = if swapped {
125                            RustClosure::new_local(move |args| {
126                                let mut args = args.to_owned();
127                                let obj_v = unsafe {
128                                    let object: Borrowed<glib::Object> =
129                                        from_glib_borrow(object_ptr);
130                                    let mut v = glib::Value::uninitialized();
131                                    glib::gobject_ffi::g_value_init(
132                                        v.to_glib_none_mut().0,
133                                        object.type_().into_glib(),
134                                    );
135                                    glib::gobject_ffi::g_value_set_object(
136                                        v.to_glib_none_mut().0,
137                                        object.as_object_ref().to_glib_none().0,
138                                    );
139                                    v
140                                };
141                                args.push(obj_v);
142                                let len = args.len();
143                                args.swap(0, len - 1);
144                                callback(&args)
145                            })
146                        } else {
147                            RustClosure::new_local(move |args| {
148                                let mut args = args.to_owned();
149                                let obj_v = unsafe {
150                                    let object: Borrowed<glib::Object> =
151                                        from_glib_borrow(object_ptr);
152                                    let mut v = glib::Value::uninitialized();
153                                    glib::gobject_ffi::g_value_init(
154                                        v.to_glib_none_mut().0,
155                                        object.type_().into_glib(),
156                                    );
157                                    glib::gobject_ffi::g_value_set_object(
158                                        v.to_glib_none_mut().0,
159                                        object.as_object_ref().to_glib_none().0,
160                                    );
161                                    v
162                                };
163                                args.push(obj_v);
164                                callback(&args)
165                            })
166                        };
167                        object.watch_closure(closure.as_ref());
168                        closure.as_ref().clone()
169                    } else {
170                        if swapped {
171                            RustClosure::new_local(move |args| {
172                                let mut args = args.to_owned();
173                                if !args.is_empty() {
174                                    let len = args.len();
175                                    args.swap(0, len - 1);
176                                }
177                                callback(&args)
178                            })
179                        } else {
180                            RustClosure::new_local(move |args| callback(args))
181                        }
182                        .as_ref()
183                        .clone()
184                    }
185                })
186        }
187    }
188
189    impl BuilderCScopeImpl for BuilderRustScope {}
190}
191
192#[cfg(test)]
193mod tests {
194    use super::BuilderRustScope;
195    use crate::{self as gtk4, prelude::*, subclass::prelude::*, Builder};
196
197    const SIGNAL_XML: &str = r#"
198    <?xml version="1.0" encoding="UTF-8"?>
199    <interface>
200      <object class="GtkButton" id="button">
201        <property name="label">Hello World</property>
202        <signal name="clicked" handler="button_clicked"/>
203      </object>
204    </interface>
205    "#;
206
207    #[crate::test]
208    fn test_rust_builder_scope_signal_handler() {
209        use crate::Button;
210
211        pub struct Callbacks {}
212        #[template_callbacks]
213        impl Callbacks {
214            #[template_callback]
215            fn button_clicked(button: &Button) {
216                skip_assert_initialized!();
217                assert_eq!(button.label().unwrap().as_str(), "Hello World");
218                button.set_label("Clicked");
219            }
220        }
221
222        let builder = Builder::new();
223        let scope = BuilderRustScope::new();
224        Callbacks::add_callbacks_to_scope(&scope);
225        builder.set_scope(Some(&scope));
226        builder.add_from_string(SIGNAL_XML).unwrap();
227        let button = builder.object::<Button>("button").unwrap();
228        button.emit_clicked();
229        assert_eq!(button.label().unwrap().as_str(), "Clicked");
230    }
231
232    const CLOSURE_XML: &str = r#"
233    <?xml version="1.0" encoding="UTF-8"?>
234    <interface>
235      <object class="GtkEntry" id="entry_a"/>
236      <object class="GtkEntry" id="entry_b">
237        <binding name="text">
238          <closure type="gchararray" function="string_uppercase">
239            <lookup type="GtkEntry" name="text">entry_a</lookup>
240          </closure>
241        </binding>
242      </object>
243    </interface>
244    "#;
245
246    #[crate::test]
247    fn test_rust_builder_scope_closure() {
248        use crate::Entry;
249
250        pub struct StringCallbacks {}
251        #[template_callbacks]
252        impl StringCallbacks {
253            #[template_callback(function)]
254            fn uppercase(s: &str) -> String {
255                skip_assert_initialized!();
256                s.to_uppercase()
257            }
258        }
259
260        let builder = Builder::new();
261        let scope = BuilderRustScope::new();
262        StringCallbacks::add_callbacks_to_scope_prefixed(&scope, "string_");
263        builder.set_scope(Some(&scope));
264        builder.add_from_string(CLOSURE_XML).unwrap();
265        let entry_a = builder.object::<Entry>("entry_a").unwrap();
266        let entry_b = builder.object::<Entry>("entry_b").unwrap();
267        entry_a.set_text("Hello World");
268        assert_eq!(entry_b.text().as_str(), "HELLO WORLD");
269    }
270
271    #[allow(unused)]
272    #[should_panic(
273        expected = "Closure returned a value of type guint64 but caller expected gchararray"
274    )]
275    fn test_rust_builder_scope_closure_return_mismatch() {
276        use crate::Entry;
277
278        pub struct StringCallbacks {}
279        #[template_callbacks]
280        impl StringCallbacks {
281            #[template_callback(function, name = "uppercase")]
282            fn to_u64(s: &str) -> u64 {
283                skip_assert_initialized!();
284                s.parse().unwrap_or(0)
285            }
286        }
287
288        let builder = Builder::new();
289        let scope = BuilderRustScope::new();
290        StringCallbacks::add_callbacks_to_scope_prefixed(&scope, "string_");
291        builder.set_scope(Some(&scope));
292        builder.add_from_string(CLOSURE_XML).unwrap();
293        let entry_a = builder.object::<Entry>("entry_a").unwrap();
294        entry_a.set_text("Hello World");
295    }
296
297    const DISPOSE_XML: &str = r#"
298    <?xml version="1.0" encoding="UTF-8"?>
299    <interface>
300      <object class="MyObject" id="obj">
301        <signal name="destroyed" handler="my_object_destroyed" object="obj" swapped="true" />
302      </object>
303    </interface>
304    "#;
305
306    #[crate::test]
307    fn test_rust_builder_scope_object_during_dispose() {
308        use glib::subclass::Signal;
309        use std::sync::OnceLock;
310        use std::{cell::Cell, rc::Rc};
311
312        #[derive(Debug, Default)]
313        pub struct MyObjectPrivate {
314            counter: Rc<Cell<u64>>,
315        }
316        #[glib::object_subclass]
317        impl ObjectSubclass for MyObjectPrivate {
318            const NAME: &'static str = "MyObject";
319            type Type = MyObject;
320        }
321        impl ObjectImpl for MyObjectPrivate {
322            fn signals() -> &'static [Signal] {
323                static SIGNALS: OnceLock<Vec<Signal>> = OnceLock::new();
324                SIGNALS.get_or_init(|| vec![Signal::builder("destroyed").build()])
325            }
326            fn dispose(&self) {
327                self.obj().emit_by_name::<()>("destroyed", &[]);
328            }
329        }
330        glib::wrapper! {
331            pub struct MyObject(ObjectSubclass<MyObjectPrivate>);
332        }
333        #[template_callbacks]
334        impl MyObject {
335            #[template_callback]
336            fn my_object_destroyed(&self) {
337                self.imp().counter.set(self.imp().counter.get() + 1);
338            }
339        }
340
341        let counter = {
342            MyObject::static_type();
343            let builder = Builder::new();
344            let scope = BuilderRustScope::new();
345            MyObject::add_callbacks_to_scope(&scope);
346            builder.set_scope(Some(&scope));
347            builder.add_from_string(DISPOSE_XML).unwrap();
348            let obj = builder.object::<MyObject>("obj").unwrap();
349            obj.imp().counter.clone()
350        };
351        assert_eq!(counter.get(), 1);
352    }
353}