diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt
index 1f71631980..a388a4d5e9 100644
--- a/Telegram/CMakeLists.txt
+++ b/Telegram/CMakeLists.txt
@@ -870,11 +870,25 @@ PRIVATE
dialogs/suggestions/suggestion_userpic_setup.cpp
dialogs/dialogs_widget.cpp
dialogs/dialogs_widget.h
+ editor/audio_track_timeline.cpp
+ editor/audio_track_timeline.h
editor/color_picker.cpp
editor/color_picker.h
editor/controllers/controllers.h
editor/controllers/stickers_panel_controller.cpp
editor/controllers/stickers_panel_controller.h
+ editor/editor_audio_timeline.cpp
+ editor/editor_audio_timeline.h
+ editor/editor_keys_legend.cpp
+ editor/editor_keys_legend.h
+ editor/editor_link_box.cpp
+ editor/editor_link_box.h
+ editor/editor_message_render.cpp
+ editor/editor_message_render.h
+ editor/editor_message_source.cpp
+ editor/editor_message_source.h
+ editor/editor_message_video.cpp
+ editor/editor_message_video.h
editor/editor_paint.cpp
editor/editor_paint.h
editor/photo_editor.cpp
@@ -885,8 +899,14 @@ PRIVATE
editor/photo_editor_controls.h
editor/photo_editor_layer_widget.cpp
editor/photo_editor_layer_widget.h
+ editor/scene/scene_item_message.cpp
+ editor/scene/scene_item_message.h
editor/scene/scene_item_sticker.cpp
editor/scene/scene_item_sticker.h
+ editor/scene/scene_item_video.cpp
+ editor/scene/scene_item_video.h
+ editor/video/video_clip.cpp
+ editor/video/video_clip.h
editor/video/video_editor.cpp
editor/video/video_editor.h
editor/video/video_editor_common.cpp
@@ -897,8 +917,14 @@ PRIVATE
editor/video/video_editor_quality.h
editor/video/video_quality_slider.cpp
editor/video/video_quality_slider.h
+ editor/video/video_segment_player.cpp
+ editor/video/video_segment_player.h
editor/video/video_timeline.cpp
editor/video/video_timeline.h
+ editor/video/video_timeline_seeker.cpp
+ editor/video/video_timeline_seeker.h
+ editor/video_item_timeline.cpp
+ editor/video_item_timeline.h
export/export_manager.cpp
export/export_manager.h
export/view/export_view_content.cpp
diff --git a/Telegram/Resources/animations/photo_editor/trim_shortest.tgs b/Telegram/Resources/animations/photo_editor/trim_shortest.tgs
new file mode 100644
index 0000000000..6629b13395
Binary files /dev/null and b/Telegram/Resources/animations/photo_editor/trim_shortest.tgs differ
diff --git a/Telegram/Resources/animations/sun_outline.tgs b/Telegram/Resources/animations/sun_outline.tgs
new file mode 100644
index 0000000000..b21ef4c1f4
Binary files /dev/null and b/Telegram/Resources/animations/sun_outline.tgs differ
diff --git a/Telegram/Resources/icons/photo_editor/keyboard.svg b/Telegram/Resources/icons/photo_editor/keyboard.svg
new file mode 100644
index 0000000000..e1d8731da0
--- /dev/null
+++ b/Telegram/Resources/icons/photo_editor/keyboard.svg
@@ -0,0 +1,16 @@
+
diff --git a/Telegram/Resources/icons/photo_editor/link.png b/Telegram/Resources/icons/photo_editor/link.png
new file mode 100644
index 0000000000..949ebfce97
Binary files /dev/null and b/Telegram/Resources/icons/photo_editor/link.png differ
diff --git a/Telegram/Resources/icons/photo_editor/link@2x.png b/Telegram/Resources/icons/photo_editor/link@2x.png
new file mode 100644
index 0000000000..cb5834ab72
Binary files /dev/null and b/Telegram/Resources/icons/photo_editor/link@2x.png differ
diff --git a/Telegram/Resources/icons/photo_editor/link@3x.png b/Telegram/Resources/icons/photo_editor/link@3x.png
new file mode 100644
index 0000000000..da43c82ed7
Binary files /dev/null and b/Telegram/Resources/icons/photo_editor/link@3x.png differ
diff --git a/Telegram/Resources/icons/photo_editor/widget_music.png b/Telegram/Resources/icons/photo_editor/widget_music.png
new file mode 100644
index 0000000000..789aeac95a
Binary files /dev/null and b/Telegram/Resources/icons/photo_editor/widget_music.png differ
diff --git a/Telegram/Resources/icons/photo_editor/widget_music@2x.png b/Telegram/Resources/icons/photo_editor/widget_music@2x.png
new file mode 100644
index 0000000000..3535d85dbf
Binary files /dev/null and b/Telegram/Resources/icons/photo_editor/widget_music@2x.png differ
diff --git a/Telegram/Resources/icons/photo_editor/widget_music@3x.png b/Telegram/Resources/icons/photo_editor/widget_music@3x.png
new file mode 100644
index 0000000000..8f464aaca7
Binary files /dev/null and b/Telegram/Resources/icons/photo_editor/widget_music@3x.png differ
diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings
index 9ba7668f72..12fc0d0e21 100644
--- a/Telegram/Resources/langs/lang.strings
+++ b/Telegram/Resources/langs/lang.strings
@@ -3176,6 +3176,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_attach_failed" = "Failed";
"lng_attach_file" = "File";
"lng_attach_photo" = "Photo";
+"lng_attach_audio" = "Audio";
"lng_attach_camera" = "Camera";
"lng_attach_document" = "Document";
"lng_attach_photo_or_video" = "Photo or video";
@@ -5355,6 +5356,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_duration_played" = "{played} / {duration}";
"lng_date_and_duration" = "{date}, {duration}";
"lng_choose_image" = "Choose an image";
+"lng_choose_audio" = "Choose an audio file";
"lng_choose_file" = "Choose a file";
"lng_choose_files" = "Choose Files";
"lng_music_attach_choose_from_files" = "Choose from files";
@@ -7954,6 +7956,50 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_photo_editor_shape_arrow" = "Arrow";
"lng_photo_editor_shape_filled" = "Filled";
+"lng_photo_editor_audio_remove" = "Remove Audio";
+"lng_photo_editor_trim_shortest" = "Trim to shortest";
+
+"lng_photo_editor_keys_tooltip" = "Keyboard shortcuts";
+"lng_photo_editor_keys_gesture" = "{modifier}-{gesture}";
+"lng_photo_editor_keys_gesture_drag" = "drag";
+"lng_photo_editor_keys_gesture_drag_edge" = "drag edge";
+"lng_photo_editor_keys_gesture_drag_handle" = "drag handle";
+"lng_photo_editor_keys_gesture_click" = "click";
+"lng_photo_editor_keys_gesture_wheel" = "wheel";
+"lng_photo_editor_keys_wheel" = "Wheel";
+"lng_photo_editor_keys_middle_drag" = "Middle drag";
+"lng_photo_editor_keys_double_click" = "Double click";
+"lng_photo_editor_keys_legend" = "Show these shortcuts";
+"lng_photo_editor_keys_crop" = "Crop";
+"lng_photo_editor_keys_crop_room" = "Zoom out for room around the photo";
+"lng_photo_editor_keys_crop_expand" = "Grow canvas past the photo";
+"lng_photo_editor_keys_fixed_zoom" = "Zoom picture inside the frame";
+"lng_photo_editor_keys_fixed_zoom_fine" = "Zoom in finer steps";
+"lng_photo_editor_keys_fixed_pan" = "Move picture inside the frame";
+"lng_photo_editor_keys_canvas" = "Canvas";
+"lng_photo_editor_keys_canvas_zoom" = "Zoom canvas at cursor";
+"lng_photo_editor_keys_canvas_pan" = "Pan zoomed canvas";
+"lng_photo_editor_keys_canvas_paste" = "Paste image, sticker, file or link";
+"lng_photo_editor_keys_canvas_undo" = "Undo";
+"lng_photo_editor_keys_canvas_redo" = "Redo";
+"lng_photo_editor_keys_shapes" = "Shapes";
+"lng_photo_editor_keys_shapes_proportions" = "Keep proportions or snap arrow angle";
+"lng_photo_editor_keys_shapes_instant" = "Place shape from the menu right away";
+"lng_photo_editor_keys_objects" = "Objects";
+"lng_photo_editor_keys_objects_snap" = "Snap to canvas guides";
+"lng_photo_editor_keys_objects_rotate" = "Rotate only, in 45° steps";
+"lng_photo_editor_keys_objects_stretch" = "Stretch shape freely";
+"lng_photo_editor_keys_objects_duplicate" = "Duplicate selection";
+"lng_photo_editor_keys_objects_flip" = "Flip selection horizontally";
+"lng_photo_editor_keys_objects_delete" = "Delete selection";
+"lng_photo_editor_keys_objects_text" = "Edit text";
+"lng_photo_editor_keys_timeline" = "Timeline";
+"lng_photo_editor_keys_timeline_zoom" = "Zoom timeline";
+"lng_photo_editor_keys_timeline_scroll" = "Scroll timeline";
+"lng_photo_editor_keys_timeline_window" = "Move trimmed range";
+
+"lng_photo_editor_link_customize" = "Customize Link Text";
+
"lng_voice_speed_slow" = "Slow";
"lng_voice_speed_normal" = "Normal";
"lng_voice_speed_medium" = "Medium";
diff --git a/Telegram/Resources/qrc/telegram/animations.qrc b/Telegram/Resources/qrc/telegram/animations.qrc
index d160a9ba3d..2785cc7695 100644
--- a/Telegram/Resources/qrc/telegram/animations.qrc
+++ b/Telegram/Resources/qrc/telegram/animations.qrc
@@ -30,6 +30,7 @@
../../animations/hours.tgs
../../animations/phone.tgs
../../animations/call_rate.tgs
+ ../../animations/sun_outline.tgs
../../animations/chat_link.tgs
../../animations/diamond.tgs
../../animations/collectible_username.tgs
@@ -105,6 +106,7 @@
../../animations/photo_editor/text_align.tgs
../../animations/photo_editor/eraser.tgs
../../animations/photo_editor/blur.tgs
+ ../../animations/photo_editor/trim_shortest.tgs
../../animations/swipe_action/archive.tgs
../../animations/swipe_action/unarchive.tgs
diff --git a/Telegram/SourceFiles/boxes/send_files_box.cpp b/Telegram/SourceFiles/boxes/send_files_box.cpp
index 27f298682d..10bc780e74 100644
--- a/Telegram/SourceFiles/boxes/send_files_box.cpp
+++ b/Telegram/SourceFiles/boxes/send_files_box.cpp
@@ -1431,6 +1431,8 @@ void SendFilesBox::pushBlock(int from, int till) {
show->session().saveSettings();
}
*openedOnce = true;
+ const auto videos = (_limits & SendFilesAllow::Videos);
+ const auto animated = videos || (_limits & SendFilesAllow::Gifs);
Editor::OpenWithPreparedFile(
this,
show,
@@ -1439,7 +1441,8 @@ void SendFilesBox::pushBlock(int from, int till) {
std::move(done),
PhotoSideLimit(true),
QSize(),
- true);
+ animated,
+ videos);
};
const auto replaceAttachment = [=, show = _show](int index) {
applyBlockChanges();
diff --git a/Telegram/SourceFiles/chat_helpers/chat_helpers.style b/Telegram/SourceFiles/chat_helpers/chat_helpers.style
index 3f55c335e1..b301638a17 100644
--- a/Telegram/SourceFiles/chat_helpers/chat_helpers.style
+++ b/Telegram/SourceFiles/chat_helpers/chat_helpers.style
@@ -154,6 +154,8 @@ EmojiPan {
searchBackTextLeft: pixels;
searchBackTextTop: pixels;
photoButtonIcon: icon;
+ audioButtonIcon: icon;
+ linkButtonIcon: icon;
colorAll: IconButton;
colorAllLabel: FlatLabel;
removeSet: IconButton;
@@ -636,7 +638,8 @@ stickersPhotoButtonHeight: 36px;
stickersPhotoButtonRadius: 8px;
stickersPhotoButtonPadding: margins(6px, 0px, 12px, 0px);
stickersPhotoButtonIconSkip: 4px;
-stickersPhotoButtonFont: font(21px bold);
+stickersPhotoButtonFont: font(14px bold);
+stickersPhotoButtonSkip: 10px;
mediaPreviewPhotoSkip: 48px;
emojiPanColorAll: IconButton(stickerPanRemoveSet) {
@@ -831,6 +834,8 @@ defaultEmojiPan: EmojiPan {
searchBackTextLeft: 40px;
searchBackTextTop: 10px;
photoButtonIcon: icon {{ "photo_editor/camera-24x24", windowFg }};
+ audioButtonIcon: icon {{ "photo_editor/widget_music", windowFg }};
+ linkButtonIcon: icon {{ "limits/links", windowFg }};
colorAll: emojiPanColorAll;
colorAllLabel: emojiPanColorAllLabel;
removeSet: stickerPanRemoveSet;
diff --git a/Telegram/SourceFiles/chat_helpers/compose/compose_features.h b/Telegram/SourceFiles/chat_helpers/compose/compose_features.h
index 744104878b..0302308dfa 100644
--- a/Telegram/SourceFiles/chat_helpers/compose/compose_features.h
+++ b/Telegram/SourceFiles/chat_helpers/compose/compose_features.h
@@ -23,6 +23,8 @@ struct ComposeFeatures {
bool stickersSettings : 1 = true;
bool openStickerSets : 1 = true;
bool photoButton : 1 = false;
+ bool audioButton : 1 = false;
+ bool linkButton : 1 = false;
bool autocompleteHashtags : 1 = true;
bool autocompleteMentions : 1 = true;
bool autocompleteCommands : 1 = true;
diff --git a/Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp b/Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp
index f928810107..c8d5551311 100644
--- a/Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp
+++ b/Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp
@@ -240,9 +240,8 @@ StickersListWidget::StickersListWidget(
, _megagroupSetAbout(st::columnMinimalWidthThird
- st::emojiScroll.width
- st().headerLeft)
-, _photoButtonBg(st::stickersPhotoButtonRadius, st().overBg)
-, _photoButtonText(tr::lng_attach_photo(tr::now).toUpper())
-, _photoButtonTextWidth(st::stickersPhotoButtonFont->width(_photoButtonText))
+, _mediaButtonBg(st::stickersPhotoButtonRadius, st().overBg)
+, _mediaButtons(MakeMediaButtons(_features, st()))
, _addText(tr::lng_stickers_featured_add(tr::now))
, _addWidth(st::stickersTrendingAdd.style.font->width(_addText))
, _installedText(tr::lng_stickers_featured_installed(tr::now))
@@ -324,6 +323,14 @@ rpl::producer<> StickersListWidget::photoRequests() const {
return _photoRequests.events();
}
+rpl::producer<> StickersListWidget::audioRequests() const {
+ return _audioRequests.events();
+}
+
+rpl::producer<> StickersListWidget::linkRequests() const {
+ return _linkRequests.events();
+}
+
rpl::producer<> StickersListWidget::scrollUpdated() const {
return _scrollUpdated.events();
}
@@ -509,7 +516,7 @@ template
bool StickersListWidget::enumerateSections(Callback callback) const {
auto info = SectionInfo();
info.top = _search ? _search->height() : 0;
- info.top += photoRowHeight() + searchShortcutsHeight();
+ info.top += mediaButtonsRowHeight() + searchShortcutsHeight();
const auto &sets = shownSets();
for (auto i = 0; i != sets.size(); ++i) {
auto &set = sets[i];
@@ -976,69 +983,135 @@ void StickersListWidget::startSearchSwapAnimation(
}
int StickersListWidget::searchShortcutsTop() const {
- return (_search ? _search->height() : 0) + photoRowHeight();
+ return (_search ? _search->height() : 0) + mediaButtonsRowHeight();
}
-int StickersListWidget::photoRowHeight() const {
- if (!_features.photoButton || _isMasks || _section == Section::Search) {
+std::vector
+StickersListWidget::MakeMediaButtons(
+ const ComposeFeatures &features,
+ const style::EmojiPan &st) {
+ auto result = std::vector();
+ const auto add = [&](
+ MediaButton::Kind kind,
+ const QString &text,
+ const style::icon *icon) {
+ auto button = MediaButton{
+ .kind = kind,
+ .text = text.toUpper(),
+ .icon = icon,
+ };
+ button.textWidth = st::stickersPhotoButtonFont->width(button.text);
+ result.push_back(std::move(button));
+ };
+ if (features.linkButton) {
+ add(
+ MediaButton::Kind::Link,
+ tr::lng_link_header_short(tr::now),
+ &st.linkButtonIcon);
+ }
+ if (features.photoButton) {
+ add(
+ MediaButton::Kind::Photo,
+ tr::lng_attach_photo(tr::now),
+ &st.photoButtonIcon);
+ }
+ if (features.audioButton) {
+ add(
+ MediaButton::Kind::Audio,
+ tr::lng_attach_audio(tr::now),
+ &st.audioButtonIcon);
+ }
+ return result;
+}
+
+int StickersListWidget::mediaButtonsRowHeight() const {
+ if (_mediaButtons.empty() || _isMasks || _section == Section::Search) {
return 0;
}
const auto &padding = st::stickersPhotoRowPadding;
return padding.top() + st::stickersPhotoButtonHeight + padding.bottom();
}
-QRect StickersListWidget::photoButtonRect() const {
+int StickersListWidget::mediaButtonWidth(const MediaButton &button) const {
const auto &padding = st::stickersPhotoButtonPadding;
- const auto &icon = st().photoButtonIcon;
- const auto buttonWidth = padding.left()
- + icon.width()
+ return padding.left()
+ + button.icon->width()
+ st::stickersPhotoButtonIconSkip
- + _photoButtonTextWidth
+ + button.textWidth
+ padding.right();
+}
+
+QRect StickersListWidget::mediaButtonRect(int index) const {
+ Expects(index >= 0 && index < int(_mediaButtons.size()));
+
+ const auto skip = st::stickersPhotoButtonSkip;
+ auto full = -skip;
+ for (const auto &button : _mediaButtons) {
+ full += mediaButtonWidth(button) + skip;
+ }
+ auto left = (width() - full) / 2;
+ for (auto i = 0; i != index; ++i) {
+ left += mediaButtonWidth(_mediaButtons[i]) + skip;
+ }
const auto top = (_search ? _search->height() : 0)
+ st::stickersPhotoRowPadding.top();
return QRect(
- (width() - buttonWidth) / 2,
+ left,
top,
- buttonWidth,
+ mediaButtonWidth(_mediaButtons[index]),
st::stickersPhotoButtonHeight);
}
-void StickersListWidget::paintPhotoButton(Painter &p, QRect clip) {
- if (!photoRowHeight()) {
- return;
+int StickersListWidget::mediaButtonIndexAt(QPoint point) const {
+ if (!mediaButtonsRowHeight()) {
+ return -1;
}
- const auto rect = photoButtonRect();
- if (!rect.intersects(clip)) {
- return;
- }
- _photoButtonBg.paint(p, myrtlrect(rect));
- if (_photoButtonRipple) {
- _photoButtonRipple->paint(p, myrtlrect(rect).x(), rect.y(), width());
- if (_photoButtonRipple->empty()) {
- _photoButtonRipple.reset();
+ for (auto i = 0; i != int(_mediaButtons.size()); ++i) {
+ if (myrtlrect(mediaButtonRect(i)).contains(point)) {
+ return i;
}
}
- const auto &padding = st::stickersPhotoButtonPadding;
- const auto &icon = st().photoButtonIcon;
- icon.paint(
- p,
- rect.x() + padding.left(),
- rect.y() + (rect.height() - icon.height()) / 2,
- width());
- const auto &font = st::stickersPhotoButtonFont;
- const auto textLeft = rect.x()
- + padding.left()
- + icon.width()
- + st::stickersPhotoButtonIconSkip;
- p.setFont(font);
- p.setPen(st().textFg);
- p.drawTextLeft(
- textLeft,
- rect.y() + (rect.height() - font->height) / 2,
- width(),
- _photoButtonText,
- _photoButtonTextWidth);
+ return -1;
+}
+
+void StickersListWidget::paintMediaButtons(Painter &p, QRect clip) {
+ if (!mediaButtonsRowHeight()) {
+ return;
+ }
+ for (auto i = 0; i != int(_mediaButtons.size()); ++i) {
+ auto &button = _mediaButtons[i];
+ const auto rect = mediaButtonRect(i);
+ if (!rect.intersects(clip)) {
+ continue;
+ }
+ _mediaButtonBg.paint(p, myrtlrect(rect));
+ if (button.ripple) {
+ button.ripple->paint(p, myrtlrect(rect).x(), rect.y(), width());
+ if (button.ripple->empty()) {
+ button.ripple.reset();
+ }
+ }
+ const auto &padding = st::stickersPhotoButtonPadding;
+ const auto &icon = *button.icon;
+ icon.paint(
+ p,
+ rect.x() + padding.left(),
+ rect.y() + (rect.height() - icon.height()) / 2,
+ width());
+ const auto &font = st::stickersPhotoButtonFont;
+ const auto textLeft = rect.x()
+ + padding.left()
+ + icon.width()
+ + st::stickersPhotoButtonIconSkip;
+ p.setFont(font);
+ p.setPen(st().textFg);
+ p.drawTextLeft(
+ textLeft,
+ rect.y() + (rect.height() - font->height) / 2,
+ width(),
+ button.text,
+ button.textWidth);
+ }
}
int StickersListWidget::searchShortcutsHeight() const {
@@ -1624,7 +1697,7 @@ void StickersListWidget::paintStickers(Painter &p, QRect clip) {
_paintAsPremium = session().premium();
_pathGradient->startFrame(0, width(), width() / 2);
paintSearchShortcuts(p, clip);
- paintPhotoButton(p, clip);
+ paintMediaButtons(p, clip);
auto &sets = shownSets();
const auto selectedSticker = std::get_if(&_selected);
@@ -2482,9 +2555,11 @@ void StickersListWidget::setPressed(OverState newPressed) {
if (_megagroupSetButtonRipple) {
_megagroupSetButtonRipple->lastStop();
}
- } else if (std::get_if(&_pressed)) {
- if (_photoButtonRipple) {
- _photoButtonRipple->lastStop();
+ } else if (const auto media = std::get_if(&_pressed)) {
+ if (media->index >= 0 && media->index < int(_mediaButtons.size())) {
+ if (const auto &ripple = _mediaButtons[media->index].ripple) {
+ ripple->lastStop();
+ }
}
}
_pressed = newPressed;
@@ -2519,18 +2594,23 @@ void StickersListWidget::setPressed(OverState newPressed) {
}
_megagroupSetButtonRipple->add(mapFromGlobal(QCursor::pos())
- myrtlrect(megagroupSetButtonRectFinal()).topLeft());
- } else if (std::get_if(&_pressed)) {
- if (!_photoButtonRipple) {
+ } else if (const auto media = std::get_if(&_pressed)) {
+ const auto index = media->index;
+ if (index < 0 || index >= int(_mediaButtons.size())) {
+ return;
+ }
+ auto &button = _mediaButtons[index];
+ if (!button.ripple) {
auto mask = Ui::RippleAnimation::RoundRectMask(
- photoButtonRect().size(),
+ mediaButtonRect(index).size(),
st::stickersPhotoButtonRadius);
- _photoButtonRipple = std::make_unique(
+ button.ripple = std::make_unique(
st().searchPackRipple,
std::move(mask),
- [this] { rtlupdate(photoButtonRect()); });
+ [this, index] { rtlupdate(mediaButtonRect(index)); });
}
- _photoButtonRipple->add(mapFromGlobal(QCursor::pos())
- - myrtlrect(photoButtonRect()).topLeft());
+ button.ripple->add(mapFromGlobal(QCursor::pos())
+ - myrtlrect(mediaButtonRect(index)).topLeft());
}
}
@@ -2865,8 +2945,19 @@ void StickersListWidget::mouseReleaseEvent(QMouseEvent *e) {
if (std::get_if(&pressed)) {
backToSearchResults();
return;
- } else if (std::get_if(&pressed)) {
- _photoRequests.fire({});
+ } else if (const auto media = std::get_if(
+ &pressed)) {
+ const auto index = media->index;
+ if (index >= 0 && index < int(_mediaButtons.size())) {
+ const auto kind = _mediaButtons[index].kind;
+ if (kind == MediaButton::Kind::Photo) {
+ _photoRequests.fire({});
+ } else if (kind == MediaButton::Kind::Audio) {
+ _audioRequests.fire({});
+ } else if (kind == MediaButton::Kind::Link) {
+ _linkRequests.fire({});
+ }
+ }
return;
} else if (auto shortcut = std::get_if(&pressed)) {
toggleSearchShortcut(shortcut->index);
@@ -3645,8 +3736,8 @@ void StickersListWidget::updateSelected() {
}
setSelected(newSelected);
return;
- } else if (photoRowHeight() && myrtlrect(photoButtonRect()).contains(p)) {
- setSelected(OverPhotoButton{});
+ } else if (const auto media = mediaButtonIndexAt(p); media >= 0) {
+ setSelected(OverMediaButton{ media });
return;
}
const auto &sets = shownSets();
@@ -3759,8 +3850,12 @@ void StickersListWidget::setSelected(OverState newSelected) {
rtlupdate(searchBackRect());
} else if (std::get_if(&_selected)) {
rtlupdate(megagroupSetButtonRectFinal());
- } else if (std::get_if(&_selected)) {
- rtlupdate(photoButtonRect());
+ } else if (const auto media = std::get_if(
+ &_selected)) {
+ if (media->index >= 0
+ && media->index < int(_mediaButtons.size())) {
+ rtlupdate(mediaButtonRect(media->index));
+ }
}
};
updateSelected();
diff --git a/Telegram/SourceFiles/chat_helpers/stickers_list_widget.h b/Telegram/SourceFiles/chat_helpers/stickers_list_widget.h
index 8244c43e4f..dd9d6d70c5 100644
--- a/Telegram/SourceFiles/chat_helpers/stickers_list_widget.h
+++ b/Telegram/SourceFiles/chat_helpers/stickers_list_widget.h
@@ -105,6 +105,8 @@ public:
rpl::producer chosen() const;
[[nodiscard]] rpl::producer<> photoRequests() const;
+ [[nodiscard]] rpl::producer<> audioRequests() const;
+ [[nodiscard]] rpl::producer<> linkRequests() const;
rpl::producer<> scrollUpdated() const;
rpl::producer choosingUpdated() const;
@@ -233,11 +235,13 @@ private:
return !(*this == other);
}
};
- struct OverPhotoButton {
- inline bool operator==(OverPhotoButton other) const {
- return true;
+ struct OverMediaButton {
+ int index = 0;
+
+ inline bool operator==(OverMediaButton other) const {
+ return (index == other.index);
}
- inline bool operator!=(OverPhotoButton other) const {
+ inline bool operator!=(OverMediaButton other) const {
return !(*this == other);
}
};
@@ -249,7 +253,7 @@ private:
OverSearchShortcut,
OverSearchBack,
OverGroupAdd,
- OverPhotoButton>;
+ OverMediaButton>;
struct SectionInfo {
int section = 0;
@@ -320,9 +324,26 @@ private:
void readVisibleFeatured(int visibleTop, int visibleBottom);
void paintStickers(Painter &p, QRect clip);
- void paintPhotoButton(Painter &p, QRect clip);
- [[nodiscard]] int photoRowHeight() const;
- [[nodiscard]] QRect photoButtonRect() const;
+ struct MediaButton {
+ enum class Kind {
+ Photo,
+ Audio,
+ Link,
+ };
+ Kind kind = Kind::Photo;
+ QString text;
+ int textWidth = 0;
+ const style::icon *icon = nullptr;
+ std::unique_ptr ripple;
+ };
+ [[nodiscard]] static std::vector MakeMediaButtons(
+ const ComposeFeatures &features,
+ const style::EmojiPan &st);
+ void paintMediaButtons(Painter &p, QRect clip);
+ [[nodiscard]] int mediaButtonsRowHeight() const;
+ [[nodiscard]] int mediaButtonWidth(const MediaButton &button) const;
+ [[nodiscard]] QRect mediaButtonRect(int index) const;
+ [[nodiscard]] int mediaButtonIndexAt(QPoint point) const;
void paintMegagroupEmptySet(Painter &p, int y, bool buttonSelected);
void paintSticker(
Painter &p,
@@ -507,10 +528,8 @@ private:
QRect _megagroupSetButtonRect;
std::unique_ptr _megagroupSetButtonRipple;
- Ui::RoundRect _photoButtonBg;
- QString _photoButtonText;
- int _photoButtonTextWidth = 0;
- std::unique_ptr _photoButtonRipple;
+ Ui::RoundRect _mediaButtonBg;
+ std::vector _mediaButtons;
QString _addText;
int _addWidth;
@@ -551,6 +570,8 @@ private:
rpl::event_stream _chosen;
rpl::event_stream<> _photoRequests;
+ rpl::event_stream<> _audioRequests;
+ rpl::event_stream<> _linkRequests;
rpl::event_stream<> _scrollUpdated;
rpl::event_stream _choosingUpdated;
diff --git a/Telegram/SourceFiles/chat_helpers/tabbed_selector.cpp b/Telegram/SourceFiles/chat_helpers/tabbed_selector.cpp
index 65fc07c80b..d19c389536 100644
--- a/Telegram/SourceFiles/chat_helpers/tabbed_selector.cpp
+++ b/Telegram/SourceFiles/chat_helpers/tabbed_selector.cpp
@@ -760,6 +760,14 @@ rpl::producer<> TabbedSelector::photoRequests() const {
return hasStickersTab() ? stickers()->photoRequests() : rpl::never<>();
}
+rpl::producer<> TabbedSelector::audioRequests() const {
+ return hasStickersTab() ? stickers()->audioRequests() : rpl::never<>();
+}
+
+rpl::producer<> TabbedSelector::linkRequests() const {
+ return hasStickersTab() ? stickers()->linkRequests() : rpl::never<>();
+}
+
auto TabbedSelector::choosingStickerUpdated() const
-> rpl::producer{
return hasStickersTab()
diff --git a/Telegram/SourceFiles/chat_helpers/tabbed_selector.h b/Telegram/SourceFiles/chat_helpers/tabbed_selector.h
index 91d286c6b4..72187a56e2 100644
--- a/Telegram/SourceFiles/chat_helpers/tabbed_selector.h
+++ b/Telegram/SourceFiles/chat_helpers/tabbed_selector.h
@@ -151,6 +151,8 @@ public:
[[nodiscard]] rpl::producer photoChosen() const;
[[nodiscard]] rpl::producer inlineResultChosen() const;
[[nodiscard]] rpl::producer<> photoRequests() const;
+ [[nodiscard]] rpl::producer<> audioRequests() const;
+ [[nodiscard]] rpl::producer<> linkRequests() const;
[[nodiscard]] rpl::producer<> cancelled() const;
[[nodiscard]] rpl::producer<> checkForHide() const;
diff --git a/Telegram/SourceFiles/core/core_settings.cpp b/Telegram/SourceFiles/core/core_settings.cpp
index 83deb40e5b..1982dfc075 100644
--- a/Telegram/SourceFiles/core/core_settings.cpp
+++ b/Telegram/SourceFiles/core/core_settings.cpp
@@ -696,6 +696,9 @@ void Settings::addFromSerialized(const QByteArray &serialized) {
for (auto i = 0; i != soundOverridesCount; ++i) {
QString key, value;
stream >> key >> value;
+ if (stream.status() != QDataStream::Ok) {
+ break;
+ }
soundOverrides.emplace(key, value);
}
}
@@ -719,6 +722,9 @@ void Settings::addFromSerialized(const QByteArray &serialized) {
for (auto i = 0; i != dictionariesEnabledCount; ++i) {
qint64 langId;
stream >> langId;
+ if (stream.status() != QDataStream::Ok) {
+ break;
+ }
dictionariesEnabled.emplace_back(langId);
}
}
@@ -843,6 +849,9 @@ void Settings::addFromSerialized(const QByteArray &serialized) {
for (auto i = 0; i != accountsOrderCount; ++i) {
quint64 sessionUniqueId;
stream >> sessionUniqueId;
+ if (stream.status() != QDataStream::Ok) {
+ break;
+ }
accountsOrder.emplace_back(sessionUniqueId);
}
}
@@ -872,6 +881,9 @@ void Settings::addFromSerialized(const QByteArray &serialized) {
for (auto i = 0; i != skipTranslationLanguagesCount; ++i) {
quint64 language;
stream >> language;
+ if (stream.status() != QDataStream::Ok) {
+ break;
+ }
skipTranslationLanguages.push_back({
QLocale::Language(language)
});
@@ -911,9 +923,10 @@ void Settings::addFromSerialized(const QByteArray &serialized) {
for (auto i = 0; i != count; ++i) {
auto id = QString();
stream >> id;
- if (stream.status() == QDataStream::Ok) {
- recentEmojiSkip.emplace(id);
+ if (stream.status() != QDataStream::Ok) {
+ break;
}
+ recentEmojiSkip.emplace(id);
}
}
}
@@ -1021,11 +1034,13 @@ void Settings::addFromSerialized(const QByteArray &serialized) {
auto prefsCount = quint32();
stream >> prefsCount;
auto prefs = base::flat_map();
- prefs.reserve(prefsCount);
for (auto i = quint32(); i != prefsCount; ++i) {
auto key = QByteArray();
auto value = QByteArray();
stream >> key >> value;
+ if (stream.status() != QDataStream::Ok) {
+ break;
+ }
prefs.emplace(std::move(key), std::move(value));
}
if (stream.status() == QDataStream::Ok) {
diff --git a/Telegram/SourceFiles/data/data_chat_participant_status.cpp b/Telegram/SourceFiles/data/data_chat_participant_status.cpp
index 7d862f0ac0..83a6fe123f 100644
--- a/Telegram/SourceFiles/data/data_chat_participant_status.cpp
+++ b/Telegram/SourceFiles/data/data_chat_participant_status.cpp
@@ -504,7 +504,9 @@ SendError FileRestrictionError(
}
switch (file.type) {
case Type::Photo:
- if (compress == true && photos) {
+ if (compress == true && videos && file.hasAudioEditScene()) {
+ return videos;
+ } else if (compress == true && photos) {
return photos;
} else if (const auto other = file.isSticker() ? stickers : files) {
if ((compress == false || photos) && other) {
diff --git a/Telegram/SourceFiles/data/data_document.cpp b/Telegram/SourceFiles/data/data_document.cpp
index 567643f257..1129bc471c 100644
--- a/Telegram/SourceFiles/data/data_document.cpp
+++ b/Telegram/SourceFiles/data/data_document.cpp
@@ -1270,21 +1270,14 @@ void DocumentData::save(
status = FileReady;
auto reader = owner().streaming().sharedReader(this, origin, true);
if (reader) {
- _loader = std::make_unique(
- &session(),
- id,
- _dc,
- origin,
- Data::DocumentCacheKey(_dc, id),
- mediaKey(),
+ _loader = createStreamedDownloader(
std::move(reader),
+ origin,
+ mediaKey(),
toFile,
- size,
- locationType(),
(saveToCache() ? LoadToCacheAsWell : LoadToFileOnly),
fromCloud,
- autoLoading,
- cacheTag());
+ autoLoading);
} else if (hasWebLocation()) {
_loader = std::make_unique(
&session(),
@@ -1655,6 +1648,32 @@ bool DocumentData::hasRemoteLocation() const {
return (_dc != 0 && _access != 0);
}
+auto DocumentData::createStreamedDownloader(
+ std::shared_ptr reader,
+ Data::FileOrigin origin,
+ std::optional fileLocationKey,
+ const QString &toFile,
+ LoadToCacheSetting toCache,
+ LoadFromCloudSetting fromCloud,
+ bool autoLoading) const
+-> std::unique_ptr {
+ return std::make_unique(
+ &session(),
+ id,
+ _dc,
+ origin,
+ Data::DocumentCacheKey(_dc, id),
+ fileLocationKey,
+ std::move(reader),
+ toFile,
+ size,
+ locationType(),
+ toCache,
+ fromCloud,
+ autoLoading,
+ cacheTag());
+}
+
bool DocumentData::useStreamingLoader() const {
if (size <= 0) {
return false;
diff --git a/Telegram/SourceFiles/data/data_document.h b/Telegram/SourceFiles/data/data_document.h
index 5806b03250..8a8614b21d 100644
--- a/Telegram/SourceFiles/data/data_document.h
+++ b/Telegram/SourceFiles/data/data_document.h
@@ -27,6 +27,7 @@ enum class NameType : uchar;
} // namespace Core
namespace Storage {
+class StreamedFileDownloader;
namespace Cache {
struct Key;
} // namespace Cache
@@ -38,6 +39,7 @@ struct VideoQuality;
namespace Media::Streaming {
class Loader;
+class Reader;
} // namespace Media::Streaming
namespace Data {
@@ -301,6 +303,7 @@ public:
[[nodiscard]] MediaKey mediaKey() const;
[[nodiscard]] Storage::Cache::Key cacheKey() const;
+ [[nodiscard]] LocationType locationType() const;
[[nodiscard]] uint8 cacheTag() const;
[[nodiscard]] bool canBeStreamed() const;
@@ -309,6 +312,15 @@ public:
bool forceRemoteLoader) const
-> std::unique_ptr;
[[nodiscard]] bool useStreamingLoader() const;
+ [[nodiscard]] auto createStreamedDownloader(
+ std::shared_ptr reader,
+ Data::FileOrigin origin,
+ std::optional fileLocationKey,
+ const QString &toFile,
+ LoadToCacheSetting toCache,
+ LoadFromCloudSetting fromCloud,
+ bool autoLoading) const
+ -> std::unique_ptr;
void setInappPlaybackFailed();
[[nodiscard]] bool inappPlaybackFailed() const;
@@ -370,7 +382,6 @@ private:
friend class Serialize::Document;
- [[nodiscard]] LocationType locationType() const;
void validateLottieSticker();
void setMaybeSupportsStreaming(bool supports);
void setLoadedInMediaCacheLocation();
diff --git a/Telegram/SourceFiles/data/data_media_types.h b/Telegram/SourceFiles/data/data_media_types.h
index 862c1ba7b1..8f1890c5e2 100644
--- a/Telegram/SourceFiles/data/data_media_types.h
+++ b/Telegram/SourceFiles/data/data_media_types.h
@@ -210,6 +210,8 @@ struct GiftCode {
inline constexpr auto kTimeToLiveSingleView = crl::time(0x7FFFFFFF);
+class MediaDice;
+
class Media {
public:
Media(not_null parent);
diff --git a/Telegram/SourceFiles/editor/audio_track_timeline.cpp b/Telegram/SourceFiles/editor/audio_track_timeline.cpp
new file mode 100644
index 0000000000..e51372ff9a
--- /dev/null
+++ b/Telegram/SourceFiles/editor/audio_track_timeline.cpp
@@ -0,0 +1,80 @@
+/*
+This file is part of Telegram Desktop,
+the official desktop application for the Telegram messaging service.
+
+For license and copyright information please follow this link:
+https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
+*/
+#include "editor/audio_track_timeline.h"
+
+#include "editor/editor_audio_timeline.h"
+#include "editor/photo_editor_common.h"
+#include "styles/style_editor.h"
+
+namespace Editor {
+
+AudioTrackTimeline::AudioTrackTimeline(not_null parent)
+: RpWidget(parent) {
+}
+
+AudioTrackTimeline::~AudioTrackTimeline() = default;
+
+void AudioTrackTimeline::setTrack(std::shared_ptr track) {
+ if (!track) {
+ if (_timeline) {
+ _timeline->setPlaying(false);
+ }
+ return;
+ } else if (_track != track) {
+ _track = std::move(track);
+ _timeline = base::make_unique_q(this, _track);
+ _timeline->removeRequests(
+ ) | rpl::start_to_stream(_removeRequests, _timeline->lifetime());
+ _timeline->trimChanges(
+ ) | rpl::on_next([=] {
+ _lengthChanges.fire(_track->length());
+ }, _timeline->lifetime());
+ _timeline->show();
+ if (width() > 0) {
+ resizeToWidth(width());
+ }
+ }
+ _timeline->setPlaying(true);
+}
+
+rpl::producer AudioTrackTimeline::lengthChanges() const {
+ return _lengthChanges.events();
+}
+
+void AudioTrackTimeline::refreshTrim() {
+ if (_timeline) {
+ _timeline->refreshTrim();
+ }
+}
+
+void AudioTrackTimeline::commitPendingEdit() {
+ if (_timeline) {
+ _timeline->commitPendingEdit();
+ }
+}
+
+void AudioTrackTimeline::refreshVolume() {
+ if (_timeline) {
+ _timeline->refreshVolume();
+ }
+}
+
+rpl::producer<> AudioTrackTimeline::removeRequests() const {
+ return _removeRequests.events();
+}
+
+int AudioTrackTimeline::resizeGetHeight(int newWidth) {
+ const auto height = st::photoEditorButtonBarHeight;
+ if (_timeline) {
+ _timeline->resizeToWidth(std::max(newWidth, 1));
+ _timeline->moveToLeft(0, (height - _timeline->height()) / 2);
+ }
+ return height;
+}
+
+} // namespace Editor
diff --git a/Telegram/SourceFiles/editor/audio_track_timeline.h b/Telegram/SourceFiles/editor/audio_track_timeline.h
new file mode 100644
index 0000000000..37844e8e8d
--- /dev/null
+++ b/Telegram/SourceFiles/editor/audio_track_timeline.h
@@ -0,0 +1,42 @@
+/*
+This file is part of Telegram Desktop,
+the official desktop application for the Telegram messaging service.
+
+For license and copyright information please follow this link:
+https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
+*/
+#pragma once
+
+#include "base/unique_qptr.h"
+#include "ui/rp_widget.h"
+
+namespace Editor {
+
+class AudioTimeline;
+struct AudioTrack;
+
+class AudioTrackTimeline final : public Ui::RpWidget {
+public:
+ explicit AudioTrackTimeline(not_null parent);
+ ~AudioTrackTimeline();
+
+ void setTrack(std::shared_ptr track);
+ void refreshTrim();
+ void commitPendingEdit();
+ void refreshVolume();
+
+ [[nodiscard]] rpl::producer<> removeRequests() const;
+
+ [[nodiscard]] rpl::producer lengthChanges() const;
+
+ int resizeGetHeight(int newWidth) override;
+
+private:
+ std::shared_ptr _track;
+ base::unique_qptr _timeline;
+ rpl::event_stream<> _removeRequests;
+ rpl::event_stream _lengthChanges;
+
+};
+
+} // namespace Editor
diff --git a/Telegram/SourceFiles/editor/color_picker.cpp b/Telegram/SourceFiles/editor/color_picker.cpp
index 7e29244eca..255583a2f6 100644
--- a/Telegram/SourceFiles/editor/color_picker.cpp
+++ b/Telegram/SourceFiles/editor/color_picker.cpp
@@ -15,6 +15,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/painter.h"
#include "ui/rect.h"
#include "ui/rp_widget.h"
+#include "ui/ui_utility.h"
#include "ui/widgets/color_editor.h"
#include "styles/style_editor.h"
@@ -293,6 +294,7 @@ ColorPicker::ColorPicker(
, _sizeControlHoverArea(std::in_place, parent)
, _sizeControl(std::in_place, parent)
, _toolSelection(std::in_place, parent)
+, _rowFade(std::in_place, parent)
, _brush(savedBrushes[ToolIndex(savedTool)])
, _toolBrushes(savedBrushes) {
_colorButton->resize(Size(st::photoEditorColorButtonSize));
@@ -328,6 +330,13 @@ ColorPicker::ColorPicker(
-padding - half);
p.drawEllipse(rect);
});
+ _rowFade->setAttribute(Qt::WA_TransparentForMouseEvents);
+ _rowFade->setAttribute(Qt::WA_TranslucentBackground, true);
+ _rowFade->setVisible(false);
+ _rowFade->paintOn([=](QPainter &p) {
+ p.setOpacity(_rowFadeAnimation.value(_rowFadeShown ? 1. : 0.));
+ p.drawImage(0, 0, _rowFadeImage);
+ });
_toolButtons.push_back(base::make_unique_q(
parent,
@@ -636,7 +645,15 @@ void ColorPicker::paintSizeControl(QPainter &p) {
p.drawEllipse(handleRect);
}
-void ColorPicker::setVisible(bool visible) {
+void ColorPicker::setVisible(bool visible, anim::type animated) {
+ _rowFadeAnimation.stop();
+ _rowFade->hide();
+ const auto fade = (animated == anim::type::normal)
+ && !_paletteVisible
+ && (visible != _colorButton->isVisible());
+ if (fade && !visible) {
+ _rowFadeImage = grabRow();
+ }
if (!visible) {
_paletteVisible = false;
_sizeDown.pressed = false;
@@ -647,10 +664,21 @@ void ColorPicker::setVisible(bool visible) {
_sizeControlPositionAnimation.stop();
_toolSelectionAnimation.stop();
}
- _colorButton->setVisible(visible && !_paletteVisible);
_paletteWrap->setVisible(visible && _paletteVisible);
_sizeControlHoverArea->setVisible(visible);
_sizeControl->setVisible(visible);
+ setRowVisible(visible);
+ if (!fade) {
+ return;
+ } else if (visible) {
+ _rowFadeImage = grabRow();
+ setRowVisible(false);
+ }
+ startRowFade(visible);
+}
+
+void ColorPicker::setRowVisible(bool visible) {
+ _colorButton->setVisible(visible && !_paletteVisible);
const auto showTools = visible
&& !_paletteVisible
&& !_toolSelectionSuppressed;
@@ -663,6 +691,57 @@ void ColorPicker::setVisible(bool visible) {
}
}
+QRect ColorPicker::rowRect() const {
+ auto result = _colorButton->geometry();
+ for (const auto &button : _toolButtons) {
+ result = result.united(button->geometry());
+ }
+ return result.united(_toolSelection->geometry());
+}
+
+QImage ColorPicker::grabRow() const {
+ const auto rect = rowRect();
+ const auto ratio = style::DevicePixelRatio();
+ auto result = QImage(
+ rect.size() * ratio,
+ QImage::Format_ARGB32_Premultiplied);
+ result.setDevicePixelRatio(ratio);
+ result.fill(Qt::transparent);
+ auto p = QPainter(&result);
+ const auto draw = [&](not_null widget) {
+ if (widget->isVisible()) {
+ p.drawImage(
+ widget->pos() - rect.topLeft(),
+ Ui::GrabWidgetToImage(widget));
+ }
+ };
+ draw(_colorButton.get());
+ for (const auto &button : _toolButtons) {
+ draw(button.get());
+ }
+ draw(_toolSelection.get());
+ return result;
+}
+
+void ColorPicker::startRowFade(bool shown) {
+ _rowFadeShown = shown;
+ _rowFade->setGeometry(rowRect());
+ _rowFade->raise();
+ _rowFade->show();
+ _rowFadeAnimation.start([=] {
+ _rowFade->update();
+ if (!_rowFadeAnimation.animating()) {
+ _rowFade->hide();
+ if (_rowFadeShown) {
+ setRowVisible(true);
+ }
+ }
+ },
+ shown ? 0. : 1.,
+ shown ? 1. : 0.,
+ st::photoEditorBarAnimationDuration);
+}
+
rpl::producer ColorPicker::saveBrushRequests() const {
return _saveBrushRequests.events_starting_with_copy(_brush);
}
diff --git a/Telegram/SourceFiles/editor/color_picker.h b/Telegram/SourceFiles/editor/color_picker.h
index 3d64431ce6..c8c1caa910 100644
--- a/Telegram/SourceFiles/editor/color_picker.h
+++ b/Telegram/SourceFiles/editor/color_picker.h
@@ -29,7 +29,9 @@ public:
void moveLine(const QPoint &position);
void setCanvasRect(const QRect &rect);
- void setVisible(bool visible);
+ void setVisible(
+ bool visible,
+ anim::type animated = anim::type::instant);
void setColor(const QColor &color);
void setToolSelectionVisible(bool visible);
bool preventHandleKeyPress() const;
@@ -42,6 +44,10 @@ private:
void rebuildPalette();
void updateToolButtonsGeometry();
void updateToolSelection(bool animated);
+ void setRowVisible(bool visible);
+ [[nodiscard]] QRect rowRect() const;
+ [[nodiscard]] QImage grabRow() const;
+ void startRowFade(bool shown);
void setTool(Brush::Tool tool);
void storeCurrentBrush();
void updateColorButtonColor(const QColor &color, bool animated);
@@ -73,6 +79,9 @@ private:
const base::unique_qptr _sizeControl;
const base::unique_qptr _toolSelection;
std::vector> _toolButtons;
+ const base::unique_qptr _rowFade;
+ QImage _rowFadeImage;
+ bool _rowFadeShown = false;
struct {
int y = 0;
@@ -96,6 +105,7 @@ private:
Ui::Animations::Simple _sizeControlPositionAnimation;
Ui::Animations::Simple _colorButtonAnimation;
Ui::Animations::Simple _toolSelectionAnimation;
+ Ui::Animations::Simple _rowFadeAnimation;
rpl::event_stream _saveBrushRequests;
rpl::event_stream<> _toolClicks;
diff --git a/Telegram/SourceFiles/editor/controllers/controllers.h b/Telegram/SourceFiles/editor/controllers/controllers.h
index f694fa6075..3a046615d5 100644
--- a/Telegram/SourceFiles/editor/controllers/controllers.h
+++ b/Telegram/SourceFiles/editor/controllers/controllers.h
@@ -11,16 +11,24 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "editor/controllers/undo_controller.h"
#include "ui/layers/show.h"
+namespace ChatHelpers {
+class Show;
+} // namespace ChatHelpers
+
namespace Editor {
struct Controllers final {
Controllers(
std::unique_ptr stickersPanelController,
std::unique_ptr undoController,
- std::shared_ptr show)
+ std::shared_ptr show,
+ std::shared_ptr layerShow,
+ std::shared_ptr sessionShow)
: stickersPanelController(std::move(stickersPanelController))
, undoController(std::move(undoController))
- , show(std::move(show)) {
+ , show(std::move(show))
+ , layerShow(std::move(layerShow))
+ , sessionShow(std::move(sessionShow)) {
}
~Controllers() {
};
@@ -28,6 +36,8 @@ struct Controllers final {
const std::unique_ptr stickersPanelController;
const std::unique_ptr undoController;
const std::shared_ptr show;
+ const std::shared_ptr layerShow;
+ const std::shared_ptr sessionShow;
};
} // namespace Editor
diff --git a/Telegram/SourceFiles/editor/controllers/stickers_panel_controller.cpp b/Telegram/SourceFiles/editor/controllers/stickers_panel_controller.cpp
index 1c32ecfdba..63112443ab 100644
--- a/Telegram/SourceFiles/editor/controllers/stickers_panel_controller.cpp
+++ b/Telegram/SourceFiles/editor/controllers/stickers_panel_controller.cpp
@@ -15,10 +15,26 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "styles/style_media_view.h"
namespace Editor {
+namespace {
+
+// Works around MSVC 14.44 ICE on bitfields set in a member-init list.
+[[nodiscard]] ChatHelpers::ComposeFeatures PrepareFeatures(bool withAudio) {
+ return {
+ .megagroupSet = false,
+ .stickersSettings = false,
+ .openStickerSets = false,
+ .photoButton = true,
+ .audioButton = withAudio,
+ .linkButton = true,
+ };
+}
+
+} // namespace
StickersPanelController::StickersPanelController(
not_null panelContainer,
- std::shared_ptr show)
+ std::shared_ptr show,
+ bool withAudio)
: _stickersPanel(
base::make_unique_q(
panelContainer,
@@ -30,12 +46,7 @@ StickersPanelController::StickersPanelController(
.st = st::mediaviewEmojiPan,
.level = Window::GifPauseReason::Layer,
.mode = ChatHelpers::TabbedSelector::Mode::MediaEditor,
- .features = {
- .megagroupSet = false,
- .stickersSettings = false,
- .openStickerSets = false,
- .photoButton = true,
- },
+ .features = PrepareFeatures(withAudio),
}),
})) {
_stickersPanel->setDesiredHeightValues(
@@ -57,6 +68,14 @@ rpl::producer<> StickersPanelController::photoRequests() const {
return _stickersPanel->selector()->photoRequests();
}
+rpl::producer<> StickersPanelController::audioRequests() const {
+ return _stickersPanel->selector()->audioRequests();
+}
+
+rpl::producer<> StickersPanelController::linkRequests() const {
+ return _stickersPanel->selector()->linkRequests();
+}
+
rpl::producer StickersPanelController::panelShown() const {
return _stickersPanel->shownValue();
}
diff --git a/Telegram/SourceFiles/editor/controllers/stickers_panel_controller.h b/Telegram/SourceFiles/editor/controllers/stickers_panel_controller.h
index 91a8b8c793..641c3f18ad 100644
--- a/Telegram/SourceFiles/editor/controllers/stickers_panel_controller.h
+++ b/Telegram/SourceFiles/editor/controllers/stickers_panel_controller.h
@@ -31,11 +31,14 @@ public:
StickersPanelController(
not_null panelContainer,
- std::shared_ptr show);
+ std::shared_ptr show,
+ bool withAudio);
[[nodiscard]] auto stickerChosen() const
-> rpl::producer>;
[[nodiscard]] rpl::producer<> photoRequests() const;
+ [[nodiscard]] rpl::producer<> audioRequests() const;
+ [[nodiscard]] rpl::producer<> linkRequests() const;
[[nodiscard]] rpl::producer panelShown() const;
void setShowRequestChanges(rpl::producer &&showRequest);
diff --git a/Telegram/SourceFiles/editor/editor.style b/Telegram/SourceFiles/editor/editor.style
index d368bce23e..ed4188cea1 100644
--- a/Telegram/SourceFiles/editor/editor.style
+++ b/Telegram/SourceFiles/editor/editor.style
@@ -206,18 +206,96 @@ photoEditorCropPointSize: 10px;
photoEditorCropMinSize: 20px;
photoEditorItemHandleSize: 10px;
+photoEditorStickyTrigger: 12px;
+photoEditorStickyLineWidth: 2px;
+
+photoEditorKeysButton: IconButton(photoEditorRotateButton) {
+ icon: icon {{ "photo_editor/keyboard", photoEditorButtonIconFg }};
+ iconOver: icon {{ "photo_editor/keyboard", photoEditorButtonIconFgOver }};
+}
+photoEditorKeysButtonLeft: 16px;
+photoEditorKeysMenuSkip: 6px;
+photoEditorKeysMenu: PopupMenu(photoEditorCropRatioMenu) {
+ scrollPadding: margins(0px, 4px, 0px, 4px);
+ menu: Menu(mediaviewMenu) {
+ widthMin: 220px;
+ widthMax: 600px;
+ itemPadding: margins(14px, 8px, 14px, 8px);
+ arrow: photoEditorMenuSubmenuArrow;
+ }
+}
+photoEditorKeysTitleStyle: TextStyle(semiboldTextStyle) {
+ font: font(12px semibold);
+}
+photoEditorKeysTitleFg: groupCallMemberNotJoinedStatus;
+photoEditorKeysTitleSkip: 6px;
+photoEditorKeysRowSkip: 6px;
+photoEditorKeysCapStyle: TextStyle(semiboldTextStyle) {
+ font: font(12px semibold);
+}
+photoEditorKeysCapHeight: 20px;
+photoEditorKeysCapMinWidth: 22px;
+photoEditorKeysCapPadding: 6px;
+photoEditorKeysCapRadius: 4px;
+photoEditorKeysCapSkip: 4px;
+photoEditorKeysCapBg: groupCallMenuBgOver;
+photoEditorKeysCapBorder: groupCallMemberNotJoinedStatus;
+photoEditorKeysCapFg: groupCallMembersFg;
+photoEditorKeysTextSkip: 12px;
+photoEditorKeysTextWidth: 360px;
+photoEditorKeysTextMinWidth: 100px;
+photoEditorKeysTextFg: groupCallMembersFg;
+
+photoEditorAudioDiscSize: 48px;
+photoEditorAudioDiscSkip: 12px;
+photoEditorAudioDiscCoverSize: 30px;
+photoEditorAudioDiscRing: 2px;
+photoEditorAudioDiscRingFg: mediaviewControlFg;
+photoEditorAudioDiscIcon: icon {{ "photo_editor/widget_music", photoEditorButtonIconFg }};
+photoEditorAudioDiscIconOver: icon {{ "photo_editor/widget_music", photoEditorButtonIconFgOver }};
+photoEditorMenuDelete: icon {{ "menu/delete", mediaviewMenuFg }};
+photoEditorMediaMenu: PopupMenu(photoEditorCropRatioMenu) {
+ scrollPadding: margins(0px, 3px, 0px, 8px);
+ menu: Menu(mediaviewMenu) {
+ widthMin: 210px;
+ arrow: photoEditorMenuSubmenuArrow;
+ }
+}
+photoEditorAudioVolumeItem: Menu(mediaviewMenu) {
+ itemBgOver: groupCallMenuBg;
+ arrow: photoEditorMenuSubmenuArrow;
+}
+photoEditorAudioVolumePadding: margins(15px, 6px, 17px, 5px);
+photoEditorAudioVolumeMargin: margins(54px, 0px, 17px, 0px);
+
+photoEditorAudioTimelineBarWidth: 2px;
+photoEditorAudioTimelineBarSkip: 3px;
+photoEditorAudioTimelineBarMin: 1px;
+photoEditorAudioTimelineBarRatio: 0.6;
+photoEditorAudioTimelineBarOpacity: 0.4;
+photoEditorAudioTimelineIcon: 13px;
+photoEditorAudioTimelineIconSkip: 3px;
+photoEditorAudioTimelineDotSkip: 10px;
+photoEditorAudioTimelineDotRadius: 1px;
+photoEditorAudioTimelineTextSkip: 12px;
+photoEditorAudioTimelineFade: 8px;
+photoEditorAudioTimelineAuthorFont: font(12px semibold);
+photoEditorAudioTimelineFont: font(12px);
+photoEditorTrimShortestIconSize: 24px;
videoTimelineStripHeight: 54px;
videoTimelineLabelHeight: 18px;
// Wide enough to hold the cover dot without it touching either side.
videoTimelineLabelSkip: 17px;
-videoTimelineHeight: videoTimelineLabelHeight + videoTimelineLabelSkip + videoTimelineStripHeight;
+videoTimelineTrimStripHeight: 32px;
+videoTimelineTrimLabelSkip: 10px;
videoTimelineDotSize: 5px;
videoTimelineDotActiveSize: 9px;
videoTimelineDotSkip: 2px;
videoTimelineDotFg: callAnswerBg;
videoTimelineRadius: 6px;
videoTimelineHandleWidth: 11px;
+videoTimelineHandleMinWidth: 4px;
videoTimelineHandleGripWidth: 2px;
videoTimelineHandleGripHeight: 16px;
videoTimelineHandleHitSlop: 12px;
@@ -230,9 +308,30 @@ videoTimelinePlaceholderBg: mediaviewPipPlaybackInactive;
videoTimelineDurationFg: mediaviewCaptionFg;
videoTimelineSizeFg: mediaviewCaptionFg;
videoTimelineSizeSkip: 12px;
+videoTimelineScrollHeight: 2px;
+videoTimelineScrollSkip: 3px;
+videoTimelineScrollMinWidth: 16px;
+videoTimelineOverviewSelectionHeight: 3px;
+videoTimelineOverviewFg: windowBgActive;
+videoTimelineHintSkip: 4px;
+videoTimelineHintWidth: 18px;
+videoTimelineHintHeight: 20px;
+videoTimelineHintRadius: 6px;
+videoTimelineHintStroke: 2px;
+videoTimelineHintArrowWidth: 5px;
+videoTimelineHintArrowHeight: 10px;
videoTimelineDurationStyle: TextStyle(defaultTextStyle) {
font: font(12px semibold);
}
+videoTimelineDurationFieldSkip: 4px;
+videoTimelineDurationField: InputField(defaultInputField) {
+ textBg: transparent;
+ textFg: videoTimelineDurationFg;
+ textMargins: margins(0px, 0px, 0px, 0px);
+ border: 0px;
+ borderActive: 0px;
+ style: videoTimelineDurationStyle;
+}
videoEditorGifButton: IconButton(photoEditorRotateButton) {
icon: icon {{ "photo_editor/gif", photoEditorButtonIconFg }};
@@ -259,6 +358,7 @@ videoQualitySliderStyle: TextStyle(defaultTextStyle) {
videoQualitySliderHeight: 32px;
videoEditorTimelineTop: 10px;
+photoEditorTimelineSkip: 6px;
videoEditorHintSkip: 8px;
videoEditorQualitySkip: 6px;
videoEditorBarSkip: 12px;
@@ -279,3 +379,19 @@ videoEditorPlayBadgeBg: mediaviewCaptionBg;
videoEditorAbout: FlatLabel(photoEditorAbout) {
}
videoEditorAboutMargin: margins(10px, 10px, 10px, 0px);
+
+photoEditorMessageReferenceWidth: 600px;
+photoEditorLinkPreviewMargin: margins(24px, 12px, 24px, 6px);
+photoEditorLinkPreviewMaxHeight: 320px;
+photoEditorLinkPreviewPadding: 12px;
+photoEditorLinkPillIcon: icon {{ "photo_editor/link", windowFg }};
+photoEditorLinkThemeSize: 36px;
+photoEditorLinkThemeIconSize: 28px;
+photoEditorLinkThemeSkip: 6px;
+photoEditorLinkThemeBg: mediaviewCaptionBg;
+photoEditorLinkLoadingRadius: bubbleRadiusLarge;
+photoEditorLinkLoadingPadding: margins(12px, 11px, 12px, 11px);
+photoEditorLinkLoadingLine: 10px;
+photoEditorLinkLoadingSkip: 8px;
+photoEditorLinkLoadingQuote: 3px;
+photoEditorLinkLoadingQuoteSkip: 8px;
diff --git a/Telegram/SourceFiles/editor/editor_audio_disc_button.cpp b/Telegram/SourceFiles/editor/editor_audio_disc_button.cpp
new file mode 100644
index 0000000000..511682df64
--- /dev/null
+++ b/Telegram/SourceFiles/editor/editor_audio_disc_button.cpp
@@ -0,0 +1,154 @@
+/*
+This file is part of Telegram Desktop,
+the official desktop application for the Telegram messaging service.
+
+For license and copyright information please follow this link:
+https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
+*/
+#include "editor/editor_audio_disc_button.h"
+
+#include "editor/editor_audio_menu.h"
+#include "editor/photo_editor_common.h"
+#include "ui/image/image_prepare.h"
+#include "ui/painter.h"
+#include "ui/rect.h"
+#include "ui/widgets/popup_menu.h"
+#include "styles/style_editor.h"
+
+#include
+
+namespace Editor {
+namespace {
+
+constexpr auto kSpinDuration = crl::time(4000);
+
+} // namespace
+
+AudioDiscButton::AudioDiscButton(QWidget *parent)
+: RippleButton(parent, st::photoEditorRotateButton.ripple) {
+ resize(Size(st::photoEditorAudioDiscSize));
+ _spin.init([=](crl::time now) {
+ update();
+ return _active;
+ });
+}
+
+AudioDiscButton::~AudioDiscButton() = default;
+
+void AudioDiscButton::setTrack(std::shared_ptr track) {
+ if (_track == track) {
+ return;
+ }
+ _track = std::move(track);
+ if (!_track || _track->cover.isNull()) {
+ _cover = QImage();
+ } else {
+ const auto ratio = style::DevicePixelRatio();
+ const auto side = st::photoEditorAudioDiscCoverSize * ratio;
+ auto scaled = _track->cover.scaled(
+ side,
+ side,
+ Qt::KeepAspectRatioByExpanding,
+ Qt::SmoothTransformation);
+ const auto crop = QRect(
+ (scaled.width() - side) / 2,
+ (scaled.height() - side) / 2,
+ side,
+ side);
+ _cover = Images::Circle(scaled.copy(crop));
+ _cover.setDevicePixelRatio(ratio);
+ }
+ update();
+}
+
+void AudioDiscButton::setActive(bool active) {
+ if (_active == active) {
+ return;
+ }
+ _spinBase = angle();
+ _active = active;
+ _spinStarted = crl::now();
+ if (_active) {
+ _spin.start();
+ } else {
+ _spin.stop();
+ }
+ update();
+}
+
+rpl::producer<> AudioDiscButton::volumeChanges() const {
+ return _volumeChanges.events();
+}
+
+rpl::producer<> AudioDiscButton::removeRequests() const {
+ return _removeRequests.events();
+}
+
+float64 AudioDiscButton::angle() const {
+ if (!_active) {
+ return _spinBase;
+ }
+ const auto passed = crl::now() - _spinStarted;
+ return std::fmod(_spinBase + passed * 360. / kSpinDuration, 360.);
+}
+
+void AudioDiscButton::paintEvent(QPaintEvent *e) {
+ auto p = QPainter(this);
+ auto hq = PainterHighQualityEnabler(p);
+ const auto full = QRectF(rect());
+ p.setPen(Qt::NoPen);
+ p.setBrush(st::roundedBg);
+ p.drawEllipse(full);
+ paintRipple(p, QPoint());
+ if (_active) {
+ const auto ring = float64(st::photoEditorAudioDiscRing);
+ auto pen = QPen(st::photoEditorAudioDiscRingFg->c);
+ pen.setWidthF(ring);
+ p.setPen(pen);
+ p.setBrush(Qt::NoBrush);
+ p.drawEllipse(full.marginsRemoved(
+ { ring / 2., ring / 2., ring / 2., ring / 2. }));
+ p.setPen(Qt::NoPen);
+ }
+ if (_cover.isNull()) {
+ const auto &icon = isOver()
+ ? st::photoEditorAudioDiscIconOver
+ : st::photoEditorAudioDiscIcon;
+ icon.paintInCenter(p, rect());
+ return;
+ }
+ const auto cover = float64(st::photoEditorAudioDiscCoverSize);
+ p.save();
+ p.translate(rect::center(full));
+ p.rotate(angle());
+ p.drawImage(QRectF(-cover / 2., -cover / 2., cover, cover), _cover);
+ p.restore();
+}
+
+void AudioDiscButton::contextMenuEvent(QContextMenuEvent *e) {
+ if (!_track) {
+ return;
+ }
+ _menu = CreateAudioMenu(
+ this,
+ _track,
+ [=] { _volumeChanges.fire({}); },
+ [=] { _removeRequests.fire({}); });
+ _menu->popup(e->globalPos());
+ e->accept();
+}
+
+void AudioDiscButton::onStateChanged(State was, StateChangeSource source) {
+ RippleButton::onStateChanged(was, source);
+ update();
+}
+
+QImage AudioDiscButton::prepareRippleMask() const {
+ return Images::EllipseMask(size());
+}
+
+QPoint AudioDiscButton::prepareRippleStartPosition() const {
+ return mapFromGlobal(QCursor::pos());
+}
+
+} // namespace Editor
diff --git a/Telegram/SourceFiles/editor/editor_audio_disc_button.h b/Telegram/SourceFiles/editor/editor_audio_disc_button.h
new file mode 100644
index 0000000000..a72d4b46ea
--- /dev/null
+++ b/Telegram/SourceFiles/editor/editor_audio_disc_button.h
@@ -0,0 +1,57 @@
+/*
+This file is part of Telegram Desktop,
+the official desktop application for the Telegram messaging service.
+
+For license and copyright information please follow this link:
+https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
+*/
+#pragma once
+
+#include "base/unique_qptr.h"
+#include "ui/effects/animations.h"
+#include "ui/widgets/buttons.h"
+
+namespace Ui {
+class PopupMenu;
+} // namespace Ui
+
+namespace Editor {
+
+struct AudioTrack;
+
+class AudioDiscButton final : public Ui::RippleButton {
+public:
+ explicit AudioDiscButton(QWidget *parent);
+ ~AudioDiscButton();
+
+ void setTrack(std::shared_ptr track);
+ void setActive(bool active);
+
+ [[nodiscard]] rpl::producer<> volumeChanges() const;
+ [[nodiscard]] rpl::producer<> removeRequests() const;
+
+protected:
+ void paintEvent(QPaintEvent *e) override;
+ void contextMenuEvent(QContextMenuEvent *e) override;
+ void onStateChanged(State was, StateChangeSource source) override;
+
+ QImage prepareRippleMask() const override;
+ QPoint prepareRippleStartPosition() const override;
+
+private:
+ [[nodiscard]] float64 angle() const;
+
+ std::shared_ptr _track;
+ QImage _cover;
+ Ui::Animations::Basic _spin;
+ crl::time _spinStarted = 0;
+ float64 _spinBase = 0.;
+ bool _active = false;
+
+ base::unique_qptr _menu;
+ rpl::event_stream<> _volumeChanges;
+ rpl::event_stream<> _removeRequests;
+
+};
+
+} // namespace Editor
diff --git a/Telegram/SourceFiles/editor/editor_audio_menu.cpp b/Telegram/SourceFiles/editor/editor_audio_menu.cpp
new file mode 100644
index 0000000000..046e1d1cd9
--- /dev/null
+++ b/Telegram/SourceFiles/editor/editor_audio_menu.cpp
@@ -0,0 +1,228 @@
+/*
+This file is part of Telegram Desktop,
+the official desktop application for the Telegram messaging service.
+
+For license and copyright information please follow this link:
+https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
+*/
+#include "editor/editor_audio_menu.h"
+
+#include "editor/photo_editor_common.h"
+#include "lang/lang_keys.h"
+#include "ui/effects/animation_value.h"
+#include "ui/effects/cross_line.h"
+#include "ui/paint/arcs.h"
+#include "ui/widgets/continuous_sliders.h"
+#include "ui/widgets/popup_menu.h"
+#include "ui/color_int_conversion.h"
+#include "ui/rect.h"
+#include "styles/style_calls.h"
+#include "styles/style_editor.h"
+
+namespace Editor {
+namespace {
+
+constexpr auto kMuteDuration = crl::time(150);
+
+const auto kSpeakerThresholds = std::vector{ 0.05f, 0.25f, 0.75f };
+
+constexpr auto kVolumeStickedValues
+ = std::array, 3>{{
+ { 0.25, 0.02 },
+ { 0.5, 0.02 },
+ { 0.75, 0.02 },
+ }};
+
+} // namespace
+
+MenuVolumeItem::MenuVolumeItem(
+ not_null parent,
+ const style::Menu &st,
+ float64 volume)
+: Ui::Menu::ItemBase(parent, st)
+, _muted(volume <= 0.)
+, _slider(base::make_unique_q(
+ this,
+ st::groupCallMenuVolumeSlider))
+, _dummyAction(new QAction(parent))
+, _st(st)
+, _stCross(st::groupCallMuteCrossLine)
+, _crossLineMute(std::make_unique(_stCross, true))
+, _arcs(std::make_unique(
+ st::groupCallSpeakerArcsAnimation,
+ kSpeakerThresholds,
+ volume,
+ Ui::Paint::ArcsAnimation::Direction::Right)) {
+ fitToMenuWidth();
+ enableMouseSelecting();
+ enableMouseSelecting(_slider.get());
+
+ _slider->setAlwaysDisplayMarker(true);
+
+ sizeValue(
+ ) | rpl::on_next([=](const QSize &size) {
+ const auto &margin = st::photoEditorAudioVolumeMargin;
+ _itemRect = Rect(size) - st::photoEditorAudioVolumePadding;
+ _speakerRect = QRect(_itemRect.topLeft(), _stCross.icon.size());
+ _arcPosition = rect::center(_speakerRect)
+ + QPoint(0, st::groupCallMenuSpeakerArcsSkip);
+ _slider->setGeometry(
+ margin.left(),
+ _speakerRect.y(),
+ size.width() - margin.left() - margin.right(),
+ _speakerRect.height());
+ }, lifetime());
+
+ _slider->setValue(volume);
+ updateSliderColor(volume);
+
+ paintRequest(
+ ) | rpl::on_next([=](const QRect &clip) {
+ auto p = QPainter(this);
+ const auto muteProgress = _crossLineAnimation.value(_muted ? 1. : 0.);
+ p.fillRect(clip, isSelected() ? _st.itemBgOver : _st.itemBg);
+ const auto mutePen = anim::color(
+ unmuteColor(),
+ muteColor(),
+ muteProgress);
+ _crossLineMute->paint(
+ p,
+ _speakerRect.topLeft(),
+ muteProgress,
+ (muteProgress > 0) ? std::make_optional(mutePen) : std::nullopt);
+ p.translate(_arcPosition);
+ _arcs->paint(p);
+ }, lifetime());
+
+ _slider->setChangeProgressCallback([=](float64 value) {
+ applyVolume(value);
+ });
+ _slider->setAdjustCallback([=](float64 value) {
+ for (const auto &snap : kVolumeStickedValues) {
+ if (value > (snap.first - snap.second)
+ && value < (snap.first + snap.second)) {
+ return snap.first;
+ }
+ }
+ return value;
+ });
+
+ initArcsAnimation();
+}
+
+void MenuVolumeItem::initArcsAnimation() {
+ _arcsAnimation.init([=](crl::time now) {
+ _arcs->update(now);
+ update(_speakerRect);
+ });
+
+ _arcs->startUpdateRequests(
+ ) | rpl::on_next([=] {
+ if (!_arcsAnimation.animating()) {
+ _arcsAnimation.start();
+ }
+ }, lifetime());
+
+ _arcs->stopUpdateRequests(
+ ) | rpl::on_next([=] {
+ _arcsAnimation.stop();
+ }, lifetime());
+}
+
+void MenuVolumeItem::applyVolume(float64 value) {
+ const auto muted = (value <= 0.);
+ if (_muted != muted) {
+ _muted = muted;
+ _crossLineAnimation.start(
+ [=] { update(_speakerRect); },
+ muted ? 0. : 1.,
+ muted ? 1. : 0.,
+ kMuteDuration);
+ }
+ _arcs->setValue(value);
+ updateSliderColor(value);
+ _changes.fire_copy(value);
+}
+
+QColor MenuVolumeItem::unmuteColor() const {
+ return (isSelected()
+ ? _st.itemFgOver
+ : isEnabled()
+ ? _st.itemFg
+ : _st.itemFgDisabled)->c;
+}
+
+QColor MenuVolumeItem::muteColor() const {
+ return (isSelected()
+ ? st::attentionButtonFgOver
+ : st::attentionButtonFg)->c;
+}
+
+void MenuVolumeItem::updateSliderColor(float64 value) {
+ value = std::clamp(value, 0., 1.);
+ const auto colors = std::array{ {
+ Ui::ColorFromSerialized(0xF66464),
+ Ui::ColorFromSerialized(0xD0B738),
+ Ui::ColorFromSerialized(0x24CD80),
+ Ui::ColorFromSerialized(0x3BBCEC),
+ } };
+ _slider->setColorOverrides({
+ .activeFg = (value < 0.25)
+ ? anim::color(colors[0], colors[1], value / 0.25)
+ : (value < 0.5)
+ ? anim::color(colors[1], colors[2], (value - 0.25) / 0.25)
+ : anim::color(colors[2], colors[3], (value - 0.5) / 0.5),
+ });
+}
+
+not_null MenuVolumeItem::action() const {
+ return _dummyAction;
+}
+
+bool MenuVolumeItem::isEnabled() const {
+ return true;
+}
+
+int MenuVolumeItem::contentHeight() const {
+ return rect::m::sum::v(st::photoEditorAudioVolumePadding)
+ + _stCross.icon.height();
+}
+
+rpl::producer MenuVolumeItem::changes() const {
+ return _changes.events();
+}
+
+void AddVolumeAction(
+ not_null menu,
+ float64 volume,
+ Fn changed) {
+ auto item = base::make_unique_q(
+ menu->menu(),
+ st::photoEditorAudioVolumeItem,
+ volume);
+ item->changes(
+ ) | rpl::on_next(std::move(changed), item->lifetime());
+ menu->addAction(std::move(item));
+}
+
+base::unique_qptr CreateAudioMenu(
+ not_null parent,
+ std::shared_ptr track,
+ Fn volumeChanged,
+ Fn remove) {
+ auto result = base::make_unique_q(
+ parent,
+ st::photoEditorMediaMenu);
+ AddVolumeAction(result.get(), track->volume, [=](float64 value) {
+ track->volume = value;
+ volumeChanged();
+ });
+ result->addSeparator();
+ result->addAction(
+ tr::lng_photo_editor_audio_remove(tr::now),
+ std::move(remove),
+ &st::photoEditorMenuDelete);
+ return result;
+}
+
+} // namespace Editor
diff --git a/Telegram/SourceFiles/editor/editor_audio_menu.h b/Telegram/SourceFiles/editor/editor_audio_menu.h
new file mode 100644
index 0000000000..1a643999bd
--- /dev/null
+++ b/Telegram/SourceFiles/editor/editor_audio_menu.h
@@ -0,0 +1,80 @@
+/*
+This file is part of Telegram Desktop,
+the official desktop application for the Telegram messaging service.
+
+For license and copyright information please follow this link:
+https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
+*/
+#pragma once
+
+#include "base/unique_qptr.h"
+#include "ui/effects/animations.h"
+#include "ui/widgets/menu/menu_item_base.h"
+
+namespace Ui {
+class CrossLineAnimation;
+class MediaSlider;
+class PopupMenu;
+namespace Paint {
+class ArcsAnimation;
+} // namespace Paint
+} // namespace Ui
+
+namespace Editor {
+
+struct AudioTrack;
+
+class MenuVolumeItem final : public Ui::Menu::ItemBase {
+public:
+ MenuVolumeItem(
+ not_null parent,
+ const style::Menu &st,
+ float64 volume);
+
+ not_null action() const override;
+ bool isEnabled() const override;
+
+ [[nodiscard]] rpl::producer changes() const;
+
+protected:
+ int contentHeight() const override;
+
+private:
+ void initArcsAnimation();
+ void applyVolume(float64 value);
+ void updateSliderColor(float64 value);
+
+ [[nodiscard]] QColor unmuteColor() const;
+ [[nodiscard]] QColor muteColor() const;
+
+ QRect _itemRect;
+ QRect _speakerRect;
+ QPoint _arcPosition;
+ bool _muted = false;
+
+ const base::unique_qptr _slider;
+ const not_null _dummyAction;
+ const style::Menu &_st;
+ const style::CrossLineAnimation &_stCross;
+
+ const std::unique_ptr _crossLineMute;
+ Ui::Animations::Simple _crossLineAnimation;
+ const std::unique_ptr _arcs;
+ Ui::Animations::Basic _arcsAnimation;
+
+ rpl::event_stream _changes;
+
+};
+
+void AddVolumeAction(
+ not_null menu,
+ float64 volume,
+ Fn changed);
+
+[[nodiscard]] base::unique_qptr CreateAudioMenu(
+ not_null parent,
+ std::shared_ptr track,
+ Fn volumeChanged,
+ Fn remove);
+
+} // namespace Editor
diff --git a/Telegram/SourceFiles/editor/editor_audio_timeline.cpp b/Telegram/SourceFiles/editor/editor_audio_timeline.cpp
new file mode 100644
index 0000000000..2539ca9f9c
--- /dev/null
+++ b/Telegram/SourceFiles/editor/editor_audio_timeline.cpp
@@ -0,0 +1,298 @@
+/*
+This file is part of Telegram Desktop,
+the official desktop application for the Telegram messaging service.
+
+For license and copyright information please follow this link:
+https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
+*/
+#include "editor/editor_audio_timeline.h"
+
+#include "editor/editor_audio_menu.h"
+#include "editor/photo_editor_common.h"
+#include "editor/video/video_segment_player.h"
+#include "editor/video/video_timeline_seeker.h"
+#include "media/media_audio_waveform.h"
+#include "ui/widgets/popup_menu.h"
+#include "ui/painter.h"
+#include "ui/rect.h"
+#include "styles/style_editor.h"
+
+#include
+#include
+#include
+
+namespace Editor {
+namespace {
+
+constexpr auto kWaveformBarDuration = crl::time(20);
+constexpr auto kMaxWaveformBars = 16384;
+constexpr auto kLoadedDuration = crl::time(600);
+
+[[nodiscard]] TrimTimelineDescriptor TrimDescriptor(const AudioTrack &track) {
+ return {
+ .duration = track.duration,
+ .from = track.from,
+ .till = track.till,
+ .trimOnly = true,
+ };
+}
+
+[[nodiscard]] QString TitleFor(const AudioTrack &track) {
+ return !track.title.isEmpty()
+ ? track.title
+ : !track.path.isEmpty()
+ ? QFileInfo(track.path).completeBaseName()
+ : QString();
+}
+
+} // namespace
+
+AudioTimeline::AudioTimeline(
+ not_null parent,
+ std::shared_ptr track)
+: TrimTimeline(parent, TrimDescriptor(*track))
+, _track(std::move(track))
+, _title(TitleFor(*_track))
+, _performer(_track->performer) {
+ _track->from = from();
+ _track->till = till();
+
+ trimChanges(
+ ) | rpl::on_next([=] {
+ _track->from = from();
+ _track->till = till();
+ }, lifetime());
+
+ loadWaveform();
+}
+
+AudioTimeline::~AudioTimeline() {
+ if (_waveformCancel) {
+ _waveformCancel->store(true);
+ }
+}
+
+const std::shared_ptr &AudioTimeline::track() const {
+ return _track;
+}
+
+void AudioTimeline::setPlaying(bool playing) {
+ if (playing == (_player != nullptr)) {
+ return;
+ } else if (!playing) {
+ _seeker = nullptr;
+ _player = nullptr;
+ return;
+ }
+ _player = std::make_unique(
+ _track->path,
+ _track->content,
+ SegmentPlayerOptions{ .audio = true, .volume = _track->volume });
+ _seeker = std::make_unique(this, _player.get());
+ _player->start();
+}
+
+void AudioTimeline::refreshTrim() {
+ setTrim(_track->from, _track->till);
+}
+
+void AudioTimeline::refreshVolume() {
+ if (_player) {
+ _player->setVolume(_track->volume);
+ }
+}
+
+rpl::producer<> AudioTimeline::removeRequests() const {
+ return _removeRequests.events();
+}
+
+void AudioTimeline::contextMenuEvent(QContextMenuEvent *e) {
+ _menu = CreateAudioMenu(
+ this,
+ _track,
+ [=] { refreshVolume(); },
+ [=] { _removeRequests.fire({}); });
+ _menu->popup(e->globalPos());
+ e->accept();
+}
+
+void AudioTimeline::loadWaveform() {
+ const auto count = int(std::clamp(
+ duration() / kWaveformBarDuration,
+ crl::time(1),
+ crl::time(kMaxWaveformBars)));
+ const auto weak = base::make_weak(this);
+ _waveformCancel = std::make_shared>(false);
+ const auto cancel = _waveformCancel;
+ crl::async([=, path = _track->path, content = _track->content] {
+ auto result = std::make_shared(
+ Media::Audio::LoadWaveform(path, content, count, cancel));
+ crl::on_main(weak, [=] {
+ if (result->empty()) {
+ return;
+ }
+ _waveform = result;
+ _waveformMax = 0;
+ for (const auto bar : result->bars) {
+ accumulate_max(_waveformMax, int(bar));
+ }
+ _loadedAnimation.start(
+ [=] { update(); },
+ 0.,
+ 1.,
+ kLoadedDuration);
+ update();
+ });
+ });
+}
+
+void AudioTimeline::paintStrip(QPainter &p, const QRect &strip) {
+ p.fillRect(strip, st::videoTimelinePlaceholderBg);
+ paintWaveform(p, strip);
+ paintText(p, strip);
+}
+
+void AudioTimeline::paintWaveform(QPainter &p, const QRect &strip) {
+ if (!_waveform || _waveform->bars.empty() || !_waveformMax) {
+ return;
+ }
+ const auto loaded = _loadedAnimation.value(1.);
+ if (loaded <= 0.) {
+ return;
+ }
+ const auto &bars = _waveform->bars;
+ const auto count = int(bars.size());
+ const auto slots = std::max(
+ strip.width() / st::photoEditorAudioTimelineBarSkip,
+ 1);
+ const auto spacing = strip.width() / float64(slots);
+ const auto from = float64(visibleFrom());
+ const auto span = visibleSpan();
+ const auto barDuration = float64(duration()) / count;
+ const auto barWidth = float64(st::photoEditorAudioTimelineBarWidth);
+ const auto minHeight = float64(st::photoEditorAudioTimelineBarMin);
+ const auto ratio = st::photoEditorAudioTimelineBarRatio;
+ auto color = st::videoTimelineFg->c;
+ color.setAlphaF(st::photoEditorAudioTimelineBarOpacity * loaded);
+ p.setPen(Qt::NoPen);
+ p.setBrush(color);
+ const auto bottom = float64(rect::bottom(strip));
+ for (auto i = 0; i != slots; ++i) {
+ const auto slotFrom = (from + i * span / slots) / barDuration;
+ const auto slotTill = (from + (i + 1) * span / slots) / barDuration;
+ const auto first = std::clamp(
+ int(std::floor(slotFrom)),
+ 0,
+ count - 1);
+ const auto last = std::clamp(
+ int(std::ceil(slotTill)) - 1,
+ first,
+ count - 1);
+ auto peak = uint16(0);
+ for (auto j = first; j <= last; ++j) {
+ accumulate_max(peak, bars[j]);
+ }
+ const auto x = strip.x()
+ + i * spacing
+ + (spacing - barWidth) / 2.;
+ const auto height = std::max(
+ peak / float64(_waveformMax) * strip.height() * ratio,
+ minHeight);
+ p.drawRoundedRect(
+ QRectF(x, bottom - height, barWidth, height),
+ barWidth / 2.,
+ barWidth / 2.);
+ }
+}
+
+void AudioTimeline::paintText(QPainter &p, const QRect &strip) {
+ const auto left = std::max(xAt(from()), strip.x());
+ const auto right = std::min(xAt(till()), rect::right(strip));
+ const auto skip = st::photoEditorAudioTimelineTextSkip;
+ const auto available = right - left - 2 * skip;
+ const auto icon = st::photoEditorAudioTimelineIcon;
+ if (available <= icon) {
+ return;
+ }
+ const auto &authorFont = st::photoEditorAudioTimelineAuthorFont;
+ const auto &titleFont = st::photoEditorAudioTimelineFont;
+ const auto dotSkip = (_performer.isEmpty() || _title.isEmpty())
+ ? 0
+ : int(st::photoEditorAudioTimelineDotSkip);
+ const auto full = icon
+ + st::photoEditorAudioTimelineIconSkip
+ + (_performer.isEmpty() ? 0 : authorFont->width(_performer))
+ + dotSkip
+ + (_title.isEmpty() ? 0 : titleFont->width(_title));
+ const auto shown = std::min(full, available);
+ const auto x = (left + right) / 2 - shown / 2;
+ const auto centerY = rect::center(strip).y();
+ if (full <= available) {
+ paintTextLine(p, x, centerY, shown);
+ return;
+ }
+ const auto ratio = style::DevicePixelRatio();
+ auto image = QImage(
+ QSize(shown, strip.height()) * ratio,
+ QImage::Format_ARGB32_Premultiplied);
+ image.setDevicePixelRatio(ratio);
+ image.fill(Qt::transparent);
+ {
+ auto q = QPainter(&image);
+ auto hq = PainterHighQualityEnabler(q);
+ paintTextLine(q, 0, strip.height() / 2, shown);
+ const auto fade = float64(st::photoEditorAudioTimelineFade);
+ auto gradient = QLinearGradient(shown - fade, 0, shown, 0);
+ gradient.setColorAt(0., QColor(0, 0, 0, 255));
+ gradient.setColorAt(1., QColor(0, 0, 0, 0));
+ q.setCompositionMode(QPainter::CompositionMode_DestinationIn);
+ q.fillRect(QRectF(shown - fade, 0, fade, strip.height()), gradient);
+ }
+ p.drawImage(x, strip.y(), image);
+}
+
+void AudioTimeline::paintTextLine(
+ QPainter &p,
+ int x,
+ int centerY,
+ int width) {
+ const auto icon = st::photoEditorAudioTimelineIcon;
+ const auto image = st::photoEditorAudioDiscIcon.instance(
+ st::videoTimelineFg->c);
+ p.setClipRect(x, centerY - height(), width, height() * 2);
+ p.setRenderHint(QPainter::SmoothPixmapTransform);
+ p.drawImage(
+ QRectF(x, centerY - icon / 2., icon, icon),
+ image);
+ x += icon + st::photoEditorAudioTimelineIconSkip;
+ p.setPen(st::videoTimelineFg);
+ if (!_performer.isEmpty()) {
+ const auto &font = st::photoEditorAudioTimelineAuthorFont;
+ p.setFont(font);
+ p.drawText(
+ QPointF(x, centerY - font->height / 2. + font->ascent),
+ _performer);
+ x += font->width(_performer);
+ }
+ if (!_performer.isEmpty() && !_title.isEmpty()) {
+ const auto dotSkip = st::photoEditorAudioTimelineDotSkip;
+ const auto radius = float64(st::photoEditorAudioTimelineDotRadius);
+ auto dot = st::videoTimelineFg->c;
+ dot.setAlphaF(dot.alphaF() / 2.);
+ p.setPen(Qt::NoPen);
+ p.setBrush(dot);
+ p.drawEllipse(QPointF(x + dotSkip / 2., centerY), radius, radius);
+ p.setPen(st::videoTimelineFg);
+ x += dotSkip;
+ }
+ if (!_title.isEmpty()) {
+ const auto &font = st::photoEditorAudioTimelineFont;
+ p.setFont(font);
+ p.drawText(
+ QPointF(x, centerY - font->height / 2. + font->ascent),
+ _title);
+ }
+ p.setClipping(false);
+}
+
+} // namespace Editor
diff --git a/Telegram/SourceFiles/editor/editor_audio_timeline.h b/Telegram/SourceFiles/editor/editor_audio_timeline.h
new file mode 100644
index 0000000000..1503eab60b
--- /dev/null
+++ b/Telegram/SourceFiles/editor/editor_audio_timeline.h
@@ -0,0 +1,69 @@
+/*
+This file is part of Telegram Desktop,
+the official desktop application for the Telegram messaging service.
+
+For license and copyright information please follow this link:
+https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
+*/
+#pragma once
+
+#include "base/unique_qptr.h"
+#include "editor/editor_trim_timeline.h"
+#include "ui/effects/animations.h"
+
+namespace Media::Audio {
+struct Waveform;
+} // namespace Media::Audio
+
+namespace Ui {
+class PopupMenu;
+} // namespace Ui
+
+namespace Editor {
+
+class SegmentPlayer;
+class TimelineSeeker;
+struct AudioTrack;
+
+class AudioTimeline final : public TrimTimeline {
+public:
+ AudioTimeline(
+ not_null parent,
+ std::shared_ptr track);
+ ~AudioTimeline();
+
+ [[nodiscard]] const std::shared_ptr &track() const;
+
+ void setPlaying(bool playing);
+ void refreshTrim();
+ void refreshVolume();
+
+ [[nodiscard]] rpl::producer<> removeRequests() const;
+
+private:
+ void paintStrip(QPainter &p, const QRect &strip) override;
+ void contextMenuEvent(QContextMenuEvent *e) override;
+
+ void loadWaveform();
+ void paintWaveform(QPainter &p, const QRect &strip);
+ void paintText(QPainter &p, const QRect &strip);
+ void paintTextLine(QPainter &p, int x, int centerY, int width);
+
+ const std::shared_ptr _track;
+ const QString _title;
+ const QString _performer;
+
+ std::unique_ptr _player;
+ std::unique_ptr _seeker;
+
+ std::shared_ptr _waveform;
+ std::shared_ptr> _waveformCancel;
+ int _waveformMax = 0;
+ Ui::Animations::Simple _loadedAnimation;
+
+ base::unique_qptr _menu;
+ rpl::event_stream<> _removeRequests;
+
+};
+
+} // namespace Editor
diff --git a/Telegram/SourceFiles/editor/editor_crop.cpp b/Telegram/SourceFiles/editor/editor_crop.cpp
index 0a8556b6d5..67a23177e5 100644
--- a/Telegram/SourceFiles/editor/editor_crop.cpp
+++ b/Telegram/SourceFiles/editor/editor_crop.cpp
@@ -26,6 +26,7 @@ constexpr auto kEAll = Qt::TopEdge
| Qt::LeftEdge
| Qt::BottomEdge
| Qt::RightEdge;
+constexpr auto kMaxCanvasRatio = 2.;
std::tuple RectEdges(const QRectF &r) {
return { r.left(), r.top(), r.left() + r.width(), r.top() + r.height() };
@@ -92,7 +93,6 @@ Crop::Crop(
, _pointSizeH(_pointSize / 2.)
, _innerMargins(QMarginsF(_pointSizeH, _pointSizeH, _pointSizeH, _pointSizeH)
.toMargins())
-, _offset(_innerMargins.left(), _innerMargins.top())
, _edgePointMargins(_pointSizeH, _pointSizeH, -_pointSizeH, -_pointSizeH)
, _imageSize(imageSize)
, _data(std::move(data))
@@ -125,6 +125,7 @@ Crop::Crop(
void Crop::applyTransform(
const QRect &geometry,
+ QPoint imagePosition,
int angle,
bool flipped,
const QSizeF &scaledImageSize) {
@@ -132,7 +133,9 @@ void Crop::applyTransform(
return;
}
setGeometry(geometry);
- _innerRect = QRectF(_offset, FlipSizeByRotation(scaledImageSize, angle));
+ _innerRect = QRectF(
+ imagePosition,
+ FlipSizeByRotation(scaledImageSize, angle));
_ratio.w = scaledImageSize.width() / float64(_imageSize.width());
_ratio.h = scaledImageSize.height() / float64(_imageSize.height());
_flipped = flipped;
@@ -153,25 +156,21 @@ void Crop::applyTransform(
.scale(_ratio.w, _ratio.h)
.mapRect(_cropOriginal)
.translated(
- -cropHolderRotated.x() + _offset.x(),
- -cropHolderRotated.y() + _offset.y());
+ imagePosition.x() - cropHolderRotated.x(),
+ imagePosition.y() - cropHolderRotated.y());
- auto adjusted = false;
const auto min = float64(st::photoEditorCropMinSize);
if ((cropPaint.width() < min) || (cropPaint.height() < min)) {
+ const auto bounds = _innerRect.united(cropPaint);
cropPaint.setWidth(std::max(
- std::min(min, _innerRect.width()),
+ std::min(min, bounds.width()),
cropPaint.width()));
cropPaint.setHeight(std::max(
- std::min(min, _innerRect.height()),
+ std::min(min, bounds.height()),
cropPaint.height()));
- adjusted = true;
+ AdjustCropToInner(cropPaint, bounds);
}
- adjusted = AdjustCropToInner(cropPaint, _innerRect) || adjusted;
setCropPaint(std::move(cropPaint));
- if (adjusted) {
- convertCropPaintToOriginal();
- }
}
QPainterPath Crop::cropPath() const {
@@ -282,14 +281,16 @@ void Crop::paintGrid(QPainter &p, float64 opacity) {
}
void Crop::setCropPaint(QRectF &&rect) {
- AdjustCropToInner(rect, _innerRect);
_cropPaint = std::move(rect);
updateEdges();
+ updatePainterPath();
+}
- _painterPath.clear();
- _painterPath.addRect(_innerRect);
- _painterPath.addPath(cropPath());
+void Crop::updatePainterPath() {
+ auto inner = QPainterPath();
+ inner.addRect(_innerRect);
+ _painterPath = inner.subtracted(cropPath());
}
void Crop::convertCropPaintToOriginal() {
@@ -307,7 +308,7 @@ void Crop::convertCropPaintToOriginal() {
const auto cropHolderRotated = matrix.mapRect(cropHolder);
_cropOriginal = matrix
- .mapRect(QRectF(_cropPaint).translated(-_offset))
+ .mapRect(QRectF(_cropPaint).translated(-_innerRect.topLeft()))
.translated(
-cropHolderRotated.x(),
-cropHolderRotated.y());
@@ -369,7 +370,13 @@ void Crop::mousePressEvent(QMouseEvent *e) {
if (_data.fixedCrop && e->button() != Qt::LeftButton) {
return;
}
- computeDownState(e->pos());
+ const auto edge = mouseState(e->pos());
+ if (edge) {
+ _dragChanges.fire(true);
+ }
+ const auto expanding = _expansionAllowed
+ && e->modifiers().testFlag(Qt::ControlModifier);
+ computeDownState(e->pos(), edge, expanding);
if (_down.edge) {
setGridVisible(true, false);
}
@@ -379,29 +386,43 @@ void Crop::mouseReleaseEvent(QMouseEvent *e) {
if (_data.fixedCrop && e->button() != Qt::LeftButton) {
return;
}
- const auto hadEdge = bool(_down.edge);
- if (hadEdge) {
- setGridVisible(false, true);
+ finishDrag(true);
+}
+
+void Crop::hideEvent(QHideEvent *e) {
+ finishDrag(false);
+}
+
+void Crop::finishDrag(bool animated) {
+ if (!_down.edge) {
+ return;
}
+ setGridVisible(false, animated);
clearDownState();
const auto was = saveCropRect();
convertCropPaintToOriginal();
+ _dragChanges.fire(false);
if (saveCropRect() != was) {
_changes.fire({});
}
}
-void Crop::computeDownState(const QPoint &p) {
- const auto edge = mouseState(p);
- const auto &inner = _innerRect;
+void Crop::computeDownState(
+ const QPoint &p,
+ Qt::Edges edge,
+ bool expanding) {
const auto &crop = _cropPaint;
- const auto &[iLeft, iTop, iRight, iBottom] = RectEdges(inner);
+ const auto bounds = (expanding && (edge != kEAll))
+ ? expansionBounds()
+ : _innerRect.united(crop);
+ const auto &[iLeft, iTop, iRight, iBottom] = RectEdges(bounds);
const auto &[cLeft, cTop, cRight, cBottom] = RectEdges(crop);
_down = InfoAtDown{
.rect = crop,
.edge = edge,
.point = (p - PointOfEdge(edge, crop)),
.cropRatio = (_cropOriginal.width() / _cropOriginal.height()),
+ .bounds = bounds,
.borders = InfoAtDown::Borders{
.left = iLeft - cLeft,
.right = iRight - cRight,
@@ -419,15 +440,11 @@ void Crop::computeDownState(const QPoint &p) {
auto &xSide = (hasLeft ? _down.borders.left : _down.borders.right);
auto &ySide = (hasTop ? _down.borders.top : _down.borders.bottom);
- const auto min = std::abs(std::min(xSign * xSide, ySign * ySide));
- const auto xIsMin = ((xSign * xSide) < (ySign * ySide));
- xSide = xSign * min;
- ySide = ySign * min;
- if (!xIsMin) {
- xSide *= _down.cropRatio;
- } else {
- ySide /= _down.cropRatio;
- }
+ const auto xRoom = std::min(
+ float64(xSign * xSide),
+ ySign * ySide * _down.cropRatio);
+ xSide = int(xSign * xRoom);
+ ySide = int(ySign * xRoom / _down.cropRatio);
}
}
@@ -435,6 +452,20 @@ void Crop::clearDownState() {
_down = InfoAtDown();
}
+QRectF Crop::expansionBounds() const {
+ const auto united = _innerRect.united(_cropPaint);
+ const auto maxWidth = _innerRect.width() * kMaxCanvasRatio;
+ const auto maxHeight = _innerRect.height() * kMaxCanvasRatio;
+ const auto limit = QRectF(
+ QPointF(
+ std::min(united.right() - maxWidth, united.left()),
+ std::min(united.bottom() - maxHeight, united.top())),
+ QPointF(
+ std::max(united.left() + maxWidth, united.right()),
+ std::max(united.top() + maxHeight, united.bottom())));
+ return (QRectF(rect()) - QMarginsF(_innerMargins)).intersected(limit);
+}
+
void Crop::setGridVisible(bool visible, bool animated) {
if ((_gridVisible == visible) && !_gridOpacityAnimation.animating()) {
return;
@@ -483,10 +514,18 @@ void Crop::performCrop(const QPoint &pos) {
const auto minH = (_keepAspectRatio && cropRatio < 1.)
? (minSize / cropRatio)
: float64(minSize);
- const auto xMin = xFactor * int(
- crop.width() - std::min(minW, crop.width()));
- const auto yMin = yFactor * int(
- crop.height() - std::min(minH, crop.height()));
+ const auto xInward = std::min(
+ crop.width() - minW,
+ hasLeft
+ ? (_innerRect.right() - minW - crop.left())
+ : (crop.right() - minW - _innerRect.left()));
+ const auto yInward = std::min(
+ crop.height() - minH,
+ hasTop
+ ? (_innerRect.bottom() - minH - crop.top())
+ : (crop.bottom() - minH - _innerRect.top()));
+ const auto xMin = xFactor * int(std::max(xInward, 0.));
+ const auto yMin = yFactor * int(std::max(yInward, 0.));
const auto x = std::clamp(
diff.x(),
@@ -498,11 +537,17 @@ void Crop::performCrop(const QPoint &pos) {
hasTop ? yMin : borders.bottom);
return QPoint(x, y);
}();
- setCropPaint(crop - QMargins(
+ auto result = crop - QMargins(
hasLeft ? diff.x() : 0,
hasTop ? diff.y() : 0,
hasRight ? -diff.x() : 0,
- hasBottom ? -diff.y() : 0));
+ hasBottom ? -diff.y() : 0);
+ const auto &bounds = _down.bounds;
+ result.setLeft(std::max(result.left(), bounds.left()));
+ result.setTop(std::max(result.top(), bounds.top()));
+ result.setRight(std::min(result.right(), bounds.right()));
+ result.setBottom(std::min(result.bottom(), bounds.bottom()));
+ setCropPaint(std::move(result));
}
void Crop::performMove(const QPoint &pos) {
@@ -510,7 +555,9 @@ void Crop::performMove(const QPoint &pos) {
const auto &b = _down.borders;
const auto diffX = std::clamp(pos.x() - _down.point.x(), b.left, b.right);
const auto diffY = std::clamp(pos.y() - _down.point.y(), b.top, b.bottom);
- setCropPaint(inner.translated(diffX, diffY));
+ auto result = inner.translated(diffX, diffY);
+ AdjustCropToInner(result, _down.bounds);
+ setCropPaint(std::move(result));
}
void Crop::mouseMoveEvent(QMouseEvent *e) {
@@ -555,8 +602,9 @@ void Crop::setAspectRatio(float64 ratio) {
_keepAspectRatio = !free;
if (!free) {
- const auto maxW = _innerRect.width();
- const auto maxH = _innerRect.height();
+ const auto bounds = _innerRect.united(_cropPaint);
+ const auto maxW = bounds.width();
+ const auto maxH = bounds.height();
auto newW = maxW;
auto newH = maxW / ratio;
if (newH > maxH) {
@@ -571,23 +619,29 @@ void Crop::setAspectRatio(float64 ratio) {
newW,
newH);
- AdjustCropToInner(adjusted, _innerRect);
+ AdjustCropToInner(adjusted, bounds);
setCropPaint(std::move(adjusted));
+ const auto was = saveCropRect();
convertCropPaintToOriginal();
+ if (saveCropRect() != was) {
+ _changes.fire({});
+ }
} else {
updateEdges();
}
update();
}
+void Crop::setExpansionAllowed(bool allowed) {
+ _expansionAllowed = allowed;
+}
+
void Crop::setCornersLevel(RoundedCornersLevel level) {
if (_cornersLevel == level) {
return;
}
_cornersLevel = level;
- _painterPath.clear();
- _painterPath.addRect(_innerRect);
- _painterPath.addPath(cropPath());
+ updatePainterPath();
update();
}
@@ -595,6 +649,10 @@ QRect Crop::paintRect() const {
return _cropPaint.toRect();
}
+QRect Crop::cropRect() const {
+ return _cropOriginal.toRect();
+}
+
QRect Crop::saveCropRect() {
const auto savedCrop = _cropOriginal.toRect();
return (!savedCrop.topLeft().isNull() || (savedCrop.size() != _imageSize))
diff --git a/Telegram/SourceFiles/editor/editor_crop.h b/Telegram/SourceFiles/editor/editor_crop.h
index b502d516e1..1125c86bda 100644
--- a/Telegram/SourceFiles/editor/editor_crop.h
+++ b/Telegram/SourceFiles/editor/editor_crop.h
@@ -26,22 +26,30 @@ public:
void applyTransform(
const QRect &geometry,
+ QPoint imagePosition,
int angle,
bool flipped,
const QSizeF &scaledImageSize);
[[nodiscard]] QRect saveCropRect();
+ [[nodiscard]] QRect cropRect() const;
[[nodiscard]] rpl::producer<> changes() const {
return _changes.events();
}
+ [[nodiscard]] rpl::producer dragChanges() const {
+ return _dragChanges.events();
+ }
[[nodiscard]] QRect paintRect() const;
+ [[nodiscard]] QPainterPath cropPath() const;
[[nodiscard]] style::margins cropMargins() const;
void setAspectRatio(float64 ratio);
void setCornersLevel(RoundedCornersLevel level);
+ void setExpansionAllowed(bool allowed);
protected:
void mousePressEvent(QMouseEvent *e) override;
void mouseReleaseEvent(QMouseEvent *e) override;
void mouseMoveEvent(QMouseEvent *e) override;
+ void hideEvent(QHideEvent *e) override;
private:
struct InfoAtDown {
@@ -49,6 +57,7 @@ private:
Qt::Edges edge;
QPoint point;
float64 cropRatio = 0.;
+ QRectF bounds;
struct Borders {
int left = 0;
@@ -61,25 +70,27 @@ private:
void paintFrame(QPainter &p);
void paintGrid(QPainter &p, float64 opacity);
void setGridVisible(bool visible, bool animated);
- [[nodiscard]] QPainterPath cropPath() const;
void updateEdges();
+ void updatePainterPath();
[[nodiscard]] QPoint pointOfEdge(Qt::Edges e) const;
+ [[nodiscard]] QRectF expansionBounds() const;
void setCropPaint(QRectF &&rect);
void convertCropPaintToOriginal();
- void computeDownState(const QPoint &p);
+ void computeDownState(const QPoint &p, Qt::Edges edge, bool expanding);
void clearDownState();
[[nodiscard]] Qt::Edges mouseState(const QPoint &p);
void performCrop(const QPoint &pos);
void performMove(const QPoint &pos);
+ void finishDrag(bool animated);
rpl::event_stream<> _changes;
+ rpl::event_stream _dragChanges;
const int _pointSize;
const float _pointSizeH;
const style::margins _innerMargins;
- const QPoint _offset;
const QMarginsF _edgePointMargins;
const QSize _imageSize;
const EditorData _data;
@@ -105,6 +116,7 @@ private:
bool _gridVisible = false;
bool _keepAspectRatio = false;
+ bool _expansionAllowed = false;
RoundedCornersLevel _cornersLevel = RoundedCornersLevel::Large;
diff --git a/Telegram/SourceFiles/editor/editor_keys_legend.cpp b/Telegram/SourceFiles/editor/editor_keys_legend.cpp
new file mode 100644
index 0000000000..d77ce36b86
--- /dev/null
+++ b/Telegram/SourceFiles/editor/editor_keys_legend.cpp
@@ -0,0 +1,513 @@
+/*
+This file is part of Telegram Desktop,
+the official desktop application for the Telegram messaging service.
+
+For license and copyright information please follow this link:
+https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
+*/
+#include "editor/editor_keys_legend.h"
+
+#include "base/platform/base_platform_info.h"
+#include "lang/lang_keys.h"
+#include "ui/effects/panel_animation.h"
+#include "ui/painter.h"
+#include "ui/qt_object_factory.h"
+#include "ui/rect.h"
+#include "ui/text/text.h"
+#include "ui/ui_utility.h"
+#include "ui/widgets/menu/menu.h"
+#include "ui/widgets/menu/menu_item_base.h"
+#include "ui/widgets/popup_menu.h"
+#include "styles/style_editor.h"
+
+#include
+#include
+
+namespace Editor {
+namespace {
+
+struct Row {
+ std::vector keys;
+ QString text;
+};
+
+struct Section {
+ QString title;
+ std::vector rows;
+};
+
+[[nodiscard]] QString CtrlKey() {
+ return Platform::IsMac() ? QString(QChar(0x2318)) : u"Ctrl"_q;
+}
+
+[[nodiscard]] QString ShiftKey() {
+ return Platform::IsMac() ? QString(QChar(0x21E7)) : u"Shift"_q;
+}
+
+[[nodiscard]] QString DeleteKey() {
+ return Platform::IsMac() ? QString(QChar(0x232B)) : u"Del"_q;
+}
+
+[[nodiscard]] QString Gesture(
+ const QString &modifier,
+ const QString &gesture) {
+ return tr::lng_photo_editor_keys_gesture(
+ tr::now,
+ lt_modifier,
+ modifier,
+ lt_gesture,
+ gesture);
+}
+
+[[nodiscard]] QString DragEdge() {
+ return tr::lng_photo_editor_keys_gesture_drag_edge(tr::now);
+}
+
+[[nodiscard]] QString DragHandle() {
+ return tr::lng_photo_editor_keys_gesture_drag_handle(tr::now);
+}
+
+[[nodiscard]] QString Drag() {
+ return tr::lng_photo_editor_keys_gesture_drag(tr::now);
+}
+
+[[nodiscard]] QString Click() {
+ return tr::lng_photo_editor_keys_gesture_click(tr::now);
+}
+
+[[nodiscard]] QString Wheel() {
+ return tr::lng_photo_editor_keys_gesture_wheel(tr::now);
+}
+
+[[nodiscard]] QString WheelKey() {
+ return tr::lng_photo_editor_keys_wheel(tr::now);
+}
+
+[[nodiscard]] QString MiddleDragKey() {
+ return tr::lng_photo_editor_keys_middle_drag(tr::now);
+}
+
+[[nodiscard]] QString DoubleClickKey() {
+ return tr::lng_photo_editor_keys_double_click(tr::now);
+}
+
+[[nodiscard]] std::vector FixedCropRows() {
+ return {
+ { { WheelKey() }, tr::lng_photo_editor_keys_fixed_zoom(tr::now) },
+ {
+ { Gesture(ShiftKey(), Wheel()) },
+ tr::lng_photo_editor_keys_fixed_zoom_fine(tr::now),
+ },
+ {
+ { MiddleDragKey() },
+ tr::lng_photo_editor_keys_fixed_pan(tr::now),
+ },
+ };
+}
+
+[[nodiscard]] Section CropSection(const KeysLegendContext &context) {
+ auto result = Section{ .title = tr::lng_photo_editor_keys_crop(tr::now) };
+ if (context.fixedCrop) {
+ result.rows = FixedCropRows();
+ } else {
+ result.rows = {
+ { { CtrlKey() }, tr::lng_photo_editor_keys_crop_room(tr::now) },
+ {
+ { Gesture(CtrlKey(), DragEdge()) },
+ tr::lng_photo_editor_keys_crop_expand(tr::now),
+ },
+ };
+ }
+ return result;
+}
+
+[[nodiscard]] Section CanvasSection(const KeysLegendContext &context) {
+ auto result = Section{
+ .title = tr::lng_photo_editor_keys_canvas(tr::now),
+ };
+ if (context.fixedCrop) {
+ result.rows = FixedCropRows();
+ } else {
+ result.rows = {
+ {
+ { WheelKey() },
+ tr::lng_photo_editor_keys_canvas_zoom(tr::now),
+ },
+ {
+ { MiddleDragKey() },
+ tr::lng_photo_editor_keys_canvas_pan(tr::now),
+ },
+ };
+ }
+ result.rows.push_back({
+ { CtrlKey(), u"V"_q },
+ tr::lng_photo_editor_keys_canvas_paste(tr::now),
+ });
+ result.rows.push_back({
+ { CtrlKey(), u"Z"_q },
+ tr::lng_photo_editor_keys_canvas_undo(tr::now),
+ });
+ result.rows.push_back({
+ { CtrlKey(), ShiftKey(), u"Z"_q },
+ tr::lng_photo_editor_keys_canvas_redo(tr::now),
+ });
+ return result;
+}
+
+[[nodiscard]] Section ShapesSection() {
+ return {
+ .title = tr::lng_photo_editor_keys_shapes(tr::now),
+ .rows = {
+ {
+ { Gesture(ShiftKey(), Drag()) },
+ tr::lng_photo_editor_keys_shapes_proportions(tr::now),
+ },
+ {
+ { Gesture(ShiftKey(), Click()) },
+ tr::lng_photo_editor_keys_shapes_instant(tr::now),
+ },
+ },
+ };
+}
+
+[[nodiscard]] Section ObjectsSection() {
+ return {
+ .title = tr::lng_photo_editor_keys_objects(tr::now),
+ .rows = {
+ {
+ { Gesture(ShiftKey(), Drag()) },
+ tr::lng_photo_editor_keys_objects_snap(tr::now),
+ },
+ {
+ { Gesture(ShiftKey(), DragHandle()) },
+ tr::lng_photo_editor_keys_objects_rotate(tr::now),
+ },
+ {
+ { Gesture(CtrlKey(), DragHandle()) },
+ tr::lng_photo_editor_keys_objects_stretch(tr::now),
+ },
+ {
+ { CtrlKey(), u"D"_q },
+ tr::lng_photo_editor_keys_objects_duplicate(tr::now),
+ },
+ {
+ { CtrlKey(), u"S"_q },
+ tr::lng_photo_editor_keys_objects_flip(tr::now),
+ },
+ {
+ { DeleteKey() },
+ tr::lng_photo_editor_keys_objects_delete(tr::now),
+ },
+ {
+ { DoubleClickKey() },
+ tr::lng_photo_editor_keys_objects_text(tr::now),
+ },
+ },
+ };
+}
+
+[[nodiscard]] Section TimelineSection() {
+ return {
+ .title = tr::lng_photo_editor_keys_timeline(tr::now),
+ .rows = {
+ {
+ { WheelKey() },
+ tr::lng_photo_editor_keys_timeline_zoom(tr::now),
+ },
+ {
+ { Gesture(ShiftKey(), Wheel()) },
+ tr::lng_photo_editor_keys_timeline_scroll(tr::now),
+ },
+ {
+ { Gesture(ShiftKey(), Drag()) },
+ tr::lng_photo_editor_keys_timeline_window(tr::now),
+ },
+ },
+ };
+}
+
+[[nodiscard]] std::vector Sections(
+ const KeysLegendContext &context) {
+ auto result = (context.mode == PhotoEditorMode::Mode::Transform)
+ ? std::vector{ CropSection(context) }
+ : std::vector{
+ CanvasSection(context),
+ ShapesSection(),
+ ObjectsSection(),
+ };
+ if (context.timeline
+ && (context.mode == PhotoEditorMode::Mode::Paint)) {
+ result.push_back(TimelineSection());
+ }
+ return result;
+}
+
+[[nodiscard]] Section LegendSection() {
+ return {
+ .rows = {
+ { { u"?"_q }, tr::lng_photo_editor_keys_legend(tr::now) },
+ },
+ };
+}
+
+struct Cap {
+ QString text;
+ int width = 0;
+};
+
+[[nodiscard]] Cap MakeCap(const QString &text) {
+ const auto &st = st::photoEditorKeysCapStyle;
+ return {
+ .text = text,
+ .width = std::max(
+ st.font->width(text) + 2 * st::photoEditorKeysCapPadding,
+ st::photoEditorKeysCapMinWidth),
+ };
+}
+
+[[nodiscard]] int CapsWidth(const std::vector &caps) {
+ auto result = 0;
+ for (const auto &cap : caps) {
+ result += cap.width;
+ }
+ return result + (int(caps.size()) - 1) * st::photoEditorKeysCapSkip;
+}
+
+class SectionItem final : public Ui::Menu::ItemBase {
+public:
+ SectionItem(
+ not_null parent,
+ const style::Menu &st,
+ const Section §ion,
+ int keysWidth);
+
+ not_null action() const override;
+ bool isEnabled() const override;
+
+private:
+ struct PreparedRow {
+ std::vector caps;
+ Ui::Text::String text;
+ };
+
+ int contentHeight() const override;
+ void paintEvent(QPaintEvent *e) override;
+ void paintCap(QPainter &p, const Cap &cap, int x, int y) const;
+
+ const style::Menu &_st;
+ const int _keysWidth;
+ const Ui::Text::String _title;
+ std::vector _rows;
+ int _textWidth = 0;
+ const not_null _dummyAction;
+
+};
+
+SectionItem::SectionItem(
+ not_null parent,
+ const style::Menu &st,
+ const Section §ion,
+ int keysWidth)
+: ItemBase(parent, st)
+, _st(st)
+, _keysWidth(keysWidth)
+, _title(st::photoEditorKeysTitleStyle, section.title)
+, _dummyAction(Ui::CreateChild(parent.get())) {
+ setPointerCursor(false);
+ auto textWidth = 0;
+ for (const auto &row : section.rows) {
+ auto caps = std::vector();
+ for (const auto &key : row.keys) {
+ caps.push_back(MakeCap(key));
+ }
+ auto text = Ui::Text::String(
+ _st.itemStyle,
+ row.text,
+ kDefaultTextOptions,
+ st::photoEditorKeysTextMinWidth);
+ textWidth = std::max(textWidth, text.maxWidth());
+ _rows.push_back({ std::move(caps), std::move(text) });
+ }
+ const auto available = _st.widthMax
+ - rect::m::sum::h(_st.itemPadding)
+ - _keysWidth
+ - st::photoEditorKeysTextSkip;
+ _textWidth = std::max(
+ std::min({ textWidth, st::photoEditorKeysTextWidth, available }),
+ st::photoEditorKeysTextMinWidth);
+ setMinWidth(rect::m::sum::h(_st.itemPadding)
+ + _keysWidth
+ + st::photoEditorKeysTextSkip
+ + _textWidth);
+ parent->widthValue() | rpl::on_next([=](int width) {
+ resize(width, contentHeight());
+ }, lifetime());
+}
+
+not_null SectionItem::action() const {
+ return _dummyAction;
+}
+
+bool SectionItem::isEnabled() const {
+ return false;
+}
+
+int SectionItem::contentHeight() const {
+ auto result = rect::m::sum::v(_st.itemPadding);
+ if (!_title.isEmpty()) {
+ result += _title.minHeight() + st::photoEditorKeysTitleSkip;
+ }
+ for (const auto &row : _rows) {
+ result += std::max(
+ st::photoEditorKeysCapHeight,
+ row.text.countHeight(_textWidth));
+ }
+ return result + (int(_rows.size()) - 1) * st::photoEditorKeysRowSkip;
+}
+
+void SectionItem::paintCap(
+ QPainter &p,
+ const Cap &cap,
+ int x,
+ int y) const {
+ const auto rect = QRect(x, y, cap.width, st::photoEditorKeysCapHeight);
+ const auto radius = st::photoEditorKeysCapRadius;
+ auto hq = PainterHighQualityEnabler(p);
+ p.setPen(QPen(st::photoEditorKeysCapBorder, st::lineWidth));
+ p.setBrush(st::photoEditorKeysCapBg);
+ p.drawRoundedRect(
+ QRectF(rect).adjusted(.5, .5, -.5, -.5),
+ radius,
+ radius);
+ p.setPen(st::photoEditorKeysCapFg);
+ p.setFont(st::photoEditorKeysCapStyle.font);
+ p.drawText(rect, cap.text, style::al_center);
+}
+
+void SectionItem::paintEvent(QPaintEvent *e) {
+ auto p = Painter(this);
+ p.fillRect(rect(), _st.itemBg);
+
+ const auto &padding = _st.itemPadding;
+ const auto textLeft = padding.left()
+ + _keysWidth
+ + st::photoEditorKeysTextSkip;
+ auto top = padding.top();
+ if (!_title.isEmpty()) {
+ p.setPen(st::photoEditorKeysTitleFg);
+ _title.draw(
+ p,
+ padding.left(),
+ top,
+ width() - rect::m::sum::h(padding));
+ top += _title.minHeight() + st::photoEditorKeysTitleSkip;
+ }
+
+ for (const auto &row : _rows) {
+ const auto textHeight = row.text.countHeight(_textWidth);
+ const auto rowHeight = std::max(
+ st::photoEditorKeysCapHeight,
+ textHeight);
+ auto capLeft = padding.left();
+ const auto capTop = top
+ + (rowHeight - st::photoEditorKeysCapHeight) / 2;
+ for (const auto &cap : row.caps) {
+ paintCap(p, cap, capLeft, capTop);
+ capLeft += cap.width + st::photoEditorKeysCapSkip;
+ }
+ p.setPen(st::photoEditorKeysTextFg);
+ row.text.draw(
+ p,
+ textLeft,
+ top + (rowHeight - textHeight) / 2,
+ _textWidth);
+ top += rowHeight + st::photoEditorKeysRowSkip;
+ }
+}
+
+void FillKeysLegend(
+ not_null menu,
+ const KeysLegendContext &context) {
+ auto sections = Sections(context);
+ sections.push_back(LegendSection());
+ auto keysWidth = 0;
+ for (const auto §ion : sections) {
+ for (const auto &row : section.rows) {
+ auto caps = std::vector();
+ for (const auto &key : row.keys) {
+ caps.push_back(MakeCap(key));
+ }
+ keysWidth = std::max(keysWidth, CapsWidth(caps));
+ }
+ }
+ for (const auto §ion : sections) {
+ if (section.title.isEmpty()) {
+ menu->addSeparator();
+ }
+ menu->addAction(base::make_unique_q(
+ menu->menu(),
+ menu->st().menu,
+ section,
+ keysWidth));
+ }
+}
+
+} // namespace
+
+KeysLegendButton::KeysLegendButton(
+ not_null parent,
+ Fn context)
+: IconButton(parent, st::photoEditorKeysButton)
+, _context(std::move(context)) {
+ events(
+ ) | rpl::on_next([=](not_null event) {
+ if (event->type() == QEvent::Enter) {
+ Ui::Tooltip::Show(1000, this);
+ } else if (event->type() == QEvent::Leave) {
+ Ui::Tooltip::Hide();
+ }
+ }, lifetime());
+ setClickedCallback([=] { toggle(); });
+}
+
+void KeysLegendButton::toggle() {
+ if (_menu) {
+ _menu->hideMenu();
+ } else {
+ showLegend();
+ }
+}
+
+QString KeysLegendButton::tooltipText() const {
+ return tr::lng_photo_editor_keys_tooltip(tr::now);
+}
+
+QPoint KeysLegendButton::tooltipPos() const {
+ return QCursor::pos();
+}
+
+bool KeysLegendButton::tooltipWindowActive() const {
+ return Ui::AppInFocus() && Ui::InFocusChain(window());
+}
+
+void KeysLegendButton::showLegend() {
+ Ui::Tooltip::Hide();
+ _menu = base::make_unique_q(
+ this,
+ st::photoEditorKeysMenu);
+ FillKeysLegend(_menu.get(), _context());
+ const auto skip = st::photoEditorKeysMenuSkip;
+ const auto above = mapToGlobal(QPoint(0, -skip));
+ const auto screen = QGuiApplication::screenAt(above);
+ const auto fitsAbove = !screen
+ || (above.y() - _menu->height() >= screen->availableGeometry().y());
+ if (fitsAbove) {
+ _menu->setForcedOrigin(Ui::PanelAnimation::Origin::BottomLeft);
+ _menu->popup(above);
+ } else {
+ _menu->setForcedOrigin(Ui::PanelAnimation::Origin::TopLeft);
+ _menu->popup(mapToGlobal(QPoint(0, height())));
+ }
+}
+
+} // namespace Editor
diff --git a/Telegram/SourceFiles/editor/editor_keys_legend.h b/Telegram/SourceFiles/editor/editor_keys_legend.h
new file mode 100644
index 0000000000..ff8261cf01
--- /dev/null
+++ b/Telegram/SourceFiles/editor/editor_keys_legend.h
@@ -0,0 +1,49 @@
+/*
+This file is part of Telegram Desktop,
+the official desktop application for the Telegram messaging service.
+
+For license and copyright information please follow this link:
+https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
+*/
+#pragma once
+
+#include "base/unique_qptr.h"
+#include "editor/photo_editor_inner_common.h"
+#include "ui/widgets/buttons.h"
+#include "ui/widgets/tooltip.h"
+
+namespace Ui {
+class PopupMenu;
+} // namespace Ui
+
+namespace Editor {
+
+struct KeysLegendContext {
+ PhotoEditorMode::Mode mode = PhotoEditorMode::Mode::Transform;
+ bool fixedCrop = false;
+ bool timeline = false;
+};
+
+class KeysLegendButton final
+ : public Ui::IconButton
+ , public Ui::AbstractTooltipShower {
+public:
+ KeysLegendButton(
+ not_null parent,
+ Fn context);
+
+ void toggle();
+
+ QString tooltipText() const override;
+ QPoint tooltipPos() const override;
+ bool tooltipWindowActive() const override;
+
+private:
+ void showLegend();
+
+ const Fn _context;
+ base::unique_qptr _menu;
+
+};
+
+} // namespace Editor
diff --git a/Telegram/SourceFiles/editor/editor_layer_widget.cpp b/Telegram/SourceFiles/editor/editor_layer_widget.cpp
index 6c6d43f1c8..9d96ea89fc 100644
--- a/Telegram/SourceFiles/editor/editor_layer_widget.cpp
+++ b/Telegram/SourceFiles/editor/editor_layer_widget.cpp
@@ -208,7 +208,25 @@ void LayerWidget::parentResized() {
}
void LayerWidget::keyPressEvent(QKeyEvent *e) {
+ forwardKeyEvent(e);
+}
+
+void LayerWidget::keyReleaseEvent(QKeyEvent *e) {
+ forwardKeyEvent(e);
+}
+
+void LayerWidget::forwardKeyEvent(not_null e) {
+ // Qt bubbles a key event ignored by the content back to its parent.
+ if (_forwardingKeyEvent) {
+ _keyEventIgnored = true;
+ e->accept();
+ return;
+ }
+ _forwardingKeyEvent = true;
+ _keyEventIgnored = false;
QGuiApplication::sendEvent(_content.get(), e);
+ _forwardingKeyEvent = false;
+ e->setAccepted(!_keyEventIgnored);
}
int LayerWidget::resizeGetHeight(int newWidth) {
diff --git a/Telegram/SourceFiles/editor/editor_layer_widget.h b/Telegram/SourceFiles/editor/editor_layer_widget.h
index ba8cb72bf2..4411d03659 100644
--- a/Telegram/SourceFiles/editor/editor_layer_widget.h
+++ b/Telegram/SourceFiles/editor/editor_layer_widget.h
@@ -34,7 +34,9 @@ public:
private:
bool eventHook(QEvent *e) override;
void keyPressEvent(QKeyEvent *e) override;
+ void keyReleaseEvent(QKeyEvent *e) override;
int resizeGetHeight(int newWidth) override;
+ void forwardKeyEvent(not_null e);
void start();
void cacheBackground();
@@ -53,6 +55,8 @@ private:
crl::time _lastAreaChangeTime = 0;
bool _backgroundCaching = false;
bool _backgroundNight = false;
+ bool _forwardingKeyEvent = false;
+ bool _keyEventIgnored = false;
};
diff --git a/Telegram/SourceFiles/editor/editor_link_box.cpp b/Telegram/SourceFiles/editor/editor_link_box.cpp
new file mode 100644
index 0000000000..5bfb20a616
--- /dev/null
+++ b/Telegram/SourceFiles/editor/editor_link_box.cpp
@@ -0,0 +1,1005 @@
+/*
+This file is part of Telegram Desktop,
+the official desktop application for the Telegram messaging service.
+
+For license and copyright information please follow this link:
+https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
+*/
+#include "editor/editor_link_box.h"
+
+#include "api/api_single_message_search.h"
+#include "base/qthelp_url.h"
+#include "base/timer.h"
+#include "chat_helpers/compose/compose_show.h"
+#include "data/data_document.h"
+#include "data/data_session.h"
+#include "data/data_web_page.h"
+#include "editor/editor_link_pill.h"
+#include "editor/editor_message_render.h"
+#include "history/history_item.h"
+#include "history/view/controls/history_view_webpage_processor.h"
+#include "lang/lang_keys.h"
+#include "lottie/lottie_icon.h"
+#include "main/main_session.h"
+#include "ui/abstract_button.h"
+#include "ui/chat/chat_theme.h"
+#include "ui/effects/animations.h"
+#include "ui/effects/glare.h"
+#include "ui/effects/loading_element.h"
+#include "ui/layers/generic_box.h"
+#include "ui/painter.h"
+#include "ui/rect.h"
+#include "ui/rp_widget.h"
+#include "ui/widgets/buttons.h"
+#include "ui/widgets/fields/input_field.h"
+#include "ui/wrap/slide_wrap.h"
+#include "ui/wrap/vertical_layout.h"
+#include "window/section_widget.h"
+#include "window/themes/window_theme.h"
+#include "window/themes/window_themes_embedded.h"
+#include "styles/style_calls.h"
+#include "styles/style_editor.h"
+#include "styles/style_layers.h"
+
+namespace Editor {
+namespace {
+
+constexpr auto kResolveDelay = crl::time(700);
+constexpr auto kLoadingTitleRatio = 0.45;
+constexpr auto kLoadingLineRatios = std::array{ 0.32, 0.58, 0.86 };
+constexpr auto kGlareTimeout = crl::time(1000);
+constexpr auto kGlareDuration = crl::time(1000);
+
+[[nodiscard]] const style::palette &DefaultPalette(bool dark) {
+ static auto cache = std::array, 2>();
+ auto &result = cache[dark ? 1 : 0];
+ if (!result) {
+ result = std::make_unique();
+ Window::Theme::PreparePaletteCallback(dark, std::nullopt)(*result);
+ }
+ return *result;
+}
+
+class LoadingMessage final : public Ui::LoadingElement {
+public:
+ LoadingMessage(const style::palette &palette, bool captionAbove);
+
+ [[nodiscard]] bool captionAbove() const;
+
+ [[nodiscard]] int height() const override;
+ void paint(QPainter &p, int width) override;
+
+private:
+ void paintCaption(QPainter &p, int inner);
+ void paintPreview(QPainter &p, int inner);
+
+ const style::palette &_palette;
+ Ui::LoadingLine _line;
+ bool _captionAbove = true;
+
+};
+
+LoadingMessage::LoadingMessage(
+ const style::palette &palette,
+ bool captionAbove)
+: _palette(palette)
+, _line(
+ st::photoEditorLinkLoadingLine,
+ st::photoEditorLinkLoadingSkip,
+ palette.windowBgRipple()->c)
+, _captionAbove(captionAbove) {
+}
+
+bool LoadingMessage::captionAbove() const {
+ return _captionAbove;
+}
+
+int LoadingMessage::height() const {
+ const auto padding = st::photoEditorLinkLoadingPadding;
+ const auto lines = 1 + int(kLoadingLineRatios.size());
+ return padding.top()
+ + lines * _line.height()
+ - st::photoEditorLinkLoadingSkip
+ + padding.bottom();
+}
+
+void LoadingMessage::paint(QPainter &p, int width) {
+ const auto radius = st::photoEditorLinkLoadingRadius;
+ p.setPen(Qt::NoPen);
+ p.setBrush(_palette.msgInBg()->c);
+ p.drawRoundedRect(QRect(0, 0, width, height()), radius, radius);
+
+ const auto padding = st::photoEditorLinkLoadingPadding;
+ const auto inner = width - padding.left() - padding.right();
+ p.translate(padding.left(), padding.top());
+ if (_captionAbove) {
+ paintCaption(p, inner);
+ paintPreview(p, inner);
+ } else {
+ paintPreview(p, inner);
+ paintCaption(p, inner);
+ }
+}
+
+void LoadingMessage::paintCaption(QPainter &p, int inner) {
+ _line.paint(p, int(inner * kLoadingTitleRatio));
+ p.translate(0, _line.height());
+}
+
+void LoadingMessage::paintPreview(QPainter &p, int inner) {
+ const auto quote = st::photoEditorLinkLoadingQuote;
+ const auto quoteSkip = quote + st::photoEditorLinkLoadingQuoteSkip;
+ const auto line = _line.height();
+ const auto lines = int(kLoadingLineRatios.size());
+ p.setPen(Qt::NoPen);
+ p.setBrush(_palette.windowBgRipple()->c);
+ p.drawRoundedRect(
+ QRect(0, 0, quote, lines * line - st::photoEditorLinkLoadingSkip),
+ quote / 2.,
+ quote / 2.);
+ p.translate(quoteSkip, 0);
+ for (const auto ratio : kLoadingLineRatios) {
+ _line.paint(p, int((inner - quoteSkip) * ratio));
+ p.translate(0, line);
+ }
+ p.translate(-quoteSkip, 0);
+}
+
+struct Resolved {
+ FullMsgId messageId;
+ WebPageData *webpage = nullptr;
+};
+
+class LinkResolver final : public base::has_weak_ptr {
+public:
+ explicit LinkResolver(not_null session);
+
+ void resolve(const QString &url);
+ void cancel();
+ [[nodiscard]] rpl::producer resolved() const;
+
+private:
+ void resolveMessage();
+ void resolveWebpage();
+ void finish(Resolved result);
+
+ const not_null _session;
+ Api::SingleMessageSearch _search;
+ HistoryView::Controls::WebpageResolver _webpages;
+ QString _url;
+ int _generation = 0;
+ rpl::event_stream _resolved;
+ rpl::lifetime _webpageLifetime;
+
+};
+LinkResolver::LinkResolver(not_null session)
+: _session(session)
+, _search(session)
+, _webpages(session) {
+}
+
+void LinkResolver::resolve(const QString &url) {
+ cancel();
+ _url = url;
+ resolveMessage();
+}
+
+void LinkResolver::cancel() {
+ ++_generation;
+ _search.clear();
+ _webpages.cancel(_url);
+ _webpageLifetime.destroy();
+ _url = QString();
+}
+
+rpl::producer LinkResolver::resolved() const {
+ return _resolved.events();
+}
+
+void LinkResolver::resolveMessage() {
+ const auto generation = _generation;
+ const auto item = _search.lookup(_url, crl::guard(this, [=] {
+ if (generation == _generation) {
+ resolveMessage();
+ }
+ }));
+ if (!item) {
+ return;
+ } else if (*item && CanRenderMessage(*item)) {
+ finish({ .messageId = MessageToRender(*item)->fullId() });
+ } else {
+ resolveWebpage();
+ }
+}
+
+void LinkResolver::resolveWebpage() {
+ const auto url = _url;
+ const auto generation = _generation;
+ const auto finishWith = [=](WebPageData *page) {
+ if (generation == _generation) {
+ finish({ .webpage = page });
+ }
+ };
+ const auto waitForPending = [=](not_null page) {
+ _session->data().webPageUpdates(
+ ) | rpl::filter([=](not_null updated) {
+ return (updated == page) && !page->pendingTill;
+ }) | rpl::on_next([=] {
+ finishWith(page->failed ? nullptr : page.get());
+ }, _webpageLifetime);
+ };
+ _webpages.resolved(
+ ) | rpl::filter([=](const QString &link) {
+ return (link == url);
+ }) | rpl::on_next([=] {
+ const auto page = _webpages.lookup(url).value_or(nullptr);
+ if (page && page->pendingTill > 0) {
+ waitForPending(page);
+ } else {
+ finishWith(page);
+ }
+ }, _webpageLifetime);
+ _webpages.request(url, true);
+}
+
+void LinkResolver::finish(Resolved result) {
+ _url = QString();
+ _resolved.fire(std::move(result));
+ ++_generation;
+}
+
+constexpr auto kToDarkDuration = crl::time(450);
+constexpr auto kToLightDuration = crl::time(320);
+
+class ThemeButton final : public Ui::AbstractButton {
+public:
+ ThemeButton(QWidget *parent, bool dark);
+
+ void setDark(bool dark);
+
+private:
+ void paintEvent(QPaintEvent *e) override;
+
+ std::unique_ptr _icon;
+ bool _dark = false;
+
+};
+
+ThemeButton::ThemeButton(QWidget *parent, bool dark)
+: AbstractButton(parent)
+, _icon(Lottie::MakeIcon({
+ .name = u"sun_outline"_q,
+ .color = &st::groupCallMembersFg,
+ .sizeOverride = Size(st::photoEditorLinkThemeIconSize),
+}))
+, _dark(dark) {
+ setObjectName(u"photoEditorLinkThemeToggle"_q);
+ resize(st::photoEditorLinkThemeSize, st::photoEditorLinkThemeSize);
+ if (_icon->valid() && _dark) {
+ _icon->jumpTo(_icon->framesCount() - 1, [=] { update(); });
+ }
+}
+
+void ThemeButton::setDark(bool dark) {
+ if (_dark == dark) {
+ return;
+ }
+ _dark = dark;
+ if (_icon->valid()) {
+ _icon->animate(
+ [=] { update(); },
+ _icon->frameIndex(),
+ dark ? (_icon->framesCount() - 1) : 0);
+ }
+ update();
+}
+
+void ThemeButton::paintEvent(QPaintEvent *e) {
+ auto p = QPainter(this);
+ auto hq = PainterHighQualityEnabler(p);
+ p.setPen(Qt::NoPen);
+ p.setBrush(st::photoEditorLinkThemeBg);
+ p.drawEllipse(rect());
+ if (_icon->valid()) {
+ _icon->paintInCenter(p, rect());
+ }
+}
+
+class PreviewWidget final : public Ui::RpWidget {
+public:
+ explicit PreviewWidget(QWidget *parent);
+
+ void setSource(std::shared_ptr source, bool dark);
+ void setPill(const LinkPreview &link);
+ void setLoading(bool dark, bool captionAbove);
+ void clear();
+ [[nodiscard]] rpl::producer<> themeToggles() const;
+ [[nodiscard]] rpl::producer<> sourceRemovals() const;
+
+private:
+ void paintEvent(QPaintEvent *e) override;
+ void resizeEvent(QResizeEvent *e) override;
+ void paintFrame(QPainter &p);
+ void paintContent(QPainter &p);
+ void paintLoading(QPainter &p, QRectF plate);
+ void paintGlare(QPainter &p, QRectF plate, int radius);
+ void clearLoading();
+ void validateGlare();
+ [[nodiscard]] QSize loadingSize() const;
+ [[nodiscard]] QImage snapshot();
+ void beginTransition(bool radial, bool dark);
+ void scheduleRefresh();
+ void refresh();
+ [[nodiscard]] not_null background(bool dark);
+ [[nodiscard]] float64 contentScale() const;
+ [[nodiscard]] int targetHeight() const;
+ void applyHeight();
+ [[nodiscard]] int innerWidth() const;
+ [[nodiscard]] bool hasContent() const;
+
+ const not_null _theme;
+ std::array, 2> _backgrounds;
+ std::shared_ptr _source;
+ std::unique_ptr _renderer;
+ std::optional _pillLink;
+ std::optional _pill;
+ std::optional _loading;
+ QImage _image;
+ QSize _size;
+ QImage _from;
+ Ui::Animations::Simple _progress;
+ Ui::GlareEffect _glare;
+ rpl::event_stream<> _themeToggles;
+ rpl::event_stream<> _sourceRemovals;
+ rpl::lifetime _sourceLifetime;
+ int _heightFrom = 0;
+ int _heightTo = 0;
+ bool _radial = false;
+ bool _dark = false;
+ bool _refreshScheduled = false;
+
+};
+
+PreviewWidget::PreviewWidget(QWidget *parent)
+: RpWidget(parent)
+, _theme(Ui::CreateChild(this, false)) {
+ _theme->hide();
+ _theme->setClickedCallback([=] { _themeToggles.fire({}); });
+ widthValue() | rpl::on_next([=] {
+ if (_pillLink) {
+ _pill.emplace(
+ *_pillLink,
+ LinkPill::DensityFor(innerWidth()),
+ innerWidth());
+ _size = _pill->size().toSize();
+ } else if (_loading) {
+ _size = loadingSize();
+ validateGlare();
+ }
+ applyHeight();
+ }, lifetime());
+}
+
+rpl::producer<> PreviewWidget::themeToggles() const {
+ return _themeToggles.events();
+}
+
+rpl::producer<> PreviewWidget::sourceRemovals() const {
+ return _sourceRemovals.events();
+}
+
+int PreviewWidget::innerWidth() const {
+ return std::max(width() - 2 * st::photoEditorLinkPreviewPadding, 1);
+}
+
+bool PreviewWidget::hasContent() const {
+ return !_size.isEmpty()
+ && (_loading || !_image.isNull() || _pill.has_value());
+}
+
+QSize PreviewWidget::loadingSize() const {
+ return QSize(innerWidth(), _loading->height());
+}
+
+void PreviewWidget::clearLoading() {
+ _loading.reset();
+ _glare.animation.stop();
+}
+
+void PreviewWidget::validateGlare() {
+ _glare.width = _size.width();
+ _glare.validate(
+ DefaultPalette(_dark).msgInBg()->c,
+ [=] { update(); },
+ kGlareTimeout,
+ kGlareDuration);
+}
+
+void PreviewWidget::setLoading(bool dark, bool captionAbove) {
+ if (_loading
+ && (_dark == dark)
+ && (_loading->captionAbove() == captionAbove)) {
+ return;
+ }
+ if (hasContent()) {
+ beginTransition(false, dark);
+ }
+ _dark = dark;
+ _loading.emplace(DefaultPalette(dark), captionAbove);
+ _sourceLifetime.destroy();
+ _source = nullptr;
+ _renderer = nullptr;
+ _image = QImage();
+ _pill.reset();
+ _pillLink.reset();
+ _size = loadingSize();
+ _theme->hide();
+ validateGlare();
+ applyHeight();
+ update();
+}
+
+void PreviewWidget::setSource(
+ std::shared_ptr source,
+ bool dark) {
+ if (hasContent()) {
+ beginTransition(_renderer && (_dark != dark), dark);
+ }
+ _dark = dark;
+ clearLoading();
+ _pill.reset();
+ _pillLink.reset();
+ _sourceLifetime.destroy();
+ _source = std::move(source);
+ _source->removed(
+ ) | rpl::on_next([=] {
+ _sourceRemovals.fire({});
+ }, _sourceLifetime);
+ _renderer = std::make_unique(_source);
+ _renderer->setDark(dark);
+ _renderer->setRepaintCallback([=] { scheduleRefresh(); });
+ _theme->setDark(dark);
+ _theme->show();
+ refresh();
+}
+
+void PreviewWidget::setPill(const LinkPreview &link) {
+ if (hasContent()) {
+ beginTransition(false, _dark);
+ }
+ _dark = link.dark;
+ clearLoading();
+ _sourceLifetime.destroy();
+ _source = nullptr;
+ _renderer = nullptr;
+ _image = QImage();
+ _pillLink = link;
+ _pill.emplace(link, LinkPill::DensityFor(innerWidth()), innerWidth());
+ _size = _pill->size().toSize();
+ _theme->hide();
+ applyHeight();
+ update();
+}
+
+void PreviewWidget::clear() {
+ if (hasContent()) {
+ beginTransition(false, _dark);
+ }
+ clearLoading();
+ _sourceLifetime.destroy();
+ _source = nullptr;
+ _renderer = nullptr;
+ _image = QImage();
+ _pill.reset();
+ _pillLink.reset();
+ _size = QSize();
+ _theme->hide();
+ applyHeight();
+ update();
+}
+
+void PreviewWidget::beginTransition(bool radial, bool dark) {
+ _from = snapshot();
+ _heightFrom = height();
+ _radial = radial;
+ _progress = {};
+ _progress.start(
+ [=] { applyHeight(); update(); },
+ 0.,
+ 1.,
+ (!radial
+ ? st::slideWrapDuration
+ : dark
+ ? kToDarkDuration
+ : kToLightDuration),
+ (!radial
+ ? anim::linear
+ : dark
+ ? anim::easeOutQuint
+ : anim::easeInCubic));
+}
+
+QImage PreviewWidget::snapshot() {
+ const auto ratio = style::DevicePixelRatio();
+ auto result = QImage(size() * ratio, QImage::Format_ARGB32_Premultiplied);
+ result.setDevicePixelRatio(ratio);
+ result.fill(Qt::transparent);
+ auto p = QPainter(&result);
+ paintFrame(p);
+ return result;
+}
+
+void PreviewWidget::scheduleRefresh() {
+ if (_refreshScheduled) {
+ return;
+ }
+ _refreshScheduled = true;
+ crl::on_main(this, [=] {
+ _refreshScheduled = false;
+ refresh();
+ });
+}
+
+void PreviewWidget::refresh() {
+ if (!_renderer) {
+ return;
+ }
+ _image = _renderer->render(style::DevicePixelRatio());
+ _size = _renderer->size();
+ applyHeight();
+ update();
+}
+
+not_null PreviewWidget::background(bool dark) {
+ auto &theme = _backgrounds[dark ? 1 : 0];
+ if (!theme) {
+ theme = std::make_unique();
+ theme->setBackground(Window::Theme::PrepareDefaultBackground(dark));
+ theme->repaintBackgroundRequests(
+ ) | rpl::on_next([=] { update(); }, lifetime());
+ }
+ return theme.get();
+}
+
+float64 PreviewWidget::contentScale() const {
+ return std::min({
+ 1.,
+ innerWidth() / float64(_size.width()),
+ st::photoEditorLinkPreviewMaxHeight / float64(_size.height()),
+ });
+}
+
+int PreviewWidget::targetHeight() const {
+ if (_size.isEmpty() || !width()) {
+ return 0;
+ }
+ const auto padding = st::photoEditorLinkPreviewPadding;
+ return int(std::ceil(_size.height() * contentScale())) + 2 * padding;
+}
+
+void PreviewWidget::applyHeight() {
+ _heightTo = targetHeight();
+ const auto animated = _progress.animating();
+ const auto progress = animated ? _progress.value(1.) : 1.;
+ const auto shown = animated
+ ? anim::interpolate(_heightFrom, _heightTo, progress)
+ : _heightTo;
+ if (height() != shown) {
+ resize(width(), shown);
+ }
+ if (!animated && !_from.isNull()) {
+ _from = QImage();
+ }
+}
+
+void PreviewWidget::resizeEvent(QResizeEvent *e) {
+ const auto skip = st::photoEditorLinkThemeSkip;
+ _theme->moveToRight(skip, skip, width());
+}
+
+void PreviewWidget::paintContent(QPainter &p) {
+ auto hq = PainterHighQualityEnabler(p);
+ Window::SectionWidget::PaintBackground(
+ p,
+ background(_dark),
+ QSize(width(), height() * 3),
+ rect());
+ if (!hasContent()) {
+ return;
+ }
+ const auto scale = contentScale();
+ const auto size = QSizeF(_size) * scale;
+ const auto origin = QPointF(
+ (width() - size.width()) / 2.,
+ st::photoEditorLinkPreviewPadding);
+ if (_loading) {
+ paintLoading(p, QRectF(origin, size));
+ } else if (_pill) {
+ _pill->paint(p, origin, scale);
+ } else {
+ p.drawImage(QRectF(origin, size), _image);
+ }
+}
+
+void PreviewWidget::paintLoading(QPainter &p, QRectF plate) {
+ p.save();
+ p.translate(plate.topLeft());
+ _loading->paint(p, _size.width());
+ p.restore();
+ paintGlare(p, plate, st::photoEditorLinkLoadingRadius);
+}
+
+void PreviewWidget::paintGlare(QPainter &p, QRectF plate, int radius) {
+ if (!_glare.glare.birthTime) {
+ return;
+ }
+ const auto progress = _glare.progress(crl::now());
+ if (progress < 0. || progress > 1.) {
+ return;
+ }
+ const auto width = float64(_glare.width);
+ const auto shift = plate.x()
+ - width
+ + (plate.width() + width * 2) * progress;
+ auto path = QPainterPath();
+ path.addRoundedRect(plate, radius, radius);
+ p.save();
+ p.setClipPath(path);
+ p.drawTiledPixmap(
+ QRectF(shift, plate.y(), width, plate.height()),
+ _glare.pixmap,
+ QPointF());
+ p.restore();
+}
+
+void PreviewWidget::paintFrame(QPainter &p) {
+ auto clip = QPainterPath();
+ clip.addRoundedRect(rect(), st::boxRadius, st::boxRadius);
+ p.setClipPath(clip);
+ paintContent(p);
+ if (!_progress.animating() || _from.isNull()) {
+ return;
+ }
+ const auto progress = _progress.value(1.);
+ auto hq = PainterHighQualityEnabler(p);
+ if (_radial) {
+ const auto full = std::hypot(width(), height());
+ const auto radius = full * (_dark ? (1. - progress) : progress);
+ const auto center = QPointF(rect::center(_theme->geometry()));
+ auto circle = QPainterPath();
+ circle.addEllipse(center, radius, radius);
+ p.setClipPath(_dark
+ ? clip.intersected(circle)
+ : clip.subtracted(circle));
+ } else {
+ p.setOpacity(1. - progress);
+ }
+ p.drawImage(0, 0, _from);
+}
+
+void PreviewWidget::paintEvent(QPaintEvent *e) {
+ if (!hasContent() && _from.isNull()) {
+ return;
+ }
+ auto p = QPainter(this);
+ paintFrame(p);
+}
+
+[[nodiscard]] QString StripDoubledPrefix(const QString &text) {
+ const auto https = u"https://"_q;
+ const auto doubled = {
+ u"https://https://"_q,
+ u"https://http://"_q,
+ };
+ for (const auto &prefix : doubled) {
+ if (text.startsWith(prefix, Qt::CaseInsensitive)) {
+ return text.mid(https.size());
+ }
+ }
+ return QString();
+}
+
+[[nodiscard]] bool HasPhoto(WebPageData *webpage) {
+ return webpage
+ && webpage->hasLargeMedia
+ && (webpage->photo || webpage->document);
+}
+
+[[nodiscard]] bool HasVideo(WebPageData *webpage) {
+ return webpage
+ && !webpage->photo
+ && webpage->document
+ && webpage->document->isVideoFile();
+}
+
+} // namespace
+
+object_ptr LinkBox(LinkBoxArgs &&args) {
+ return Box([args = std::move(args)](not_null box) {
+ const auto session = &args.show->session();
+ struct State {
+ explicit State(not_null session)
+ : resolver(session) {
+ }
+
+ LinkResolver resolver;
+ base::Timer timer;
+ std::optional resolved;
+ QString url;
+ rpl::variable valid = false;
+ rpl::variable loading = false;
+ rpl::variable captionAbove = true;
+ rpl::variable largePhoto = false;
+ rpl::variable video = false;
+ rpl::variable withoutPreview = false;
+ rpl::variable dark = false;
+ rpl::variable customName = false;
+ };
+ const auto state = box->lifetime().make_state(session);
+ const auto &editing = args.editing;
+ state->dark = editing
+ ? editing->dark
+ : Window::Theme::IsNightMode();
+ if (editing) {
+ state->captionAbove = editing->captionAbove;
+ state->largePhoto = editing->largePhoto;
+ state->withoutPreview = !editing->preview;
+ state->customName = !editing->name.isEmpty();
+ }
+
+ box->setTitle(editing
+ ? tr::lng_formatting_link_edit_title()
+ : tr::lng_formatting_link_create_title());
+ box->setWidth(st::boxWideWidth);
+
+ const auto url = box->addRow(object_ptr(
+ box,
+ st::groupCallField,
+ tr::lng_formatting_link_url(),
+ (editing
+ ? editing->url
+ : !args.url.isEmpty()
+ ? args.url
+ : u"https://"_q)));
+ box->setFocusCallback([=] { url->setFocusFast(); });
+
+ const auto preview = box->addRow(
+ object_ptr>(
+ box,
+ object_ptr(box)),
+ st::photoEditorLinkPreviewMargin);
+ preview->hide(anim::type::instant);
+
+ const auto layout = box->verticalLayout();
+ const auto options = layout->add(
+ object_ptr>(
+ box,
+ object_ptr(box)));
+ options->hide(anim::type::instant);
+ const auto optionsInner = options->entity();
+ const auto addRow = [&](rpl::producer text) {
+ return optionsInner->add(
+ object_ptr>(
+ optionsInner,
+ object_ptr(
+ optionsInner,
+ std::move(text),
+ st::groupCallSettingsButton)));
+ };
+ const auto above = addRow(
+ state->captionAbove.value() | rpl::map([](bool above) {
+ return above
+ ? tr::lng_link_move_up(tr::now)
+ : tr::lng_link_move_down(tr::now);
+ }));
+ const auto photo = addRow(rpl::combine(
+ state->largePhoto.value(),
+ state->video.value()
+ ) | rpl::map([](bool large, bool video) {
+ return large
+ ? (video
+ ? tr::lng_link_shrink_video(tr::now)
+ : tr::lng_link_shrink_photo(tr::now))
+ : (video
+ ? tr::lng_link_enlarge_video(tr::now)
+ : tr::lng_link_enlarge_photo(tr::now));
+ }));
+ const auto previewRow = addRow(
+ tr::lng_shortcuts_toggle_link_preview());
+ previewRow->entity()->toggleOn(
+ state->withoutPreview.value() | rpl::map([](bool without) {
+ return !without;
+ }));
+ const auto customize = addRow(
+ tr::lng_photo_editor_link_customize());
+ customize->entity()->toggleOn(state->customName.value());
+ const auto nameWrap = optionsInner->add(
+ object_ptr>(
+ optionsInner,
+ object_ptr(
+ optionsInner,
+ st::groupCallField,
+ tr::lng_formatting_link_text(),
+ editing ? editing->name : QString()),
+ st::boxRowPadding));
+ const auto name = nameWrap->entity();
+ nameWrap->toggle(state->customName.current(), anim::type::instant);
+
+ const auto currentLink = [=] {
+ return LinkPreview{
+ .url = state->url,
+ .name = (state->customName.current()
+ ? name->getLastText().trimmed()
+ : QString()),
+ .captionAbove = state->captionAbove.current(),
+ .largePhoto = state->largePhoto.current(),
+ .preview = !state->withoutPreview.current(),
+ .dark = state->dark.current(),
+ };
+ };
+ const auto makeResult = [=] {
+ auto result = LinkBoxResult{ .dark = state->dark.current() };
+ const auto &resolved = state->resolved;
+ if (!resolved) {
+ return result;
+ } else if (resolved->messageId) {
+ const auto item = session->data().message(
+ resolved->messageId);
+ if (item && CanRenderMessage(item)) {
+ result.message = std::make_shared(item);
+ return result;
+ } else if (item && !MessageForbidsRender(item)) {
+ auto link = currentLink();
+ link.preview = false;
+ result.pill = std::move(link);
+ return result;
+ }
+ }
+ auto link = currentLink();
+ const auto webpage = link.preview ? resolved->webpage : nullptr;
+ if (webpage) {
+ result.message = std::make_shared(
+ session,
+ std::move(link),
+ webpage);
+ } else {
+ result.pill = std::move(link);
+ }
+ return result;
+ };
+ const auto refreshPreview = [=] {
+ const auto result = makeResult();
+ const auto message = result.message && !result.message->link();
+ const auto bubble = result.message && !message;
+ const auto loading = state->loading.current();
+ const auto webpage = state->resolved
+ ? state->resolved->webpage
+ : nullptr;
+ if (result.message) {
+ preview->entity()->setSource(result.message, result.dark);
+ } else if (result.pill) {
+ preview->entity()->setPill(*result.pill);
+ } else if (loading) {
+ preview->entity()->setLoading(
+ state->dark.current(),
+ state->captionAbove.current());
+ } else {
+ preview->entity()->clear();
+ }
+ const auto shown = result.message || result.pill || loading;
+ preview->toggle(shown, anim::type::normal);
+ options->toggle(shown && !loading, anim::type::normal);
+ above->toggle(bubble, anim::type::normal);
+ photo->toggle(bubble && HasPhoto(webpage), anim::type::normal);
+ previewRow->toggle(webpage != nullptr, anim::type::normal);
+ customize->toggle(!message, anim::type::normal);
+ nameWrap->toggle(
+ !message && state->customName.current(),
+ anim::type::normal);
+ };
+
+ above->entity()->setClickedCallback([=] {
+ state->captionAbove = !state->captionAbove.current();
+ refreshPreview();
+ });
+ photo->entity()->setClickedCallback([=] {
+ state->largePhoto = !state->largePhoto.current();
+ refreshPreview();
+ });
+ previewRow->entity()->toggledChanges(
+ ) | rpl::on_next([=](bool shown) {
+ state->withoutPreview = !shown;
+ refreshPreview();
+ }, previewRow->lifetime());
+ preview->entity()->themeToggles(
+ ) | rpl::on_next([=] {
+ state->dark = !state->dark.current();
+ refreshPreview();
+ }, preview->lifetime());
+ customize->entity()->toggledChanges(
+ ) | rpl::on_next([=](bool enabled) {
+ state->customName = enabled;
+ if (enabled) {
+ name->setFocusFast();
+ }
+ refreshPreview();
+ }, customize->lifetime());
+ name->changes() | rpl::on_next([=] {
+ if (state->customName.current()) {
+ refreshPreview();
+ }
+ }, name->lifetime());
+
+ const auto check = [=] {
+ const auto text = url->getLastText().trimmed();
+ const auto stripped = StripDoubledPrefix(text);
+ if (!stripped.isEmpty()) {
+ InvokeQueued(url, [=] { url->setText(stripped); });
+ return;
+ }
+ const auto validated = qthelp::validate_url(text);
+ state->valid = !validated.isEmpty();
+ if (validated == state->url) {
+ return;
+ }
+ state->url = validated;
+ state->resolved = std::nullopt;
+ state->resolver.cancel();
+ state->timer.cancel();
+ state->loading = !validated.isEmpty();
+ refreshPreview();
+ if (state->loading.current()) {
+ state->timer.callOnce(kResolveDelay);
+ }
+ };
+ state->timer.setCallback([=] {
+ state->resolver.resolve(state->url);
+ });
+ state->resolver.resolved(
+ ) | rpl::on_next([=](Resolved resolved) {
+ state->loading = false;
+ state->resolved = resolved;
+ state->video = HasVideo(resolved.webpage);
+ refreshPreview();
+ }, box->lifetime());
+ url->changes() | rpl::on_next(check, url->lifetime());
+ preview->entity()->sourceRemovals(
+ ) | rpl::on_next([=] {
+ state->url = QString();
+ check();
+ }, preview->lifetime());
+ check();
+
+ const auto submit = [=] {
+ if (!state->valid.current() || state->loading.current()) {
+ return;
+ }
+ auto result = makeResult();
+ if (!result.message && !result.pill) {
+ result.pill = currentLink();
+ }
+ const auto done = args.done;
+ box->closeBox();
+ done(std::move(result));
+ };
+ const auto button = box->addButton(
+ (editing
+ ? tr::lng_settings_save()
+ : tr::lng_formatting_link_create()),
+ submit);
+ rpl::combine(
+ state->valid.value(),
+ state->loading.value()
+ ) | rpl::on_next([=](bool valid, bool loading) {
+ button->setDisabled(!valid || loading);
+ }, button->lifetime());
+ box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
+
+ rpl::merge(
+ url->submits() | rpl::to_empty,
+ name->submits() | rpl::to_empty
+ ) | rpl::on_next(submit, box->lifetime());
+ });
+}
+
+} // namespace Editor
diff --git a/Telegram/SourceFiles/editor/editor_link_box.h b/Telegram/SourceFiles/editor/editor_link_box.h
new file mode 100644
index 0000000000..ddfe7e7fb8
--- /dev/null
+++ b/Telegram/SourceFiles/editor/editor_link_box.h
@@ -0,0 +1,37 @@
+/*
+This file is part of Telegram Desktop,
+the official desktop application for the Telegram messaging service.
+
+For license and copyright information please follow this link:
+https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
+*/
+#pragma once
+
+#include "editor/editor_message_source.h"
+
+namespace ChatHelpers {
+class Show;
+} // namespace ChatHelpers
+
+namespace Ui {
+class BoxContent;
+} // namespace Ui
+
+namespace Editor {
+
+struct LinkBoxResult {
+ std::shared_ptr message;
+ std::optional pill;
+ bool dark = false;
+};
+
+struct LinkBoxArgs {
+ std::shared_ptr show;
+ QString url;
+ std::optional editing;
+ Fn done;
+};
+
+[[nodiscard]] object_ptr LinkBox(LinkBoxArgs &&args);
+
+} // namespace Editor
diff --git a/Telegram/SourceFiles/editor/editor_link_pill.cpp b/Telegram/SourceFiles/editor/editor_link_pill.cpp
new file mode 100644
index 0000000000..9f53885dff
--- /dev/null
+++ b/Telegram/SourceFiles/editor/editor_link_pill.cpp
@@ -0,0 +1,135 @@
+/*
+This file is part of Telegram Desktop,
+the official desktop application for the Telegram messaging service.
+
+For license and copyright information please follow this link:
+https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
+*/
+#include "editor/editor_link_pill.h"
+
+#include "ui/painter.h"
+#include "styles/style_editor.h"
+
+namespace Editor {
+namespace {
+
+constexpr auto kDpPerWidth = 360.;
+constexpr auto kPaddingLeft = 4.;
+constexpr auto kPaddingTop = 4.33;
+constexpr auto kPaddingRight = 7.66;
+constexpr auto kPaddingBottom = 3.;
+constexpr auto kIconSize = 30.;
+constexpr auto kIconPadding = 3.25;
+constexpr auto kFontSize = 24.;
+constexpr auto kRadiusRatio = 0.2;
+constexpr auto kSemiTransparentAlpha = 0x4C;
+
+[[nodiscard]] QString WithoutScheme(const QString &url) {
+ auto result = url;
+ const auto scheme = result.indexOf(u"://"_q);
+ if (scheme >= 0) {
+ result = result.mid(scheme + 3);
+ }
+ while (result.endsWith('/')) {
+ result.chop(1);
+ }
+ return result;
+}
+
+} // namespace
+
+LinkPill::LinkPill(const LinkPreview &link, float64 density, int maxWidth)
+: _density(density)
+, _font(
+ int(base::SafeRound(kFontSize * density)),
+ style::FontFlag::Bold,
+ QString()) {
+ const auto text = link.name.isEmpty()
+ ? WithoutScheme(link.url).toUpper()
+ : link.name;
+ const auto fixed = (kPaddingLeft
+ + kIconSize
+ + kIconPadding
+ + kPaddingRight) * _density;
+ const auto maxTextWidth = std::max(maxWidth - int(fixed), 1);
+ _text = _font->elided(text, maxTextWidth);
+ _textWidth = _font->width(_text);
+ _size = QSizeF(
+ fixed + _textWidth,
+ (kPaddingTop + kPaddingBottom) * _density
+ + std::max(kIconSize * _density, float64(_font->height)));
+ updateIcon();
+}
+
+QSizeF LinkPill::size() const {
+ return _size;
+}
+
+LinkStyle LinkPill::style() const {
+ return _style;
+}
+
+void LinkPill::setStyle(LinkStyle style) {
+ _style = style;
+ updateIcon();
+}
+
+QColor LinkPill::background() const {
+ switch (_style) {
+ case LinkStyle::Framed:
+ return QColor(255, 255, 255);
+ case LinkStyle::SemiTransparent:
+ return QColor(0, 0, 0, kSemiTransparentAlpha);
+ case LinkStyle::Opaque:
+ return QColor(0, 0, 0);
+ }
+ Unexpected("Link style in LinkPill::background.");
+}
+
+QColor LinkPill::foreground() const {
+ return (_style == LinkStyle::Framed)
+ ? QColor(0x33, 0x91, 0xD4)
+ : QColor(255, 255, 255);
+}
+
+float64 LinkPill::DensityFor(int canvasWidth) {
+ return canvasWidth / kDpPerWidth;
+}
+
+void LinkPill::updateIcon() {
+ _icon = st::photoEditorLinkPillIcon.instance(
+ foreground(),
+ style::kScaleMax);
+}
+
+void LinkPill::paint(QPainter &p, QPointF origin, float64 scale) const {
+ p.save();
+ auto hq = PainterHighQualityEnabler(p);
+ p.translate(origin);
+ p.scale(scale, scale);
+
+ const auto pill = QRectF(QPointF(), _size);
+ const auto radius = pill.height() * kRadiusRatio;
+ p.setPen(Qt::NoPen);
+ p.setBrush(background());
+ p.drawRoundedRect(pill, radius, radius);
+
+ const auto iconSide = kIconSize * _density;
+ const auto icon = QRectF(
+ kPaddingLeft * _density,
+ (pill.height() - iconSide) / 2.,
+ iconSide,
+ iconSide);
+ p.drawImage(icon, _icon);
+
+ p.setFont(_font);
+ p.setPen(foreground());
+ p.drawText(
+ QPointF(
+ icon.x() + icon.width() + kIconPadding * _density,
+ (pill.height() - _font->height) / 2. + _font->ascent),
+ _text);
+ p.restore();
+}
+
+} // namespace Editor
diff --git a/Telegram/SourceFiles/editor/editor_link_pill.h b/Telegram/SourceFiles/editor/editor_link_pill.h
new file mode 100644
index 0000000000..66f2427d60
--- /dev/null
+++ b/Telegram/SourceFiles/editor/editor_link_pill.h
@@ -0,0 +1,58 @@
+/*
+This file is part of Telegram Desktop,
+the official desktop application for the Telegram messaging service.
+
+For license and copyright information please follow this link:
+https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
+*/
+#pragma once
+
+namespace Editor {
+
+struct LinkPreview {
+ QString url;
+ QString name;
+ bool captionAbove = true;
+ bool largePhoto = false;
+ bool preview = true;
+ bool dark = false;
+
+ friend inline bool operator==(
+ const LinkPreview &,
+ const LinkPreview &) = default;
+};
+
+enum class LinkStyle : uchar {
+ Framed,
+ SemiTransparent,
+ Opaque,
+};
+
+class LinkPill final {
+public:
+ LinkPill(const LinkPreview &link, float64 density, int maxWidth);
+
+ [[nodiscard]] static float64 DensityFor(int canvasWidth);
+
+ [[nodiscard]] QSizeF size() const;
+ [[nodiscard]] LinkStyle style() const;
+ void setStyle(LinkStyle style);
+
+ void paint(QPainter &p, QPointF origin, float64 scale) const;
+
+private:
+ [[nodiscard]] QColor background() const;
+ [[nodiscard]] QColor foreground() const;
+ void updateIcon();
+
+ float64 _density = 1.;
+ style::font _font;
+ QString _text;
+ int _textWidth = 0;
+ QSizeF _size;
+ QImage _icon;
+ LinkStyle _style = LinkStyle::Framed;
+
+};
+
+} // namespace Editor
diff --git a/Telegram/SourceFiles/editor/editor_message_render.cpp b/Telegram/SourceFiles/editor/editor_message_render.cpp
new file mode 100644
index 0000000000..efcf2d78a4
--- /dev/null
+++ b/Telegram/SourceFiles/editor/editor_message_render.cpp
@@ -0,0 +1,397 @@
+/*
+This file is part of Telegram Desktop,
+the official desktop application for the Telegram messaging service.
+
+For license and copyright information please follow this link:
+https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
+*/
+#include "editor/editor_message_render.h"
+
+#include "data/data_cloud_file.h"
+#include "data/data_document.h"
+#include "data/data_peer.h"
+#include "data/data_session.h"
+#include "editor/editor_message_source.h"
+#include "editor/editor_message_video.h"
+#include "history/history_item.h"
+#include "history/history_item_components.h"
+#include "history/view/history_view_element.h"
+#include "history/view/media/history_view_media.h"
+#include "main/main_session.h"
+#include "ui/cached_round_corners.h"
+#include "ui/chat/chat_style.h"
+#include "ui/chat/chat_theme.h"
+#include "ui/effects/path_shift_gradient.h"
+#include "ui/empty_userpic.h"
+#include "ui/painter.h"
+#include "ui/rect.h"
+#include "window/themes/window_theme.h"
+#include "window/themes/window_themes_embedded.h"
+#include "styles/style_chat.h"
+
+namespace Editor {
+namespace {
+
+[[nodiscard]] QRect OpaqueBounds(const QImage &image) {
+ Expects(image.format() == QImage::Format_ARGB32_Premultiplied);
+
+ const auto width = image.width();
+ const auto height = image.height();
+ const auto rowHasContent = [&](int y) {
+ const auto row = reinterpret_cast(
+ image.constScanLine(y));
+ for (auto x = 0; x != width; ++x) {
+ if (row[x]) {
+ return true;
+ }
+ }
+ return false;
+ };
+ auto top = 0;
+ while (top != height && !rowHasContent(top)) {
+ ++top;
+ }
+ if (top == height) {
+ return QRect();
+ }
+ auto bottom = height - 1;
+ while (bottom > top && !rowHasContent(bottom)) {
+ --bottom;
+ }
+ auto left = width;
+ auto right = -1;
+ for (auto y = top; y <= bottom; ++y) {
+ const auto row = reinterpret_cast(
+ image.constScanLine(y));
+ auto x = 0;
+ while (x < left && !row[x]) {
+ ++x;
+ }
+ left = std::min(left, x);
+ x = width - 1;
+ while (x > right && !row[x]) {
+ --x;
+ }
+ right = std::max(right, x);
+ }
+ return QRect(left, top, right - left + 1, bottom - top + 1);
+}
+
+[[nodiscard]] std::unique_ptr MakeChatTheme(
+ std::optional dark,
+ rpl::lifetime &lifetime) {
+ if (!dark) {
+ return Window::Theme::DefaultChatThemeOn(lifetime);
+ }
+ return std::make_unique(Ui::ChatThemeDescriptor{
+ .preparePalette = Window::Theme::PreparePaletteCallback(
+ *dark,
+ std::nullopt),
+ .basedOnDark = *dark,
+ });
+}
+
+[[nodiscard]] float64 CornerRadius(
+ Ui::BubbleCornerRounding corner,
+ int ratio) {
+ using Corner = Ui::BubbleCornerRounding;
+ using Radius = Ui::CachedCornerRadius;
+ return (corner == Corner::Small)
+ ? Ui::CachedCornerRadiusValue(Radius::BubbleSmall) * ratio
+ : (corner == Corner::Large)
+ ? Ui::CachedCornerRadiusValue(Radius::BubbleLarge) * ratio
+ : 0.;
+}
+
+[[nodiscard]] QPainterPath HolePath(
+ QRect rect,
+ Ui::BubbleRounding rounding,
+ int ratio) {
+ const auto topLeft = CornerRadius(rounding.topLeft, ratio);
+ const auto topRight = CornerRadius(rounding.topRight, ratio);
+ const auto bottomLeft = CornerRadius(rounding.bottomLeft, ratio);
+ const auto bottomRight = CornerRadius(rounding.bottomRight, ratio);
+ const auto left = float64(rect.x());
+ const auto top = float64(rect.y());
+ const auto right = float64(rect.x() + rect.width());
+ const auto bottom = float64(rect.y() + rect.height());
+ const auto corner = [](float64 x, float64 y, float64 radius) {
+ return QRectF(x, y, 2 * radius, 2 * radius);
+ };
+ auto result = QPainterPath();
+ result.moveTo(left + topLeft, top);
+ result.lineTo(right - topRight, top);
+ result.arcTo(corner(right - 2 * topRight, top, topRight), 90, -90);
+ result.lineTo(right, bottom - bottomRight);
+ result.arcTo(
+ corner(
+ right - 2 * bottomRight,
+ bottom - 2 * bottomRight,
+ bottomRight),
+ 0,
+ -90);
+ result.lineTo(left + bottomLeft, bottom);
+ result.arcTo(corner(left, bottom - 2 * bottomLeft, bottomLeft), 270, -90);
+ result.lineTo(left, top + topLeft);
+ result.arcTo(corner(left, top, topLeft), 180, -90);
+ result.closeSubpath();
+ return result;
+}
+
+} // namespace
+
+class MessageRenderer::Delegate final
+ : public HistoryView::DefaultElementDelegate {
+public:
+ explicit Delegate(not_null gradient)
+ : _gradient(gradient) {
+ }
+
+private:
+ HistoryView::Context elementContext() override {
+ return HistoryView::Context::MediaEditor;
+ }
+ bool elementAnimationsPaused() override {
+ return true;
+ }
+ not_null elementPathShiftGradient() override {
+ return _gradient;
+ }
+
+ const not_null _gradient;
+
+};
+
+MessageRenderer::MessageRenderer(std::shared_ptr source)
+: _source(std::move(source))
+, _theme(Window::Theme::DefaultChatThemeOn(_lifetime))
+, _style(
+ std::make_unique(
+ _source->session().colorIndicesValue()))
+, _pathGradient(
+ HistoryView::MakePathShiftGradient(_style.get(), [=] { repaint(); }))
+, _delegate(std::make_unique(_pathGradient.get())) {
+ _style->apply(_theme.get());
+
+ const auto session = &_source->session();
+ const auto data = &session->data();
+ data->viewRepaintRequest(
+ ) | rpl::filter([=](const Data::RequestViewRepaint &request) {
+ return (request.view == _element.get());
+ }) | rpl::on_next([=] {
+ repaint();
+ }, _lifetime);
+
+ data->viewResizeRequest(
+ ) | rpl::filter([=](not_null view) {
+ return (view == _element.get());
+ }) | rpl::on_next([=] {
+ _layoutDirty = true;
+ repaint();
+ }, _lifetime);
+
+ data->itemViewRefreshRequest(
+ ) | rpl::filter([=](not_null item) {
+ return (item == _source->item());
+ }) | rpl::on_next([=] {
+ _recreate = true;
+ repaint();
+ }, _lifetime);
+
+ data->itemDataChanges(
+ ) | rpl::filter([=](not_null item) {
+ return (item == _source->item());
+ }) | rpl::on_next([=] {
+ if (_element) {
+ _element->itemDataChanged();
+ _layoutDirty = true;
+ repaint();
+ }
+ }, _lifetime);
+
+ session->downloaderTaskFinished(
+ ) | rpl::on_next([=] {
+ repaint();
+ }, _lifetime);
+
+ style::PaletteChanged(
+ ) | rpl::on_next([=] {
+ _layoutDirty = true;
+ repaint();
+ }, _lifetime);
+
+ _source->removed(
+ ) | rpl::on_next([=] {
+ _element = nullptr;
+ }, _lifetime);
+
+ createView();
+}
+
+MessageRenderer::~MessageRenderer() = default;
+
+void MessageRenderer::setRepaintCallback(Fn callback) {
+ _repaint = std::move(callback);
+}
+
+void MessageRenderer::setDark(std::optional dark) {
+ if (_dark == dark) {
+ return;
+ }
+ _dark = dark;
+ _theme = MakeChatTheme(dark, _lifetime);
+ _style->apply(_theme.get());
+ _layoutDirty = true;
+ repaint();
+}
+
+bool MessageRenderer::ready() const {
+ return (_element != nullptr);
+}
+
+void MessageRenderer::createView() {
+ const auto item = _source->item();
+ _element = item ? item->createView(_delegate.get()) : nullptr;
+ _layoutDirty = true;
+}
+
+void MessageRenderer::repaint() {
+ _base = QImage();
+ if (_repaint) {
+ _repaint();
+ }
+}
+
+void MessageRenderer::layout() {
+ if (base::take(_recreate)) {
+ createView();
+ }
+ if (!_layoutDirty || !_element) {
+ return;
+ }
+ _layoutDirty = false;
+ _element->initDimensions();
+ _width = st::msgMargin.left()
+ + st::msgMaxWidth
+ + st::msgMargin.right()
+ + (_element->hasFromPhoto() ? st::msgPhotoSkip : 0);
+ _element->resizeGetHeight(_width);
+ _base = renderFull(1);
+ _bounds = OpaqueBounds(_base).marginsAdded(Margins(1)) & _base.rect();
+}
+
+QSize MessageRenderer::size() {
+ layout();
+ return _bounds.size();
+}
+
+QRect MessageRenderer::mediaRect() {
+ layout();
+ return _bounds.isEmpty()
+ ? QRect()
+ : elementMediaRect().translated(-_bounds.topLeft());
+}
+
+QRect MessageRenderer::elementMediaRect() const {
+ const auto media = _element ? _element->media() : nullptr;
+ return media
+ ? media->contentRectForReactions().translated(
+ _element->mediaTopLeft())
+ : QRect();
+}
+
+QImage MessageRenderer::videoMask(int ratio) {
+ Expects(ratio > 0);
+
+ layout();
+ const auto video = _source->video();
+ const auto rect = elementMediaRect();
+ if (!video || rect.isEmpty()) {
+ return QImage();
+ }
+ auto mask = QImage(
+ rect.size() * ratio,
+ QImage::Format_ARGB32_Premultiplied);
+ mask.fill(Qt::transparent);
+ auto p = QPainter(&mask);
+ auto hq = PainterHighQualityEnabler(p);
+ p.setPen(Qt::NoPen);
+ p.setBrush(Qt::white);
+ if (video->document()->isVideoMessage()) {
+ p.drawEllipse(mask.rect());
+ } else {
+ p.drawPath(HolePath(
+ mask.rect(),
+ _element->media()->adjustedBubbleRounding(),
+ ratio));
+ }
+ return mask;
+}
+
+QImage MessageRenderer::render(int ratio) {
+ Expects(ratio > 0);
+
+ layout();
+ if (!_element || _bounds.isEmpty()) {
+ return QImage();
+ }
+ const auto full = (ratio == 1 && !_base.isNull())
+ ? _base
+ : renderFull(ratio);
+ const auto bounds = QRect(
+ _bounds.topLeft() * ratio,
+ _bounds.size() * ratio);
+ auto result = full.copy(bounds);
+ result.setDevicePixelRatio(ratio);
+ return result;
+}
+
+QImage MessageRenderer::renderFull(int ratio) {
+ const auto full = QSize(_width, _element->height());
+ auto result = QImage(
+ full * ratio,
+ QImage::Format_ARGB32_Premultiplied);
+ result.setDevicePixelRatio(ratio);
+ result.fill(Qt::transparent);
+
+ auto p = Painter(&result);
+ PainterHighQualityEnabler hq(p);
+ const auto rect = Rect(full);
+ auto context = _theme->preparePaintContext(
+ _style.get(),
+ rect,
+ rect,
+ rect,
+ true);
+ context.outbg = _element->hasOutLayout();
+ _element->draw(p, context);
+ paintUserpic(p, full.width(), full.height());
+ return result;
+}
+
+void MessageRenderer::paintUserpic(Painter &p, int width, int height) {
+ if (!_element->displayFromPhoto()) {
+ return;
+ }
+ const auto item = _element->data();
+ const auto left = st::historyPhotoLeft;
+ const auto top = height - _element->marginBottom() - st::msgPhotoSize;
+ const auto size = st::msgPhotoSize;
+ if (const auto from = _element->displayFrom()) {
+ from->paintUserpicLeft(p, _userpic, left, top, width, size);
+ } else if (const auto info = item->displayHiddenSenderInfo()) {
+ if (info->customUserpic.empty()) {
+ info->emptyUserpic.paintCircle(p, left, top, width, size);
+ } else if (!info->paintCustomUserpic(
+ p,
+ _userpic,
+ left,
+ top,
+ width,
+ size)) {
+ info->customUserpic.load(&_source->session(), item->fullId());
+ }
+ }
+}
+
+} // namespace Editor
diff --git a/Telegram/SourceFiles/editor/editor_message_render.h b/Telegram/SourceFiles/editor/editor_message_render.h
new file mode 100644
index 0000000000..9602786dbe
--- /dev/null
+++ b/Telegram/SourceFiles/editor/editor_message_render.h
@@ -0,0 +1,70 @@
+/*
+This file is part of Telegram Desktop,
+the official desktop application for the Telegram messaging service.
+
+For license and copyright information please follow this link:
+https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
+*/
+#pragma once
+
+#include "ui/userpic_view.h"
+
+class Painter;
+
+namespace HistoryView {
+class Element;
+} // namespace HistoryView
+
+namespace Ui {
+class ChatTheme;
+class ChatStyle;
+class PathShiftGradient;
+} // namespace Ui
+
+namespace Editor {
+
+class MessageSource;
+
+class MessageRenderer final {
+public:
+ explicit MessageRenderer(std::shared_ptr source);
+ ~MessageRenderer();
+
+ void setRepaintCallback(Fn callback);
+ void setDark(std::optional dark);
+
+ [[nodiscard]] bool ready() const;
+ [[nodiscard]] QSize size();
+ [[nodiscard]] QRect mediaRect();
+ [[nodiscard]] QImage videoMask(int ratio);
+ [[nodiscard]] QImage render(int ratio);
+
+private:
+ class Delegate;
+
+ void createView();
+ void layout();
+ void repaint();
+ [[nodiscard]] QImage renderFull(int ratio);
+ [[nodiscard]] QRect elementMediaRect() const;
+ void paintUserpic(Painter &p, int width, int height);
+
+ rpl::lifetime _lifetime;
+ const std::shared_ptr _source;
+ std::unique_ptr _theme;
+ const std::unique_ptr _style;
+ const std::unique_ptr _pathGradient;
+ const std::unique_ptr _delegate;
+ std::unique_ptr _element;
+ Fn _repaint;
+ Ui::PeerUserpicView _userpic;
+ QImage _base;
+ QRect _bounds;
+ std::optional