Skip to main content

fractal/components/loading/
button.rs

1use adw::{prelude::*, subclass::prelude::*};
2use gtk::{glib, pango};
3
4use super::LoadingBin;
5use crate::prelude::*;
6
7mod imp {
8    use std::marker::PhantomData;
9
10    use super::*;
11
12    #[derive(Debug, Default, glib::Properties)]
13    #[properties(wrapper_type = super::LoadingButton)]
14    pub struct LoadingButton {
15        loading_bin: LoadingBin,
16        /// The label of the content of the button.
17        ///
18        /// If an icon was set, it is removed.
19        #[property(get = Self::content_label, set = Self::set_content_label, explicit_notify)]
20        content_label: PhantomData<Option<glib::GString>>,
21        /// The name of the icon of the content of the button.
22        ///
23        /// If a label was set, it is removed.
24        #[property(get = Self::content_icon_name, set = Self::set_content_icon_name, explicit_notify)]
25        content_icon_name: PhantomData<Option<glib::GString>>,
26        /// Whether to display the loading spinner.
27        ///
28        /// If this is `false`, the text or icon will be displayed.
29        #[property(get = Self::is_loading, set = Self::set_is_loading, explicit_notify)]
30        is_loading: PhantomData<bool>,
31    }
32
33    #[glib::object_subclass]
34    impl ObjectSubclass for LoadingButton {
35        const NAME: &'static str = "LoadingButton";
36        type Type = super::LoadingButton;
37        type ParentType = gtk::Button;
38    }
39
40    #[glib::derived_properties]
41    impl ObjectImpl for LoadingButton {
42        fn constructed(&self) {
43            self.parent_constructed();
44
45            self.obj().set_child(Some(&self.loading_bin));
46        }
47    }
48
49    impl WidgetImpl for LoadingButton {}
50    impl ButtonImpl for LoadingButton {}
51
52    impl LoadingButton {
53        /// The label of the content of the button.
54        fn content_label(&self) -> Option<glib::GString> {
55            self.loading_bin
56                .child()
57                .and_downcast::<gtk::Label>()
58                .map(|l| l.label())
59                .filter(|s| !s.is_empty())
60        }
61
62        /// Set the label of the content of the button.
63        fn set_content_label(&self, label: &str) {
64            if self.content_label().as_deref() == Some(label) {
65                return;
66            }
67            let obj = self.obj();
68
69            let child_label = self.loading_bin.child_or_else::<gtk::Label>(|| {
70                let child_label = gtk::Label::builder()
71                    .ellipsize(pango::EllipsizeMode::End)
72                    .use_underline(true)
73                    .mnemonic_widget(&*obj)
74                    .css_classes(["text-button"])
75                    .build();
76
77                // In case it was an image before.
78                obj.remove_css_class("image-button");
79                obj.update_relation(&[gtk::accessible::Relation::LabelledBy(&[
80                    child_label.upcast_ref()
81                ])]);
82
83                child_label
84            });
85
86            child_label.set_label(label);
87
88            obj.notify_content_label();
89        }
90
91        /// The name of the icon of the content of the button.
92        fn content_icon_name(&self) -> Option<glib::GString> {
93            self.loading_bin
94                .child()
95                .and_downcast::<gtk::Image>()
96                .and_then(|i| i.icon_name())
97        }
98
99        /// Set the name of the icon of the content of the button.
100        fn set_content_icon_name(&self, icon_name: &str) {
101            if self.content_icon_name().as_deref() == Some(icon_name) {
102                return;
103            }
104            let obj = self.obj();
105
106            let child_image = self.loading_bin.child_or_else::<gtk::Image>(|| {
107                obj.add_css_class("image-button");
108
109                gtk::Image::builder()
110                    .accessible_role(gtk::AccessibleRole::Presentation)
111                    .build()
112            });
113
114            child_image.set_icon_name(Some(icon_name));
115
116            obj.notify_content_icon_name();
117        }
118
119        /// Whether to display the loading spinner.
120        ///
121        /// If this is `false`, the text will be displayed.
122        fn is_loading(&self) -> bool {
123            self.loading_bin.is_loading()
124        }
125
126        /// Set whether to display the loading spinner.
127        fn set_is_loading(&self, is_loading: bool) {
128            if self.is_loading() == is_loading {
129                return;
130            }
131            let obj = self.obj();
132
133            // The action should have been enabled or disabled so the sensitive
134            // state should update itself.
135            if obj.action_name().is_none() {
136                obj.set_sensitive(!is_loading);
137            }
138
139            self.loading_bin.set_is_loading(is_loading);
140
141            obj.notify_is_loading();
142        }
143    }
144}
145
146glib::wrapper! {
147    /// Button showing either a spinner or a label.
148    ///
149    /// Use the `content-label` and `content-icon-name` properties instead of `label` and
150    /// `icon-name` respectively, otherwise the spinner will not appear.
151    pub struct LoadingButton(ObjectSubclass<imp::LoadingButton>)
152        @extends gtk::Widget, gtk::Button,
153        @implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget, gtk::Actionable;
154}
155
156impl LoadingButton {
157    pub fn new() -> Self {
158        glib::Object::new()
159    }
160}
161
162impl Default for LoadingButton {
163    fn default() -> Self {
164        Self::new()
165    }
166}