diff --git a/data/resources/style.css b/data/resources/style.css index 83d43d8e..7eecb8a4 100644 --- a/data/resources/style.css +++ b/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; diff --git a/src/components/crop_circle.rs b/src/components/crop_circle.rs new file mode 100644 index 00000000..6b1c0a50 --- /dev/null +++ b/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>, + /// Whether the child should be cropped. + #[property(get, set = Self::set_is_cropped, explicit_notify)] + pub is_cropped: Cell, + /// 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, + 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: >k::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) { + 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) + @extends gtk::Widget, @implements gtk::Accessible; +} + +impl CropCircle { + pub fn new() -> Self { + glib::Object::new() + } +} diff --git a/src/components/mod.rs b/src/components/mod.rs index 3fdeb923..307e7cd0 100644 --- a/src/components/mod.rs +++ b/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, diff --git a/src/components/overlapping_avatars.rs b/src/components/overlapping_avatars.rs index 2cf96951..0b765640 100644 --- a/src/components/overlapping_avatars.rs +++ b/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>, + /// The children containing the avatars. + pub children: RefCell>, /// The size of the avatars. #[property(get, set = Self::set_avatar_size, explicit_notify)] pub avatar_size: Cell, + /// The spacing between the avatars. + #[property(get, set = Self::set_spacing, explicit_notify)] + pub spacing: Cell, /// 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.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.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(); diff --git a/src/session/view/content/room_history/read_receipts_list/mod.ui b/src/session/view/content/room_history/read_receipts_list/mod.ui index b7eced0f..1619efb4 100644 --- a/src/session/view/content/room_history/read_receipts_list/mod.ui +++ b/src/session/view/content/room_history/read_receipts_list/mod.ui @@ -17,6 +17,7 @@ 20 + 1 10 diff --git a/src/session/view/content/room_history/typing_row.ui b/src/session/view/content/room_history/typing_row.ui index 378d7a33..178d6033 100644 --- a/src/session/view/content/room_history/typing_row.ui +++ b/src/session/view/content/room_history/typing_row.ui @@ -14,6 +14,7 @@ 30 + 2 10 presentation