fractal/components/avatar/
crop_circle.rs

1use adw::{prelude::*, subclass::prelude::*};
2use gtk::{glib, graphene, gsk};
3
4mod imp {
5    use std::cell::{Cell, RefCell};
6
7    use super::*;
8
9    #[derive(Debug, Default, glib::Properties)]
10    #[properties(wrapper_type = super::CropCircle)]
11    pub struct CropCircle {
12        /// The child widget to crop.
13        #[property(get, set = Self::set_child, explicit_notify, nullable)]
14        child: RefCell<Option<gtk::Widget>>,
15        /// Whether the child should be cropped.
16        #[property(get, set = Self::set_is_cropped, explicit_notify)]
17        is_cropped: Cell<bool>,
18        /// The width that should be cropped.
19        ///
20        /// This is the number of pixels from the right edge of the child
21        /// widget.
22        #[property(get, set = Self::set_cropped_width, explicit_notify)]
23        cropped_width: Cell<u32>,
24        mask: adw::Bin,
25    }
26
27    #[glib::object_subclass]
28    impl ObjectSubclass for CropCircle {
29        const NAME: &'static str = "CropCircle";
30        type Type = super::CropCircle;
31        type ParentType = gtk::Widget;
32
33        fn class_init(klass: &mut Self::Class) {
34            klass.set_css_name("crop-circle");
35        }
36    }
37
38    #[glib::derived_properties]
39    impl ObjectImpl for CropCircle {
40        fn constructed(&self) {
41            self.parent_constructed();
42
43            self.mask.set_parent(&*self.obj());
44            self.mask.add_css_class("mask");
45            self.mask
46                .set_accessible_role(gtk::AccessibleRole::Presentation);
47        }
48
49        fn dispose(&self) {
50            if let Some(child) = self.child.take() {
51                child.unparent();
52            }
53
54            self.mask.unparent();
55        }
56    }
57
58    impl WidgetImpl for CropCircle {
59        fn measure(&self, orientation: gtk::Orientation, for_size: i32) -> (i32, i32, i32, i32) {
60            if let Some(child) = self.child.borrow().as_ref() {
61                return child.measure(orientation, for_size);
62            }
63
64            (0, 0, -1, -1)
65        }
66
67        fn size_allocate(&self, width: i32, height: i32, baseline: i32) {
68            let Some(child) = self.child.borrow().clone() else {
69                return;
70            };
71
72            child.allocate(width, height, baseline, None);
73
74            let (_, child_size) = child.preferred_size();
75
76            // The x position at the right edge of the child.
77            let mut x = width.midpoint(child_size.width());
78
79            if self.is_cropped.get() {
80                let cropped_width = self
81                    .cropped_width
82                    .get()
83                    .try_into()
84                    .expect("width fits into an i32");
85                x = x.saturating_sub(cropped_width);
86            }
87
88            let transform = gsk::Transform::new().translate(&graphene::Point::new(x as f32, 0.0));
89            self.mask.allocate(width, height, baseline, Some(transform));
90        }
91
92        fn snapshot(&self, snapshot: &gtk::Snapshot) {
93            let borrow = self.child.borrow();
94            let Some(child) = borrow.as_ref() else {
95                return;
96            };
97
98            let obj = self.obj();
99
100            if !self.is_cropped.get() || self.cropped_width.get() == 0 {
101                obj.snapshot_child(child, snapshot);
102                return;
103            }
104
105            snapshot.push_mask(gsk::MaskMode::InvertedAlpha);
106
107            obj.snapshot_child(&self.mask, snapshot);
108            snapshot.pop();
109
110            obj.snapshot_child(child, snapshot);
111            snapshot.pop();
112        }
113    }
114
115    impl CropCircle {
116        /// Set the child widget to crop.
117        fn set_child(&self, child: Option<gtk::Widget>) {
118            let prev_child = self.child.borrow().clone();
119
120            if prev_child == child {
121                return;
122            }
123            let obj = self.obj();
124
125            if let Some(child) = prev_child {
126                child.unparent();
127            }
128
129            if let Some(child) = &child {
130                child.set_parent(&*obj);
131            }
132
133            self.child.replace(child);
134
135            obj.queue_resize();
136            obj.notify_child();
137        }
138
139        /// Set whether the child widget should be cropped.
140        fn set_is_cropped(&self, is_cropped: bool) {
141            if self.is_cropped.get() == is_cropped {
142                return;
143            }
144            let obj = self.obj();
145
146            self.is_cropped.set(is_cropped);
147
148            obj.queue_allocate();
149            obj.notify_is_cropped();
150        }
151
152        /// Set the width that should be cropped.
153        fn set_cropped_width(&self, width: u32) {
154            if self.cropped_width.get() == width {
155                return;
156            }
157            let obj = self.obj();
158
159            self.cropped_width.set(width);
160
161            obj.queue_allocate();
162            obj.notify_cropped_width();
163        }
164    }
165}
166
167glib::wrapper! {
168    /// A widget that crops its child with a circle.
169    pub struct CropCircle(ObjectSubclass<imp::CropCircle>)
170        @extends gtk::Widget,
171        @implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget;
172}
173
174impl CropCircle {
175    pub fn new() -> Self {
176        glib::Object::new()
177    }
178}