fractal/components/pill/
mod.rs1use 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 #[property(get, set = Self::set_source, explicit_notify, nullable)]
45 source: BoundObject<PillSource>,
46 #[property(get, set = Self::set_activatable, explicit_notify)]
48 activatable: Cell<bool>,
49 #[property(get = Self::watched_safety_setting, set = Self::set_watched_safety_setting, builder(AvatarImageSafetySetting::default()))]
52 watched_safety_setting: PhantomData<AvatarImageSafetySetting>,
53 #[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 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 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 fn watched_safety_setting(&self) -> AvatarImageSafetySetting {
181 self.avatar.watched_safety_setting()
182 }
183
184 fn set_watched_safety_setting(&self, setting: AvatarImageSafetySetting) {
187 self.avatar.set_watched_safety_setting(setting);
188 }
189
190 fn watched_room(&self) -> Option<Room> {
192 self.avatar.watched_room()
193 }
194
195 fn set_watched_room(&self, room: Option<Room>) {
197 self.avatar.set_watched_room(room);
198 }
199
200 fn set_display_name(&self, label: &str) {
202 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 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 pub struct Pill(ObjectSubclass<imp::Pill>)
253 @extends gtk::Widget,
254 @implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget;
255}
256
257impl Pill {
258 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 (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}