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!("Revealer animation failed: no source widget texture, using child snapshot as fallback");
131 obj.snapshot_child(&child, snapshot);
132 return;
133 };
134
135 if progress > 0.0 {
136 snapshot.push_cross_fade(progress);
138
139 source_widget_texture.snapshot(snapshot, obj.width().into(), obj.height().into());
140 snapshot.pop();
141
142 obj.snapshot_child(&child, snapshot);
143 snapshot.pop();
144 } else if progress <= 0.0 {
145 source_widget_texture.snapshot(snapshot, obj.width().into(), obj.height().into());
147 }
148 }
149 }
150
151 impl BinImpl for ScaleRevealer {}
152
153 impl ScaleRevealer {
154 fn animation(&self) -> &adw::TimedAnimation {
156 self.animation.get().expect("animation is initialized")
157 }
158
159 fn set_reveal_child(&self, reveal_child: bool) {
163 if self.reveal_child.get() == reveal_child {
164 return;
165 }
166 let obj = self.obj();
167
168 let animation = self.animation();
169 animation.set_value_from(animation.value());
170
171 if reveal_child {
172 animation.set_value_to(1.0);
173 obj.set_visible(true);
174
175 if let Some(source_widget) = self.source_widget.upgrade() {
176 let texture = render_widget_to_texture(&source_widget);
179 self.source_widget_texture.replace(texture);
180
181 source_widget.set_opacity(0.0);
184 } else {
185 self.source_widget_texture.replace(None);
186 }
187 } else {
188 animation.set_value_to(0.0);
189 }
190
191 self.reveal_child.set(reveal_child);
192
193 animation.play();
194
195 obj.notify_reveal_child();
196 }
197
198 fn set_source_widget(&self, source_widget: Option<>k::Widget>) {
201 if self.source_widget.upgrade().as_ref() == source_widget {
202 return;
203 }
204
205 self.source_widget.set(source_widget);
206 self.obj().notify_source_widget();
207 }
208 }
209}
210
211glib::wrapper! {
212 pub struct ScaleRevealer(ObjectSubclass<imp::ScaleRevealer>)
214 @extends gtk::Widget, adw::Bin;
215}
216
217impl ScaleRevealer {
218 pub fn new() -> Self {
219 glib::Object::new()
220 }
221
222 pub fn connect_transition_done<F: Fn(&Self) + 'static>(&self, f: F) -> glib::SignalHandlerId {
224 self.connect_closure(
225 "transition-done",
226 true,
227 closure_local!(move |obj: Self| {
228 f(&obj);
229 }),
230 )
231 }
232}
233
234impl Default for ScaleRevealer {
235 fn default() -> Self {
236 Self::new()
237 }
238}
239
240fn render_widget_to_texture(widget: &impl IsA<gtk::Widget>) -> Option<gdk::Texture> {
242 let widget_paintable = gtk::WidgetPaintable::new(Some(widget));
243 let snapshot = gtk::Snapshot::new();
244
245 widget_paintable.snapshot(
246 &snapshot,
247 widget_paintable.intrinsic_width().into(),
248 widget_paintable.intrinsic_height().into(),
249 );
250
251 let node = snapshot.to_node()?;
252 let native = widget.native()?;
253
254 Some(native.renderer()?.render_texture(node, None))
255}