Skip to main content

fractal/components/avatar/
overlapping.rs

1use adw::{prelude::*, subclass::prelude::*};
2use gtk::{gdk, gio, glib, glib::clone};
3use tracing::error;
4
5use super::{Avatar, AvatarData, crop_circle::CropCircle};
6
7/// Function to extract the avatar data from a supported `GObject`.
8type 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        /// The children containing the avatars.
22        children: RefCell<Vec<CropCircle>>,
23        /// The size of the avatars.
24        #[property(get, set = Self::set_avatar_size, explicit_notify)]
25        avatar_size: Cell<u32>,
26        /// The spacing between the avatars.
27        #[property(get, set = Self::set_spacing, explicit_notify)]
28        spacing: Cell<u32>,
29        /// The maximum number of avatars to display.
30        #[property(get = Self::max_avatars, set = Self::set_max_avatars)]
31        max_avatars: PhantomData<u32>,
32        slice_model: gtk::SliceListModel,
33        /// The method used to extract `AvatarData` from the items of the list
34        /// model, if any.
35        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            // The last avatar has no overlap.
87            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            // Hide the children in the a11y tree.
112            None
113        }
114    }
115
116    impl OverlappingAvatars {
117        /// Set the size of the avatars.
118        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            // Update the sizes of the avatars.
127            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        /// Compute the avatars overlap according to their size.
142        #[allow(clippy::cast_sign_loss)] // The result can only be positive.
143        fn overlap(&self) -> u32 {
144            let avatar_size = self.avatar_size.get();
145            // Make the overlap a little less than half the size of the avatar.
146            (f64::from(avatar_size) / 2.5) as u32
147        }
148
149        /// Compute the distance between the center of two avatars.
150        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        /// Set the spacing between the avatars.
158        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        /// The maximum number of avatars to display.
171        fn max_avatars(&self) -> u32 {
172            self.slice_model.size()
173        }
174
175        /// Set the maximum number of avatars to display.
176        fn set_max_avatars(&self, max_avatars: u32) {
177            self.slice_model.set_size(max_avatars);
178        }
179
180        /// Bind a `GListModel` to this list.
181        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        /// Handle when the items of the model changed.
192        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            // Make sure that only the last avatar is not cropped.
230            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    /// A horizontal list of overlapping avatars.
244    pub struct OverlappingAvatars(ObjectSubclass<imp::OverlappingAvatars>)
245        @extends gtk::Widget,
246        @implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget;
247}
248
249impl OverlappingAvatars {
250    /// Create an empty `OverlappingAvatars`.
251    pub fn new() -> Self {
252        glib::Object::new()
253    }
254
255    /// Bind a `GListModel` to this list.
256    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}