fractal/components/pill/
mod.rs

1use adw::{prelude::*, subclass::prelude::*};
2use gtk::{glib, glib::clone};
3
4mod at_room;
5mod search_entry;
6mod source;
7mod source_row;
8
9pub use self::{
10    at_room::AtRoom,
11    search_entry::PillSearchEntry,
12    source::{PillSource, PillSourceExt, PillSourceImpl},
13    source_row::PillSourceRow,
14};
15use super::{Avatar, AvatarImageSafetySetting, RoomPreviewDialog, UserProfileDialog};
16use crate::{
17    prelude::*,
18    session::{Member, RemoteRoom, Room},
19    session_view::SessionView,
20    utils::{BoundObject, key_bindings},
21};
22
23mod imp {
24    use std::{
25        cell::{Cell, RefCell},
26        marker::PhantomData,
27    };
28
29    use glib::subclass::InitializingObject;
30
31    use super::*;
32
33    #[derive(Debug, Default, gtk::CompositeTemplate, glib::Properties)]
34    #[template(resource = "/org/gnome/Fractal/ui/components/pill/mod.ui")]
35    #[properties(wrapper_type = super::Pill)]
36    pub struct Pill {
37        #[template_child]
38        content: TemplateChild<gtk::Box>,
39        #[template_child]
40        display_name: TemplateChild<gtk::Label>,
41        #[template_child]
42        avatar: TemplateChild<Avatar>,
43        /// The source of the data displayed by this widget.
44        #[property(get, set = Self::set_source, explicit_notify, nullable)]
45        source: BoundObject<PillSource>,
46        /// Whether the pill can be activated.
47        #[property(get, set = Self::set_activatable, explicit_notify)]
48        activatable: Cell<bool>,
49        /// The safety setting to watch to decide whether the image of the
50        /// avatar should be displayed.
51        #[property(get = Self::watched_safety_setting, set = Self::set_watched_safety_setting, builder(AvatarImageSafetySetting::default()))]
52        watched_safety_setting: PhantomData<AvatarImageSafetySetting>,
53        /// The room to watch to apply the current safety settings.
54        ///
55        /// This is required if `watched_safety_setting` is not `None`.
56        #[property(get = Self::watched_room, set = Self::set_watched_room, nullable)]
57        watched_room: PhantomData<Option<Room>>,
58        gesture_click: RefCell<Option<gtk::GestureClick>>,
59    }
60
61    #[glib::object_subclass]
62    impl ObjectSubclass for Pill {
63        const NAME: &'static str = "Pill";
64        type Type = super::Pill;
65        type ParentType = gtk::Widget;
66
67        fn class_init(klass: &mut Self::Class) {
68            Self::bind_template(klass);
69
70            klass.set_layout_manager_type::<gtk::BinLayout>();
71            klass.set_css_name("inline-pill");
72
73            klass.install_action("pill.activate", None, |obj, _, _| {
74                obj.imp().activate();
75            });
76
77            key_bindings::add_activate_bindings(klass, "pill.activate");
78        }
79
80        fn instance_init(obj: &InitializingObject<Self>) {
81            obj.init_template();
82        }
83    }
84
85    #[glib::derived_properties]
86    impl ObjectImpl for Pill {
87        fn constructed(&self) {
88            self.parent_constructed();
89
90            self.update_activatable_state();
91        }
92
93        fn dispose(&self) {
94            self.content.unparent();
95        }
96    }
97
98    impl WidgetImpl for Pill {}
99
100    impl Pill {
101        /// Set the source of the data displayed by this widget.
102        fn set_source(&self, source: Option<PillSource>) {
103            if self.source.obj() == source {
104                return;
105            }
106
107            self.source.disconnect_signals();
108
109            if let Some(source) = source {
110                let display_name_handler = source.connect_disambiguated_name_notify(clone!(
111                    #[weak(rename_to = imp)]
112                    self,
113                    move |source| {
114                        imp.set_display_name(&source.disambiguated_name());
115                    }
116                ));
117                self.set_display_name(&source.disambiguated_name());
118
119                self.source.set(source, vec![display_name_handler]);
120            }
121
122            self.obj().notify_source();
123        }
124
125        /// Set whether this widget can be activated.
126        fn set_activatable(&self, activatable: bool) {
127            if self.activatable.get() == activatable {
128                return;
129            }
130            let obj = self.obj();
131
132            if let Some(gesture_click) = self.gesture_click.take() {
133                obj.remove_controller(&gesture_click);
134            }
135
136            self.activatable.set(activatable);
137
138            if activatable {
139                let gesture_click = gtk::GestureClick::new();
140
141                gesture_click.connect_released(clone!(
142                    #[weak(rename_to = imp)]
143                    self,
144                    move |_, _, _, _| {
145                        imp.activate();
146                    }
147                ));
148
149                obj.add_controller(gesture_click.clone());
150                self.gesture_click.replace(Some(gesture_click));
151            }
152
153            self.update_activatable_state();
154            obj.notify_activatable();
155        }
156
157        fn update_activatable_state(&self) {
158            let obj = self.obj();
159            let activatable = self.activatable.get();
160
161            obj.action_set_enabled("pill.activate", activatable);
162            obj.set_focusable(activatable);
163
164            let role = if activatable {
165                gtk::AccessibleRole::Link
166            } else {
167                gtk::AccessibleRole::Group
168            };
169            obj.set_accessible_role(role);
170
171            if activatable {
172                obj.add_css_class("activatable");
173            } else {
174                obj.remove_css_class("activatable");
175            }
176        }
177
178        /// The safety setting to watch to decide whether the image of the
179        /// avatar should be displayed.
180        fn watched_safety_setting(&self) -> AvatarImageSafetySetting {
181            self.avatar.watched_safety_setting()
182        }
183
184        /// Set the safety setting to watch to decide whether the image of the
185        /// avatar should be displayed.
186        fn set_watched_safety_setting(&self, setting: AvatarImageSafetySetting) {
187            self.avatar.set_watched_safety_setting(setting);
188        }
189
190        /// The room to watch to apply the current safety settings.
191        fn watched_room(&self) -> Option<Room> {
192            self.avatar.watched_room()
193        }
194
195        /// Set the room to watch to apply the current safety settings.
196        fn set_watched_room(&self, room: Option<Room>) {
197            self.avatar.set_watched_room(room);
198        }
199
200        /// Set the display name of this pill.
201        fn set_display_name(&self, label: &str) {
202            // We ellipsize the string manually because GtkTextView uses the minimum width.
203            // Show 30 characters max.
204            let mut maybe_ellipsized = label.chars().take(30).collect::<String>();
205
206            let is_ellipsized = maybe_ellipsized.len() < label.len();
207            if is_ellipsized {
208                maybe_ellipsized.append_ellipsis();
209            }
210
211            self.display_name.set_label(&maybe_ellipsized);
212        }
213
214        /// Activate the pill.
215        ///
216        /// This opens a known room or opens the profile of a user or unknown
217        /// room.
218        fn activate(&self) {
219            let Some(source) = self.source.obj() else {
220                return;
221            };
222            let obj = self.obj();
223
224            if let Some(member) = source.downcast_ref::<Member>() {
225                let dialog = UserProfileDialog::new();
226                dialog.set_room_member(member.clone());
227                dialog.present(Some(&*obj));
228            } else if let Some(room) = source.downcast_ref::<Room>() {
229                let Some(session_view) = obj
230                    .ancestor(SessionView::static_type())
231                    .and_downcast::<SessionView>()
232                else {
233                    return;
234                };
235
236                session_view.select_room(room.clone());
237            } else if let Some(room) = source.downcast_ref::<RemoteRoom>() {
238                let Some(session) = room.session() else {
239                    return;
240                };
241
242                let dialog = RoomPreviewDialog::new(&session);
243                dialog.set_room(room);
244                dialog.present(Some(&*obj));
245            }
246        }
247    }
248}
249
250glib::wrapper! {
251    /// Inline widget displaying an emphasized `PillSource`.
252    pub struct Pill(ObjectSubclass<imp::Pill>)
253        @extends gtk::Widget,
254        @implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget;
255}
256
257impl Pill {
258    /// Create a pill with the given source and watching the given safety
259    /// setting.
260    pub fn new(
261        source: &impl IsA<PillSource>,
262        watched_safety_setting: AvatarImageSafetySetting,
263        watched_room: Option<Room>,
264    ) -> Self {
265        let source = source.upcast_ref();
266
267        let (watched_safety_setting, watched_room) = if let Some(room) = source
268            .downcast_ref::<Room>()
269            .cloned()
270            .or_else(|| source.downcast_ref::<AtRoom>().map(AtRoom::room))
271        {
272            // We must always watch the invite avatars setting for local rooms.
273            (AvatarImageSafetySetting::InviteAvatars, Some(room))
274        } else {
275            (watched_safety_setting, watched_room)
276        };
277
278        glib::Object::builder()
279            .property("source", source)
280            .property("watched-safety-setting", watched_safety_setting)
281            .property("watched-room", watched_room)
282            .build()
283    }
284}