gtk4/
builder_rust_scope.rs
1use std::rc::Rc;
4
5use crate::{subclass::prelude::*, BuilderCScope, BuilderScope};
6
7glib::wrapper! {
8 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 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 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 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}