/* 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 "settings/settings_scale_preview.h" #include "base/platform/base_platform_info.h" #include "base/event_filter.h" #include "data/data_user.h" #include "data/data_peer_values.h" #include "history/history_item_components.h" #include "main/main_session.h" #include "ui/chat/chat_style.h" #include "ui/chat/chat_theme.h" #include "ui/image/image_prepare.h" #include "ui/platform/ui_platform_utility.h" #include "ui/text/text_options.h" #include "ui/widgets/shadow.h" #include "ui/cached_round_corners.h" #include "ui/painter.h" #include "window/themes/window_theme.h" #include "window/section_widget.h" #include "window/window_controller.h" #include "window/window_session_controller.h" #include "styles/style_chat.h" #include #include namespace Settings { namespace { constexpr auto kMinTextWidth = 120; constexpr auto kMaxTextWidth = 320; constexpr auto kMaxTextLines = 3; class Preview final { public: Preview(QWidget *slider, rpl::producer userpic); void toggle(ScalePreviewShow show, int scale, int globalX); private: void init(); void initAsWindow(); void watchParent(); void reparent(); void updateToScale(int scale); void updateGlobalPosition(int globalX); void updateGlobalPosition(); void updateWindowGlobalPosition(QPoint global); void updateOuterPosition(int globalX); [[nodiscard]] QRect adjustByScreenGeometry(QRect geometry) const; void toggleShown(bool shown); void toggleFilter(); void update(); void paint(Painter &p, QRect clip); void paintLayer(Painter &p, QRect clip); void paintInner(Painter &p, QRect clip); void paintUserpic(Painter &p, QRect clip); void paintBubble(Painter &p, QRect clip); void paintContent(Painter &p, QRect clip); void paintReply(Painter &p, QRect clip); void paintMessage(Painter &p, QRect clip); void validateUserpicCache(); void validateBubbleCache(); void validateShadowCache(); [[nodiscard]] int scaled(int value) const; [[nodiscard]] QPoint scaled(QPoint value) const; [[nodiscard]] QMargins scaled(QMargins value) const; [[nodiscard]] style::font scaled( const style::font &value, int size) const; [[nodiscard]] style::QuoteStyle scaled( const style::QuoteStyle &value) const; [[nodiscard]] style::TextStyle scaled( const style::TextStyle &value, int fontSize) const; [[nodiscard]] QImage scaled( const style::icon &icon, const QColor &color) const; Ui::RpWidget _widget; not_null _slider; Ui::ChatTheme _theme; style::TextStyle _nameStyle = st::fwdTextStyle; Ui::Text::String _nameText = { kMaxTextWidth / 3 }; style::TextStyle _textStyle = st::messageTextStyle; Ui::Text::String _replyText = { kMaxTextWidth / 3 }; Ui::Text::String _messageText = { kMaxTextWidth / 3 }; style::Shadow _shadow = st::callShadow; std::array _shadowSides; std::array _shadowCorners; Ui::CornersPixmaps _bubbleCorners; QPixmap _bubbleShadowBottomRight; int _bubbleShadow = 0; int _localShiftLeft = 0; QImage _bubbleTail; QRect _replyRect; QRect _name; QRect _reply; QRect _message; QRect _content; QRect _bubble; QRect _userpic; QRect _inner; QRect _outer; QSize _minOuterSize; QSize _maxOuterSize; QImage _layer, _canvas; QPoint _cursor; std::array _canvasCornerMasks; QImage _userpicOriginal; QImage _userpicImage; int _scale = 0; int _ratio = 0; bool _window = false; Ui::Animations::Simple _shownAnimation; bool _shown = false; std::unique_ptr _filter; std::unique_ptr _parentWatcher; }; [[nodiscard]] bool UseSeparateWindow() { return !Platform::IsWayland() && Ui::Platform::TranslucentWindowsSupported(); } Preview::Preview(QWidget *slider, rpl::producer userpic) : _widget(slider->window()) , _slider(slider) , _ratio(style::DevicePixelRatio()) , _window(UseSeparateWindow()) { std::move(userpic) | rpl::start_with_next([=](QImage &&userpic) { _userpicOriginal = std::move(userpic); if (!_userpicImage.isNull()) { _userpicImage = {}; update(); } }, _widget.lifetime()); watchParent(); init(); } void Preview::watchParent() { const auto parent = _widget.parentWidget(); _parentWatcher.reset(base::install_event_filter(parent, [=]( not_null e) { if (e->type() == QEvent::ParentChange) { if (_widget.window() != parent) { reparent(); } } return base::EventFilterResult::Continue; })); } void Preview::reparent() { if (_widget.window() == &_widget) { // macOS just removes parenting for a _window. _parentWatcher = nullptr; return; } _widget.setParent(_widget.window()); if (_shown) { _widget.show(); updateGlobalPosition(); } watchParent(); } void Preview::toggle(ScalePreviewShow show, int scale, int sliderX) { if (show == ScalePreviewShow::Hide) { toggleShown(false); return; } else if (show == ScalePreviewShow::Update && !_shown) { return; } updateToScale(scale); updateGlobalPosition(sliderX); if (_widget.isHidden()) { Ui::ForceFullRepaintSync(&_widget); } toggleShown(true); } void Preview::toggleShown(bool shown) { if (_shown == shown) { return; } _shown = shown; toggleFilter(); if (_shown) { _widget.show(); } else if (_widget.isHidden()) { _shownAnimation.stop(); return; } const auto callback = [=] { update(); if (!_shown && !_shownAnimation.animating()) { _widget.hide(); } }; _shownAnimation.start( callback, shown ? 0. : 1., shown ? 1. : 0., st::slideWrapDuration); } void Preview::toggleFilter() { if (!_shown) { _filter = nullptr; return; } else if (_filter) { return; } _filter = std::make_unique(); const auto watch = [&](QWidget *widget, const auto &self) -> void { if (!widget) { return; } base::install_event_filter(_filter.get(), widget, [=]( not_null e) { if (e->type() == QEvent::Move || e->type() == QEvent::Resize || e->type() == QEvent::Show || e->type() == QEvent::ShowToParent || e->type() == QEvent::ZOrderChange) { updateGlobalPosition(); } return base::EventFilterResult::Continue; }); if (!_window && widget == _widget.window()) { return; } self(widget->parentWidget(), self); }; watch(_slider, watch); const auto checkDeactivation = [=](Qt::ApplicationState state) { if (state != Qt::ApplicationActive) { toggle(ScalePreviewShow::Hide, 0, 0); } }; QObject::connect( qApp, &QGuiApplication::applicationStateChanged, _filter.get(), checkDeactivation, Qt::QueuedConnection); } void Preview::update() { _widget.update(_outer); } void Preview::init() { const auto background = Window::Theme::Background(); const auto &paper = background->paper(); _theme.setBackground({ .prepared = background->prepared(), .preparedForTiled = background->preparedForTiled(), .gradientForFill = background->gradientForFill(), .colorForFill = background->colorForFill(), .colors = paper.backgroundColors(), .patternOpacity = paper.patternOpacity(), .gradientRotation = paper.gradientRotation(), .isPattern = paper.isPattern(), .tile = background->tile(), }); _widget.paintRequest( ) | rpl::start_with_next([=](QRect clip) { auto p = Painter(&_widget); paint(p, clip); }, _widget.lifetime()); style::PaletteChanged( ) | rpl::start_with_next([=] { _bubbleCorners = {}; _bubbleTail = {}; _bubbleShadowBottomRight = {}; update(); }, _widget.lifetime()); if (_window) { initAsWindow(); updateToScale(style::kScaleMin); _minOuterSize = _outer.size(); updateToScale(style::MaxScaleForRatio(_ratio)); _maxOuterSize = _outer.size(); } } int Preview::scaled(int value) const { return style::ConvertScale(value, _scale); } QPoint Preview::scaled(QPoint value) const { return { scaled(value.x()), scaled(value.y()) }; } QMargins Preview::scaled(QMargins value) const { return { scaled(value.left()), scaled(value.top()), scaled(value.right()), scaled(value.bottom()), }; } style::font Preview::scaled(const style::font &font, int size) const { return style::font(scaled(size), font->flags(), font->family()); } style::QuoteStyle Preview::scaled(const style::QuoteStyle &value) const { return { .icon = value.icon, .scrollable = value.scrollable, }; } style::TextStyle Preview::scaled( const style::TextStyle &value, int fontSize) const { return { .font = scaled(value.font, fontSize), .linkUnderline = value.linkUnderline, .blockquote = scaled(value.blockquote), .pre = scaled(value.pre), }; } QImage Preview::scaled( const style::icon &icon, const QColor &color) const { return icon.instance(color, _scale); } void Preview::updateToScale(int scale) { using style::ConvertScale; if (_scale == scale) { return; } _scale = scale; _nameStyle = scaled(st::fwdTextStyle, 13); _textStyle = scaled(st::messageTextStyle, 13); _textStyle.blockquote.verticalSkip = scaled(4); _textStyle.blockquote.outline = scaled(3); _textStyle.blockquote.outlineShift = scaled(2); _textStyle.blockquote.radius = scaled(5); _textStyle.blockquote.padding = scaled(QMargins{ 10, 2, 20, 2 }); _textStyle.blockquote.iconPosition = scaled(QPoint{ 4, 4 }); _textStyle.pre.verticalSkip = scaled(4); _textStyle.pre.outline = scaled(3); _textStyle.pre.outlineShift = scaled(2); _textStyle.pre.radius = scaled(5); _textStyle.pre.header = scaled(20); _textStyle.pre.headerPosition = scaled(QPoint{ 10, 2 }); _textStyle.pre.padding = scaled(QMargins{ 10, 2, 4, 2 }); _textStyle.pre.iconPosition = scaled(QPoint{ 4, 2 }); _nameText.setText( _nameStyle, u"Bob Harris"_q, Ui::NameTextOptions()); _replyText.setText( _textStyle, u"Good morning!"_q, Ui::ItemTextDefaultOptions()); _messageText.setText( _textStyle, u"Do you know what time it is?"_q, Ui::ItemTextDefaultOptions()); const auto namePosition = QPoint( scaled(11), // st::historyReplyPadding.left() scaled(2)); // st::historyReplyPadding.top() const auto replyPosition = QPoint( scaled(11), // st::historyReplyPadding.left() (scaled(2) // st::historyReplyPadding.top() + _nameStyle.font->height)); // + st::msgServiceNameFont->height const auto paddingRight = scaled(6); // st::historyReplyPadding.right() const auto wantedWidth = std::max({ namePosition.x() + _nameText.maxWidth() + paddingRight, replyPosition.x() + _replyText.maxWidth() + paddingRight, _messageText.maxWidth(), }); const auto minTextWidth = scaled(kMinTextWidth); const auto maxTextWidth = scaled(kMaxTextWidth); const auto messageWidth = std::clamp( wantedWidth, minTextWidth, maxTextWidth); const auto messageHeight = std::min( _messageText.countHeight(maxTextWidth), kMaxTextLines * _textStyle.font->height); _replyRect = QRect( 0, // st::msgReplyBarPos.x(), scaled(2),// st::historyReplyTop messageWidth, (scaled(2) // st::historyReplyPadding.top() + _nameStyle.font->height // + st::msgServiceNameFont->height + _textStyle.font->height // + st::normalFont->height + scaled(2))); // + st::historyReplyPadding.bottom() _name = QRect( _replyRect.topLeft() + namePosition, QSize(messageWidth - namePosition.x(), _nameStyle.font->height)); _reply = QRect( _replyRect.topLeft() + replyPosition, QSize(messageWidth - replyPosition.x(), _textStyle.font->height)); _message = QRect(0, 0, messageWidth, messageHeight); // replyRect.bottom + st::historyReplyBottom; const auto replySkip = _replyRect.y() + _replyRect.height() + scaled(2); _message.moveTop(replySkip); _content = QRect(0, 0, messageWidth, replySkip + messageHeight); const auto msgPadding = scaled(QMargins(13, 7, 13, 8)); // st::msgPadding _bubble = _content.marginsAdded(msgPadding); _content.moveTopLeft(-_bubble.topLeft()); _bubble.moveTopLeft({}); _bubbleShadow = scaled(2); // st::msgShadow _bubbleCorners = {}; _bubbleTail = {}; _bubbleShadowBottomRight = {}; const auto hasUserpic = !_userpicOriginal.isNull(); const auto bubbleMargin = scaled(QMargins(20, 16, 20, 16)); const auto userpicSkip = hasUserpic ? scaled(40) : 0; // st::msgPhotoSkip _inner = _bubble.marginsAdded( bubbleMargin + QMargins(userpicSkip, 0, 0, 0)); _bubble.moveTopLeft(-_inner.topLeft()); _inner.moveTopLeft({}); if (hasUserpic) { const auto userpicSize = scaled(33); // st::msgPhotoSize _userpic = QRect( bubbleMargin.left(), _bubble.y() + _bubble.height() - userpicSize, userpicSize, userpicSize); _userpicImage = {}; } _shadow.extend = scaled(QMargins(9, 8, 9, 10)); // st::callShadow.extend _shadowSides = {}; _shadowCorners = {}; update(); _outer = _inner.marginsAdded(_shadow.extend); _inner.moveTopLeft(-_outer.topLeft()); _outer.moveTopLeft({}); _layer = QImage( _outer.size() * _ratio, QImage::Format_ARGB32_Premultiplied); _layer.setDevicePixelRatio(_ratio); _canvas = QImage( _inner.size() * _ratio, QImage::Format_ARGB32_Premultiplied); _canvas.setDevicePixelRatio(_ratio); _canvas.fill(Qt::transparent); _canvasCornerMasks = Images::CornersMask(scaled(6)); // st::callRadius } void Preview::updateGlobalPosition(int sliderX) { _localShiftLeft = sliderX; if (_window) { updateWindowGlobalPosition(_slider->mapToGlobal(QPoint())); } else { updateGlobalPosition(); } } void Preview::updateGlobalPosition() { if (_window) { const auto global = _slider->mapToGlobal(QPoint()); updateWindowGlobalPosition(global); } else { const auto parent = _widget.parentWidget(); const auto global = Ui::MapFrom(parent, _slider, QPoint()); const auto desiredLeft = global.x() + _localShiftLeft - (_outer.width() / 2); const auto desiredTop = global.y() - _outer.height(); const auto requiredRight = std::min( desiredLeft + _outer.width(), parent->width()); const auto left = std::max( std::min(desiredLeft, requiredRight - _outer.width()), 0); _widget.setGeometry(QRect(QPoint(left, desiredTop), _outer.size())); } _widget.raise(); } void Preview::updateWindowGlobalPosition(QPoint global) { const auto desiredLeft = global.x() - (_minOuterSize.width() / 2); const auto desiredRight = global.x() + _slider->width() + (_maxOuterSize.width() / 2); const auto requiredLeft = desiredRight - _maxOuterSize.width(); const auto left = std::min(desiredLeft, requiredLeft); const auto requiredRight = left + _maxOuterSize.width(); const auto right = std::max(desiredRight, requiredRight); const auto top = global.y() - _maxOuterSize.height(); auto result = QRect(left, top, right - left, _maxOuterSize.height()); _widget.setGeometry(adjustByScreenGeometry(result)); updateOuterPosition(global.x() + _localShiftLeft); } QRect Preview::adjustByScreenGeometry(QRect geometry) const { const auto screen = _slider->screen(); if (!screen) { return geometry; } const auto screenGeometry = screen->availableGeometry(); if (!screenGeometry.intersects(geometry) || screenGeometry.width() < _maxOuterSize.width() || screenGeometry.height() < _maxOuterSize.height()) { return geometry; } const auto edgeLeft = screenGeometry.x(); const auto edgeRight = screenGeometry.x() + screenGeometry.width(); const auto edgedRight = std::min( edgeRight, geometry.x() + geometry.width()); const auto left = std::max( std::min(geometry.x(), edgedRight - _maxOuterSize.width()), edgeLeft); const auto right = std::max(edgedRight, left + _maxOuterSize.width()); return { left, geometry.y(), right - left, geometry.height() }; } void Preview::updateOuterPosition(int globalX) { if (_window) { update(); const auto global = _widget.geometry(); const auto desiredLeft = globalX - (_outer.width() / 2) - global.x(); _outer.moveLeft(std::max( std::min(desiredLeft, global.width() - _outer.width()), 0)); _outer.moveTop(_maxOuterSize.height() - _outer.height()); update(); } } void Preview::paint(Painter &p, QRect clip) { //p.setCompositionMode(QPainter::CompositionMode_Source); //p.fillRect(clip, Qt::transparent); //p.setCompositionMode(QPainter::CompositionMode_SourceOver); const auto outer = clip.intersected(_outer); if (outer.isEmpty()) { return; } const auto local = outer.translated(-_outer.topLeft()); auto q = Painter(&_layer); q.setClipRect(local); paintLayer(q, local); q.end(); const auto shown = _shownAnimation.value(_shown ? 1. : 0.); p.setClipRect(clip); p.setOpacity(shown); auto hq = std::optional(); if (shown < 1.) { const auto middle = _outer.x() + (_outer.width() / 2); const auto bottom = _outer.y() + _outer.height(); const auto scale = 0.3 + shown * 0.7; p.translate(middle, bottom); p.scale(scale, scale); p.translate(-middle, -bottom); hq.emplace(p); } p.drawImage(_outer.topLeft(), _layer); } void Preview::paintLayer(Painter &p, QRect clip) { p.setCompositionMode(QPainter::CompositionMode_Source); validateShadowCache(); Ui::Shadow::paint( p, _inner, _outer.width(), _shadow, _shadowSides, _shadowCorners); const auto inner = clip.intersected(_inner); if (inner.isEmpty()) { return; } const auto local = inner.translated(-_inner.topLeft()); auto q = Painter(&_canvas); q.setClipRect(local); paintInner(q, local); q.end(); _canvas = Images::Round(std::move(_canvas), _canvasCornerMasks); p.setCompositionMode(QPainter::CompositionMode_SourceOver); p.drawImage(_inner.topLeft(), _canvas); } void Preview::paintInner(Painter &p, QRect clip) { Window::SectionWidget::PaintBackground( p, &_theme, QSize(_inner.width(), _inner.width() * 3), clip); paintUserpic(p, clip); p.translate(_bubble.topLeft()); paintBubble(p, clip.translated(-_bubble.topLeft())); } void Preview::paintUserpic(Painter &p, QRect clip) { if (clip.intersected(_userpic).isEmpty()) { return; } validateUserpicCache(); p.drawImage(_userpic.topLeft(), _userpicImage); } void Preview::paintBubble(Painter &p, QRect clip) { validateBubbleCache(); const auto bubble = QRect(QPoint(), _bubble.size()); const auto cornerShadow = _bubbleShadowBottomRight.size() / _bubbleShadowBottomRight.devicePixelRatio(); p.drawPixmap( bubble.width() - cornerShadow.width(), bubble.height() + _bubbleShadow - cornerShadow.height(), _bubbleShadowBottomRight); Ui::FillRoundRect(p, bubble, st::msgInBg, _bubbleCorners); const auto tail = _bubbleTail.size() / _bubbleTail.devicePixelRatio(); p.drawImage(-tail.width(), bubble.height() - tail.height(), _bubbleTail); p.fillRect( -tail.width(), bubble.height(), tail.width() + bubble.width() - cornerShadow.width(), _bubbleShadow, st::msgInShadow); const auto content = clip.intersected(_content); if (content.isEmpty()) { return; } p.translate(_content.topLeft()); const auto local = content.translated(-_content.topLeft()); p.setClipRect(local); paintContent(p, local); } void Preview::paintContent(Painter &p, QRect clip) { paintReply(p, clip); const auto message = clip.intersected(_message); if (message.isEmpty()) { return; } p.translate(_message.topLeft()); const auto local = message.translated(-_message.topLeft()); p.setClipRect(local); paintMessage(p, local); } void Preview::paintReply(Painter &p, QRect clip) { { auto hq = PainterHighQualityEnabler(p); p.setPen(Qt::NoPen); p.setBrush(st::msgInReplyBarColor); const auto outline = _textStyle.blockquote.outline; const auto radius = _textStyle.blockquote.radius; p.setOpacity(Ui::kDefaultOutline1Opacity); p.setClipRect( _replyRect.x(), _replyRect.y(), outline, _replyRect.height()); p.drawRoundedRect(_replyRect, radius, radius); p.setOpacity(Ui::kDefaultBgOpacity); p.setClipRect( _replyRect.x() + outline, _replyRect.y(), _replyRect.width() - outline, _replyRect.height()); p.drawRoundedRect(_replyRect, radius, radius); } p.setOpacity(1.); p.setClipping(false); p.setPen(st::msgInServiceFg); _nameText.drawLeftElided( p, _name.x(), _name.y(), _name.width(), _content.width()); p.setPen(st::historyTextInFg); _replyText.drawLeftElided( p, _reply.x(), _reply.y(), _reply.width(), _content.width()); } void Preview::paintMessage(Painter &p, QRect clip) { p.setPen(st::historyTextInFg); _messageText.drawLeftElided( p, 0, 0, _message.width(), _message.width(), kMaxTextLines); } void Preview::validateUserpicCache() { if (!_userpicImage.isNull() || _userpicOriginal.isNull() || _userpic.isEmpty()) { return; } _userpicImage = Images::Circle(_userpicOriginal.scaled( _userpic.size() * _ratio, Qt::IgnoreAspectRatio, Qt::SmoothTransformation)); _userpicImage.setDevicePixelRatio(_ratio); } void Preview::validateBubbleCache() { if (!_bubbleCorners.p[0].isNull()) { return; } const auto radius = scaled(16); // st::bubbleRadiusLarge _bubbleCorners = Ui::PrepareCornerPixmaps(radius, st::msgInBg); _bubbleCorners.p[2] = {}; _bubbleTail = scaled(st::historyBubbleTailInLeft, st::msgInBg->c); _bubbleShadowBottomRight = Ui::PrepareCornerPixmaps(radius, st::msgInShadow).p[3]; } void Preview::validateShadowCache() { if (!_shadowSides[0].isNull()) { return; } const auto &shadowColor = st::windowShadowFg->c; _shadowSides[0] = scaled(st::callShadow.left, shadowColor); _shadowSides[1] = scaled(st::callShadow.top, shadowColor); _shadowSides[2] = scaled(st::callShadow.right, shadowColor); _shadowSides[3] = scaled(st::callShadow.bottom, shadowColor); _shadowCorners[0] = scaled(st::callShadow.topLeft, shadowColor); _shadowCorners[1] = scaled(st::callShadow.bottomLeft, shadowColor); _shadowCorners[2] = scaled(st::callShadow.topRight, shadowColor); _shadowCorners[3] = scaled(st::callShadow.bottomRight, shadowColor); } void Preview::initAsWindow() { _widget.setWindowFlags(Qt::WindowFlags(Qt::FramelessWindowHint) | Qt::BypassWindowManagerHint | Qt::NoDropShadowWindowHint | Qt::ToolTip); _widget.setAttribute(Qt::WA_TransparentForMouseEvents); _widget.hide(); _widget.setAttribute(Qt::WA_NoSystemBackground); _widget.setAttribute(Qt::WA_TranslucentBackground); } } // namespace [[nodiscard]] Fn SetupScalePreview( not_null window, not_null slider) { const auto controller = window->sessionController(); const auto user = controller ? controller->session().user().get() : nullptr; const auto preview = slider->lifetime().make_state( slider.get(), user ? Data::PeerUserpicImageValue(user, 160, 0) : nullptr); return [=](ScalePreviewShow show, int scale, int globalX) { preview->toggle(show, scale, globalX); }; } } // namespace Settings