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