Skip to main content

glib/gobject/
binding_group.rs

1// Take a look at the license at the top of the repository in the LICENSE file.
2
3use std::{fmt, ptr};
4
5use crate::{
6    Binding, BindingFlags, BindingGroup, BoolError, Object, ParamSpec, Value, ffi, gobject_ffi,
7    object::ObjectRef, prelude::*, translate::*,
8};
9
10impl BindingGroup {
11    /// Creates a binding between `source_property` on the source object
12    /// and `target_property` on `target`. Whenever the `source_property`
13    /// is changed the `target_property` is updated using the same value.
14    /// The binding flag [`BindingFlags::SYNC_CREATE`][crate::BindingFlags::SYNC_CREATE] is automatically specified.
15    ///
16    /// See [`ObjectExt::bind_property()`][crate::prelude::ObjectExt::bind_property()] for more information.
17    /// ## `source_property`
18    /// the property on the source to bind
19    /// ## `target`
20    /// the target [`Object`][crate::Object]
21    /// ## `target_property`
22    /// the property on `target` to bind
23    /// ## `flags`
24    /// the flags used to create the [`Binding`][crate::Binding]
25    #[doc(alias = "bind_with_closures")]
26    pub fn bind<'a, O: ObjectType>(
27        &'a self,
28        source_property: &'a str,
29        target: &'a O,
30        target_property: &'a str,
31    ) -> BindingGroupBuilder<'a> {
32        BindingGroupBuilder::new(self, source_property, target, target_property)
33    }
34}
35
36type TransformFn = Option<Box<dyn Fn(&Binding, &Value) -> Option<Value> + Send + Sync + 'static>>;
37
38// rustdoc-stripper-ignore-next
39/// Builder for binding group bindings.
40#[must_use = "The builder must be built to be used"]
41pub struct BindingGroupBuilder<'a> {
42    group: &'a BindingGroup,
43    source_property: &'a str,
44    target: &'a ObjectRef,
45    target_property: &'a str,
46    flags: BindingFlags,
47    transform_to: TransformFn,
48    transform_from: TransformFn,
49}
50
51impl fmt::Debug for BindingGroupBuilder<'_> {
52    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
53        f.debug_struct("BindingGroupBuilder")
54            .field("group", &self.group)
55            .field("source_property", &self.source_property)
56            .field("target", &self.target)
57            .field("target_property", &self.target_property)
58            .field("flags", &self.flags)
59            .finish()
60    }
61}
62
63impl<'a> BindingGroupBuilder<'a> {
64    fn new(
65        group: &'a BindingGroup,
66        source_property: &'a str,
67        target: &'a impl ObjectType,
68        target_property: &'a str,
69    ) -> Self {
70        Self {
71            group,
72            source_property,
73            target: target.as_object_ref(),
74            target_property,
75            flags: BindingFlags::DEFAULT,
76            transform_to: None,
77            transform_from: None,
78        }
79    }
80
81    // rustdoc-stripper-ignore-next
82    /// Transform changed property values from the target object to the source object with the given closure.
83    pub fn transform_from<F: Fn(&Binding, &Value) -> Option<Value> + Send + Sync + 'static>(
84        self,
85        func: F,
86    ) -> Self {
87        Self {
88            transform_from: Some(Box::new(func)),
89            ..self
90        }
91    }
92
93    // rustdoc-stripper-ignore-next
94    /// Transform changed property values from the source object to the target object with the given closure.
95    pub fn transform_to<F: Fn(&Binding, &Value) -> Option<Value> + Send + Sync + 'static>(
96        self,
97        func: F,
98    ) -> Self {
99        Self {
100            transform_to: Some(Box::new(func)),
101            ..self
102        }
103    }
104
105    // rustdoc-stripper-ignore-next
106    /// Bind the properties with the given flags.
107    pub fn flags(self, flags: BindingFlags) -> Self {
108        Self { flags, ..self }
109    }
110
111    // rustdoc-stripper-ignore-next
112    /// Set the binding flags to [`BIDIRECTIONAL`][crate::BindingFlags::BIDIRECTIONAL].
113    pub fn bidirectional(mut self) -> Self {
114        self.flags |= crate::BindingFlags::BIDIRECTIONAL;
115        self
116    }
117
118    // rustdoc-stripper-ignore-next
119    /// Set the binding flags to [`SYNC_CREATE`][crate::BindingFlags::SYNC_CREATE].
120    pub fn sync_create(mut self) -> Self {
121        self.flags |= crate::BindingFlags::SYNC_CREATE;
122        self
123    }
124
125    // rustdoc-stripper-ignore-next
126    /// Set the binding flags to [`INVERT_BOOLEAN`][crate::BindingFlags::INVERT_BOOLEAN].
127    pub fn invert_boolean(mut self) -> Self {
128        self.flags |= crate::BindingFlags::INVERT_BOOLEAN;
129        self
130    }
131
132    // rustdoc-stripper-ignore-next
133    /// Establish the property binding.
134    ///
135    /// This fails if the provided properties do not exist.
136    pub fn try_build(self) -> Result<(), BoolError> {
137        unsafe extern "C" fn transform_to_trampoline(
138            binding: *mut gobject_ffi::GBinding,
139            from_value: *const gobject_ffi::GValue,
140            to_value: *mut gobject_ffi::GValue,
141            user_data: ffi::gpointer,
142        ) -> ffi::gboolean {
143            unsafe {
144                let transform_data =
145                    &*(user_data as *const (TransformFn, TransformFn, String, ParamSpec));
146
147                match (transform_data.0.as_ref().unwrap())(
148                &from_glib_borrow(binding),
149                &*(from_value as *const Value),
150            ) {
151                None => false,
152                Some(res) => {
153                    assert!(
154                        res.type_().is_a(transform_data.3.value_type()),
155                        "Target property {} expected type {} but transform_to function returned {}",
156                        transform_data.3.name(),
157                        transform_data.3.value_type(),
158                        res.type_()
159                    );
160                    *to_value = res.into_raw();
161                    true
162                }
163            }
164            .into_glib()
165            }
166        }
167
168        unsafe extern "C" fn transform_from_trampoline(
169            binding: *mut gobject_ffi::GBinding,
170            from_value: *const gobject_ffi::GValue,
171            to_value: *mut gobject_ffi::GValue,
172            user_data: ffi::gpointer,
173        ) -> ffi::gboolean {
174            unsafe {
175                let transform_data =
176                    &*(user_data as *const (TransformFn, TransformFn, String, ParamSpec));
177                let binding = from_glib_borrow(binding);
178
179                match (transform_data.1.as_ref().unwrap())(
180                &binding,
181                &*(from_value as *const Value),
182            ) {
183                None => false,
184                Some(res) => {
185                    let pspec_name = transform_data.2.clone();
186                    let source = binding.source().unwrap();
187                    let pspec = source.find_property(&pspec_name);
188                    assert!(pspec.is_some(), "Source object does not have a property {pspec_name}");
189                    let pspec = pspec.unwrap();
190
191                    assert!(
192                        res.type_().is_a(pspec.value_type()),
193                        "Source property {pspec_name} expected type {} but transform_from function returned {}",
194                        pspec.value_type(),
195                        res.type_()
196                    );
197                    *to_value = res.into_raw();
198                    true
199                }
200            }
201            .into_glib()
202            }
203        }
204
205        unsafe extern "C" fn free_transform_data(data: ffi::gpointer) {
206            unsafe {
207                let _ = Box::from_raw(data as *mut (TransformFn, TransformFn, String, ParamSpec));
208            }
209        }
210
211        let mut _source_property_name_cstr = None;
212        let source_property_name = match self.group.source() {
213            Some(source) => {
214                let source_property =
215                    source.find_property(self.source_property).ok_or_else(|| {
216                        bool_error!(
217                            "Source property {} on type {} not found",
218                            self.source_property,
219                            source.type_()
220                        )
221                    })?;
222
223                // This is NUL-terminated from the C side
224                source_property.name().as_ptr()
225            }
226            _ => {
227                // This is a Rust &str and needs to be NUL-terminated first
228                let source_property_name = std::ffi::CString::new(self.source_property).unwrap();
229                let source_property_name_ptr = source_property_name.as_ptr() as *const u8;
230                _source_property_name_cstr = Some(source_property_name);
231
232                source_property_name_ptr
233            }
234        };
235
236        unsafe {
237            let target: Object = from_glib_none(self.target.clone().to_glib_none().0);
238
239            let target_property = target.find_property(self.target_property).ok_or_else(|| {
240                bool_error!(
241                    "Target property {} on type {} not found",
242                    self.target_property,
243                    target.type_()
244                )
245            })?;
246
247            let target_property_name = target_property.name().as_ptr();
248
249            let have_transform_to = self.transform_to.is_some();
250            let have_transform_from = self.transform_from.is_some();
251            let transform_data = if have_transform_to || have_transform_from {
252                Box::into_raw(Box::new((
253                    self.transform_to,
254                    self.transform_from,
255                    String::from_glib_none(source_property_name as *const _),
256                    target_property,
257                )))
258            } else {
259                ptr::null_mut()
260            };
261
262            gobject_ffi::g_binding_group_bind_full(
263                self.group.to_glib_none().0,
264                source_property_name as *const _,
265                target.to_glib_none().0,
266                target_property_name as *const _,
267                self.flags.into_glib(),
268                if have_transform_to {
269                    Some(transform_to_trampoline)
270                } else {
271                    None
272                },
273                if have_transform_from {
274                    Some(transform_from_trampoline)
275                } else {
276                    None
277                },
278                transform_data as ffi::gpointer,
279                if transform_data.is_null() {
280                    None
281                } else {
282                    Some(free_transform_data)
283                },
284            );
285        }
286
287        Ok(())
288    }
289
290    // rustdoc-stripper-ignore-next
291    /// Similar to `try_build` but panics instead of failing.
292    pub fn build(self) {
293        self.try_build().unwrap()
294    }
295}
296
297#[cfg(test)]
298mod test {
299    use crate::{prelude::*, subclass::prelude::*};
300
301    #[test]
302    fn binding_without_source() {
303        let binding_group = crate::BindingGroup::new();
304
305        let source = TestObject::default();
306        let target = TestObject::default();
307
308        assert!(source.find_property("name").is_some());
309        binding_group
310            .bind("name", &target, "name")
311            .bidirectional()
312            .build();
313
314        binding_group.set_source(Some(&source));
315
316        source.set_name("test_source_name");
317        assert_eq!(source.name(), target.name());
318
319        target.set_name("test_target_name");
320        assert_eq!(source.name(), target.name());
321    }
322
323    #[test]
324    fn binding_with_source() {
325        let binding_group = crate::BindingGroup::new();
326
327        let source = TestObject::default();
328        let target = TestObject::default();
329
330        binding_group.set_source(Some(&source));
331
332        binding_group.bind("name", &target, "name").build();
333
334        source.set_name("test_source_name");
335        assert_eq!(source.name(), target.name());
336    }
337
338    #[test]
339    fn binding_to_transform() {
340        let binding_group = crate::BindingGroup::new();
341
342        let source = TestObject::default();
343        let target = TestObject::default();
344
345        binding_group.set_source(Some(&source));
346        binding_group
347            .bind("name", &target, "name")
348            .sync_create()
349            .transform_to(|_binding, value| {
350                let value = value.get::<&str>().unwrap();
351                Some(format!("{value} World").to_value())
352            })
353            .transform_from(|_binding, value| {
354                let value = value.get::<&str>().unwrap();
355                Some(format!("{value} World").to_value())
356            })
357            .build();
358
359        source.set_name("Hello");
360        assert_eq!(target.name(), "Hello World");
361    }
362
363    #[test]
364    fn binding_from_transform() {
365        let binding_group = crate::BindingGroup::new();
366
367        let source = TestObject::default();
368        let target = TestObject::default();
369
370        binding_group.set_source(Some(&source));
371        binding_group
372            .bind("name", &target, "name")
373            .sync_create()
374            .bidirectional()
375            .transform_to(|_binding, value| {
376                let value = value.get::<&str>().unwrap();
377                Some(format!("{value} World").to_value())
378            })
379            .transform_from(|_binding, value| {
380                let value = value.get::<&str>().unwrap();
381                Some(format!("{value} World").to_value())
382            })
383            .build();
384
385        target.set_name("Hello");
386        assert_eq!(source.name(), "Hello World");
387    }
388
389    #[test]
390    fn binding_to_transform_change_type() {
391        let binding_group = crate::BindingGroup::new();
392
393        let source = TestObject::default();
394        let target = TestObject::default();
395
396        binding_group.set_source(Some(&source));
397        binding_group
398            .bind("name", &target, "enabled")
399            .sync_create()
400            .transform_to(|_binding, value| {
401                let value = value.get::<&str>().unwrap();
402                Some((value == "Hello").to_value())
403            })
404            .transform_from(|_binding, value| {
405                let value = value.get::<bool>().unwrap();
406                Some((if value { "Hello" } else { "World" }).to_value())
407            })
408            .build();
409
410        source.set_name("Hello");
411        assert!(target.enabled());
412
413        source.set_name("Hello World");
414        assert!(!target.enabled());
415    }
416
417    #[test]
418    fn binding_from_transform_change_type() {
419        let binding_group = crate::BindingGroup::new();
420
421        let source = TestObject::default();
422        let target = TestObject::default();
423
424        binding_group.set_source(Some(&source));
425        binding_group
426            .bind("name", &target, "enabled")
427            .sync_create()
428            .bidirectional()
429            .transform_to(|_binding, value| {
430                let value = value.get::<&str>().unwrap();
431                Some((value == "Hello").to_value())
432            })
433            .transform_from(|_binding, value| {
434                let value = value.get::<bool>().unwrap();
435                Some((if value { "Hello" } else { "World" }).to_value())
436            })
437            .build();
438
439        target.set_enabled(true);
440        assert_eq!(source.name(), "Hello");
441        target.set_enabled(false);
442        assert_eq!(source.name(), "World");
443    }
444
445    mod imp {
446        use std::{cell::RefCell, sync::OnceLock};
447
448        use super::*;
449        use crate as glib;
450
451        #[derive(Debug, Default)]
452        pub struct TestObject {
453            pub name: RefCell<String>,
454            pub enabled: RefCell<bool>,
455        }
456
457        #[crate::object_subclass]
458        impl ObjectSubclass for TestObject {
459            const NAME: &'static str = "TestBindingGroup";
460            type Type = super::TestObject;
461        }
462
463        impl ObjectImpl for TestObject {
464            fn properties() -> &'static [crate::ParamSpec] {
465                static PROPERTIES: OnceLock<Vec<crate::ParamSpec>> = OnceLock::new();
466                PROPERTIES.get_or_init(|| {
467                    vec![
468                        crate::ParamSpecString::builder("name")
469                            .explicit_notify()
470                            .build(),
471                        crate::ParamSpecBoolean::builder("enabled")
472                            .explicit_notify()
473                            .build(),
474                    ]
475                })
476            }
477
478            fn property(&self, _id: usize, pspec: &crate::ParamSpec) -> crate::Value {
479                let obj = self.obj();
480                match pspec.name() {
481                    "name" => obj.name().to_value(),
482                    "enabled" => obj.enabled().to_value(),
483                    _ => unimplemented!(),
484                }
485            }
486
487            fn set_property(&self, _id: usize, value: &crate::Value, pspec: &crate::ParamSpec) {
488                let obj = self.obj();
489                match pspec.name() {
490                    "name" => obj.set_name(value.get().unwrap()),
491                    "enabled" => obj.set_enabled(value.get().unwrap()),
492                    _ => unimplemented!(),
493                };
494            }
495        }
496    }
497
498    crate::wrapper! {
499        pub struct TestObject(ObjectSubclass<imp::TestObject>);
500    }
501
502    impl Default for TestObject {
503        fn default() -> Self {
504            crate::Object::new()
505        }
506    }
507
508    impl TestObject {
509        fn name(&self) -> String {
510            self.imp().name.borrow().clone()
511        }
512
513        fn set_name(&self, name: &str) {
514            if name != self.imp().name.replace(name.to_string()).as_str() {
515                self.notify("name");
516            }
517        }
518
519        fn enabled(&self) -> bool {
520            *self.imp().enabled.borrow()
521        }
522
523        fn set_enabled(&self, enabled: bool) {
524            if enabled != self.imp().enabled.replace(enabled) {
525                self.notify("enabled");
526            }
527        }
528    }
529}