Browse Source

overlapping-avatars: Crop avatars instead of using borders

Avoids issues when the background color behind the avatars changes.
fractal-7
Kévin Commaille 2 years ago
parent
commit
3568cf2028
No known key found for this signature in database
GPG Key ID: 29A48C1F03620416
  1. 17
      data/resources/style.css
  2. 172
      src/components/crop_circle.rs
  3. 2
      src/components/mod.rs
  4. 146
      src/components/overlapping_avatars.rs
  5. 1
      src/session/view/content/room_history/read_receipts_list/mod.ui
  6. 1
      src/session/view/content/room_history/typing_row.ui

17
data/resources/style.css

@ -336,6 +336,11 @@ row.button-row box.header {
min-height: 40px;
}
crop-circle > .mask {
background: black;
border-radius: 9999px;
}
/* Login */
@ -685,12 +690,6 @@ read-receipts-list:checked {
background-color: alpha(currentColor, .1);
}
read-receipts-list .cutout {
background-color: @view_bg_color;
border-radius: 999px;
padding: 1px;
}
.divider-row {
font-size: 0.9em;
font-weight: bold;
@ -730,12 +729,6 @@ typing-row {
font-style: italic;
}
typing-row .cutout {
background-color: @view_bg_color;
border-radius: 999px;
padding: 2px;
}
room-history-row .h1, .related-event-content .h1 {
font-weight: 800;
font-size: 15pt;

172
src/components/crop_circle.rs

@ -0,0 +1,172 @@
use adw::{prelude::*, subclass::prelude::*};
use gtk::{glib, graphene, gsk};
mod imp {
use std::cell::{Cell, RefCell};
use super::*;
#[derive(Debug, Default, glib::Properties)]
#[properties(wrapper_type = super::CropCircle)]
pub struct CropCircle {
/// The child widget to crop.
#[property(get, set = Self::set_child, explicit_notify, nullable)]
pub child: RefCell<Option<gtk::Widget>>,
/// Whether the child should be cropped.
#[property(get, set = Self::set_is_cropped, explicit_notify)]
pub is_cropped: Cell<bool>,
/// The width that should be cropped.
///
/// This is the number of pixels from the right edge of the child
/// widget.
#[property(get, set = Self::set_cropped_width, explicit_notify)]
pub cropped_width: Cell<u32>,
mask: adw::Bin,
}
#[glib::object_subclass]
impl ObjectSubclass for CropCircle {
const NAME: &'static str = "CropCircle";
type Type = super::CropCircle;
type ParentType = gtk::Widget;
fn class_init(klass: &mut Self::Class) {
klass.set_css_name("crop-circle");
}
}
#[glib::derived_properties]
impl ObjectImpl for CropCircle {
fn constructed(&self) {
self.parent_constructed();
self.mask.set_parent(&*self.obj());
self.mask.add_css_class("mask");
self.mask
.set_accessible_role(gtk::AccessibleRole::Presentation);
}
fn dispose(&self) {
if let Some(child) = self.child.take() {
child.unparent();
}
self.mask.unparent();
}
}
impl WidgetImpl for CropCircle {
fn measure(&self, orientation: gtk::Orientation, for_size: i32) -> (i32, i32, i32, i32) {
if let Some(child) = self.child.borrow().as_ref() {
return child.measure(orientation, for_size);
}
(0, 0, -1, -1)
}
fn size_allocate(&self, width: i32, height: i32, baseline: i32) {
let Some(child) = self.child.borrow().clone() else {
return;
};
child.allocate(width, height, baseline, None);
let (_, child_size) = child.preferred_size();
// The x position at the right edge of the child.
let mut x = (width + child_size.width()) / 2;
if self.is_cropped.get() {
x = x.saturating_sub(self.cropped_width.get() as i32);
}
let transform = gsk::Transform::new().translate(&graphene::Point::new(x as f32, 0.0));
self.mask.allocate(width, height, baseline, Some(transform));
}
fn snapshot(&self, snapshot: &gtk::Snapshot) {
let borrow = self.child.borrow();
let Some(child) = borrow.as_ref() else {
return;
};
let obj = self.obj();
if !self.is_cropped.get() || self.cropped_width.get() == 0 {
obj.snapshot_child(child, snapshot);
return;
}
snapshot.push_mask(gsk::MaskMode::InvertedAlpha);
obj.snapshot_child(&self.mask, snapshot);
snapshot.pop();
obj.snapshot_child(child, snapshot);
snapshot.pop();
}
}
impl CropCircle {
/// Set the child widget to crop.
fn set_child(&self, child: Option<gtk::Widget>) {
let prev_child = self.child.borrow().clone();
if prev_child == child {
return;
}
let obj = self.obj();
if let Some(child) = prev_child {
child.unparent();
}
if let Some(child) = &child {
child.set_parent(&*obj);
}
self.child.replace(child);
obj.queue_resize();
obj.notify_child();
}
/// Set whether the child widget should be cropped.
fn set_is_cropped(&self, is_cropped: bool) {
if self.is_cropped.get() == is_cropped {
return;
}
let obj = self.obj();
self.is_cropped.set(is_cropped);
obj.queue_allocate();
obj.notify_is_cropped();
}
/// Set the width that should be cropped.
fn set_cropped_width(&self, width: u32) {
if self.cropped_width.get() == width {
return;
}
let obj = self.obj();
self.cropped_width.set(width);
obj.queue_allocate();
obj.notify_cropped_width();
}
}
}
glib::wrapper! {
/// A widget that crops its child with a circle.
pub struct CropCircle(ObjectSubclass<imp::CropCircle>)
@extends gtk::Widget, @implements gtk::Accessible;
}
impl CropCircle {
pub fn new() -> Self {
glib::Object::new()
}
}

2
src/components/mod.rs

@ -3,6 +3,7 @@ mod audio_player;
mod auth_dialog;
mod avatar;
mod context_menu_bin;
mod crop_circle;
pub mod crypto;
mod custom_entry;
mod drag_overlay;
@ -36,6 +37,7 @@ pub use self::{
auth_dialog::{AuthDialog, AuthError},
avatar::{Avatar, AvatarData, AvatarImage, AvatarUriSource},
context_menu_bin::{ContextMenuBin, ContextMenuBinExt, ContextMenuBinImpl},
crop_circle::CropCircle,
custom_entry::CustomEntry,
drag_overlay::DragOverlay,
editable_avatar::EditableAvatar,

146
src/components/overlapping_avatars.rs

@ -1,7 +1,7 @@
use adw::prelude::*;
use gtk::{gdk, gio, glib, glib::clone, subclass::prelude::*};
use super::{Avatar, AvatarData};
use super::{Avatar, AvatarData, CropCircle};
use crate::utils::BoundObject;
/// Compute the overlap according to the child's size.
@ -20,11 +20,14 @@ mod imp {
#[derive(Default, glib::Properties)]
#[properties(wrapper_type = super::OverlappingAvatars)]
pub struct OverlappingAvatars {
/// The child avatars.
pub avatars: RefCell<Vec<adw::Bin>>,
/// The children containing the avatars.
pub children: RefCell<Vec<CropCircle>>,
/// The size of the avatars.
#[property(get, set = Self::set_avatar_size, explicit_notify)]
pub avatar_size: Cell<i32>,
/// The spacing between the avatars.
#[property(get, set = Self::set_spacing, explicit_notify)]
pub spacing: Cell<u32>,
/// The maximum number of avatars to display.
///
/// `0` means that all avatars are displayed.
@ -51,61 +54,48 @@ mod imp {
#[glib::derived_properties]
impl ObjectImpl for OverlappingAvatars {
fn dispose(&self) {
for avatar in self.avatars.take() {
avatar.unparent();
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) {
let mut size = 0;
// child_size = avatar_size + cutout_borders
let child_size = self.avatar_size.get() + 2;
if orientation == gtk::Orientation::Vertical {
if self.avatars.borrow().is_empty() {
return (0, 0, -1, 1);
} else {
return (child_size, child_size, -1, -1);
}
if self.children.borrow().is_empty() {
return (0, 0, -1, 1);
}
let overlap = overlap(child_size);
for avatar in self.avatars.borrow().iter() {
if !avatar.should_layout() {
continue;
}
let avatar_size = self.avatar_size.get();
size += child_size - overlap;
if orientation == gtk::Orientation::Vertical {
return (avatar_size, avatar_size, -1, -1);
}
// The last child doesn't have an overlap.
if size > 0 {
size += overlap;
}
let n_children = self.children.borrow().len() as i32;
let overlap = overlap(avatar_size);
let spacing = self.spacing.get() as i32;
// The last avatar has no overlap.
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 pos = 0;
// child_size = avatar_size + cutout_borders
let child_size = self.avatar_size.get() + 2;
let overlap = overlap(child_size);
for avatar in self.avatars.borrow().iter() {
if !avatar.should_layout() {
continue;
}
let mut next_pos = 0;
let avatar_size = self.avatar_size.get();
let overlap = overlap(avatar_size);
let spacing = self.spacing.get() as i32;
let x = pos;
pos += child_size - overlap;
for child in self.children.borrow().iter() {
let x = next_pos;
let allocation = gdk::Rectangle::new(x, 0, child_size, child_size);
let allocation = gdk::Rectangle::new(x, 0, avatar_size, avatar_size);
child.size_allocate(&allocation, -1);
avatar.size_allocate(&allocation, -1);
next_pos += avatar_size - overlap + spacing;
}
}
}
@ -126,18 +116,32 @@ mod imp {
let obj = self.obj();
self.avatar_size.set(size);
obj.notify_avatar_size();
// Update the sizes of the avatars.
for avatar in self
.avatars
.borrow()
.iter()
.filter_map(|bin| bin.child().and_downcast::<Avatar>())
{
avatar.set_size(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();
}
/// Set the spacing between the avatars.
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();
}
/// Set the maximum number of avatars to display.
@ -150,12 +154,19 @@ mod imp {
let obj = self.obj();
self.max_avatars.set(max_avatars);
if max_avatars != 0 && self.avatars.borrow().len() > max_avatars as usize {
if max_avatars != 0 && self.children.borrow().len() > max_avatars as usize {
// We have more children than we should, remove them.
let children = self.avatars.borrow_mut().split_off(max_avatars as usize);
for widget in children {
widget.unparent()
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;
@ -194,8 +205,8 @@ impl OverlappingAvatars {
let imp = self.imp();
imp.bound_model.disconnect_signals();
for avatar in imp.avatars.take() {
avatar.unparent();
for child in imp.children.take() {
child.unparent();
}
imp.extract_avatar_data_fn.take();
@ -232,18 +243,20 @@ impl OverlappingAvatars {
}
let imp = self.imp();
let mut avatars = imp.avatars.borrow_mut();
let avatar_size = self.avatar_size();
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 >= avatars.len() {
if position as usize >= children.len() {
break;
}
let avatar = avatars.remove(position as usize);
avatar.unparent();
let child = children.remove(position as usize);
child.unparent();
removed -= 1;
}
@ -259,13 +272,18 @@ impl OverlappingAvatars {
avatar.set_data(Some(avatar_data));
avatar.set_size(avatar_size);
let cutout = adw::Bin::builder()
.child(&avatar)
.css_classes(["cutout"])
.build();
cutout.set_parent(self);
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);
}
avatars.insert(i as usize, cutout);
// Make sure that only the last avatar is not cropped.
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();

1
src/session/view/content/room_history/read_receipts_list/mod.ui

@ -17,6 +17,7 @@
<child>
<object class="OverlappingAvatars" id="avatar_list">
<property name="avatar-size">20</property>
<property name="spacing">1</property>
<property name="max-avatars">10</property>
</object>
</child>

1
src/session/view/content/room_history/typing_row.ui

@ -14,6 +14,7 @@
<child>
<object class="OverlappingAvatars" id="avatar_list">
<property name="avatar-size">30</property>
<property name="spacing">2</property>
<property name="max-avatars">10</property>
<property name="accessible-role">presentation</property>
</object>

Loading…
Cancel
Save