Skip to content

Commit a5ab736

Browse files
committed
rlottie: Add support of animated stickers
1 parent eb4980f commit a5ab736

8 files changed

Lines changed: 394 additions & 85 deletions

File tree

Cargo.lock

Lines changed: 203 additions & 36 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,4 @@ regex = "1.7"
2222
tdlib = { version = "0.4", default-features = false }
2323
temp-dir = "0.1"
2424
thiserror = "1.0"
25+
rlt = { package="gtk-rlottie", git="https://github.com/YuraIz/gtk-rlottie-rs" }

build-aux/com.github.melix99.telegrand.Devel.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,18 @@
3636
"*.a"
3737
],
3838
"modules": [
39+
{
40+
"name": "rlottie",
41+
"buildsystem": "meson",
42+
"config-opts": ["-Dwerror=false"],
43+
"sources": [
44+
{
45+
"type": "git",
46+
"url": "https://github.com/melix99/rlottie",
47+
"branch": "fix-build"
48+
}
49+
]
50+
},
3951
{
4052
"name": "gtk",
4153
"buildsystem": "meson",

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

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,10 @@
11
<?xml version="1.0" encoding="UTF-8"?>
22
<interface>
33
<template class="ContentMessageSticker" parent="ContentMessageBase">
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="ContentStickerPicture" id="picture"/>
7+
<object class="AdwBin" id="bin"/>
118
</child>
129
<child type="overlay">
1310
<object class="MessageIndicators" id="indicators">

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;
33
mod snow;
4+
mod vector_path;
45

56
pub(crate) use self::avatar::Avatar;
67
pub(crate) use self::message_entry::MessageEntry;
78
pub(crate) use self::snow::Snow;
9+
pub(crate) use self::vector_path::StickerPreview;
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
use gtk::glib;
2+
use gtk::prelude::*;
3+
use gtk::subclass::prelude::*;
4+
5+
use tdlib::types::ClosedVectorPath;
6+
7+
mod imp {
8+
use super::*;
9+
use gtk::graphene;
10+
use std::cell::RefCell;
11+
use tdlib::enums::VectorPathCommand::{CubicBezierCurve, Line};
12+
use tdlib::types::VectorPathCommandCubicBezierCurve as Curve;
13+
14+
#[derive(Default)]
15+
pub struct StickerPreview {
16+
pub(super) path: RefCell<Vec<ClosedVectorPath>>,
17+
}
18+
19+
#[glib::object_subclass]
20+
impl ObjectSubclass for StickerPreview {
21+
const NAME: &'static str = "ComponentsVectorPath";
22+
type Type = super::StickerPreview;
23+
type ParentType = gtk::Widget;
24+
}
25+
26+
impl ObjectImpl for StickerPreview {}
27+
28+
impl WidgetImpl for StickerPreview {
29+
fn snapshot(&self, snapshot: &gtk::Snapshot) {
30+
let widget = self.obj();
31+
32+
let context = snapshot.append_cairo(&graphene::Rect::new(0.0, 0.0, 512.0, 512.0));
33+
34+
let scale = widget.width().max(widget.height()) as f64 / 512.0;
35+
context.scale(scale, scale);
36+
37+
context.set_source_rgba(0.5, 0.5, 0.5, 0.4);
38+
39+
let outline = &*self.path.borrow();
40+
41+
for closed_path in outline {
42+
for command in &closed_path.commands {
43+
match command {
44+
Line(line) => {
45+
let e = &line.end_point;
46+
context.line_to(e.x, e.y);
47+
}
48+
CubicBezierCurve(curve) => {
49+
let Curve {
50+
start_control_point: sc,
51+
end_control_point: ec,
52+
end_point: e,
53+
} = curve;
54+
55+
context.curve_to(sc.x, sc.y, ec.x, ec.y, e.x, e.y);
56+
}
57+
}
58+
}
59+
_ = context.fill();
60+
}
61+
}
62+
}
63+
}
64+
65+
glib::wrapper! {
66+
pub struct StickerPreview(ObjectSubclass<imp::StickerPreview>)
67+
@extends gtk::Widget;
68+
}
69+
70+
impl StickerPreview {
71+
pub fn new(outline: Vec<ClosedVectorPath>) -> Self {
72+
let obj: Self = glib::Object::new(&[]);
73+
obj.imp().path.replace(outline);
74+
obj
75+
}
76+
}

src/session/content/message_row/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -320,7 +320,7 @@ impl MessageRow {
320320
self.update_specific_content::<_, MessagePhoto>(message_.clone());
321321
}
322322
MessageContent::MessageSticker(data)
323-
if data.sticker.format == StickerFormat::Webp =>
323+
if matches!(data.sticker.format, StickerFormat::Webp | StickerFormat::Tgs) =>
324324
{
325325
self.update_specific_content::<_, MessageSticker>(message_.clone());
326326
}

src/session/content/message_row/sticker.rs

Lines changed: 97 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,15 @@
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};
99
use tdlib::types::File;
1010

11-
use crate::session::content::message_row::{
12-
MessageBase, MessageBaseImpl, MessageIndicators, StickerPicture,
13-
};
11+
use crate::session::components::StickerPreview;
12+
use crate::session::content::message_row::{MessageBase, MessageBaseImpl, MessageIndicators};
1413
use crate::tdlib::Message;
1514
use crate::utils::spawn;
1615

@@ -19,14 +18,19 @@ use super::base::MessageBaseExt;
1918
mod imp {
2019
use super::*;
2120
use once_cell::sync::Lazy;
22-
use std::cell::RefCell;
21+
use once_cell::unsync::OnceCell;
22+
use std::cell::{Cell, RefCell};
2323

2424
#[derive(Debug, Default, CompositeTemplate)]
2525
#[template(resource = "/com/github/melix99/telegrand/ui/content-message-sticker.ui")]
2626
pub(crate) struct MessageSticker {
27+
pub(super) format: OnceCell<StickerFormat>,
28+
pub(super) aspect_ratio: Cell<f64>,
2729
pub(super) message: RefCell<Option<Message>>,
2830
#[template_child]
29-
pub(super) picture: TemplateChild<StickerPicture>,
31+
pub(super) overlay: TemplateChild<gtk::Overlay>,
32+
#[template_child]
33+
pub(super) bin: TemplateChild<adw::Bin>,
3034
#[template_child]
3135
pub(super) indicators: TemplateChild<MessageIndicators>,
3236
}
@@ -74,7 +78,31 @@ mod imp {
7478
}
7579
}
7680

77-
impl WidgetImpl for MessageSticker {}
81+
impl WidgetImpl for MessageSticker {
82+
fn measure(&self, orientation: gtk::Orientation, for_size: i32) -> (i32, i32, i32, i32) {
83+
const SIZE: i32 = 208;
84+
let aspect_ratio = self.aspect_ratio.get();
85+
let min_size = self.overlay.measure(orientation, for_size).0;
86+
let size = if let gtk::Orientation::Horizontal = orientation {
87+
if aspect_ratio >= 1.0 {
88+
SIZE
89+
} else {
90+
(SIZE as f64 * aspect_ratio) as i32
91+
}
92+
} else if aspect_ratio >= 1.0 {
93+
(SIZE as f64 / aspect_ratio) as i32
94+
} else {
95+
SIZE
96+
}
97+
.max(min_size);
98+
99+
(size, size, -1, -1)
100+
}
101+
102+
fn size_allocate(&self, width: i32, height: i32, baseline: i32) {
103+
self.overlay.allocate(width, height, baseline, None);
104+
}
105+
}
78106
impl MessageBaseImpl for MessageSticker {}
79107
}
80108

@@ -95,15 +123,19 @@ impl MessageBaseExt for MessageSticker {
95123

96124
imp.indicators.set_message(message.clone().upcast());
97125

98-
imp.picture.set_texture(None);
99-
100126
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);
127+
let sticker = data.sticker;
104128

105-
if data.sticker.sticker.local.is_downloading_completed {
106-
self.load_sticker(&data.sticker.sticker.local.path);
129+
imp.format.set(sticker.format).unwrap();
130+
131+
imp.aspect_ratio
132+
.set(sticker.width as f64 / sticker.height as f64);
133+
134+
let preview = StickerPreview::new(sticker.outline.clone());
135+
self.imp().bin.set_child(Some(&preview));
136+
137+
if sticker.sticker.local.is_downloading_completed {
138+
self.load_sticker(&sticker.sticker.local.path);
107139
} else {
108140
let (sender, receiver) =
109141
glib::MainContext::sync_channel::<File>(Default::default(), 5);
@@ -122,45 +154,67 @@ impl MessageBaseExt for MessageSticker {
122154
message
123155
.chat()
124156
.session()
125-
.download_file(data.sticker.sticker.id, sender);
157+
.download_file(sticker.sticker.id, sender);
126158
}
127159
}
128-
129160
imp.message.replace(Some(message));
161+
130162
self.notify("message");
131163
}
132164
}
133165

134166
impl MessageSticker {
135167
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);
168+
let path = path.to_owned();
169+
spawn(clone!(@weak self as obj => async move {
170+
let widget: gtk::Widget = match obj.imp().format.get().unwrap() {
171+
StickerFormat::Tgs => {
172+
let animation = rlt::Animation::from_filename(&path);
173+
animation.set_loop(true);
174+
animation.use_cache(true);
175+
animation.play();
176+
animation.upcast()
177+
}
178+
StickerFormat::Webp => {
179+
let file = gio::File::for_path(&path);
180+
match file.load_bytes_future().await {
181+
Ok((bytes, _)) => {
182+
let flat_samples =
183+
ImageReader::with_format(Cursor::new(bytes), ImageFormat::WebP)
184+
.decode()
185+
.unwrap()
186+
.into_rgba8()
187+
.into_flat_samples();
188+
189+
let (stride, width, height) = flat_samples.extents();
190+
let gtk_stride = stride * width;
191+
192+
let bytes = glib::Bytes::from_owned(flat_samples.samples);
193+
let texture = gdk::MemoryTexture::new(
194+
width as i32,
195+
height as i32,
196+
gdk::MemoryFormat::R8g8b8a8,
197+
&bytes,
198+
gtk_stride,
199+
);
200+
201+
let picture = gtk::Picture::new();
202+
203+
picture.set_paintable(Some(&texture));
204+
205+
picture.upcast()
206+
}
207+
Err(e) => {
208+
log::warn!("Failed to load a sticker: {}", e);
209+
return;
210+
}
162211
}
163212
}
213+
_ => unimplemented!(),
214+
};
215+
216+
obj.imp().bin.set_child(Some(&widget));
217+
164218
}));
165219
}
166220
}

0 commit comments

Comments
 (0)