use adw::prelude::*;
use gtk::{gdk, gio, glib, glib::clone, subclass::prelude::*};
use super::{crop_circle::CropCircle, Avatar, AvatarData};
use crate::utils::BoundObject;
fn overlap(for_size: i32) -> i32 {
(for_size as f64 / 2.5) as i32
}
pub type ExtractAvatarDataFn = dyn Fn(&glib::Object) -> AvatarData + 'static;
mod imp {
use std::cell::{Cell, RefCell};
use super::*;
#[derive(Default, glib::Properties)]
#[properties(wrapper_type = super::OverlappingAvatars)]
pub struct OverlappingAvatars {
pub children: RefCell<Vec<CropCircle>>,
#[property(get, set = Self::set_avatar_size, explicit_notify)]
pub avatar_size: Cell<i32>,
#[property(get, set = Self::set_spacing, explicit_notify)]
pub spacing: Cell<u32>,
#[property(get, set = Self::set_max_avatars, explicit_notify)]
pub max_avatars: Cell<u32>,
pub bound_model: BoundObject<gio::ListModel>,
pub extract_avatar_data_fn: RefCell<Option<Box<ExtractAvatarDataFn>>>,
}
#[glib::object_subclass]
impl ObjectSubclass for OverlappingAvatars {
const NAME: &'static str = "OverlappingAvatars";
type Type = super::OverlappingAvatars;
type ParentType = gtk::Widget;
fn class_init(klass: &mut Self::Class) {
klass.set_accessible_role(gtk::AccessibleRole::Img);
}
}
#[glib::derived_properties]
impl ObjectImpl for OverlappingAvatars {
fn dispose(&self) {
for child in self.children.take() {
child.unparent();
}
}
}
impl WidgetImpl for OverlappingAvatars {
fn measure(&self, orientation: gtk::Orientation, _for_size: i32) -> (i32, i32, i32, i32) {
if self.children.borrow().is_empty() {
return (0, 0, -1, 1);
}
let avatar_size = self.avatar_size.get();
if orientation == gtk::Orientation::Vertical {
return (avatar_size, avatar_size, -1, -1);
}
let n_children = self.children.borrow().len() as i32;
let overlap = overlap(avatar_size);
let spacing = self.spacing.get() as i32;
let mut size = (n_children - 1) * (avatar_size - overlap + spacing);
size += avatar_size;
(size, size, -1, -1)
}
fn size_allocate(&self, _width: i32, _height: i32, _baseline: i32) {
let mut next_pos = 0;
let avatar_size = self.avatar_size.get();
let overlap = overlap(avatar_size);
let spacing = self.spacing.get() as i32;
for child in self.children.borrow().iter() {
let x = next_pos;
let allocation = gdk::Rectangle::new(x, 0, avatar_size, avatar_size);
child.size_allocate(&allocation, -1);
next_pos += avatar_size - overlap + spacing;
}
}
}
impl AccessibleImpl for OverlappingAvatars {
fn first_accessible_child(&self) -> Option<gtk::Accessible> {
None
}
}
impl OverlappingAvatars {
fn set_avatar_size(&self, size: i32) {
if self.avatar_size.get() == size {
return;
}
let obj = self.obj();
self.avatar_size.set(size);
let overlap = overlap(size);
for child in self.children.borrow().iter() {
child.set_cropped_width(overlap as u32);
if let Some(avatar) = child.child().and_downcast::<Avatar>() {
avatar.set_size(size);
}
}
obj.queue_resize();
obj.notify_avatar_size();
}
fn set_spacing(&self, spacing: u32) {
if self.spacing.get() == spacing {
return;
}
self.spacing.set(spacing);
let obj = self.obj();
obj.queue_resize();
obj.notify_avatar_size();
}
fn set_max_avatars(&self, max_avatars: u32) {
let old_max_avatars = self.max_avatars.get();
if old_max_avatars == max_avatars {
return;
}
let obj = self.obj();
self.max_avatars.set(max_avatars);
if max_avatars != 0 && self.children.borrow().len() > max_avatars as usize {
let children = self.children.borrow_mut().split_off(max_avatars as usize);
for child in children {
child.unparent();
}
if let Some(child) = self.children.borrow().last() {
child.set_is_cropped(false);
}
obj.queue_resize();
} else if max_avatars == 0 || (old_max_avatars != 0 && max_avatars > old_max_avatars) {
let Some(model) = self.bound_model.obj() else {
return;
};
let diff = model.n_items() - old_max_avatars;
if diff > 0 {
obj.handle_items_changed(&model, old_max_avatars, 0, diff);
}
}
obj.notify_max_avatars();
}
}
}
glib::wrapper! {
pub struct OverlappingAvatars(ObjectSubclass<imp::OverlappingAvatars>)
@extends gtk::Widget, @implements gtk::Accessible;
}
impl OverlappingAvatars {
pub fn new() -> Self {
glib::Object::new()
}
pub fn bind_model<P: Fn(&glib::Object) -> AvatarData + 'static>(
&self,
model: Option<impl IsA<gio::ListModel>>,
extract_avatar_data_fn: P,
) {
let imp = self.imp();
imp.bound_model.disconnect_signals();
for child in imp.children.take() {
child.unparent();
}
imp.extract_avatar_data_fn.take();
let Some(model) = model else {
return;
};
let signal_handler_id = model.connect_items_changed(clone!(
#[weak(rename_to = obj)]
self,
move |model, position, removed, added| {
obj.handle_items_changed(model, position, removed, added)
}
));
imp.bound_model
.set(model.clone().upcast(), vec![signal_handler_id]);
imp.extract_avatar_data_fn
.replace(Some(Box::new(extract_avatar_data_fn)));
self.handle_items_changed(&model, 0, 0, model.n_items())
}
fn handle_items_changed(
&self,
model: &impl IsA<gio::ListModel>,
position: u32,
mut removed: u32,
added: u32,
) {
let max_avatars = self.max_avatars();
if max_avatars != 0 && position >= max_avatars {
return;
}
let imp = self.imp();
let mut children = imp.children.borrow_mut();
let extract_avatar_data_fn_borrow = imp.extract_avatar_data_fn.borrow();
let extract_avatar_data_fn = extract_avatar_data_fn_borrow.as_ref().unwrap();
let avatar_size = self.avatar_size();
let cropped_width = overlap(avatar_size) as u32;
while removed > 0 {
if position as usize >= children.len() {
break;
}
let child = children.remove(position as usize);
child.unparent();
removed -= 1;
}
for i in position..(position + added) {
if max_avatars != 0 && i >= max_avatars {
break;
}
let item = model.item(i).unwrap();
let avatar_data = extract_avatar_data_fn(&item);
let avatar = Avatar::new();
avatar.set_data(Some(avatar_data));
avatar.set_size(avatar_size);
let child = CropCircle::new();
child.set_child(Some(avatar));
child.set_cropped_width(cropped_width);
child.set_parent(self);
children.insert(i as usize, child);
}
let last_pos = children.len().saturating_sub(1);
for (i, child) in children.iter().enumerate() {
child.set_is_cropped(i != last_pos);
}
self.queue_resize();
}
}