Skip to content

Commit e2cc14b

Browse files
committed
rlottie: Add support of animated stickers and emojis
1 parent 185cf8a commit e2cc14b

File tree

5 files changed

+175
-204
lines changed

5 files changed

+175
-204
lines changed

data/resources/style-dark.css

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22
background-color: @dark_2;
33
}
44

5+
/* sticker must be repainted to a text color in messages */
6+
messagesticker.needs-repainting > overlay > widget > widget {
7+
filter: invert(1);
8+
}
9+
510
.chat-list row .unread-count-muted {
611
background-color: @dark_2;
712
}

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

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
<?xml version="1.0" encoding="UTF-8"?>
22
<interface>
33
<template class="MessageSticker" parent="MessageBase">
4-
<property name="layout-manager">
5-
<object class="GtkBinLayout"/>
6-
</property>
74
<child>
8-
<object class="GtkOverlay">
5+
<object class="GtkOverlay" id="overlay">
96
<child>
10-
<object class="MessageStickerPicture" id="picture"/>
7+
<object class="GtkGestureClick" id="click">
8+
<property name="button">1</property>
9+
</object>
10+
</child>
11+
<child>
12+
<object class="AdwBin" id="bin"/>
1113
</child>
1214
<child type="overlay">
1315
<object class="MessageIndicators" id="indicators">

src/session/content/message_row/mod.rs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ mod label;
77
mod media_picture;
88
mod photo;
99
mod sticker;
10-
mod sticker_picture;
1110
mod text;
1211
mod video;
1312

@@ -19,7 +18,6 @@ use self::label::MessageLabel;
1918
use self::media_picture::MediaPicture;
2019
use self::photo::MessagePhoto;
2120
use self::sticker::MessageSticker;
22-
use self::sticker_picture::StickerPicture;
2321
use self::text::MessageText;
2422
use self::video::MessageVideo;
2523

@@ -336,11 +334,17 @@ impl MessageRow {
336334
MessageContent::MessageAnimation(_) /*| MessageContent::MessageVideo(_)*/ => {
337335
self.update_specific_content::<_, MessageVideo>(message_.clone());
338336
}
337+
MessageContent::MessageAnimatedEmoji(data)
338+
if data.animated_emoji.sticker.clone().map(
339+
|s| matches!(s.format, StickerFormat::Webp | StickerFormat::Tgs)
340+
).unwrap_or_default() => {
341+
self.update_specific_content::<_, MessageSticker>(message_.clone());
342+
}
339343
MessageContent::MessagePhoto(_) => {
340344
self.update_specific_content::<_, MessagePhoto>(message_.clone());
341345
}
342346
MessageContent::MessageSticker(data)
343-
if data.sticker.format == StickerFormat::Webp =>
347+
if matches!(data.sticker.format, StickerFormat::Webp | StickerFormat::Tgs) =>
344348
{
345349
self.update_specific_content::<_, MessageSticker>(message_.clone());
346350
}
Lines changed: 156 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,14 @@
1+
use adw::prelude::*;
12
use glib::clone;
2-
use gtk::prelude::*;
33
use gtk::subclass::prelude::*;
44
use gtk::{gdk, gio, glib, CompositeTemplate};
55
use image::io::Reader as ImageReader;
66
use image::ImageFormat;
77
use std::io::Cursor;
8-
use tdlib::enums::MessageContent;
8+
use tdlib::enums::{MessageContent, StickerFormat, StickerFullType};
99
use tdlib::types::File;
1010

11-
use crate::session::content::message_row::{
12-
MessageBase, MessageBaseImpl, MessageIndicators, StickerPicture,
13-
};
11+
use crate::session::content::message_row::{MessageBase, MessageBaseImpl, MessageIndicators};
1412
use crate::tdlib::Message;
1513
use crate::utils::spawn;
1614

@@ -19,14 +17,21 @@ use super::base::MessageBaseExt;
1917
mod imp {
2018
use super::*;
2119
use once_cell::sync::Lazy;
22-
use std::cell::RefCell;
20+
use std::cell::{Cell, RefCell};
2321

2422
#[derive(Debug, Default, CompositeTemplate)]
2523
#[template(resource = "/com/github/melix99/telegrand/ui/content-message-sticker.ui")]
2624
pub(crate) struct MessageSticker {
25+
pub(super) format: RefCell<Option<StickerFormat>>,
26+
pub(super) aspect_ratio: Cell<f64>,
27+
pub(super) is_emoji: Cell<bool>,
2728
pub(super) message: RefCell<Option<Message>>,
2829
#[template_child]
29-
pub(super) picture: TemplateChild<StickerPicture>,
30+
pub(super) overlay: TemplateChild<gtk::Overlay>,
31+
#[template_child]
32+
pub(super) click: TemplateChild<gtk::GestureClick>,
33+
#[template_child]
34+
pub(super) bin: TemplateChild<adw::Bin>,
3035
#[template_child]
3136
pub(super) indicators: TemplateChild<MessageIndicators>,
3237
}
@@ -72,9 +77,61 @@ mod imp {
7277
_ => unimplemented!(),
7378
}
7479
}
80+
81+
fn constructed(&self) {
82+
let obj = self.obj();
83+
self.click
84+
.connect_released(clone!(@weak obj => move |_, _, _, _| {
85+
let imp = obj.imp();
86+
if imp.is_emoji.get() {
87+
if let Some(animation) = imp.bin.child() {
88+
if let Ok(animation) = animation.downcast::<rlt::Animation>() {
89+
if !animation.is_playing() {
90+
// TODO: animated emoji needs to play
91+
// effect when someone clicks on it
92+
animation.play();
93+
}
94+
}
95+
}
96+
}
97+
}));
98+
}
7599
}
76100

77-
impl WidgetImpl for MessageSticker {}
101+
impl WidgetImpl for MessageSticker {
102+
fn measure(&self, orientation: gtk::Orientation, for_size: i32) -> (i32, i32, i32, i32) {
103+
const STICKER_SIZE: i32 = 176;
104+
const EMOJI_SIZE: i32 = 112;
105+
const EMOJI_BOTTOM_MARGIN: i32 = 16;
106+
107+
let (size, bottom_margin) = if self.is_emoji.get() {
108+
(EMOJI_SIZE, EMOJI_BOTTOM_MARGIN)
109+
} else {
110+
(STICKER_SIZE, 0)
111+
};
112+
113+
let aspect_ratio = self.aspect_ratio.get();
114+
let min_size = self.overlay.measure(orientation, for_size).0;
115+
let size = if let gtk::Orientation::Horizontal = orientation {
116+
if aspect_ratio >= 1.0 {
117+
size
118+
} else {
119+
(size as f64 * aspect_ratio) as i32
120+
}
121+
} else if aspect_ratio >= 1.0 {
122+
(size as f64 / aspect_ratio) as i32 + bottom_margin
123+
} else {
124+
size + bottom_margin
125+
}
126+
.max(min_size);
127+
128+
(size, size, -1, -1)
129+
}
130+
131+
fn size_allocate(&self, width: i32, height: i32, baseline: i32) {
132+
self.overlay.allocate(width, height, baseline, None);
133+
}
134+
}
78135
impl MessageBaseImpl for MessageSticker {}
79136
}
80137

@@ -95,35 +152,54 @@ impl MessageBaseExt for MessageSticker {
95152

96153
imp.indicators.set_message(message.clone().upcast());
97154

98-
imp.picture.set_texture(None);
155+
let (sticker, looped) = match message.content().0 {
156+
MessageContent::MessageSticker(data) => {
157+
let sticker = data.sticker;
158+
(sticker, true)
159+
}
160+
MessageContent::MessageAnimatedEmoji(data) => {
161+
self.imp().is_emoji.set(true);
162+
let sticker = data.animated_emoji.sticker.unwrap();
163+
let looped = matches!(sticker.full_type, StickerFullType::CustomEmoji(_));
164+
(sticker, looped)
165+
}
166+
_ => unreachable!(),
167+
};
99168

100-
if let MessageContent::MessageSticker(data) = message.content().0 {
101-
self.imp()
102-
.picture
103-
.set_aspect_ratio(data.sticker.width as f64 / data.sticker.height as f64);
169+
match sticker.full_type {
170+
StickerFullType::CustomEmoji(data) if data.needs_repainting => {
171+
self.add_css_class("needs-repainting")
172+
}
173+
_ => self.remove_css_class("needs-repainting"),
174+
}
104175

105-
if data.sticker.sticker.local.is_downloading_completed {
106-
self.load_sticker(&data.sticker.sticker.local.path);
107-
} else {
108-
let (sender, receiver) =
109-
glib::MainContext::sync_channel::<File>(Default::default(), 5);
110-
111-
receiver.attach(
112-
None,
113-
clone!(@weak self as obj => @default-return glib::Continue(false), move |file| {
114-
if file.local.is_downloading_completed {
115-
obj.load_sticker(&file.local.path);
116-
}
176+
imp.format.replace(Some(sticker.format));
117177

118-
glib::Continue(true)
119-
}),
120-
);
178+
imp.aspect_ratio
179+
.set(sticker.width as f64 / sticker.height as f64);
121180

122-
message
123-
.chat()
124-
.session()
125-
.download_file(data.sticker.sticker.id, sender);
126-
}
181+
// TODO: draw sticker outline with cairo
182+
183+
if sticker.sticker.local.is_downloading_completed {
184+
self.load_sticker(&sticker.sticker.local.path, looped);
185+
} else {
186+
let (sender, receiver) = glib::MainContext::sync_channel::<File>(Default::default(), 5);
187+
188+
receiver.attach(
189+
None,
190+
clone!(@weak self as obj => @default-return glib::Continue(false), move |file| {
191+
if file.local.is_downloading_completed {
192+
obj.load_sticker(&file.local.path, looped);
193+
}
194+
195+
glib::Continue(true)
196+
}),
197+
);
198+
199+
message
200+
.chat()
201+
.session()
202+
.download_file(sticker.sticker.id, sender);
127203
}
128204

129205
imp.message.replace(Some(message));
@@ -132,35 +208,55 @@ impl MessageBaseExt for MessageSticker {
132208
}
133209

134210
impl MessageSticker {
135-
fn load_sticker(&self, path: &str) {
136-
let picture = &*self.imp().picture;
137-
let file = gio::File::for_path(path);
138-
spawn(clone!(@weak picture => async move {
139-
match file.load_bytes_future().await {
140-
Ok((bytes, _)) => {
141-
let flat_samples = ImageReader::with_format(Cursor::new(bytes), ImageFormat::WebP)
142-
.decode()
143-
.unwrap()
144-
.into_rgba8()
145-
.into_flat_samples();
146-
147-
let (stride, width, height) = flat_samples.extents();
148-
let gtk_stride = stride * width;
149-
150-
let bytes = glib::Bytes::from_owned(flat_samples.samples);
151-
let texture = gdk::MemoryTexture::new(
152-
width as i32,
153-
height as i32,
154-
gdk::MemoryFormat::R8g8b8a8,
155-
&bytes,
156-
gtk_stride,
157-
);
158-
picture.set_texture(Some(texture.upcast()));
159-
}
160-
Err(e) => {
161-
log::warn!("Failed to load a sticker: {}", e);
211+
fn load_sticker(&self, path: &str, looped: bool) {
212+
let path = path.to_owned();
213+
let format = self.imp().format.borrow().clone().unwrap();
214+
spawn(clone!(@weak self as obj => async move {
215+
let widget: gtk::Widget = match format {
216+
StickerFormat::Tgs => {
217+
let animation = rlt::Animation::from_filename(&path);
218+
animation.set_loop(looped);
219+
animation.use_cache(looped);
220+
animation.play();
221+
animation.upcast()
222+
}
223+
StickerFormat::Webp => {
224+
let file = gio::File::for_path(&path);
225+
match file.load_bytes_future().await {
226+
Ok((bytes, _)) => {
227+
let flat_samples =
228+
ImageReader::with_format(Cursor::new(bytes), ImageFormat::WebP)
229+
.decode()
230+
.unwrap()
231+
.into_rgba8()
232+
.into_flat_samples();
233+
234+
let (stride, width, height) = flat_samples.extents();
235+
let gtk_stride = stride * width;
236+
237+
let bytes = glib::Bytes::from_owned(flat_samples.samples);
238+
let texture = gdk::MemoryTexture::new(
239+
width as i32,
240+
height as i32,
241+
gdk::MemoryFormat::R8g8b8a8,
242+
&bytes,
243+
gtk_stride,
244+
);
245+
246+
let picture = gtk::Picture::new();
247+
picture.set_paintable(Some(&texture));
248+
picture.upcast()
249+
}
250+
Err(e) => {
251+
log::warn!("Failed to load a sticker: {}", e);
252+
return;
253+
}
162254
}
163255
}
256+
_ => unimplemented!(),
257+
};
258+
259+
obj.imp().bin.set_child(Some(&widget));
164260
}));
165261
}
166262
}

0 commit comments

Comments
 (0)