|
| 1 | +use adw::prelude::AnimationExt; |
| 2 | +use glib::clone; |
| 3 | +use gtk::prelude::*; |
| 4 | +use gtk::subclass::prelude::*; |
| 5 | +use gtk::{gdk, glib, graphene, gsk, CompositeTemplate}; |
| 6 | + |
| 7 | +const ROUNDING: f32 = 12.0; |
| 8 | + |
| 9 | +mod imp { |
| 10 | + use super::*; |
| 11 | + use std::cell::RefCell; |
| 12 | + |
| 13 | + #[derive(Debug, Default, CompositeTemplate)] |
| 14 | + #[template(string = r#" |
| 15 | + <interface> |
| 16 | + <template class="MediaViewer" parent="GtkWidget"> |
| 17 | + <property name="layout-manager"> |
| 18 | + <object class="GtkBinLayout"/> |
| 19 | + </property> |
| 20 | + <child> |
| 21 | + <object class="GtkHeaderBar" id="header_bar"> |
| 22 | + <property name="valign">start</property> |
| 23 | + <style> |
| 24 | + <class name="flat"/> |
| 25 | + </style> |
| 26 | + <child type="start"> |
| 27 | + <object class="GtkButton"> |
| 28 | + <property name="action-name">media-viewer.go-back</property> |
| 29 | + <property name="icon-name">go-previous-symbolic</property> |
| 30 | + </object> |
| 31 | + </child> |
| 32 | + </object> |
| 33 | + </child> |
| 34 | + </template> |
| 35 | + </interface> |
| 36 | + "#)] |
| 37 | + pub(crate) struct MediaViewer { |
| 38 | + pub(super) paintable: RefCell<Option<gdk::Paintable>>, |
| 39 | + pub(super) widget_bounds: RefCell<Option<graphene::Rect>>, |
| 40 | + pub(super) animation: RefCell<Option<adw::TimedAnimation>>, |
| 41 | + pub(super) target_widget: RefCell<Option<gtk::Widget>>, |
| 42 | + #[template_child] |
| 43 | + pub(super) header_bar: TemplateChild<gtk::HeaderBar>, |
| 44 | + } |
| 45 | + |
| 46 | + #[glib::object_subclass] |
| 47 | + impl ObjectSubclass for MediaViewer { |
| 48 | + const NAME: &'static str = "MediaViewer"; |
| 49 | + type Type = super::MediaViewer; |
| 50 | + type ParentType = gtk::Widget; |
| 51 | + |
| 52 | + fn class_init(klass: &mut Self::Class) { |
| 53 | + Self::bind_template(klass); |
| 54 | + |
| 55 | + klass.set_css_name("mediaviewer"); |
| 56 | + klass.install_action("media-viewer.go-back", None, move |widget, _, _| { |
| 57 | + widget.go_back(); |
| 58 | + }); |
| 59 | + } |
| 60 | + |
| 61 | + fn instance_init(obj: &glib::subclass::InitializingObject<Self>) { |
| 62 | + obj.init_template(); |
| 63 | + } |
| 64 | + } |
| 65 | + |
| 66 | + impl ObjectImpl for MediaViewer {} |
| 67 | + |
| 68 | + impl WidgetImpl for MediaViewer { |
| 69 | + fn snapshot(&self, widget: &Self::Type, snapshot: >k::Snapshot) { |
| 70 | + if let Some(paintable) = self.paintable.borrow().as_ref() { |
| 71 | + let widget_width = widget.width() as f64; |
| 72 | + let widget_height = widget.height() as f64; |
| 73 | + let widget_ratio = widget_width / widget_height; |
| 74 | + let paintable_ratio = paintable.intrinsic_aspect_ratio(); |
| 75 | + let perc = self.animation.borrow().as_ref().unwrap().value(); |
| 76 | + |
| 77 | + // Background color |
| 78 | + let background_color = gdk::RGBA::new(0.0, 0.0, 0.0, 1.0 * perc as f32); |
| 79 | + let bounds = |
| 80 | + graphene::Rect::new(0.0, 0.0, widget.width() as f32, widget.height() as f32); |
| 81 | + snapshot.append_color(&background_color, &bounds); |
| 82 | + |
| 83 | + // Target coords of the media |
| 84 | + let (target_x, target_y, target_width, target_height) = |
| 85 | + if widget_ratio > paintable_ratio { |
| 86 | + let paintable_width = widget_width * paintable_ratio / widget_ratio; |
| 87 | + ( |
| 88 | + (widget_width - paintable_width) / 2.0, |
| 89 | + 0.0, |
| 90 | + paintable_width, |
| 91 | + widget_height, |
| 92 | + ) |
| 93 | + } else { |
| 94 | + let paintable_height = widget_height / paintable_ratio * widget_ratio; |
| 95 | + ( |
| 96 | + 0.0, |
| 97 | + (widget_height - paintable_height) / 2.0, |
| 98 | + widget_width, |
| 99 | + paintable_height, |
| 100 | + ) |
| 101 | + }; |
| 102 | + |
| 103 | + // Actual media coords considering the animation |
| 104 | + let (x, y, width, height) = { |
| 105 | + let bounds_ref = self.widget_bounds.borrow(); |
| 106 | + let bounds = bounds_ref.as_ref().unwrap(); |
| 107 | + ( |
| 108 | + ((target_x - bounds.x() as f64) * perc) + bounds.x() as f64, |
| 109 | + ((target_y - bounds.y() as f64) * perc) + bounds.y() as f64, |
| 110 | + ((target_width - bounds.width() as f64) * perc) + bounds.width() as f64, |
| 111 | + ((target_height - bounds.height() as f64) * perc) + bounds.height() as f64, |
| 112 | + ) |
| 113 | + }; |
| 114 | + |
| 115 | + // Media |
| 116 | + snapshot.save(); |
| 117 | + snapshot.translate(&graphene::Point::new(x as f32, y as f32)); |
| 118 | + let perc_rounding = ROUNDING - (ROUNDING * perc as f32); |
| 119 | + let rounding_size = graphene::Size::new(perc_rounding, perc_rounding); |
| 120 | + let rounding_bounds = gsk::RoundedRect::new( |
| 121 | + graphene::Rect::new(0.0, 0.0, width as f32, height as f32), |
| 122 | + rounding_size, |
| 123 | + rounding_size, |
| 124 | + rounding_size, |
| 125 | + rounding_size, |
| 126 | + ); |
| 127 | + snapshot.push_rounded_clip(&rounding_bounds); |
| 128 | + paintable.snapshot(snapshot.upcast_ref(), width, height); |
| 129 | + snapshot.pop(); |
| 130 | + snapshot.restore(); |
| 131 | + |
| 132 | + // UI Overlay |
| 133 | + snapshot.push_opacity(perc); |
| 134 | + widget.snapshot_child(&*self.header_bar, snapshot); |
| 135 | + snapshot.pop(); |
| 136 | + } |
| 137 | + } |
| 138 | + } |
| 139 | +} |
| 140 | + |
| 141 | +glib::wrapper! { |
| 142 | + pub(crate) struct MediaViewer(ObjectSubclass<imp::MediaViewer>) |
| 143 | + @extends gtk::Widget; |
| 144 | +} |
| 145 | + |
| 146 | +impl MediaViewer { |
| 147 | + pub(crate) fn open_media( |
| 148 | + &self, |
| 149 | + paintable: gdk::Paintable, |
| 150 | + target_widget: impl IsA<gtk::Widget>, |
| 151 | + ) { |
| 152 | + let imp = self.imp(); |
| 153 | + imp.paintable.replace(Some(paintable)); |
| 154 | + |
| 155 | + let bounds = target_widget.compute_bounds(self).unwrap(); |
| 156 | + imp.widget_bounds.replace(Some(bounds)); |
| 157 | + |
| 158 | + // Hide the widget so that we don't see the original media while transitioning. |
| 159 | + // We don't use the visible property here because otherwise the widgets doesn't |
| 160 | + // get allocated anymore. |
| 161 | + target_widget.set_opacity(0.0); |
| 162 | + |
| 163 | + imp.target_widget.replace(Some(target_widget.upcast())); |
| 164 | + |
| 165 | + let animation = adw::TimedAnimation::builder() |
| 166 | + .widget(self) |
| 167 | + .value_from(0.0) |
| 168 | + .value_to(1.0) |
| 169 | + .duration(300) |
| 170 | + .target(&adw::CallbackAnimationTarget::new(Some(Box::new( |
| 171 | + clone!(@weak self as obj => move |_| { |
| 172 | + obj.queue_draw(); |
| 173 | + }), |
| 174 | + )))) |
| 175 | + .easing(adw::Easing::EaseOutQuart) |
| 176 | + .build(); |
| 177 | + imp.animation.replace(Some(animation.clone())); |
| 178 | + |
| 179 | + // Make the overlay visible first, so that the animation isn't automatically skipped |
| 180 | + self.set_visible(true); |
| 181 | + |
| 182 | + animation.play(); |
| 183 | + } |
| 184 | + |
| 185 | + fn go_back(&self) { |
| 186 | + let animation_ref = self.imp().animation.borrow(); |
| 187 | + let animation = animation_ref.as_ref().unwrap(); |
| 188 | + |
| 189 | + // Reverse the animation and go back to the previous state |
| 190 | + animation.set_reverse(true); |
| 191 | + animation.set_value_to(animation.value()); |
| 192 | + animation.set_easing(adw::Easing::EaseInQuart); |
| 193 | + animation.connect_done(clone!(@weak self as obj => move |_| { |
| 194 | + obj.set_visible(false); |
| 195 | + |
| 196 | + let imp = obj.imp(); |
| 197 | + imp.animation.take(); |
| 198 | + |
| 199 | + // Reveal the target widget |
| 200 | + let widget = imp.target_widget.take().unwrap(); |
| 201 | + widget.set_opacity(1.0); |
| 202 | + })); |
| 203 | + |
| 204 | + animation.play(); |
| 205 | + } |
| 206 | +} |
0 commit comments