fractal/session/view/sidebar/
room_row.rs

1use adw::{prelude::*, subclass::prelude::*};
2use gtk::{gdk, glib, glib::clone, CompositeTemplate};
3
4use super::SidebarRow;
5use crate::{
6    components::Avatar,
7    i18n::{gettext_f, ngettext_f},
8    prelude::*,
9    session::model::{HighlightFlags, Room, RoomCategory},
10    utils::BoundObject,
11};
12
13mod imp {
14    use std::cell::RefCell;
15
16    use glib::subclass::InitializingObject;
17
18    use super::*;
19
20    #[derive(Debug, Default, CompositeTemplate, glib::Properties)]
21    #[template(resource = "/org/gnome/Fractal/ui/session/view/sidebar/room_row.ui")]
22    #[properties(wrapper_type = super::SidebarRoomRow)]
23    pub struct SidebarRoomRow {
24        /// The room represented by this row.
25        #[property(get, set = Self::set_room, explicit_notify, nullable)]
26        room: BoundObject<Room>,
27        #[template_child]
28        avatar: TemplateChild<Avatar>,
29        #[template_child]
30        display_name_box: TemplateChild<gtk::Box>,
31        #[template_child]
32        display_name: TemplateChild<gtk::Label>,
33        #[template_child]
34        notification_count: TemplateChild<gtk::Label>,
35        direct_icon: RefCell<Option<gtk::Image>>,
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
47            klass.set_css_name("room");
48            klass.set_accessible_role(gtk::AccessibleRole::Group);
49        }
50
51        fn instance_init(obj: &InitializingObject<Self>) {
52            obj.init_template();
53        }
54    }
55
56    #[glib::derived_properties]
57    impl ObjectImpl for SidebarRoomRow {
58        fn constructed(&self) {
59            self.parent_constructed();
60
61            // Allow to drag rooms
62            let drag = gtk::DragSource::builder()
63                .actions(gdk::DragAction::MOVE)
64                .build();
65            drag.connect_prepare(clone!(
66                #[weak(rename_to = imp)]
67                self,
68                #[upgrade_or]
69                None,
70                move |drag, x, y| imp.prepare_drag(drag, x, y)
71            ));
72            drag.connect_drag_begin(clone!(
73                #[weak(rename_to = imp)]
74                self,
75                move |_, _| {
76                    imp.begin_drag();
77                }
78            ));
79            drag.connect_drag_end(clone!(
80                #[weak(rename_to = imp)]
81                self,
82                move |_, _, _| {
83                    imp.end_drag();
84                }
85            ));
86            self.obj().add_controller(drag);
87        }
88    }
89
90    impl WidgetImpl for SidebarRoomRow {}
91    impl BinImpl for SidebarRoomRow {}
92
93    impl SidebarRoomRow {
94        /// Set the room represented by this row.
95        fn set_room(&self, room: Option<Room>) {
96            if self.room.obj() == room {
97                return;
98            }
99
100            self.room.disconnect_signals();
101
102            if let Some(room) = room {
103                let highlight_handler = room.connect_highlight_notify(clone!(
104                    #[weak(rename_to = imp)]
105                    self,
106                    move |_| {
107                        imp.update_highlight();
108                    }
109                ));
110                let direct_handler = room.connect_is_direct_notify(clone!(
111                    #[weak(rename_to = imp)]
112                    self,
113                    move |_| {
114                        imp.update_direct_icon();
115                    }
116                ));
117                let name_handler = room.connect_display_name_notify(clone!(
118                    #[weak(rename_to = imp)]
119                    self,
120                    move |_| {
121                        imp.update_accessibility_label();
122                    }
123                ));
124                let notifications_count_handler = room.connect_notification_count_notify(clone!(
125                    #[weak(rename_to = imp)]
126                    self,
127                    move |_| {
128                        imp.update_accessibility_label();
129                    }
130                ));
131                let category_handler = room.connect_category_notify(clone!(
132                    #[weak(rename_to = imp)]
133                    self,
134                    move |_| {
135                        imp.update_display_name();
136                    }
137                ));
138
139                self.room.set(
140                    room,
141                    vec![
142                        highlight_handler,
143                        direct_handler,
144                        name_handler,
145                        notifications_count_handler,
146                        category_handler,
147                    ],
148                );
149
150                self.update_accessibility_label();
151            }
152
153            self.update_display_name();
154            self.update_highlight();
155            self.update_direct_icon();
156            self.obj().notify_room();
157        }
158
159        /// Update the display name of the room according to the current state.
160        fn update_display_name(&self) {
161            let Some(room) = self.room.obj() else {
162                return;
163            };
164
165            if matches!(room.category(), RoomCategory::Left) {
166                self.display_name.add_css_class("dimmed");
167            } else {
168                self.display_name.remove_css_class("dimmed");
169            }
170        }
171
172        /// Update how this row is highlighted according to the current state.
173        fn update_highlight(&self) {
174            if let Some(room) = self.room.obj() {
175                let flags = room.highlight();
176
177                if flags.contains(HighlightFlags::HIGHLIGHT) {
178                    self.notification_count.add_css_class("highlight");
179                } else {
180                    self.notification_count.remove_css_class("highlight");
181                }
182
183                if flags.contains(HighlightFlags::BOLD) {
184                    self.display_name.add_css_class("bold");
185                } else {
186                    self.display_name.remove_css_class("bold");
187                }
188            } else {
189                self.notification_count.remove_css_class("highlight");
190                self.display_name.remove_css_class("bold");
191            }
192        }
193
194        /// The parent `SidebarRow` of this row.
195        fn parent_row(&self) -> Option<SidebarRow> {
196            self.obj().parent().and_downcast()
197        }
198
199        /// Prepare a drag action.
200        fn prepare_drag(
201            &self,
202            drag: &gtk::DragSource,
203            x: f64,
204            y: f64,
205        ) -> Option<gdk::ContentProvider> {
206            let room = self.room.obj()?;
207
208            if let Some(parent) = self.parent_row() {
209                let paintable = gtk::WidgetPaintable::new(Some(&parent));
210                // FIXME: The hotspot coordinates don't work.
211                // See https://gitlab.gnome.org/GNOME/gtk/-/issues/2341
212                drag.set_icon(Some(&paintable), x as i32, y as i32);
213            }
214
215            Some(gdk::ContentProvider::for_value(&room.to_value()))
216        }
217
218        /// Begin a drag action.
219        fn begin_drag(&self) {
220            let Some(room) = self.room.obj() else {
221                return;
222            };
223            let Some(row) = self.parent_row() else {
224                return;
225            };
226            let Some(sidebar) = row.sidebar() else {
227                return;
228            };
229            row.add_css_class("drag");
230
231            sidebar.set_drop_source_category(Some(room.category()));
232        }
233
234        /// End a drag action.
235        fn end_drag(&self) {
236            let Some(row) = self.parent_row() else {
237                return;
238            };
239            let Some(sidebar) = row.sidebar() else {
240                return;
241            };
242            sidebar.set_drop_source_category(None);
243            row.remove_css_class("drag");
244        }
245
246        /// Update the icon showing whether a room is direct or not.
247        fn update_direct_icon(&self) {
248            let is_direct = self.room.obj().is_some_and(|room| room.is_direct());
249
250            if is_direct {
251                if self.direct_icon.borrow().is_none() {
252                    let icon = gtk::Image::builder()
253                        .icon_name("person-symbolic")
254                        .icon_size(gtk::IconSize::Normal)
255                        .css_classes(["dimmed"])
256                        .build();
257
258                    self.display_name_box.prepend(&icon);
259                    self.direct_icon.replace(Some(icon));
260                }
261            } else if let Some(icon) = self.direct_icon.take() {
262                self.display_name_box.remove(&icon);
263            }
264        }
265
266        /// Update the accessibility label of this row.
267        fn update_accessibility_label(&self) {
268            let Some(parent) = self.obj().parent() else {
269                return;
270            };
271            parent.update_property(&[gtk::accessible::Property::Label(&self.accessible_label())]);
272        }
273
274        /// Compute the accessibility label of this row.
275        fn accessible_label(&self) -> String {
276            let Some(room) = self.room.obj() else {
277                return String::new();
278            };
279
280            let name = if room.is_direct() {
281                gettext_f(
282                    // Translators: Do NOT translate the content between '{' and '}', this is a
283                    // variable name. Presented to screen readers when a
284                    // room is a direct chat with another user.
285                    "Direct chat with {name}",
286                    &[("name", &room.display_name())],
287                )
288            } else {
289                room.display_name()
290            };
291
292            if room.notification_count() > 0 {
293                let count = ngettext_f(
294                    // Translators: Do NOT translate the content between '{' and '}', this is a
295                    // variable name. Presented to screen readers when a room has notifications
296                    // for unread messages.
297                    "1 notification",
298                    "{count} notifications",
299                    room.notification_count() as u32,
300                    &[("count", &room.notification_count().to_string())],
301                );
302                format!("{name} {count}")
303            } else {
304                name
305            }
306        }
307    }
308}
309
310glib::wrapper! {
311    /// A sidebar row representing a room.
312    pub struct SidebarRoomRow(ObjectSubclass<imp::SidebarRoomRow>)
313        @extends gtk::Widget, adw::Bin, @implements gtk::Accessible;
314}
315
316impl SidebarRoomRow {
317    pub fn new() -> Self {
318        glib::Object::new()
319    }
320}
321
322impl Default for SidebarRoomRow {
323    fn default() -> Self {
324        Self::new()
325    }
326}