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