Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Media viewer #345

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion data/resources/ui/content-message-photo.ui
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,14 @@
<class name="media"/>
</style>
<property name="prefix">
<object class="ContentMediaPicture" id="picture"/>
<object class="ContentMediaPicture" id="picture">
<child>
<object class="GtkGestureClick">
<property name="button">1</property>
<signal name="released" handler="on_released" swapped="true"/>
</object>
</child>
</object>
</property>
</object>
</child>
Expand Down
9 changes: 9 additions & 0 deletions data/resources/ui/session.ui
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@
<interface>
<template class="Session" parent="AdwBin">
<child>
<object class="GtkOverlay">
<child type="overlay">
<object class="MediaViewer" id="media_viewer">
<property name="visible">False</property>
</object>
</child>
<child>
<object class="AdwLeaflet" id="leaflet">
<property name="can-navigate-back">True</property>
<child>
Expand All @@ -24,6 +31,8 @@
<property name="compact" bind-source="leaflet" bind-property="folded" bind-flags="sync-create"/>
</object>
</child>
</object>
</child>
</object>
</child>
</template>
Expand Down
2 changes: 2 additions & 0 deletions src/session/components/mod.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
mod avatar;
mod message_entry;
mod scale_revealer;
mod snow;

pub(crate) use self::avatar::Avatar;
pub(crate) use self::message_entry::MessageEntry;
pub(crate) use self::scale_revealer::ScaleRevealer;
pub(crate) use self::snow::Snow;
273 changes: 273 additions & 0 deletions src/session/components/scale_revealer.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,273 @@
use adw::prelude::*;
use adw::subclass::prelude::*;
use gtk::{gdk, glib, graphene};

const ANIMATION_DURATION: u32 = 250;

mod imp {
use std::cell::{Cell, RefCell};

use glib::subclass::Signal;
use glib::{clone, WeakRef};
use once_cell::sync::Lazy;
use once_cell::unsync::OnceCell;

use super::*;

#[derive(Debug, Default)]
pub struct ScaleRevealer {
pub reveal_child: Cell<bool>,
pub source_widget: WeakRef<gtk::Widget>,
pub source_widget_texture: RefCell<Option<gdk::Texture>>,
pub animation: OnceCell<adw::TimedAnimation>,
}

#[glib::object_subclass]
impl ObjectSubclass for ScaleRevealer {
const NAME: &'static str = "ComponentsScaleRevealer";
type Type = super::ScaleRevealer;
type ParentType = adw::Bin;
}

impl ObjectImpl for ScaleRevealer {
fn signals() -> &'static [Signal] {
static SIGNALS: Lazy<Vec<Signal>> =
Lazy::new(|| vec![Signal::builder("transition-done").build()]);
SIGNALS.as_ref()
}

fn properties() -> &'static [glib::ParamSpec] {
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
vec![
glib::ParamSpecBoolean::builder("reveal-child")
.explicit_notify()
.build(),
glib::ParamSpecObject::builder::<gtk::Widget>("source-widget")
.explicit_notify()
.build(),
]
});

PROPERTIES.as_ref()
}

fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) {
match pspec.name() {
"reveal-child" => self.obj().set_reveal_child(value.get().unwrap()),
"source-widget" => self
.obj()
.set_source_widget(value.get::<Option<&gtk::Widget>>().unwrap()),
_ => unimplemented!(),
}
}

fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
match pspec.name() {
"reveal-child" => self.obj().reveals_child().to_value(),
"source-widget" => self.obj().source_widget().to_value(),
_ => unimplemented!(),
}
}

fn constructed(&self) {
self.parent_constructed();

let obj = self.obj();
let target = adw::CallbackAnimationTarget::new(clone!(@weak obj => move |_| {
obj.queue_draw();
}));
let animation = adw::TimedAnimation::new(&*obj, 0.0, 1.0, ANIMATION_DURATION, &target);

animation.set_easing(adw::Easing::EaseOutQuart);
animation.connect_done(clone!(@weak obj => move |_| {
let imp = obj.imp();

if !imp.reveal_child.get() {
if let Some(source_widget) = imp.source_widget.upgrade() {
// Show the original source widget now that the
// transition is over.
source_widget.set_opacity(1.0);
}
obj.set_visible(false);
}

obj.emit_by_name::<()>("transition-done", &[]);
}));

self.animation.set(animation).unwrap();
obj.set_visible(false);
}
}

impl WidgetImpl for ScaleRevealer {
fn snapshot(&self, snapshot: &gtk::Snapshot) {
let obj = self.obj();
if let Some(child) = obj.child() {
let progress = self.animation.get().unwrap().value();
if progress == 1.0 {
// The transition progress is at 100%, so just show the child
obj.snapshot_child(&child, snapshot);
return;
}

let source_bounds = self
.source_widget
.upgrade()
.and_then(|s| s.compute_bounds(&*obj))
.unwrap_or_else(|| {
log::warn!(
"The source widget bounds could not be calculated, using default bounds as fallback"
);
graphene::Rect::new(0.0, 0.0, 100.0, 100.0)
});
let rev_progress = (1.0 - progress).abs();

let x_scale = source_bounds.width() / obj.width() as f32;
let y_scale = source_bounds.height() / obj.height() as f32;

let x_scale = 1.0 + (x_scale - 1.0) * rev_progress as f32;
let y_scale = 1.0 + (y_scale - 1.0) * rev_progress as f32;

let x = source_bounds.x() * rev_progress as f32;
let y = source_bounds.y() * rev_progress as f32;

snapshot.translate(&graphene::Point::new(x, y));
snapshot.scale(x_scale, y_scale);

let source_widget_texture_ref = self.source_widget_texture.borrow();

if let Some(source_widget_texture) = source_widget_texture_ref.as_ref() {
if progress > 0.0 {
// We're in the middle of the cross fade transition, so...
// do the cross fade transition.
snapshot.push_cross_fade(progress);

source_widget_texture.snapshot(
snapshot,
obj.width() as f64,
obj.height() as f64,
);
snapshot.pop();

obj.snapshot_child(&child, snapshot);
snapshot.pop();
} else if progress <= 0.0 {
source_widget_texture.snapshot(
snapshot,
obj.width() as f64,
obj.height() as f64,
);
}
} else {
log::warn!(
"The source widget texture is None, using child snapshot as fallback"
);
obj.snapshot_child(&child, snapshot);
}
}
}
}

impl BinImpl for ScaleRevealer {}
}

glib::wrapper! {
pub struct ScaleRevealer(ObjectSubclass<imp::ScaleRevealer>)
@extends gtk::Widget, adw::Bin;
}

impl ScaleRevealer {
pub fn new() -> Self {
glib::Object::new(&[])
}

/// Whether the child is revealed or not.
pub fn reveals_child(&self) -> bool {
self.imp().reveal_child.get()
}

/// Set whether the child should be revealed or not.
///
/// This will start the scale animation.
pub fn set_reveal_child(&self, reveal_child: bool) {
if self.reveals_child() == reveal_child {
return;
}

let imp = self.imp();
let animation = imp.animation.get().unwrap();
animation.set_value_from(animation.value());

if reveal_child {
animation.set_value_to(1.0);
self.set_visible(true);

if let Some(source_widget) = imp.source_widget.upgrade() {
// Render the current state of the source widget to a texture.
// This will be needed for the transition.
let texture = render_widget_to_texture(&source_widget);
imp.source_widget_texture.replace(texture);

// Hide the source widget.
// We use opacity here so that the widget will stay allocated.
source_widget.set_opacity(0.0);
} else {
imp.source_widget_texture.replace(None);
}
} else {
animation.set_value_to(0.0);
}

imp.reveal_child.set(reveal_child);

animation.play();

self.notify("reveal-child");
}

/// The source widget this revealer is transitioning from.
pub fn source_widget(&self) -> Option<gtk::Widget> {
self.imp().source_widget.upgrade()
}

/// Set the source widget this revealer should transition from to show the
/// child.
pub fn set_source_widget(&self, source_widget: Option<&impl IsA<gtk::Widget>>) {
let source_widget = source_widget.map(|s| s.as_ref());
if self.source_widget().as_ref() == source_widget {
return;
}
self.imp().source_widget.set(source_widget);
self.notify("source-widget");
}

pub fn connect_transition_done<F: Fn(&Self) + 'static>(&self, f: F) -> glib::SignalHandlerId {
self.connect_local("transition-done", true, move |values| {
let obj = values[0].get::<Self>().unwrap();
f(&obj);
None
})
}
}

impl Default for ScaleRevealer {
fn default() -> Self {
Self::new()
}
}

fn render_widget_to_texture(widget: &impl IsA<gtk::Widget>) -> Option<gdk::Texture> {
let widget_paintable = gtk::WidgetPaintable::new(Some(widget.as_ref()));
let snapshot = gtk::Snapshot::new();

widget_paintable.snapshot(
&snapshot,
widget_paintable.intrinsic_width() as f64,
widget_paintable.intrinsic_height() as f64,
);

let node = snapshot.to_node()?;
let native = widget.native()?;

Some(native.renderer().render_texture(node, None))
}
15 changes: 15 additions & 0 deletions src/session/content/message_row/photo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use tdlib::types::File;
use crate::session::content::message_row::{
MediaPicture, MessageBase, MessageBaseImpl, MessageBubble,
};
use crate::session::content::ChatHistory;
use crate::tdlib::{BoxedMessageContent, Message};
use crate::utils::parse_formatted_text;
use crate::Session;
Expand Down Expand Up @@ -39,6 +40,7 @@ mod imp {

fn class_init(klass: &mut Self::Class) {
klass.bind_template();
klass.bind_template_instance_callbacks();
}

fn instance_init(obj: &glib::subclass::InitializingObject<Self>) {
Expand Down Expand Up @@ -136,7 +138,20 @@ impl MessageBaseExt for MessagePhoto {
}
}

#[gtk::template_callbacks]
impl MessagePhoto {
#[template_callback]
fn on_released(&self) {
let chat_history = self.ancestor(ChatHistory::static_type()).unwrap();
let chat = chat_history
.downcast_ref::<ChatHistory>()
.unwrap()
.chat()
.unwrap();
chat.session()
.open_media(self.message(), &*self.imp().picture);
}

fn update_photo(&self, message: &Message) {
if let MessageContent::MessagePhoto(data) = message.content().0 {
let imp = self.imp();
Expand Down
Loading