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