fractal/components/rows/
combo_loading_row.rs

1use adw::{prelude::*, subclass::prelude::*};
2use gtk::{glib, glib::clone, pango, CompositeTemplate};
3
4use crate::{components::LoadingBin, utils::BoundObject};
5
6mod imp {
7    use std::{
8        cell::{Cell, RefCell},
9        marker::PhantomData,
10    };
11
12    use glib::subclass::InitializingObject;
13
14    use super::*;
15
16    #[derive(Debug, Default, CompositeTemplate, glib::Properties)]
17    #[template(resource = "/org/gnome/Fractal/ui/components/rows/combo_loading_row.ui")]
18    #[properties(wrapper_type = super::ComboLoadingRow)]
19    pub struct ComboLoadingRow {
20        #[template_child]
21        loading_bin: TemplateChild<LoadingBin>,
22        #[template_child]
23        popover: TemplateChild<gtk::Popover>,
24        #[template_child]
25        list: TemplateChild<gtk::ListBox>,
26        /// The string model to build the list.
27        #[property(get, set = Self::set_string_model, explicit_notify, nullable)]
28        string_model: BoundObject<gtk::StringList>,
29        /// The position of the selected string.
30        #[property(get, default = gtk::INVALID_LIST_POSITION)]
31        selected: Cell<u32>,
32        /// The selected string.
33        #[property(get, set = Self::set_selected_string, explicit_notify, nullable)]
34        selected_string: RefCell<Option<String>>,
35        /// Whether the row is loading.
36        #[property(get = Self::is_loading, set = Self::set_is_loading)]
37        is_loading: PhantomData<bool>,
38        /// Whether the row is read-only.
39        #[property(get, set = Self::set_read_only, explicit_notify)]
40        read_only: Cell<bool>,
41        selected_handlers: RefCell<Vec<glib::SignalHandlerId>>,
42    }
43
44    #[glib::object_subclass]
45    impl ObjectSubclass for ComboLoadingRow {
46        const NAME: &'static str = "ComboLoadingRow";
47        type Type = super::ComboLoadingRow;
48        type ParentType = adw::ActionRow;
49
50        fn class_init(klass: &mut Self::Class) {
51            Self::bind_template(klass);
52            Self::bind_template_callbacks(klass);
53
54            klass.set_accessible_role(gtk::AccessibleRole::ComboBox);
55        }
56
57        fn instance_init(obj: &InitializingObject<Self>) {
58            obj.init_template();
59        }
60    }
61
62    #[glib::derived_properties]
63    impl ObjectImpl for ComboLoadingRow {}
64
65    impl WidgetImpl for ComboLoadingRow {}
66    impl ListBoxRowImpl for ComboLoadingRow {}
67    impl PreferencesRowImpl for ComboLoadingRow {}
68
69    impl ActionRowImpl for ComboLoadingRow {
70        fn activate(&self) {
71            if !self.is_loading() {
72                self.popover.popup();
73            }
74        }
75    }
76
77    #[gtk::template_callbacks]
78    impl ComboLoadingRow {
79        /// Set the string model to build the list.
80        fn set_string_model(&self, model: Option<gtk::StringList>) {
81            if self.string_model.obj() == model {
82                return;
83            }
84            let obj = self.obj();
85
86            for handler in self.selected_handlers.take() {
87                obj.disconnect(handler);
88            }
89            self.string_model.disconnect_signals();
90
91            self.list.bind_model(
92                model.as_ref(),
93                clone!(
94                    #[weak]
95                    obj,
96                    #[upgrade_or_else]
97                    || { gtk::ListBoxRow::new().upcast() },
98                    move |item| {
99                        let Some(item) = item.downcast_ref::<gtk::StringObject>() else {
100                            return gtk::ListBoxRow::new().upcast();
101                        };
102
103                        let string = item.string();
104                        let child = gtk::Box::new(gtk::Orientation::Horizontal, 6);
105
106                        let label = gtk::Label::builder()
107                            .xalign(0.0)
108                            .ellipsize(pango::EllipsizeMode::End)
109                            .max_width_chars(40)
110                            .valign(gtk::Align::Center)
111                            .label(string)
112                            .build();
113                        child.append(&label);
114
115                        let icon = gtk::Image::builder()
116                            .accessible_role(gtk::AccessibleRole::Presentation)
117                            .icon_name("object-select-symbolic")
118                            .build();
119
120                        let selected_handler = obj.connect_selected_string_notify(clone!(
121                            #[weak]
122                            label,
123                            #[weak]
124                            icon,
125                            move |obj| {
126                                let is_selected =
127                                    obj.selected_string().is_some_and(|s| s == label.label());
128                                let opacity = if is_selected { 1.0 } else { 0.0 };
129                                icon.set_opacity(opacity);
130                            }
131                        ));
132                        obj.imp()
133                            .selected_handlers
134                            .borrow_mut()
135                            .push(selected_handler);
136
137                        let is_selected = obj.selected_string().is_some_and(|s| s == label.label());
138                        let opacity = if is_selected { 1.0 } else { 0.0 };
139                        icon.set_opacity(opacity);
140                        child.append(&icon);
141
142                        gtk::ListBoxRow::builder().child(&child).build().upcast()
143                    }
144                ),
145            );
146
147            if let Some(model) = model {
148                let items_changed_handler = model.connect_items_changed(clone!(
149                    #[weak(rename_to = imp)]
150                    self,
151                    move |_, _, _, _| {
152                        imp.update_selected();
153                    }
154                ));
155
156                self.string_model.set(model, vec![items_changed_handler]);
157            }
158
159            self.update_selected();
160            obj.notify_string_model();
161        }
162
163        /// Set whether the row is loading.
164        fn set_selected_string(&self, string: Option<String>) {
165            if *self.selected_string.borrow() == string {
166                return;
167            }
168            let obj = self.obj();
169
170            obj.set_subtitle(string.as_deref().unwrap_or_default());
171            self.selected_string.replace(string);
172
173            self.update_selected();
174            obj.notify_selected_string();
175        }
176
177        /// Update the position of the selected string.
178        fn update_selected(&self) {
179            let mut selected = gtk::INVALID_LIST_POSITION;
180
181            if let Some((string_model, selected_string)) = self
182                .string_model
183                .obj()
184                .zip(self.selected_string.borrow().clone())
185            {
186                for (pos, item) in string_model.iter::<glib::Object>().enumerate() {
187                    let Some(item) = item.ok().and_downcast::<gtk::StringObject>() else {
188                        // The iterator is broken.
189                        break;
190                    };
191
192                    if item.string() == selected_string {
193                        selected = pos as u32;
194                        break;
195                    }
196                }
197            }
198
199            if self.selected.get() == selected {
200                return;
201            }
202
203            self.selected.set(selected);
204            self.obj().notify_selected();
205        }
206
207        /// Whether the row is loading.
208        fn is_loading(&self) -> bool {
209            self.loading_bin.is_loading()
210        }
211
212        /// Set whether the row is loading.
213        fn set_is_loading(&self, loading: bool) {
214            if self.is_loading() == loading {
215                return;
216            }
217
218            self.loading_bin.set_is_loading(loading);
219            self.obj().notify_is_loading();
220        }
221
222        /// Set whether the row is read-only.
223        fn set_read_only(&self, read_only: bool) {
224            if self.read_only.get() == read_only {
225                return;
226            }
227            let obj = self.obj();
228
229            self.read_only.set(read_only);
230
231            obj.update_property(&[gtk::accessible::Property::ReadOnly(read_only)]);
232            obj.notify_read_only();
233        }
234
235        /// A row was activated.
236        #[template_callback]
237        fn row_activated(&self, row: &gtk::ListBoxRow) {
238            let Some(string) = row
239                .child()
240                .and_downcast::<gtk::Box>()
241                .and_then(|b| b.first_child())
242                .and_downcast::<gtk::Label>()
243                .map(|l| l.label())
244            else {
245                return;
246            };
247
248            self.popover.popdown();
249            self.set_selected_string(Some(string.into()));
250        }
251
252        /// The popover's visibility changed.
253        #[template_callback]
254        fn popover_visible(&self) {
255            let obj = self.obj();
256            let is_visible = self.popover.is_visible();
257
258            if is_visible {
259                obj.add_css_class("has-open-popup");
260            } else {
261                obj.remove_css_class("has-open-popup");
262            }
263        }
264    }
265}
266
267glib::wrapper! {
268    /// An `AdwActionRow` behaving like a combo box, with a loading state.
269    pub struct ComboLoadingRow(ObjectSubclass<imp::ComboLoadingRow>)
270        @extends gtk::Widget, gtk::ListBoxRow, adw::PreferencesRow, adw::ActionRow,
271        @implements gtk::Actionable, gtk::Accessible;
272}
273
274impl ComboLoadingRow {
275    pub fn new() -> Self {
276        glib::Object::new()
277    }
278}