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