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    #[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            let transform_data =
144                &*(user_data as *const (TransformFn, TransformFn, String, ParamSpec));
145
146            match (transform_data.0.as_ref().unwrap())(
147                &from_glib_borrow(binding),
148                &*(from_value as *const Value),
149            ) {
150                None => false,
151                Some(res) => {
152                    assert!(
153                        res.type_().is_a(transform_data.3.value_type()),
154                        "Target property {} expected type {} but transform_to function returned {}",
155                        transform_data.3.name(),
156                        transform_data.3.value_type(),
157                        res.type_()
158                    );
159                    *to_value = res.into_raw();
160                    true
161                }
162            }
163            .into_glib()
164        }
165
166        unsafe extern "C" fn transform_from_trampoline(
167            binding: *mut gobject_ffi::GBinding,
168            from_value: *const gobject_ffi::GValue,
169            to_value: *mut gobject_ffi::GValue,
170            user_data: ffi::gpointer,
171        ) -> ffi::gboolean {
172            let transform_data =
173                &*(user_data as *const (TransformFn, TransformFn, String, ParamSpec));
174            let binding = from_glib_borrow(binding);
175
176            match (transform_data.1.as_ref().unwrap())(
177                &binding,
178                &*(from_value as *const Value),
179            ) {
180                None => false,
181                Some(res) => {
182                    let pspec_name = transform_data.2.clone();
183                    let source = binding.source().unwrap();
184                    let pspec = source.find_property(&pspec_name);
185                    assert!(pspec.is_some(), "Source object does not have a property {pspec_name}");
186                    let pspec = pspec.unwrap();
187
188                    assert!(
189                        res.type_().is_a(pspec.value_type()),
190                        "Source property {pspec_name} expected type {} but transform_from function returned {}",
191                        pspec.value_type(),
192                        res.type_()
193                    );
194                    *to_value = res.into_raw();
195                    true
196                }
197            }
198            .into_glib()
199        }
200
201        unsafe extern "C" fn free_transform_data(data: ffi::gpointer) {
202            let _ = Box::from_raw(data as *mut (TransformFn, TransformFn, String, ParamSpec));
203        }
204
205        let mut _source_property_name_cstr = None;
206        let source_property_name = if let Some(source) = self.group.source() {
207            let source_property = source.find_property(self.source_property).ok_or_else(|| {
208                bool_error!(
209                    "Source property {} on type {} not found",
210                    self.source_property,
211                    source.type_()
212                )
213            })?;
214
215            // This is NUL-terminated from the C side
216            source_property.name().as_ptr()
217        } else {
218            // This is a Rust &str and needs to be NUL-terminated first
219            let source_property_name = std::ffi::CString::new(self.source_property).unwrap();
220            let source_property_name_ptr = source_property_name.as_ptr() as *const u8;
221            _source_property_name_cstr = Some(source_property_name);
222
223            source_property_name_ptr
224        };
225
226        unsafe {
227            let target: Object = from_glib_none(self.target.clone().to_glib_none().0);
228
229            let target_property = target.find_property(self.target_property).ok_or_else(|| {
230                bool_error!(
231                    "Target property {} on type {} not found",
232                    self.target_property,
233                    target.type_()
234                )
235            })?;
236
237            let target_property_name = target_property.name().as_ptr();
238
239            let have_transform_to = self.transform_to.is_some();
240            let have_transform_from = self.transform_from.is_some();
241            let transform_data = if have_transform_to || have_transform_from {
242                Box::into_raw(Box::new((
243                    self.transform_to,
244                    self.transform_from,
245                    String::from_glib_none(source_property_name as *const _),
246                    target_property,
247                )))
248            } else {
249                ptr::null_mut()
250            };
251
252            gobject_ffi::g_binding_group_bind_full(
253                self.group.to_glib_none().0,
254                source_property_name as *const _,
255                target.to_glib_none().0,
256                target_property_name as *const _,
257                self.flags.into_glib(),
258                if have_transform_to {
259                    Some(transform_to_trampoline)
260                } else {
261                    None
262                },
263                if have_transform_from {
264                    Some(transform_from_trampoline)
265                } else {
266                    None
267                },
268                transform_data as ffi::gpointer,
269                if transform_data.is_null() {
270                    None
271                } else {
272                    Some(free_transform_data)
273                },
274            );
275        }
276
277        Ok(())
278    }
279
280    // rustdoc-stripper-ignore-next
281    /// Similar to `try_build` but panics instead of failing.
282    pub fn build(self) {
283        self.try_build().unwrap()
284    }
285}
286
287#[cfg(test)]
288mod test {
289    use crate::{prelude::*, subclass::prelude::*};
290
291    #[test]
292    fn binding_without_source() {
293        let binding_group = crate::BindingGroup::new();
294
295        let source = TestObject::default();
296        let target = TestObject::default();
297
298        assert!(source.find_property("name").is_some());
299        binding_group
300            .bind("name", &target, "name")
301            .bidirectional()
302            .build();
303
304        binding_group.set_source(Some(&source));
305
306        source.set_name("test_source_name");
307        assert_eq!(source.name(), target.name());
308
309        target.set_name("test_target_name");
310        assert_eq!(source.name(), target.name());
311    }
312
313    #[test]
314    fn binding_with_source() {
315        let binding_group = crate::BindingGroup::new();
316
317        let source = TestObject::default();
318        let target = TestObject::default();
319
320        binding_group.set_source(Some(&source));
321
322        binding_group.bind("name", &target, "name").build();
323
324        source.set_name("test_source_name");
325        assert_eq!(source.name(), target.name());
326    }
327
328    #[test]
329    fn binding_to_transform() {
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        binding_group
337            .bind("name", &target, "name")
338            .sync_create()
339            .transform_to(|_binding, value| {
340                let value = value.get::<&str>().unwrap();
341                Some(format!("{value} World").to_value())
342            })
343            .transform_from(|_binding, value| {
344                let value = value.get::<&str>().unwrap();
345                Some(format!("{value} World").to_value())
346            })
347            .build();
348
349        source.set_name("Hello");
350        assert_eq!(target.name(), "Hello World");
351    }
352
353    #[test]
354    fn binding_from_transform() {
355        let binding_group = crate::BindingGroup::new();
356
357        let source = TestObject::default();
358        let target = TestObject::default();
359
360        binding_group.set_source(Some(&source));
361        binding_group
362            .bind("name", &target, "name")
363            .sync_create()
364            .bidirectional()
365            .transform_to(|_binding, value| {
366                let value = value.get::<&str>().unwrap();
367                Some(format!("{value} World").to_value())
368            })
369            .transform_from(|_binding, value| {
370                let value = value.get::<&str>().unwrap();
371                Some(format!("{value} World").to_value())
372            })
373            .build();
374
375        target.set_name("Hello");
376        assert_eq!(source.name(), "Hello World");
377    }
378
379    #[test]
380    fn binding_to_transform_change_type() {
381        let binding_group = crate::BindingGroup::new();
382
383        let source = TestObject::default();
384        let target = TestObject::default();
385
386        binding_group.set_source(Some(&source));
387        binding_group
388            .bind("name", &target, "enabled")
389            .sync_create()
390            .transform_to(|_binding, value| {
391                let value = value.get::<&str>().unwrap();
392                Some((value == "Hello").to_value())
393            })
394            .transform_from(|_binding, value| {
395                let value = value.get::<bool>().unwrap();
396                Some((if value { "Hello" } else { "World" }).to_value())
397            })
398            .build();
399
400        source.set_name("Hello");
401        assert!(target.enabled());
402
403        source.set_name("Hello World");
404        assert!(!target.enabled());
405    }
406
407    #[test]
408    fn binding_from_transform_change_type() {
409        let binding_group = crate::BindingGroup::new();
410
411        let source = TestObject::default();
412        let target = TestObject::default();
413
414        binding_group.set_source(Some(&source));
415        binding_group
416            .bind("name", &target, "enabled")
417            .sync_create()
418            .bidirectional()
419            .transform_to(|_binding, value| {
420                let value = value.get::<&str>().unwrap();
421                Some((value == "Hello").to_value())
422            })
423            .transform_from(|_binding, value| {
424                let value = value.get::<bool>().unwrap();
425                Some((if value { "Hello" } else { "World" }).to_value())
426            })
427            .build();
428
429        target.set_enabled(true);
430        assert_eq!(source.name(), "Hello");
431        target.set_enabled(false);
432        assert_eq!(source.name(), "World");
433    }
434
435    mod imp {
436        use std::{cell::RefCell, sync::OnceLock};
437
438        use super::*;
439        use crate as glib;
440
441        #[derive(Debug, Default)]
442        pub struct TestObject {
443            pub name: RefCell<String>,
444            pub enabled: RefCell<bool>,
445        }
446
447        #[crate::object_subclass]
448        impl ObjectSubclass for TestObject {
449            const NAME: &'static str = "TestBindingGroup";
450            type Type = super::TestObject;
451        }
452
453        impl ObjectImpl for TestObject {
454            fn properties() -> &'static [crate::ParamSpec] {
455                static PROPERTIES: OnceLock<Vec<crate::ParamSpec>> = OnceLock::new();
456                PROPERTIES.get_or_init(|| {
457                    vec![
458                        crate::ParamSpecString::builder("name")
459                            .explicit_notify()
460                            .build(),
461                        crate::ParamSpecBoolean::builder("enabled")
462                            .explicit_notify()
463                            .build(),
464                    ]
465                })
466            }
467
468            fn property(&self, _id: usize, pspec: &crate::ParamSpec) -> crate::Value {
469                let obj = self.obj();
470                match pspec.name() {
471                    "name" => obj.name().to_value(),
472                    "enabled" => obj.enabled().to_value(),
473                    _ => unimplemented!(),
474                }
475            }
476
477            fn set_property(&self, _id: usize, value: &crate::Value, pspec: &crate::ParamSpec) {
478                let obj = self.obj();
479                match pspec.name() {
480                    "name" => obj.set_name(value.get().unwrap()),
481                    "enabled" => obj.set_enabled(value.get().unwrap()),
482                    _ => unimplemented!(),
483                };
484            }
485        }
486    }
487
488    crate::wrapper! {
489        pub struct TestObject(ObjectSubclass<imp::TestObject>);
490    }
491
492    impl Default for TestObject {
493        fn default() -> Self {
494            crate::Object::new()
495        }
496    }
497
498    impl TestObject {
499        fn name(&self) -> String {
500            self.imp().name.borrow().clone()
501        }
502
503        fn set_name(&self, name: &str) {
504            if name != self.imp().name.replace(name.to_string()).as_str() {
505                self.notify("name");
506            }
507        }
508
509        fn enabled(&self) -> bool {
510            *self.imp().enabled.borrow()
511        }
512
513        fn set_enabled(&self, enabled: bool) {
514            if enabled != self.imp().enabled.replace(enabled) {
515                self.notify("enabled");
516            }
517        }
518    }
519}