Implement nice expandable story caption viewer.

This commit is contained in:
John Preston 2023-07-25 17:37:56 +04:00
parent 8cc90c3373
commit 5aa6102903
11 changed files with 259 additions and 137 deletions

View File

@ -3846,6 +3846,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_stories_click_to_view" = "Click here to view updates from {users}.";
"lng_stories_click_to_view_and_one" = "{accumulated}, {user}";
"lng_stories_click_to_view_and_last" = "{accumulated} and {user}";
"lng_stories_show_more" = "Show more";
"lng_stories_my_title" = "Saved Stories";
"lng_stories_archive_button" = "Stories Archive";

View File

@ -147,6 +147,7 @@ void HiddenUrlClickHandler::Open(QString url, QVariant context) {
my.show->showBox(std::move(box));
} else if (use) {
use->show(std::move(box));
use->activate();
}
} else {
open();

View File

@ -7,79 +7,168 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "media/stories/media_stories_caption_full_view.h"
#include "base/event_filter.h"
#include "core/ui_integration.h"
#include "ui/widgets/scroll_area.h"
#include "chat_helpers/compose/compose_show.h"
#include "media/stories/media_stories_controller.h"
#include "media/stories/media_stories_view.h"
#include "ui/widgets/elastic_scroll.h"
#include "ui/widgets/labels.h"
#include "ui/click_handler.h"
#include "styles/style_media_view.h"
namespace Media::Stories {
CaptionFullView::CaptionFullView(
not_null<Ui::RpWidget*> parent,
not_null<Main::Session*> session,
const TextWithEntities &text,
Fn<void()> close)
: RpWidget(parent)
, _scroll(std::make_unique<Ui::ScrollArea>((RpWidget*)this))
, _text(_scroll->setOwnedWidget(
CaptionFullView::CaptionFullView(not_null<Controller*> controller)
: _controller(controller)
, _scroll(std::make_unique<Ui::ElasticScroll>(controller->wrap()))
, _wrap(_scroll->setOwnedWidget(
object_ptr<Ui::PaddingWrap<Ui::FlatLabel>>(
_scroll.get(),
object_ptr<Ui::FlatLabel>(_scroll.get(), st::storiesCaptionFull),
st::mediaviewCaptionPadding))->entity())
, _close(std::move(close))
, _background(st::storiesRadius, st::mediaviewCaptionBg) {
_text->setMarkedText(text, Core::MarkedTextContext{
.session = session,
st::mediaviewCaptionPadding)))
, _text(_wrap->entity()) {
_text->setMarkedText(controller->captionText(), Core::MarkedTextContext{
.session = &controller->uiShow()->session(),
.customEmojiRepaint = [=] { _text->update(); },
});
parent->sizeValue() | rpl::start_with_next([=](QSize size) {
setGeometry(QRect(QPoint(), size));
}, lifetime());
startAnimation();
_controller->layoutValue(
) | rpl::start_with_next([=](const Layout &layout) {
if (_outer != layout.content) {
const auto skip = layout.header.y()
+ layout.header.height()
- layout.content.y();
_outer = layout.content.marginsRemoved({ 0, skip, 0, 0 });
updateGeometry();
}
}, _scroll->lifetime());
show();
setFocus();
const auto filter = [=](not_null<QEvent*> e) {
const auto mouse = [&] {
return static_cast<QMouseEvent*>(e.get());
};
const auto type = e->type();
if (type == QEvent::MouseButtonPress
&& mouse()->button() == Qt::LeftButton
&& !ClickHandler::getActive()) {
_down = true;
} else if (type == QEvent::MouseButtonRelease && _down) {
_down = false;
if (!ClickHandler::getPressed()) {
close();
}
} else if (type == QEvent::KeyPress
&& static_cast<QKeyEvent*>(e.get())->key() == Qt::Key_Escape) {
close();
return base::EventFilterResult::Cancel;
}
return base::EventFilterResult::Continue;
};
base::install_event_filter(_text.get(), filter);
base::install_event_filter(_wrap.get(), filter);
using Type = Ui::ElasticScroll::OverscrollType;
rpl::combine(
_scroll->positionValue(),
_scroll->movementValue()
) | rpl::filter([=] {
return !_closing;
}) | rpl::start_with_next([=](
Ui::ElasticScrollPosition position,
Ui::ElasticScrollMovement movement) {
const auto overscrollTop = std::max(-position.overscroll, 0);
using Phase = Ui::ElasticScrollMovement;
if (movement == Phase::Progress) {
if (overscrollTop > 0) {
_pulling = true;
} else {
_pulling = false;
}
} else if (_pulling
&& (movement == Phase::Momentum
|| movement == Phase::Returning)) {
_pulling = false;
if (overscrollTop > st::storiesCaptionPullThreshold) {
_closingTopAdded = overscrollTop;
_scroll->setOverscrollTypes(Type::None, Type::Real);
close();
updateGeometry();
}
}
}, _scroll->lifetime());
_scroll->show();
_scroll->setOverscrollBg(QColor(0, 0, 0, 0));
_scroll->setOverscrollTypes(Type::Real, Type::Real);
_text->show();
_text->setFocus();
}
CaptionFullView::~CaptionFullView() = default;
void CaptionFullView::paintEvent(QPaintEvent *e) {
auto p = QPainter(this);
_background.paint(p, _scroll->geometry());
_background.paint(p, _scroll->geometry());
bool CaptionFullView::closing() const {
return _closing;
}
void CaptionFullView::resizeEvent(QResizeEvent *e) {
const auto wanted = _text->naturalWidth();
bool CaptionFullView::focused() const {
return Ui::InFocusChain(_scroll.get());
}
void CaptionFullView::close() {
if (_closing) {
return;
}
_closing = true;
_controller->captionClosing();
startAnimation();
}
void CaptionFullView::updateGeometry() {
if (_outer.isEmpty()) {
return;
}
const auto lineHeight = st::mediaviewCaptionStyle.font->height;
const auto padding = st::mediaviewCaptionPadding;
const auto margin = st::mediaviewCaptionMargin * 2;
const auto available = (rect() - padding).width()
- (margin.width() * 2);
const auto use = std::min(wanted, available);
_text->resizeToWidth(use);
const auto fullw = use + padding.left() + padding.right();
const auto fullh = std::min(
_text->height() + padding.top() + padding.bottom(),
height() - (margin.height() * 2));
const auto left = (width() - fullw) / 2;
const auto top = (height() - fullh) / 2;
_scroll->setGeometry(left, top, fullw, fullh);
}
void CaptionFullView::keyPressEvent(QKeyEvent *e) {
if (e->key() == Qt::Key_Escape) {
if (const auto onstack = _close) {
onstack();
}
_text->resizeToWidth(_outer.width() - padding.left() - padding.right());
const auto add = padding.top() + padding.bottom();
const auto maxShownHeight = lineHeight * kMaxShownCaptionLines;
const auto shownHeight = (_text->height() > maxShownHeight)
? (lineHeight * kCollapsedCaptionLines)
: _text->height();
const auto collapsedHeight = shownHeight + add;
const auto addedToBottom = lineHeight;
const auto expandedHeight = _text->height() + add + addedToBottom;
const auto fullHeight = std::min(expandedHeight, _outer.height());
const auto shown = _animation.value(_closing ? 0. : 1.);
const auto height = (_closing || _animation.animating())
? anim::interpolate(collapsedHeight, fullHeight, shown)
: _outer.height();
const auto added = anim::interpolate(0, _closingTopAdded, shown);
const auto bottomPadding = anim::interpolate(0, addedToBottom, shown);
const auto use = padding + ((_closing || _animation.animating())
? QMargins(0, 0, 0, bottomPadding)
: QMargins(0, height - fullHeight, 0, bottomPadding));
_wrap->setPadding(use);
_scroll->setGeometry(
_outer.x(),
added + _outer.y() + _outer.height() - height,
_outer.width(),
std::max(height - added, 0));
if (_closing && !_animation.animating()) {
_controller->captionClosed();
}
}
void CaptionFullView::mousePressEvent(QMouseEvent *e) {
if (e->button() == Qt::LeftButton) {
if (const auto onstack = _close) {
onstack();
}
}
void CaptionFullView::startAnimation() {
_animation.start(
[=] { updateGeometry(); },
_closing ? 1. : 0.,
_closing ? 0. : 1.,
st::fadeWrapDuration,
anim::sineInOut);
}
} // namespace Media::Stories

View File

@ -7,8 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "ui/rp_widget.h"
#include "ui/round_rect.h"
#include "ui/effects/animations.h"
namespace Main {
class Session;
@ -16,30 +15,38 @@ class Session;
namespace Ui {
class FlatLabel;
class ScrollArea;
class ElasticScroll;
template <typename Widget>
class PaddingWrap;
} // namespace Ui
namespace Media::Stories {
class CaptionFullView final : private Ui::RpWidget {
class Controller;
class CaptionFullView final {
public:
CaptionFullView(
not_null<Ui::RpWidget*> parent,
not_null<Main::Session*> session,
const TextWithEntities &text,
Fn<void()> close);
explicit CaptionFullView(not_null<Controller*> controller);
~CaptionFullView();
private:
void paintEvent(QPaintEvent *e) override;
void resizeEvent(QResizeEvent *e) override;
void keyPressEvent(QKeyEvent *e) override;
void mousePressEvent(QMouseEvent *e) override;
void close();
[[nodiscard]] bool closing() const;
[[nodiscard]] bool focused() const;
std::unique_ptr<Ui::ScrollArea> _scroll;
private:
void updateGeometry();
void startAnimation();
const not_null<Controller*> _controller;
const std::unique_ptr<Ui::ElasticScroll> _scroll;
const not_null<Ui::PaddingWrap<Ui::FlatLabel>*> _wrap;
const not_null<Ui::FlatLabel*> _text;
Fn<void()> _close;
Ui::RoundRect _background;
Ui::Animations::Simple _animation;
QRect _outer;
int _closingTopAdded = 0;
bool _pulling = false;
bool _closing = false;
bool _down = false;
};

View File

@ -280,9 +280,6 @@ Controller::Controller(not_null<Delegate*> delegate)
_1 || _2
) | rpl::distinct_until_changed(
) | rpl::start_with_next([=](bool active) {
if (active) {
_captionFullView = nullptr;
}
_replyActive = active;
updateContentFaded();
}, _lifetime);
@ -355,7 +352,8 @@ Controller::~Controller() {
}
void Controller::updateContentFaded() {
const auto faded = _replyActive || _captionFullView || _captionExpanded;
const auto faded = _replyActive
|| (_captionFullView && !_captionFullView->closing());
if (_contentFaded == faded) {
return;
}
@ -584,26 +582,31 @@ TextWithEntities Controller::captionText() const {
return _captionText;
}
void Controller::setCaptionExpanded(bool expanded) {
if (_captionExpanded == expanded) {
return;
}
_captionExpanded = expanded;
updateContentFaded();
bool Controller::skipCaption() const {
return _captionFullView != nullptr;
}
void Controller::showFullCaption() {
if (_captionText.empty()) {
return;
}
_captionFullView = std::make_unique<CaptionFullView>(
wrap(),
&_delegate->storiesShow()->session(),
_captionText,
[=] { _captionFullView = nullptr; updateContentFaded(); });
_captionFullView = std::make_unique<CaptionFullView>(this);
updateContentFaded();
}
void Controller::captionClosing() {
updateContentFaded();
}
void Controller::captionClosed() {
if (!_captionFullView) {
return;
} else if (_captionFullView->focused()) {
_wrap->setFocus();
}
_captionFullView = nullptr;
}
std::shared_ptr<ChatHelpers::Show> Controller::uiShow() const {
return _delegate->storiesShow();
}
@ -820,9 +823,8 @@ void Controller::show(
_slider->raise();
}
captionClosed();
_captionText = story->caption();
_captionFullView = nullptr;
_captionExpanded = false;
_contentFaded = false;
_contentFadeAnimation.stop();
const auto document = story->document();
@ -972,17 +974,13 @@ void Controller::updatePlayingAllowed() {
&& _windowActive
&& !_paused
&& !_replyActive
&& !_captionFullView
&& !_captionExpanded
&& (!_captionFullView || _captionFullView->closing())
&& !_layerShown
&& !_menuShown
&& !_tooltipShown);
}
void Controller::setPlayingAllowed(bool allowed) {
if (allowed) {
_captionFullView = nullptr;
}
if (_photoPlayback) {
_photoPlayback->togglePaused(!allowed);
} else {
@ -1192,6 +1190,9 @@ void Controller::togglePaused(bool paused) {
void Controller::contentPressed(bool pressed) {
togglePaused(pressed);
if (_captionFullView) {
_captionFullView->close();
}
if (pressed) {
_reactions->collapse();
}

View File

@ -122,8 +122,10 @@ public:
[[nodiscard]] bool closeByClickAt(QPoint position) const;
[[nodiscard]] Data::FileOrigin fileOrigin() const;
[[nodiscard]] TextWithEntities captionText() const;
void setCaptionExpanded(bool expanded);
[[nodiscard]] bool skipCaption() const;
void showFullCaption();
void captionClosing();
void captionClosed();
[[nodiscard]] std::shared_ptr<ChatHelpers::Show> uiShow() const;
[[nodiscard]] auto stickerOrEmojiChosen() const
@ -250,7 +252,6 @@ private:
Ui::Animations::Simple _contentFadeAnimation;
bool _contentFaded = false;
bool _captionExpanded = false;
bool _windowActive = false;
bool _replyFocused = false;
bool _replyActive = false;

View File

@ -123,8 +123,8 @@ TextWithEntities View::captionText() const {
return _controller->captionText();
}
void View::setCaptionExpanded(bool expanded) {
_controller->setCaptionExpanded(expanded);
bool View::skipCaption() const {
return _controller->skipCaption();
}
void View::showFullCaption() {

View File

@ -48,6 +48,9 @@ struct SiblingView {
}
};
inline constexpr auto kCollapsedCaptionLines = 2;
inline constexpr auto kMaxShownCaptionLines = 4;
class View final {
public:
explicit View(not_null<Delegate*> delegate);
@ -64,7 +67,7 @@ public:
[[nodiscard]] SiblingView sibling(SiblingType type) const;
[[nodiscard]] Data::FileOrigin fileOrigin() const;
[[nodiscard]] TextWithEntities captionText() const;
void setCaptionExpanded(bool expanded);
[[nodiscard]] bool skipCaption() const;
void showFullCaption();
void updatePlayback(const Player::TrackState &state);

View File

@ -445,7 +445,8 @@ storiesSideSkip: 145px;
storiesCaptionFull: FlatLabel(defaultFlatLabel) {
style: mediaviewCaptionStyle;
textFg: mediaviewCaptionFg;
minWidth: 360px;
palette: mediaviewTextPalette;
minWidth: 36px;
}
storiesComposeBg: groupCallMembersBg;
storiesComposeBgOver: groupCallMembersBgOver;
@ -908,3 +909,6 @@ storiesVolumeSlider: MediaSlider {
storiesInfoTooltipLabel: defaultImportantTooltipLabel;
storiesInfoTooltip: defaultImportantTooltip;
storiesInfoTooltipMaxWidth: 360px;
storiesCaptionPullThreshold: 50px;
storiesShowMorePadding: margins(6px, 4px, 6px, 4px);
storiesShowMoreFont: semiboldFont;

View File

@ -1383,6 +1383,10 @@ void OverlayWidget::resizeCenteredControls() {
}
void OverlayWidget::refreshCaptionGeometry() {
_caption.updateSkipBlock(0, 0);
_captionShowMoreWidth = 0;
_captionSkipBlockWidth = 0;
if (_caption.isEmpty()) {
_captionRect = QRect();
return;
@ -1408,32 +1412,28 @@ void OverlayWidget::refreshCaptionGeometry() {
- st::mediaviewCaptionPadding.left()
- st::mediaviewCaptionPadding.right()),
_caption.maxWidth());
const auto maxExpandedOuterHeight = (_stories
? (_h - st::storiesShadowTop.height())
: _maxUsedHeight);
const auto maxCollapsedOuterHeight = !_stories
? (_maxUsedHeight / 4)
: (_h / 3);
const auto maxExpandedHeight = maxExpandedOuterHeight
- st::mediaviewCaptionPadding.top()
- st::mediaviewCaptionPadding.bottom();
const auto maxCollapsedHeight = maxCollapsedOuterHeight
- st::mediaviewCaptionPadding.top()
- st::mediaviewCaptionPadding.bottom();
const auto lineHeight = st::mediaviewCaptionStyle.font->height;
const auto wantedHeight = _caption.countHeight(captionWidth);
const auto maxHeight = _captionExpanded
? maxExpandedHeight
: maxCollapsedHeight;
const auto maxHeight = !_stories
? (_maxUsedHeight / 4)
: (wantedHeight > lineHeight * Stories::kMaxShownCaptionLines)
? (lineHeight * Stories::kCollapsedCaptionLines)
: wantedHeight;
const auto captionHeight = std::min(
wantedHeight,
(maxHeight / lineHeight) * lineHeight);
_captionFitsIfExpanded = _stories
&& (wantedHeight <= maxExpandedHeight);
_captionShownFull = (wantedHeight <= maxCollapsedHeight);
if (_captionShownFull && _captionExpanded && _stories) {
_captionExpanded = false;
_stories->setCaptionExpanded(false);
if (_stories && captionHeight < wantedHeight) {
const auto padding = st::storiesShowMorePadding;
_captionShowMoreWidth = st::storiesShowMoreFont->width(
tr::lng_stories_show_more(tr::now));
_captionSkipBlockWidth = _captionShowMoreWidth
+ padding.left()
+ padding.right()
- st::mediaviewCaptionPadding.right();
const auto skiph = st::storiesShowMoreFont->height
+ padding.bottom()
- st::mediaviewCaptionPadding.bottom();
_caption.updateSkipBlock(_captionSkipBlockWidth, skiph);
}
_captionRect = QRect(
(width() - captionWidth) / 2,
@ -3495,7 +3495,6 @@ void OverlayWidget::updateThemePreviewGeometry() {
}
void OverlayWidget::displayFinished(anim::activation activation) {
_captionExpanded = _captionFitsIfExpanded = _captionShownFull = false;
updateControls();
if (isHidden()) {
_helper->beforeShow(_fullscreen);
@ -4502,7 +4501,8 @@ void OverlayWidget::paint(not_null<Renderer*> renderer) {
if (!_stories) {
renderer->paintFooter(footerGeometry(), opacity);
}
if (!_caption.isEmpty()) {
if (!_caption.isEmpty()
&& (!_stories || !_stories->skipCaption())) {
renderer->paintCaption(captionGeometry(), opacity);
}
if (_groupThumbs) {
@ -4912,6 +4912,7 @@ void OverlayWidget::paintCaptionContent(
}
if (inner.intersects(clip)) {
p.setPen(st::mediaviewCaptionFg);
const auto lineHeight = st::mediaviewCaptionStyle.font->height;
_caption.draw(p, {
.position = inner.topLeft(),
.availableWidth = inner.width(),
@ -4919,8 +4920,31 @@ void OverlayWidget::paintCaptionContent(
.spoiler = Ui::Text::DefaultSpoilerCache(),
.pausedEmoji = On(PowerSaving::kEmojiChat),
.pausedSpoiler = On(PowerSaving::kChatSpoiler),
.elisionLines = inner.height() / st::mediaviewCaptionStyle.font->height,
.elisionLines = inner.height() / lineHeight,
.elisionRemoveFromEnd = _captionSkipBlockWidth,
});
if (_captionShowMoreWidth > 0) {
const auto padding = st::storiesShowMorePadding;
const auto showMoreLeft = outer.x()
+ outer.width()
- padding.right()
- _captionShowMoreWidth;
const auto showMoreTop = outer.y()
+ outer.height()
- padding.bottom()
- st::storiesShowMoreFont->height;
const auto underline = _captionExpandLink
&& ClickHandler::showAsActive(_captionExpandLink);
p.setFont(underline
? st::storiesShowMoreFont->underline()
: st::storiesShowMoreFont);
p.drawTextLeft(
showMoreLeft,
showMoreTop,
width(),
tr::lng_stories_show_more(tr::now));
}
}
}
@ -5545,9 +5569,13 @@ void OverlayWidget::updateOver(QPoint pos) {
lnk = textState.link;
lnkhost = this;
} else if (_captionRect.contains(pos)) {
auto textState = _caption.getState(pos - _captionRect.topLeft(), _captionRect.width());
auto request = Ui::Text::StateRequestElided();
const auto lineHeight = st::mediaviewCaptionStyle.font->height;
request.lines = _captionRect.height() / lineHeight;
request.removeFromEnd = _captionSkipBlockWidth;
auto textState = _caption.getStateElided(pos - _captionRect.topLeft(), _captionRect.width(), request);
lnk = textState.link;
if (_stories && !_captionShownFull && !lnk) {
if (_stories && !lnk) {
lnk = ensureCaptionExpandLink();
}
lnkhost = this;
@ -5626,19 +5654,7 @@ void OverlayWidget::updateOver(QPoint pos) {
ClickHandlerPtr OverlayWidget::ensureCaptionExpandLink() {
if (!_captionExpandLink) {
const auto toggle = crl::guard(_widget, [=] {
if (!_stories) {
return;
} else if (_captionExpanded) {
_captionExpanded = false;
_stories->setCaptionExpanded(false);
refreshCaptionGeometry();
update();
} else if (_captionFitsIfExpanded) {
_captionExpanded = true;
_stories->setCaptionExpanded(true);
refreshCaptionGeometry();
update();
} else {
if (_stories) {
_stories->showFullCaption();
}
});

View File

@ -587,9 +587,8 @@ private:
Ui::Text::String _caption;
QRect _captionRect;
ClickHandlerPtr _captionExpandLink;
bool _captionShownFull = false;
bool _captionFitsIfExpanded = false;
bool _captionExpanded = false;
int _captionShowMoreWidth = 0;
int _captionSkipBlockWidth = 0;
int _topNotchSize = 0;
int _width = 0;