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