Browse Source

media-viewer: Add scale transition for opening and closing

merge-requests/1327/merge
Marco Melorio 4 years ago
parent
commit
00d5d6f02c
  1. 2
      data/resources/ui/content-message-media.ui
  2. 10
      data/resources/ui/media-viewer.ui
  3. 2
      src/components/mod.rs
  4. 270
      src/components/scale_revealer.rs
  5. 8
      src/session/content/room_history/message_row/mod.rs
  6. 27
      src/session/media_viewer.rs
  7. 4
      src/session/mod.rs

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

@ -2,7 +2,7 @@
<interface>
<template class="ContentMessageMedia" parent="GtkWidget">
<property name="focusable">True</property>
<property name="valign">center</property>
<property name="halign">start</property>
<child>
<object class="GtkOverlay" id="media">
<style>

10
data/resources/ui/media-viewer.ui

@ -48,11 +48,15 @@
</object>
</child>
<child>
<object class="ComponentsMediaContentViewer" id="media">
<property name="autoplay">true</property>
<object class="ComponentsScaleRevealer" id="revealer">
<property name="halign">center</property>
<property name="valign">center</property>
<property name="vexpand">true</property>
<property name="child">
<object class="ComponentsMediaContentViewer" id="media">
<property name="autoplay">true</property>
</object>
</property>
</object>
</child>
</object>
@ -74,4 +78,4 @@
</object>
</child>
</template>
</interface>
</interface>

2
src/components/mod.rs

@ -17,6 +17,7 @@ mod overlapping_box;
mod pill;
mod reaction_chooser;
mod room_title;
mod scale_revealer;
mod spinner_button;
mod video_player;
mod video_player_renderer;
@ -41,6 +42,7 @@ pub use self::{
pill::Pill,
reaction_chooser::ReactionChooser,
room_title::RoomTitle,
scale_revealer::ScaleRevealer,
spinner_button::SpinnerButton,
video_player::VideoPlayer,
video_player_renderer::VideoPlayerRenderer,

270
src/components/scale_revealer.rs

@ -0,0 +1,270 @@
use adw::{prelude::*, subclass::prelude::*};
use gtk::{gdk, glib, graphene};
const ANIMATION_DURATION: u32 = 250;
mod imp {
use std::cell::{Cell, RefCell};
use glib::{clone, subclass::Signal, WeakRef};
use once_cell::{sync::Lazy, 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))
}

8
src/session/content/room_history/message_row/mod.rs

@ -17,7 +17,7 @@ use gtk::{
use matrix_sdk::{room::timeline::TimelineItemContent, ruma::events::room::message::MessageType};
pub use self::content::ContentFormat;
use self::{content::MessageContent, reaction_list::MessageReactionList};
use self::{content::MessageContent, media::MessageMedia, reaction_list::MessageReactionList};
use crate::{components::Avatar, prelude::*, session::room::Event};
mod imp {
@ -208,13 +208,15 @@ impl MessageRow {
}
fn show_media(&self) {
if let Some(event) = self.imp().event.borrow().as_ref() {
let imp = self.imp();
if let Some(event) = imp.event.borrow().as_ref() {
if let TimelineItemContent::Message(content) = event.content() {
if matches!(
content.msgtype(),
MessageType::Image(_) | MessageType::Video(_)
) {
event.room().session().show_media(event);
let media_widget = imp.content.child().and_downcast::<MessageMedia>().unwrap();
event.room().session().show_media(event, &media_widget);
}
}
}

27
src/session/media_viewer.rs

@ -5,7 +5,7 @@ use matrix_sdk::{room::timeline::TimelineItemContent, ruma::events::room::messag
use super::room::{EventActions, EventTexture};
use crate::{
components::{ContentType, ImagePaintable, MediaContentViewer},
components::{ContentType, ImagePaintable, MediaContentViewer, ScaleRevealer},
session::room::Event,
spawn,
utils::cache_dir,
@ -31,6 +31,8 @@ mod imp {
#[template_child]
pub menu: TemplateChild<gtk::MenuButton>,
#[template_child]
pub revealer: TemplateChild<ScaleRevealer>,
#[template_child]
pub media: TemplateChild<MediaContentViewer>,
}
@ -50,7 +52,7 @@ mod imp {
}
obj.imp().media.stop_playback();
obj.set_visible(false);
obj.imp().revealer.set_reveal_child(false);
});
klass.add_binding_action(
gdk::Key::Escape,
@ -106,11 +108,13 @@ mod imp {
fn constructed(&self) {
self.parent_constructed();
let obj = self.obj();
self.menu
.set_menu_model(Some(Self::Type::event_media_menu_model()));
// Bind `fullscreened` to the window property of the same name.
self.obj().connect_notify_local(Some("root"), |obj, _| {
obj.connect_notify_local(Some("root"), |obj, _| {
if let Some(window) = obj.root().and_then(|root| root.downcast::<Window>().ok()) {
window
.bind_property("fullscreened", obj, "fullscreened")
@ -118,6 +122,13 @@ mod imp {
.build();
}
});
self.revealer
.connect_transition_done(clone!(@weak obj => move |revealer| {
if !revealer.reveals_child() {
obj.set_visible(false);
}
}));
}
}
@ -136,6 +147,16 @@ impl MediaViewer {
glib::Object::new(&[])
}
/// Reveal this widget by transitioning from `source_widget`.
pub fn reveal(&self, source_widget: &impl IsA<gtk::Widget>) {
let imp = self.imp();
self.set_visible(true);
imp.revealer.set_source_widget(Some(source_widget));
imp.revealer.set_reveal_child(true);
}
/// The media event to display.
pub fn event(&self) -> Option<Event> {
self.imp().event.upgrade()

4
src/session/mod.rs

@ -820,10 +820,10 @@ impl Session {
}
/// Show a media event
pub fn show_media(&self, event: &Event) {
pub fn show_media(&self, event: &Event, source_widget: &impl IsA<gtk::Widget>) {
let imp = self.imp();
imp.media_viewer.set_event(Some(event.clone()));
imp.media_viewer.set_visible(true);
imp.media_viewer.reveal(source_widget);
}
pub async fn cross_signing_status(&self) -> Option<CrossSigningStatus> {

Loading…
Cancel
Save