Skip to content

Commit 2076102

Browse files
committed
session: Add media viewer
1 parent c491748 commit 2076102

5 files changed

Lines changed: 270 additions & 18 deletions

File tree

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@
77
<child>
88
<object class="GtkOverlay" id="overlay">
99
<property name="overflow">hidden</property>
10+
<child>
11+
<object class="GtkGestureClick">
12+
<property name="button">1</property>
13+
<signal name="released" handler="on_pressed" swapped="true"/>
14+
</object>
15+
</child>
1016
<child type="overlay">
1117
<object class="GtkProgressBar" id="progress_bar">
1218
<property name="halign">center</property>

data/resources/ui/session.ui

Lines changed: 26 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,26 +2,35 @@
22
<interface>
33
<template class="Session" parent="AdwBin">
44
<child>
5-
<object class="AdwLeaflet" id="leaflet">
6-
<property name="can-navigate-back">True</property>
7-
<child>
8-
<object class="Sidebar" id="sidebar">
9-
<property name="compact" bind-source="leaflet" bind-property="folded" bind-flags="sync-create"/>
10-
<property name="selected-chat" bind-source="content" bind-property="chat" bind-flags="sync-create | bidirectional"/>
11-
<property name="session">Session</property>
12-
</object>
13-
</child>
14-
<child>
15-
<object class="AdwLeafletPage">
16-
<property name="navigatable">False</property>
17-
<property name="child">
18-
<object class="GtkSeparator"/>
19-
</property>
5+
<object class="GtkOverlay">
6+
<child type="overlay">
7+
<object class="MediaViewer" id="media_viewer">
8+
<property name="visible">False</property>
209
</object>
2110
</child>
2211
<child>
23-
<object class="Content" id="content">
24-
<property name="compact" bind-source="leaflet" bind-property="folded" bind-flags="sync-create"/>
12+
<object class="AdwLeaflet" id="leaflet">
13+
<property name="can-navigate-back">True</property>
14+
<child>
15+
<object class="Sidebar" id="sidebar">
16+
<property name="compact" bind-source="leaflet" bind-property="folded" bind-flags="sync-create"/>
17+
<property name="selected-chat" bind-source="content" bind-property="chat" bind-flags="sync-create | bidirectional"/>
18+
<property name="session">Session</property>
19+
</object>
20+
</child>
21+
<child>
22+
<object class="AdwLeafletPage">
23+
<property name="navigatable">False</property>
24+
<property name="child">
25+
<object class="GtkSeparator"/>
26+
</property>
27+
</object>
28+
</child>
29+
<child>
30+
<object class="Content" id="content">
31+
<property name="compact" bind-source="leaflet" bind-property="folded" bind-flags="sync-create"/>
32+
</object>
33+
</child>
2534
</object>
2635
</child>
2736
</object>

src/session/content/message_row/media.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ mod imp {
1010
use once_cell::unsync::OnceCell;
1111
use std::cell::RefCell;
1212

13+
use crate::session::content::ChatHistory;
14+
1315
#[derive(Debug, Default, CompositeTemplate)]
1416
#[template(resource = "/com/github/melix99/telegrand/ui/content-message-media.ui")]
1517
pub(crate) struct Media {
@@ -25,6 +27,22 @@ mod imp {
2527
pub(super) progress_bar: TemplateChild<gtk::ProgressBar>,
2628
}
2729

30+
#[gtk::template_callbacks]
31+
impl Media {
32+
#[template_callback]
33+
fn on_pressed(&self) {
34+
let obj = self.instance();
35+
let chat_history = obj.ancestor(ChatHistory::static_type()).unwrap();
36+
let chat = chat_history
37+
.downcast_ref::<ChatHistory>()
38+
.unwrap()
39+
.chat()
40+
.unwrap();
41+
chat.session()
42+
.open_media(self.picture.paintable().unwrap(), self.overlay.clone());
43+
}
44+
}
45+
2846
#[glib::object_subclass]
2947
impl ObjectSubclass for Media {
3048
const NAME: &'static str = "ContentMessageMedia";
@@ -33,6 +51,7 @@ mod imp {
3351

3452
fn class_init(klass: &mut Self::Class) {
3553
Self::bind_template(klass);
54+
Self::bind_template_callbacks(klass);
3655
klass.set_css_name("messagemedia");
3756
}
3857

src/session/media_viewer.rs

Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
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: &gtk::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+
}

src/session/mod.rs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
11
mod components;
22
mod content;
3+
mod media_viewer;
34
mod sidebar;
45

56
use self::content::Content;
7+
use self::media_viewer::MediaViewer;
68
use self::sidebar::Sidebar;
79

810
use glib::{clone, SyncSender};
911
use gtk::glib::WeakRef;
1012
use gtk::prelude::*;
1113
use gtk::subclass::prelude::*;
12-
use gtk::{glib, CompositeTemplate};
14+
use gtk::{gdk, glib, CompositeTemplate};
1315
use std::collections::hash_map::{Entry, HashMap};
1416
use tdlib::enums::{self, NotificationSettingsScope, Update};
1517
use tdlib::functions;
@@ -51,6 +53,8 @@ mod imp {
5153
RefCell<Option<BoxedScopeNotificationSettings>>,
5254
pub(super) downloading_files: RefCell<HashMap<i32, Vec<SyncSender<File>>>>,
5355
#[template_child]
56+
pub(super) media_viewer: TemplateChild<MediaViewer>,
57+
#[template_child]
5458
pub(super) leaflet: TemplateChild<adw::Leaflet>,
5559
#[template_child]
5660
pub(super) sidebar: TemplateChild<Sidebar>,
@@ -487,4 +491,12 @@ impl Session {
487491
pub(crate) fn set_sessions(&self, sessions: &gtk::SelectionModel) {
488492
self.imp().sidebar.set_sessions(sessions, self);
489493
}
494+
495+
pub(crate) fn open_media(
496+
&self,
497+
paintable: gdk::Paintable,
498+
target_widget: impl IsA<gtk::Widget>,
499+
) {
500+
self.imp().media_viewer.open_media(paintable, target_widget);
501+
}
490502
}

0 commit comments

Comments
 (0)