fractal/components/
scale_revealer.rs1use adw::{prelude::*, subclass::prelude::*};
2use gtk::{gdk, glib, glib::closure_local, graphene};
3use tracing::warn;
4
5const ANIMATION_DURATION: u32 = 250;
7
8mod imp {
9 use std::{
10 cell::{Cell, OnceCell, RefCell},
11 sync::LazyLock,
12 };
13
14 use glib::{clone, subclass::Signal};
15
16 use super::*;
17
18 #[derive(Debug, Default, glib::Properties)]
19 #[properties(wrapper_type = super::ScaleRevealer)]
20 pub struct ScaleRevealer {
21 #[property(get, set = Self::set_reveal_child, explicit_notify)]
23 reveal_child: Cell<bool>,
24 #[property(get, set = Self::set_source_widget, explicit_notify, nullable)]
26 source_widget: glib::WeakRef<gtk::Widget>,
27 source_widget_texture: RefCell<Option<gdk::Texture>>,
29 animation: OnceCell<adw::TimedAnimation>,
31 }
32
33 #[glib::object_subclass]
34 impl ObjectSubclass for ScaleRevealer {
35 const NAME: &'static str = "ScaleRevealer";
36 type Type = super::ScaleRevealer;
37 type ParentType = adw::Bin;
38 }
39
40 #[glib::derived_properties]
41 impl ObjectImpl for ScaleRevealer {
42 fn signals() -> &'static [Signal] {
43 static SIGNALS: LazyLock<Vec<Signal>> =
44 LazyLock::new(|| vec![Signal::builder("transition-done").build()]);
45 SIGNALS.as_ref()
46 }
47
48 fn constructed(&self) {
49 self.parent_constructed();
50
51 let obj = self.obj();
53 let target = adw::CallbackAnimationTarget::new(clone!(
54 #[weak]
55 obj,
56 move |_| {
57 obj.queue_draw();
59 }
60 ));
61 let animation = adw::TimedAnimation::new(&*obj, 0.0, 1.0, ANIMATION_DURATION, target);
62
63 animation.set_easing(adw::Easing::EaseOutQuart);
64 animation.connect_done(clone!(
65 #[weak(rename_to =imp)]
66 self,
67 move |_| {
68 let obj = imp.obj();
69
70 if !imp.reveal_child.get() {
71 if let Some(source_widget) = imp.source_widget.upgrade() {
72 source_widget.set_opacity(1.0);
75 }
76 obj.set_visible(false);
77 }
78
79 obj.emit_by_name::<()>("transition-done", &[]);
80 }
81 ));
82
83 self.animation
84 .set(animation)
85 .expect("animation is uninitialized");
86 obj.set_visible(false);
87 }
88 }
89
90 impl WidgetImpl for ScaleRevealer {
91 fn snapshot(&self, snapshot: >k::Snapshot) {
92 let obj = self.obj();
93 let Some(child) = obj.child() else {
94 return;
95 };
96
97 let progress = self.animation().value();
98 if (progress - 1.0).abs() < 0.0001 {
99 obj.snapshot_child(&child, snapshot);
101 return;
102 }
103
104 let source_bounds = self
105 .source_widget
106 .upgrade()
107 .and_then(|s| s.compute_bounds(&*obj))
108 .unwrap_or_else(|| {
109 warn!(
110 "The source widget bounds could not be calculated, using default bounds as fallback"
111 );
112 graphene::Rect::new(0.0, 0.0, 100.0, 100.0)
113 });
114 let rev_progress = (1.0 - progress).abs();
115
116 let x_scale = source_bounds.width() / obj.width() as f32;
117 let y_scale = source_bounds.height() / obj.height() as f32;
118
119 let x_scale = 1.0 + (x_scale - 1.0) * rev_progress as f32;
120 let y_scale = 1.0 + (y_scale - 1.0) * rev_progress as f32;
121
122 let x = source_bounds.x() * rev_progress as f32;
123 let y = source_bounds.y() * rev_progress as f32;
124
125 snapshot.translate(&graphene::Point::new(x, y));
126 snapshot.scale(x_scale, y_scale);
127
128 let borrowed_source_widget_texture = self.source_widget_texture.borrow();
129 let Some(source_widget_texture) = borrowed_source_widget_texture.as_ref() else {
130 warn!(
131 "Revealer animation failed: no source widget texture, using child snapshot as fallback"
132 );
133 obj.snapshot_child(&child, snapshot);
134 return;
135 };
136
137 if progress > 0.0 {
138 snapshot.push_cross_fade(progress);
140
141 source_widget_texture.snapshot(snapshot, obj.width().into(), obj.height().into());
142 snapshot.pop();
143
144 obj.snapshot_child(&child, snapshot);
145 snapshot.pop();
146 } else if progress <= 0.0 {
147 source_widget_texture.snapshot(snapshot, obj.width().into(), obj.height().into());
149 }
150 }
151 }
152
153 impl BinImpl for ScaleRevealer {}
154
155 impl ScaleRevealer {
156 fn animation(&self) -> &adw::TimedAnimation {
158 self.animation.get().expect("animation is initialized")
159 }
160
161 fn set_reveal_child(&self, reveal_child: bool) {
165 if self.reveal_child.get() == reveal_child {
166 return;
167 }
168 let obj = self.obj();
169
170 let animation = self.animation();
171 animation.set_value_from(animation.value());
172
173 if reveal_child {
174 animation.set_value_to(1.0);
175 obj.set_visible(true);
176
177 if let Some(source_widget) = self.source_widget.upgrade() {
178 let texture = render_widget_to_texture(&source_widget);
181 self.source_widget_texture.replace(texture);
182
183 source_widget.set_opacity(0.0);
186 } else {
187 self.source_widget_texture.replace(None);
188 }
189 } else {
190 animation.set_value_to(0.0);
191 }
192
193 self.reveal_child.set(reveal_child);
194
195 animation.play();
196
197 obj.notify_reveal_child();
198 }
199
200 fn set_source_widget(&self, source_widget: Option<>k::Widget>) {
203 if self.source_widget.upgrade().as_ref() == source_widget {
204 return;
205 }
206
207 self.source_widget.set(source_widget);
208 self.obj().notify_source_widget();
209 }
210 }
211}
212
213glib::wrapper! {
214 pub struct ScaleRevealer(ObjectSubclass<imp::ScaleRevealer>)
216 @extends gtk::Widget, adw::Bin,
217 @implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget;
218}
219
220impl ScaleRevealer {
221 pub fn new() -> Self {
222 glib::Object::new()
223 }
224
225 pub fn connect_transition_done<F: Fn(&Self) + 'static>(&self, f: F) -> glib::SignalHandlerId {
227 self.connect_closure(
228 "transition-done",
229 true,
230 closure_local!(move |obj: Self| {
231 f(&obj);
232 }),
233 )
234 }
235}
236
237impl Default for ScaleRevealer {
238 fn default() -> Self {
239 Self::new()
240 }
241}
242
243fn render_widget_to_texture(widget: &impl IsA<gtk::Widget>) -> Option<gdk::Texture> {
245 let widget_paintable = gtk::WidgetPaintable::new(Some(widget));
246 let snapshot = gtk::Snapshot::new();
247
248 widget_paintable.snapshot(
249 &snapshot,
250 widget_paintable.intrinsic_width().into(),
251 widget_paintable.intrinsic_height().into(),
252 );
253
254 let node = snapshot.to_node()?;
255 let native = widget.native()?;
256
257 Some(native.renderer()?.render_texture(node, None))
258}