fractal/session/view/sidebar/
room_row.rs1use 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 #[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 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 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 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 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 fn parent_row(&self) -> Option<SidebarRow> {
196 self.obj().parent().and_downcast()
197 }
198
199 fn prepare_drag(
201 &self,
202 drag: >k::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 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 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 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 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 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 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 "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 "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 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}