fractal/components/avatar/
overlapping.rs1use adw::{prelude::*, subclass::prelude::*};
2use gtk::{gdk, gio, glib, glib::clone};
3use tracing::error;
4
5use super::{Avatar, AvatarData, crop_circle::CropCircle};
6
7type ExtractAvatarDataFn = dyn Fn(&glib::Object) -> AvatarData + 'static;
9
10mod imp {
11 use std::{
12 cell::{Cell, RefCell},
13 marker::PhantomData,
14 };
15
16 use super::*;
17
18 #[derive(Default, glib::Properties)]
19 #[properties(wrapper_type = super::OverlappingAvatars)]
20 pub struct OverlappingAvatars {
21 children: RefCell<Vec<CropCircle>>,
23 #[property(get, set = Self::set_avatar_size, explicit_notify)]
25 avatar_size: Cell<u32>,
26 #[property(get, set = Self::set_spacing, explicit_notify)]
28 spacing: Cell<u32>,
29 #[property(get = Self::max_avatars, set = Self::set_max_avatars)]
31 max_avatars: PhantomData<u32>,
32 slice_model: gtk::SliceListModel,
33 extract_avatar_data_fn: RefCell<Option<Box<ExtractAvatarDataFn>>>,
36 }
37
38 #[glib::object_subclass]
39 impl ObjectSubclass for OverlappingAvatars {
40 const NAME: &'static str = "OverlappingAvatars";
41 type Type = super::OverlappingAvatars;
42 type ParentType = gtk::Widget;
43
44 fn class_init(klass: &mut Self::Class) {
45 klass.set_accessible_role(gtk::AccessibleRole::Img);
46 }
47 }
48
49 #[glib::derived_properties]
50 impl ObjectImpl for OverlappingAvatars {
51 fn constructed(&self) {
52 self.parent_constructed();
53
54 self.slice_model.connect_items_changed(clone!(
55 #[weak(rename_to = imp)]
56 self,
57 move |_, position, removed, added| {
58 imp.handle_items_changed(position, removed, added);
59 }
60 ));
61 }
62
63 fn dispose(&self) {
64 for child in self.children.take() {
65 child.unparent();
66 }
67 }
68 }
69
70 impl WidgetImpl for OverlappingAvatars {
71 fn measure(&self, orientation: gtk::Orientation, _for_size: i32) -> (i32, i32, i32, i32) {
72 if self.children.borrow().is_empty() {
73 return (0, 0, -1, -1);
74 }
75
76 let avatar_size = self.avatar_size.get();
77
78 if orientation == gtk::Orientation::Vertical {
79 let size = avatar_size.try_into().unwrap_or(i32::MAX);
80 return (size, size, -1, -1);
81 }
82
83 let n_children = u32::try_from(self.children.borrow().len())
84 .expect("count of children fits into u32");
85
86 let mut size = n_children.saturating_sub(1) * self.distance_between_centers();
88 size += avatar_size;
89
90 let size = size.try_into().unwrap_or(i32::MAX);
91 (size, size, -1, -1)
92 }
93
94 fn size_allocate(&self, _width: i32, _height: i32, _baseline: i32) {
95 let avatar_size = i32::try_from(self.avatar_size.get()).unwrap_or(i32::MAX);
96 let distance_between_centers = i32::try_from(self.distance_between_centers())
97 .expect("distance between centers fits into i32");
98
99 let mut x = 0;
100 for child in self.children.borrow().iter() {
101 let allocation = gdk::Rectangle::new(x, 0, avatar_size, avatar_size);
102 child.size_allocate(&allocation, -1);
103
104 x = x.saturating_add(distance_between_centers);
105 }
106 }
107 }
108
109 impl AccessibleImpl for OverlappingAvatars {
110 fn first_accessible_child(&self) -> Option<gtk::Accessible> {
111 None
113 }
114 }
115
116 impl OverlappingAvatars {
117 fn set_avatar_size(&self, size: u32) {
119 if self.avatar_size.get() == size {
120 return;
121 }
122 let obj = self.obj();
123
124 self.avatar_size.set(size);
125
126 let size = i32::try_from(size).unwrap_or(i32::MAX);
128 let overlap = self.overlap();
129 for child in self.children.borrow().iter() {
130 child.set_cropped_width(overlap);
131
132 if let Some(avatar) = child.child().and_downcast::<Avatar>() {
133 avatar.set_size(size);
134 }
135 }
136 obj.queue_resize();
137
138 obj.notify_avatar_size();
139 }
140
141 #[allow(clippy::cast_sign_loss)] fn overlap(&self) -> u32 {
144 let avatar_size = self.avatar_size.get();
145 (f64::from(avatar_size) / 2.5) as u32
147 }
148
149 fn distance_between_centers(&self) -> u32 {
151 self.avatar_size
152 .get()
153 .saturating_sub(self.overlap())
154 .saturating_add(self.spacing.get())
155 }
156
157 fn set_spacing(&self, spacing: u32) {
159 if self.spacing.get() == spacing {
160 return;
161 }
162
163 self.spacing.set(spacing);
164
165 let obj = self.obj();
166 obj.queue_resize();
167 obj.notify_avatar_size();
168 }
169
170 fn max_avatars(&self) -> u32 {
172 self.slice_model.size()
173 }
174
175 fn set_max_avatars(&self, max_avatars: u32) {
177 self.slice_model.set_size(max_avatars);
178 }
179
180 pub(super) fn bind_model<P: Fn(&glib::Object) -> AvatarData + 'static>(
182 &self,
183 model: Option<&gio::ListModel>,
184 extract_avatar_data_fn: P,
185 ) {
186 self.extract_avatar_data_fn
187 .replace(Some(Box::new(extract_avatar_data_fn)));
188 self.slice_model.set_model(model);
189 }
190
191 fn handle_items_changed(&self, position: u32, removed: u32, added: u32) {
193 let mut children = self.children.borrow_mut();
194 let prev_count = children.len();
195
196 let extract_avatar_data_fn_borrow = self.extract_avatar_data_fn.borrow();
197 let extract_avatar_data_fn = extract_avatar_data_fn_borrow
198 .as_ref()
199 .expect("extract avatar data fn should be set if model is set");
200
201 let avatar_size = i32::try_from(self.avatar_size.get()).unwrap_or(i32::MAX);
202 let cropped_width = self.overlap();
203 let obj = self.obj();
204
205 let added = (position..(position + added)).filter_map(|position| {
206 let Some(item) = self.slice_model.item(position) else {
207 error!("Could not get item in slice model at position {position}");
208 return None;
209 };
210
211 let avatar_data = extract_avatar_data_fn(&item);
212
213 let avatar = Avatar::new();
214 avatar.set_data(Some(avatar_data));
215 avatar.set_size(avatar_size);
216
217 let child = CropCircle::new();
218 child.set_child(Some(avatar));
219 child.set_cropped_width(cropped_width);
220 child.set_parent(&*obj);
221
222 Some(child)
223 });
224
225 for child in children.splice(position as usize..(position + removed) as usize, added) {
226 child.unparent();
227 }
228
229 let mut peekable_children = children.iter().peekable();
231 while let Some(child) = peekable_children.next() {
232 child.set_is_cropped(peekable_children.peek().is_some());
233 }
234
235 if prev_count != children.len() {
236 obj.queue_resize();
237 }
238 }
239 }
240}
241
242glib::wrapper! {
243 pub struct OverlappingAvatars(ObjectSubclass<imp::OverlappingAvatars>)
245 @extends gtk::Widget,
246 @implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget;
247}
248
249impl OverlappingAvatars {
250 pub fn new() -> Self {
252 glib::Object::new()
253 }
254
255 pub(crate) fn bind_model<P: Fn(&glib::Object) -> AvatarData + 'static>(
257 &self,
258 model: Option<&impl IsA<gio::ListModel>>,
259 extract_avatar_data_fn: P,
260 ) {
261 self.imp()
262 .bind_model(model.map(Cast::upcast_ref), extract_avatar_data_fn);
263 }
264}