fractal/session_view/sidebar/
room_row.rs1use adw::{prelude::*, subclass::prelude::*};
2use gettextrs::gettext;
3use gtk::{gdk, glib, glib::clone};
4
5use super::SidebarRow;
6use crate::{
7 components::Avatar,
8 i18n::{gettext_f, ngettext_f},
9 prelude::*,
10 session::{HighlightFlags, Room, RoomCategory},
11 utils::{BoundObject, TemplateCallbacks},
12};
13
14mod imp {
15 use glib::subclass::InitializingObject;
16
17 use super::*;
18
19 #[derive(Debug, Default, gtk::CompositeTemplate, glib::Properties)]
20 #[template(resource = "/org/gnome/Fractal/ui/session_view/sidebar/room_row.ui")]
21 #[properties(wrapper_type = super::SidebarRoomRow)]
22 pub struct SidebarRoomRow {
23 #[template_child]
24 avatar: TemplateChild<Avatar>,
25 #[template_child]
26 display_name_box: TemplateChild<gtk::Box>,
27 #[template_child]
28 room_icon: TemplateChild<gtk::Image>,
29 #[template_child]
30 display_name: TemplateChild<gtk::Label>,
31 #[template_child]
32 notification_count: TemplateChild<gtk::Label>,
33 #[property(get, set = Self::set_room, explicit_notify, nullable)]
35 room: BoundObject<Room>,
36 }
37
38 #[glib::object_subclass]
39 impl ObjectSubclass for SidebarRoomRow {
40 const NAME: &'static str = "SidebarRoomRow";
41 type Type = super::SidebarRoomRow;
42 type ParentType = adw::Bin;
43
44 fn class_init(klass: &mut Self::Class) {
45 Self::bind_template(klass);
46 TemplateCallbacks::bind_template_callbacks(klass);
47
48 klass.set_css_name("room");
49 klass.set_accessible_role(gtk::AccessibleRole::Group);
50 }
51
52 fn instance_init(obj: &InitializingObject<Self>) {
53 obj.init_template();
54 }
55 }
56
57 #[glib::derived_properties]
58 impl ObjectImpl for SidebarRoomRow {
59 fn constructed(&self) {
60 self.parent_constructed();
61
62 let drag = gtk::DragSource::builder()
64 .actions(gdk::DragAction::MOVE)
65 .build();
66 drag.connect_prepare(clone!(
67 #[weak(rename_to = imp)]
68 self,
69 #[upgrade_or]
70 None,
71 move |drag, x, y| imp.prepare_drag(drag, x, y)
72 ));
73 drag.connect_drag_begin(clone!(
74 #[weak(rename_to = imp)]
75 self,
76 move |_, _| {
77 imp.begin_drag();
78 }
79 ));
80 drag.connect_drag_end(clone!(
81 #[weak(rename_to = imp)]
82 self,
83 move |_, _, _| {
84 imp.end_drag();
85 }
86 ));
87 self.obj().add_controller(drag);
88 }
89 }
90
91 impl WidgetImpl for SidebarRoomRow {}
92 impl BinImpl for SidebarRoomRow {}
93
94 impl SidebarRoomRow {
95 fn set_room(&self, room: Option<Room>) {
97 if self.room.obj() == room {
98 return;
99 }
100
101 self.room.disconnect_signals();
102
103 if let Some(room) = room {
104 let highlight_handler = room.connect_highlight_notify(clone!(
105 #[weak(rename_to = imp)]
106 self,
107 move |_| {
108 imp.update_highlight();
109 }
110 ));
111 let direct_handler = room.connect_is_direct_notify(clone!(
112 #[weak(rename_to = imp)]
113 self,
114 move |_| {
115 imp.update_room_icon();
116 imp.update_accessibility_label();
117 }
118 ));
119 let name_handler = room.connect_display_name_notify(clone!(
120 #[weak(rename_to = imp)]
121 self,
122 move |_| {
123 imp.update_accessibility_label();
124 }
125 ));
126 let notifications_count_handler = room.connect_notification_count_notify(clone!(
127 #[weak(rename_to = imp)]
128 self,
129 move |_| {
130 imp.update_accessibility_label();
131 }
132 ));
133 let category_handler = room.connect_category_notify(clone!(
134 #[weak(rename_to = imp)]
135 self,
136 move |_| {
137 imp.update_display_name();
138 }
139 ));
140
141 self.room.set(
142 room,
143 vec![
144 highlight_handler,
145 direct_handler,
146 name_handler,
147 notifications_count_handler,
148 category_handler,
149 ],
150 );
151
152 self.update_accessibility_label();
153 }
154
155 self.update_display_name();
156 self.update_highlight();
157 self.update_room_icon();
158 self.obj().notify_room();
159 }
160
161 fn update_display_name(&self) {
163 let Some(room) = self.room.obj() else {
164 return;
165 };
166
167 if matches!(room.category(), RoomCategory::Left) {
168 self.display_name.add_css_class("dimmed");
169 } else {
170 self.display_name.remove_css_class("dimmed");
171 }
172 }
173
174 fn update_highlight(&self) {
176 if let Some(room) = self.room.obj() {
177 let flags = room.highlight();
178
179 if flags.contains(HighlightFlags::HIGHLIGHT) {
180 self.notification_count.add_css_class("highlight");
181 } else {
182 self.notification_count.remove_css_class("highlight");
183 }
184
185 if flags.contains(HighlightFlags::BOLD) {
186 self.display_name.add_css_class("bold");
187 } else {
188 self.display_name.remove_css_class("bold");
189 }
190 } else {
191 self.notification_count.remove_css_class("highlight");
192 self.display_name.remove_css_class("bold");
193 }
194 }
195
196 fn parent_row(&self) -> Option<SidebarRow> {
198 self.obj().parent().and_downcast()
199 }
200
201 fn prepare_drag(
203 &self,
204 drag: >k::DragSource,
205 x: f64,
206 y: f64,
207 ) -> Option<gdk::ContentProvider> {
208 let room = self.room.obj()?;
209
210 if let Some(parent) = self.parent_row() {
211 let paintable = gtk::WidgetPaintable::new(Some(&parent));
212 drag.set_icon(Some(&paintable), x as i32, y as i32);
215 }
216
217 Some(gdk::ContentProvider::for_value(&room.to_value()))
218 }
219
220 fn begin_drag(&self) {
222 let Some(room) = self.room.obj() else {
223 return;
224 };
225 let Some(row) = self.parent_row() else {
226 return;
227 };
228 let Some(sidebar) = row.sidebar() else {
229 return;
230 };
231 row.add_css_class("drag");
232
233 sidebar.set_drop_source_category(Some(room.category()));
234 }
235
236 fn end_drag(&self) {
238 let Some(row) = self.parent_row() else {
239 return;
240 };
241 let Some(sidebar) = row.sidebar() else {
242 return;
243 };
244 sidebar.set_drop_source_category(None);
245 row.remove_css_class("drag");
246 }
247
248 fn update_room_icon(&self) {
250 let is_direct = self.room.obj().is_some_and(|room| room.is_direct());
251 let is_call = self.room.obj().is_some_and(|room| room.is_call());
252
253 if is_call {
254 self.room_icon.set_icon_name(Some("video-symbolic"));
255 self.room_icon.set_tooltip_text(Some(&gettext("Call room")));
258 self.room_icon.set_visible(true);
259 } else if is_direct {
260 self.room_icon.set_icon_name(Some("person-symbolic"));
261 self.room_icon
262 .set_tooltip_text(Some(&gettext("Direct chat")));
263 self.room_icon.set_visible(true);
264 } else {
265 self.room_icon.set_visible(false);
266 }
267 }
268
269 fn update_accessibility_label(&self) {
271 let Some(parent) = self.obj().parent() else {
272 return;
273 };
274 parent.update_property(&[gtk::accessible::Property::Label(&self.accessible_label())]);
275 }
276
277 fn accessible_label(&self) -> String {
279 let Some(room) = self.room.obj() else {
280 return String::new();
281 };
282
283 let name = if room.is_call() {
284 gettext_f(
285 "{name} (call room)",
290 &[("name", &room.display_name())],
291 )
292 } else if room.is_direct() {
293 gettext_f(
294 "Direct chat with {name}",
298 &[("name", &room.display_name())],
299 )
300 } else {
301 room.display_name()
302 };
303
304 if room.notification_count() > 0 {
305 let count = ngettext_f(
306 "1 notification",
310 "{count} notifications",
311 room.notification_count() as u32,
312 &[("count", &room.notification_count().to_string())],
313 );
314 format!("{name} {count}")
315 } else {
316 name
317 }
318 }
319 }
320}
321
322glib::wrapper! {
323 pub struct SidebarRoomRow(ObjectSubclass<imp::SidebarRoomRow>)
325 @extends gtk::Widget, adw::Bin,
326 @implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget;
327}
328
329impl SidebarRoomRow {
330 pub fn new() -> Self {
331 glib::Object::new()
332 }
333}
334
335impl Default for SidebarRoomRow {
336 fn default() -> Self {
337 Self::new()
338 }
339}