Skip to content

Commit dcc98cb

Browse files
committed
session: Add media viewer
1 parent 5040b86 commit dcc98cb

File tree

7 files changed

+434
-2
lines changed

7 files changed

+434
-2
lines changed

data/resources/ui/content-message-photo.ui

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,14 @@
1010
<class name="media"/>
1111
</style>
1212
<property name="prefix">
13-
<object class="ContentMediaPicture" id="picture"/>
13+
<object class="ContentMediaPicture" id="picture">
14+
<child>
15+
<object class="GtkGestureClick">
16+
<property name="button">1</property>
17+
<signal name="released" handler="on_released" swapped="true"/>
18+
</object>
19+
</child>
20+
</object>
1421
</property>
1522
</object>
1623
</child>

data/resources/ui/session.ui

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,13 @@
22
<interface>
33
<template class="Session" parent="AdwBin">
44
<child>
5+
<object class="GtkOverlay">
6+
<child type="overlay">
7+
<object class="MediaViewer" id="media_viewer">
8+
<property name="visible">False</property>
9+
</object>
10+
</child>
11+
<child>
512
<object class="AdwLeaflet" id="leaflet">
613
<property name="can-navigate-back">True</property>
714
<child>
@@ -24,6 +31,8 @@
2431
<property name="compact" bind-source="leaflet" bind-property="folded" bind-flags="sync-create"/>
2532
</object>
2633
</child>
34+
</object>
35+
</child>
2736
</object>
2837
</child>
2938
</template>

src/session/components/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
mod avatar;
22
mod message_entry;
3+
mod scale_revealer;
34
mod snow;
45

56
pub(crate) use self::avatar::Avatar;
67
pub(crate) use self::message_entry::MessageEntry;
8+
pub(crate) use self::scale_revealer::ScaleRevealer;
79
pub(crate) use self::snow::Snow;
Lines changed: 273 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,273 @@
1+
use adw::prelude::*;
2+
use adw::subclass::prelude::*;
3+
use gtk::{gdk, glib, graphene};
4+
5+
const ANIMATION_DURATION: u32 = 250;
6+
7+
mod imp {
8+
use std::cell::{Cell, RefCell};
9+
10+
use glib::subclass::Signal;
11+
use glib::{clone, WeakRef};
12+
use once_cell::sync::Lazy;
13+
use once_cell::unsync::OnceCell;
14+
15+
use super::*;
16+
17+
#[derive(Debug, Default)]
18+
pub struct ScaleRevealer {
19+
pub reveal_child: Cell<bool>,
20+
pub source_widget: WeakRef<gtk::Widget>,
21+
pub source_widget_texture: RefCell<Option<gdk::Texture>>,
22+
pub animation: OnceCell<adw::TimedAnimation>,
23+
}
24+
25+
#[glib::object_subclass]
26+
impl ObjectSubclass for ScaleRevealer {
27+
const NAME: &'static str = "ComponentsScaleRevealer";
28+
type Type = super::ScaleRevealer;
29+
type ParentType = adw::Bin;
30+
}
31+
32+
impl ObjectImpl for ScaleRevealer {
33+
fn signals() -> &'static [Signal] {
34+
static SIGNALS: Lazy<Vec<Signal>> =
35+
Lazy::new(|| vec![Signal::builder("transition-done").build()]);
36+
SIGNALS.as_ref()
37+
}
38+
39+
fn properties() -> &'static [glib::ParamSpec] {
40+
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
41+
vec![
42+
glib::ParamSpecBoolean::builder("reveal-child")
43+
.explicit_notify()
44+
.build(),
45+
glib::ParamSpecObject::builder::<gtk::Widget>("source-widget")
46+
.explicit_notify()
47+
.build(),
48+
]
49+
});
50+
51+
PROPERTIES.as_ref()
52+
}
53+
54+
fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) {
55+
match pspec.name() {
56+
"reveal-child" => self.obj().set_reveal_child(value.get().unwrap()),
57+
"source-widget" => self
58+
.obj()
59+
.set_source_widget(value.get::<Option<&gtk::Widget>>().unwrap()),
60+
_ => unimplemented!(),
61+
}
62+
}
63+
64+
fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
65+
match pspec.name() {
66+
"reveal-child" => self.obj().reveals_child().to_value(),
67+
"source-widget" => self.obj().source_widget().to_value(),
68+
_ => unimplemented!(),
69+
}
70+
}
71+
72+
fn constructed(&self) {
73+
self.parent_constructed();
74+
75+
let obj = self.obj();
76+
let target = adw::CallbackAnimationTarget::new(clone!(@weak obj => move |_| {
77+
obj.queue_draw();
78+
}));
79+
let animation = adw::TimedAnimation::new(&*obj, 0.0, 1.0, ANIMATION_DURATION, &target);
80+
81+
animation.set_easing(adw::Easing::EaseOutQuart);
82+
animation.connect_done(clone!(@weak obj => move |_| {
83+
let imp = obj.imp();
84+
85+
if !imp.reveal_child.get() {
86+
if let Some(source_widget) = imp.source_widget.upgrade() {
87+
// Show the original source widget now that the
88+
// transition is over.
89+
source_widget.set_opacity(1.0);
90+
}
91+
obj.set_visible(false);
92+
}
93+
94+
obj.emit_by_name::<()>("transition-done", &[]);
95+
}));
96+
97+
self.animation.set(animation).unwrap();
98+
obj.set_visible(false);
99+
}
100+
}
101+
102+
impl WidgetImpl for ScaleRevealer {
103+
fn snapshot(&self, snapshot: &gtk::Snapshot) {
104+
let obj = self.obj();
105+
if let Some(child) = obj.child() {
106+
let progress = self.animation.get().unwrap().value();
107+
if progress == 1.0 {
108+
// The transition progress is at 100%, so just show the child
109+
obj.snapshot_child(&child, snapshot);
110+
return;
111+
}
112+
113+
let source_bounds = self
114+
.source_widget
115+
.upgrade()
116+
.and_then(|s| s.compute_bounds(&*obj))
117+
.unwrap_or_else(|| {
118+
log::warn!(
119+
"The source widget bounds could not be calculated, using default bounds as fallback"
120+
);
121+
graphene::Rect::new(0.0, 0.0, 100.0, 100.0)
122+
});
123+
let rev_progress = (1.0 - progress).abs();
124+
125+
let x_scale = source_bounds.width() / obj.width() as f32;
126+
let y_scale = source_bounds.height() / obj.height() as f32;
127+
128+
let x_scale = 1.0 + (x_scale - 1.0) * rev_progress as f32;
129+
let y_scale = 1.0 + (y_scale - 1.0) * rev_progress as f32;
130+
131+
let x = source_bounds.x() * rev_progress as f32;
132+
let y = source_bounds.y() * rev_progress as f32;
133+
134+
snapshot.translate(&graphene::Point::new(x, y));
135+
snapshot.scale(x_scale, y_scale);
136+
137+
let source_widget_texture_ref = self.source_widget_texture.borrow();
138+
139+
if let Some(source_widget_texture) = source_widget_texture_ref.as_ref() {
140+
if progress > 0.0 {
141+
// We're in the middle of the cross fade transition, so...
142+
// do the cross fade transition.
143+
snapshot.push_cross_fade(progress);
144+
145+
source_widget_texture.snapshot(
146+
snapshot,
147+
obj.width() as f64,
148+
obj.height() as f64,
149+
);
150+
snapshot.pop();
151+
152+
obj.snapshot_child(&child, snapshot);
153+
snapshot.pop();
154+
} else if progress <= 0.0 {
155+
source_widget_texture.snapshot(
156+
snapshot,
157+
obj.width() as f64,
158+
obj.height() as f64,
159+
);
160+
}
161+
} else {
162+
log::warn!(
163+
"The source widget texture is None, using child snapshot as fallback"
164+
);
165+
obj.snapshot_child(&child, snapshot);
166+
}
167+
}
168+
}
169+
}
170+
171+
impl BinImpl for ScaleRevealer {}
172+
}
173+
174+
glib::wrapper! {
175+
pub struct ScaleRevealer(ObjectSubclass<imp::ScaleRevealer>)
176+
@extends gtk::Widget, adw::Bin;
177+
}
178+
179+
impl ScaleRevealer {
180+
pub fn new() -> Self {
181+
glib::Object::new(&[])
182+
}
183+
184+
/// Whether the child is revealed or not.
185+
pub fn reveals_child(&self) -> bool {
186+
self.imp().reveal_child.get()
187+
}
188+
189+
/// Set whether the child should be revealed or not.
190+
///
191+
/// This will start the scale animation.
192+
pub fn set_reveal_child(&self, reveal_child: bool) {
193+
if self.reveals_child() == reveal_child {
194+
return;
195+
}
196+
197+
let imp = self.imp();
198+
let animation = imp.animation.get().unwrap();
199+
animation.set_value_from(animation.value());
200+
201+
if reveal_child {
202+
animation.set_value_to(1.0);
203+
self.set_visible(true);
204+
205+
if let Some(source_widget) = imp.source_widget.upgrade() {
206+
// Render the current state of the source widget to a texture.
207+
// This will be needed for the transition.
208+
let texture = render_widget_to_texture(&source_widget);
209+
imp.source_widget_texture.replace(texture);
210+
211+
// Hide the source widget.
212+
// We use opacity here so that the widget will stay allocated.
213+
source_widget.set_opacity(0.0);
214+
} else {
215+
imp.source_widget_texture.replace(None);
216+
}
217+
} else {
218+
animation.set_value_to(0.0);
219+
}
220+
221+
imp.reveal_child.set(reveal_child);
222+
223+
animation.play();
224+
225+
self.notify("reveal-child");
226+
}
227+
228+
/// The source widget this revealer is transitioning from.
229+
pub fn source_widget(&self) -> Option<gtk::Widget> {
230+
self.imp().source_widget.upgrade()
231+
}
232+
233+
/// Set the source widget this revealer should transition from to show the
234+
/// child.
235+
pub fn set_source_widget(&self, source_widget: Option<&impl IsA<gtk::Widget>>) {
236+
let source_widget = source_widget.map(|s| s.as_ref());
237+
if self.source_widget().as_ref() == source_widget {
238+
return;
239+
}
240+
self.imp().source_widget.set(source_widget);
241+
self.notify("source-widget");
242+
}
243+
244+
pub fn connect_transition_done<F: Fn(&Self) + 'static>(&self, f: F) -> glib::SignalHandlerId {
245+
self.connect_local("transition-done", true, move |values| {
246+
let obj = values[0].get::<Self>().unwrap();
247+
f(&obj);
248+
None
249+
})
250+
}
251+
}
252+
253+
impl Default for ScaleRevealer {
254+
fn default() -> Self {
255+
Self::new()
256+
}
257+
}
258+
259+
fn render_widget_to_texture(widget: &impl IsA<gtk::Widget>) -> Option<gdk::Texture> {
260+
let widget_paintable = gtk::WidgetPaintable::new(Some(widget.as_ref()));
261+
let snapshot = gtk::Snapshot::new();
262+
263+
widget_paintable.snapshot(
264+
&snapshot,
265+
widget_paintable.intrinsic_width() as f64,
266+
widget_paintable.intrinsic_height() as f64,
267+
);
268+
269+
let node = snapshot.to_node()?;
270+
let native = widget.native()?;
271+
272+
Some(native.renderer().render_texture(node, None))
273+
}

src/session/content/message_row/photo.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ use tdlib::types::File;
88
use crate::session::content::message_row::{
99
MediaPicture, MessageBase, MessageBaseImpl, MessageBubble,
1010
};
11+
use crate::session::content::ChatHistory;
1112
use crate::tdlib::{BoxedMessageContent, Message};
1213
use crate::utils::parse_formatted_text;
1314
use crate::Session;
@@ -39,6 +40,7 @@ mod imp {
3940

4041
fn class_init(klass: &mut Self::Class) {
4142
klass.bind_template();
43+
klass.bind_template_instance_callbacks();
4244
}
4345

4446
fn instance_init(obj: &glib::subclass::InitializingObject<Self>) {
@@ -136,7 +138,20 @@ impl MessageBaseExt for MessagePhoto {
136138
}
137139
}
138140

141+
#[gtk::template_callbacks]
139142
impl MessagePhoto {
143+
#[template_callback]
144+
fn on_released(&self) {
145+
let chat_history = self.ancestor(ChatHistory::static_type()).unwrap();
146+
let chat = chat_history
147+
.downcast_ref::<ChatHistory>()
148+
.unwrap()
149+
.chat()
150+
.unwrap();
151+
chat.session()
152+
.open_media(self.message(), &*self.imp().picture);
153+
}
154+
140155
fn update_photo(&self, message: &Message) {
141156
if let MessageContent::MessagePhoto(data) = message.content().0 {
142157
let imp = self.imp();

0 commit comments

Comments
 (0)