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