From ff331c040a8e73dbcd6adfff696ea2af9f0f6a08 Mon Sep 17 00:00:00 2001 From: John Preston Date: Thu, 5 Jan 2023 14:48:46 +0400 Subject: [PATCH] Allow huge range of interface scales. --- Telegram/CMakeLists.txt | 2 + .../SourceFiles/data/data_peer_values.cpp | 8 +- Telegram/SourceFiles/data/data_peer_values.h | 3 +- .../history/history_item_components.cpp | 3 +- .../history/history_item_components.h | 2 + Telegram/SourceFiles/settings/settings.style | 8 +- .../SourceFiles/settings/settings_common.cpp | 35 + .../SourceFiles/settings/settings_common.h | 14 + .../SourceFiles/settings/settings_main.cpp | 114 ++- .../settings/settings_scale_preview.cpp | 733 ++++++++++++++++++ .../settings/settings_scale_preview.h | 30 + .../SourceFiles/ui/chat/chat_style_radius.h | 2 +- .../ui/widgets/continuous_sliders.h | 42 +- .../SourceFiles/window/section_widget.cpp | 47 +- Telegram/SourceFiles/window/section_widget.h | 11 + Telegram/lib_ui | 2 +- 16 files changed, 987 insertions(+), 69 deletions(-) create mode 100644 Telegram/SourceFiles/settings/settings_scale_preview.cpp create mode 100644 Telegram/SourceFiles/settings/settings_scale_preview.h diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index 8f05bd413..712036097 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -1201,6 +1201,8 @@ PRIVATE settings/settings_privacy_controllers.h settings/settings_privacy_security.cpp settings/settings_privacy_security.h + settings/settings_scale_preview.cpp + settings/settings_scale_preview.h settings/settings_type.h storage/details/storage_file_utilities.cpp storage/details/storage_file_utilities.h diff --git a/Telegram/SourceFiles/data/data_peer_values.cpp b/Telegram/SourceFiles/data/data_peer_values.cpp index e3e17adc9..6004a704b 100644 --- a/Telegram/SourceFiles/data/data_peer_values.cpp +++ b/Telegram/SourceFiles/data/data_peer_values.cpp @@ -514,7 +514,8 @@ bool ChannelHasActiveCall(not_null channel) { rpl::producer PeerUserpicImageValue( not_null peer, - int size) { + int size, + std::optional radius) { return [=](auto consumer) { auto result = rpl::lifetime(); struct State { @@ -541,7 +542,10 @@ rpl::producer PeerUserpicImageValue( } state->key = key; state->empty = false; - consumer.put_next(peer->generateUserpicImage(state->view, size)); + consumer.put_next(peer->generateUserpicImage( + state->view, + size, + radius)); }; peer->session().changes().peerFlagsValue( peer, diff --git a/Telegram/SourceFiles/data/data_peer_values.h b/Telegram/SourceFiles/data/data_peer_values.h index 0a5d5f398..673a6b846 100644 --- a/Telegram/SourceFiles/data/data_peer_values.h +++ b/Telegram/SourceFiles/data/data_peer_values.h @@ -133,7 +133,8 @@ inline auto PeerFullFlagValue( [[nodiscard]] rpl::producer PeerUserpicImageValue( not_null peer, - int size); + int size, + std::optional radius = {}); [[nodiscard]] const AllowedReactions &PeerAllowedReactions( not_null peer); diff --git a/Telegram/SourceFiles/history/history_item_components.cpp b/Telegram/SourceFiles/history/history_item_components.cpp index 8556c255e..1556a579a 100644 --- a/Telegram/SourceFiles/history/history_item_components.cpp +++ b/Telegram/SourceFiles/history/history_item_components.cpp @@ -47,7 +47,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace { const auto kPsaForwardedPrefix = "cloud_lng_forwarded_psa_"; -constexpr auto kReplyBarAlpha = 230. / 255.; } // namespace @@ -465,7 +464,7 @@ void HistoryMessageReply::paint( st::msgReplyBarSize.height(), w + 2 * x); const auto opacity = p.opacity(); - p.setOpacity(opacity * kReplyBarAlpha); + p.setOpacity(opacity * kBarAlpha); p.fillRect(rbar, bar); p.setOpacity(opacity); } diff --git a/Telegram/SourceFiles/history/history_item_components.h b/Telegram/SourceFiles/history/history_item_components.h index e20da970b..d3b1f28d8 100644 --- a/Telegram/SourceFiles/history/history_item_components.h +++ b/Telegram/SourceFiles/history/history_item_components.h @@ -195,6 +195,8 @@ struct HistoryMessageReply Expects(replyToVia == nullptr); } + static constexpr auto kBarAlpha = 230. / 255.; + bool updateData(not_null holder, bool force = false); // Must be called before destructor. diff --git a/Telegram/SourceFiles/settings/settings.style b/Telegram/SourceFiles/settings/settings.style index 8650dd42b..0a64e3851 100644 --- a/Telegram/SourceFiles/settings/settings.style +++ b/Telegram/SourceFiles/settings/settings.style @@ -42,13 +42,19 @@ settingsOptionDisabled: SettingsButton(settingsButtonNoIcon) { settingsSectionSkip: 6px; settingsSeparatorPadding: margins(22px, infoProfileSkip, 0px, infoProfileSkip); settingsButtonRightSkip: 23px; -settingsScalePadding: margins(63px, 7px, 21px, 4px); +settingsScalePadding: margins(60px, 7px, 22px, 4px); settingsBigScalePadding: margins(21px, 7px, 21px, 4px); settingsSlider: SettingsSlider(defaultSettingsSlider) { barFg: windowBgOver; labelFg: windowSubTextFg; labelFgActive: windowActiveTextFg; } +settingsScale: MediaSlider(defaultContinuousSlider) { + seekSize: size(15px, 15px); +} +settingsScaleLabel: FlatLabel(defaultFlatLabel) { + textFg: windowActiveTextFg; +} settingsUpdateToggle: SettingsButton(settingsButtonNoIcon) { height: 40px; padding: margins(22px, 8px, 22px, 8px); diff --git a/Telegram/SourceFiles/settings/settings_common.cpp b/Telegram/SourceFiles/settings/settings_common.cpp index d905e90f1..7bd0e0983 100644 --- a/Telegram/SourceFiles/settings/settings_common.cpp +++ b/Telegram/SourceFiles/settings/settings_common.cpp @@ -26,6 +26,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/widgets/labels.h" #include "ui/widgets/box_content_divider.h" #include "ui/widgets/buttons.h" +#include "ui/widgets/continuous_sliders.h" #include "ui/widgets/menu/menu_add_action_callback.h" #include "ui/painter.h" #include "boxes/abstract_box.h" @@ -393,4 +394,38 @@ void FillMenu( } } +SliderWithLabel MakeSliderWithLabel( + QWidget *parent, + const style::MediaSlider &sliderSt, + const style::FlatLabel &labelSt, + int skip, + int minLabelWidth) { + auto result = object_ptr(parent); + const auto raw = result.data(); + const auto height = std::max( + sliderSt.seekSize.height(), + labelSt.style.font->height); + raw->resize(sliderSt.seekSize.width(), height); + const auto slider = Ui::CreateChild(raw, sliderSt); + const auto label = Ui::CreateChild(raw, labelSt); + slider->resize(slider->width(), sliderSt.seekSize.height()); + rpl::combine( + raw->sizeValue(), + label->sizeValue() + ) | rpl::start_with_next([=](QSize outer, QSize size) { + const auto right = std::max(size.width(), minLabelWidth) + skip; + label->moveToRight(0, (outer.height() - size.height()) / 2); + const auto width = std::max( + sliderSt.seekSize.width(), + outer.width() - right); + slider->resizeToWidth(width); + slider->moveToLeft(0, (outer.height() - slider->height()) / 2); + }, label->lifetime()); + return { + .widget = std::move(result), + .slider = slider, + .label = label, + }; +} + } // namespace Settings diff --git a/Telegram/SourceFiles/settings/settings_common.h b/Telegram/SourceFiles/settings/settings_common.h index 48158a70b..e0dffe917 100644 --- a/Telegram/SourceFiles/settings/settings_common.h +++ b/Telegram/SourceFiles/settings/settings_common.h @@ -25,6 +25,7 @@ class VerticalLayout; class FlatLabel; class SettingsButton; class AbstractButton; +class MediaSlider; } // namespace Ui namespace Ui::Menu { @@ -38,6 +39,7 @@ class SessionController; namespace style { struct FlatLabel; struct SettingsButton; +struct MediaSlider; } // namespace style namespace Lottie { @@ -228,4 +230,16 @@ void FillMenu( Fn showOther, Ui::Menu::MenuCallback addAction); +struct SliderWithLabel { + object_ptr widget; + not_null slider; + not_null label; +}; +[[nodiscard]] SliderWithLabel MakeSliderWithLabel( + QWidget *parent, + const style::MediaSlider &sliderSt, + const style::FlatLabel &labelSt, + int skip, + int minLabelWidth = 0); + } // namespace Settings diff --git a/Telegram/SourceFiles/settings/settings_main.cpp b/Telegram/SourceFiles/settings/settings_main.cpp index 10ea09c5a..e72ae77f1 100644 --- a/Telegram/SourceFiles/settings/settings_main.cpp +++ b/Telegram/SourceFiles/settings/settings_main.cpp @@ -17,6 +17,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "settings/settings_folders.h" #include "settings/settings_calls.h" #include "settings/settings_premium.h" +#include "settings/settings_scale_preview.h" #include "boxes/language_box.h" #include "boxes/username_box.h" #include "boxes/about_box.h" @@ -27,7 +28,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/wrap/slide_wrap.h" #include "ui/wrap/padding_wrap.h" #include "ui/widgets/labels.h" -#include "ui/widgets/discrete_sliders.h" +#include "ui/widgets/continuous_sliders.h" #include "ui/widgets/buttons.h" #include "ui/text/text_utilities.h" #include "ui/toast/toast.h" @@ -421,34 +422,58 @@ void SetupInterfaceScale( { icon ? &st::settingsIconInterfaceScale : nullptr, kIconLightOrange } )->toggleOn(toggled->events_starting_with_copy(switched)); - const auto slider = container->add( - object_ptr(container, st::settingsSlider), - icon ? st::settingsScalePadding : st::settingsBigScalePadding); - - static const auto ScaleValues = [&] { - auto values = (cIntRetinaFactor() > 1) - ? std::vector{ 100, 110, 120, 130, 140, 150 } - : std::vector{ 100, 125, 150, 200, 250, 300 }; - if (cConfigScale() == style::kScaleAuto) { - return values; + const auto ratio = style::DevicePixelRatio(); + const auto scaleMin = style::kScaleMin; + const auto scaleMax = style::kScaleMax / ratio; + const auto scaleConfig = cConfigScale(); + const auto step = 5; + Assert(!((scaleMax - scaleMin) % step)); + auto values = std::vector(); + for (auto i = scaleMin; i != scaleMax; i += step) { + values.push_back(i); + if (scaleConfig > i && scaleConfig < i + step) { + values.push_back(scaleConfig); } - if (ranges::find(values, cConfigScale()) == end(values)) { - values.push_back(cConfigScale()); - } - return values; - }(); + } + values.push_back(scaleMax); + const auto valuesCount = int(values.size()); - const auto sectionFromScale = [](int scale) { + const auto valueFromScale = [=](int scale) { scale = cEvalScale(scale); auto result = 0; - for (const auto value : ScaleValues) { + for (const auto value : values) { if (scale == value) { break; } ++result; } - return (result == ScaleValues.size()) ? (result - 1) : result; + return ((result == valuesCount) ? (result - 1) : result) + / float64(valuesCount - 1); }; + auto sliderWithLabel = MakeSliderWithLabel( + container, + st::settingsScale, + st::settingsScaleLabel, + st::normalFont->spacew * 2, + st::settingsScaleLabel.style.font->width("300%")); + container->add( + std::move(sliderWithLabel.widget), + icon ? st::settingsScalePadding : st::settingsBigScalePadding); + const auto slider = sliderWithLabel.slider; + const auto label = sliderWithLabel.label; + + const auto updateLabel = [=](int scale) { + const auto labelText = [&](int scale) { + if constexpr (Platform::IsMac()) { + return QString::number(scale) + '%'; + } else { + return QString::number(scale * ratio) + '%'; + } + }; + label->setText(labelText(cEvalScale(scale))); + }; + updateLabel(cConfigScale()); + const auto inSetScale = container->lifetime().make_state(); const auto setScale = [=](int scale, const auto &repeatSetScale) -> void { if (*inSetScale) { @@ -457,8 +482,9 @@ void SetupInterfaceScale( *inSetScale = true; const auto guard = gsl::finally([=] { *inSetScale = false; }); + updateLabel(scale); toggled->fire(scale == style::kScaleAuto); - slider->setActiveSection(sectionFromScale(scale)); + slider->setValue(valueFromScale(scale)); if (cEvalScale(scale) != cEvalScale(cConfigScale())) { const auto confirmed = crl::guard(button, [=] { cSetConfigScale(scale); @@ -484,31 +510,37 @@ void SetupInterfaceScale( } }; - const auto label = [](int scale) { - if constexpr (Platform::IsMac()) { - return QString::number(scale) + '%'; - } else { - return QString::number(scale * cIntRetinaFactor()) + '%'; + const auto shown = container->lifetime().make_state(); + const auto togglePreview = SetupScalePreview(window, slider); + const auto toggleForScale = [=](int scale) { + scale = cEvalScale(scale); + const auto show = *shown + ? ScalePreviewShow::Update + : ScalePreviewShow::Show; + *shown = true; + auto index = 0; + for (auto i = 0; i != valuesCount; ++i) { + if (values[i] <= scale + && (i + 1 == valuesCount || values[i + 1] > scale)) { + const auto x = (slider->width() * i) / (valuesCount - 1); + const auto globalX = slider->mapToGlobal(QPoint(x, 0)).x(); + togglePreview(show, scale, globalX); + return; + } } + togglePreview(show, scale, QCursor::pos().x()); }; - const auto scaleByIndex = [](int index) { - return *(ScaleValues.begin() + index); + const auto toggleHidePreview = [=] { + togglePreview(ScalePreviewShow::Hide, 0, 0); + *shown = false; }; - for (const auto value : ScaleValues) { - slider->addSection(label(value)); - } - slider->setActiveSectionFast(sectionFromScale(cConfigScale())); - slider->sectionActivated( - ) | rpl::map([=](int section) { - return scaleByIndex(section); - }) | rpl::filter([=](int scale) { - return cEvalScale(scale) != cEvalScale(cConfigScale()); - }) | rpl::start_with_next([=](int scale) { - setScale( - (scale == cScreenScale()) ? style::kScaleAuto : scale, - setScale); - }, slider->lifetime()); + slider->setPseudoDiscrete( + valuesCount, + [=](int index) { return values[index]; }, + cConfigScale(), + [=](int scale) { updateLabel(scale); toggleForScale(scale); }, + [=](int scale) { toggleHidePreview(); setScale(scale, setScale); }); button->toggledValue( ) | rpl::map([](bool checked) { diff --git a/Telegram/SourceFiles/settings/settings_scale_preview.cpp b/Telegram/SourceFiles/settings/settings_scale_preview.cpp new file mode 100644 index 000000000..e92e32623 --- /dev/null +++ b/Telegram/SourceFiles/settings/settings_scale_preview.cpp @@ -0,0 +1,733 @@ +/* +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_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 *parent, rpl::producer userpic); + + void toggle(ScalePreviewShow show, int scale, int globalX); + +private: + void init(); + void initAsWindow(); + + void updateToScale(int scale); + void updateGlobalPosition(int globalX); + void updateGlobalPosition(); + void updateWindowGlobalPosition(QPoint global, int globalX); + 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]] QSize scaled(QSize value) const; + [[nodiscard]] QRect scaled(QRect value) const; + [[nodiscard]] QMargins scaled(QMargins value) const; + [[nodiscard]] style::font scaled( + const style::font &value, int size) const; + [[nodiscard]] style::TextStyle scaled( + const style::TextStyle &value, + int fontSize, + int lineHeight) const; + [[nodiscard]] QImage scaled( + const style::icon &icon, + const QColor &color) const; + + Ui::RpWidget _widget; + 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 _replyBar; + 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; + +}; + +[[nodiscard]] bool UseSeparateWindow() { + return !Platform::IsWayland() + && Ui::Platform::TranslucentWindowsSupported(); +} + +Preview::Preview(QWidget *parent, rpl::producer userpic) +: _widget(parent) +, _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()); + + init(); +} + +void Preview::toggle(ScalePreviewShow show, int scale, int globalX) { + if (show == ScalePreviewShow::Hide) { + toggleShown(false); + return; + } else if (show == ScalePreviewShow::Update && !_shown) { + return; + } + updateToScale(scale); + updateGlobalPosition(globalX); + if (_widget.isHidden()) { + Ui::Platform::UpdateOverlayed(&_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(_widget.parentWidget(), 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::kScaleMax / _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()) }; +} + +QSize Preview::scaled(QSize value) const { + return { scaled(value.width()), scaled(value.height()) }; +} + +QRect Preview::scaled(QRect value) const { + return { scaled(value.topLeft()), scaled(value.size()) }; +} + +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::TextStyle Preview::scaled( + const style::TextStyle &value, + int fontSize, + int lineHeight) const { + return { + .font = scaled(value.font, fontSize), + .linkFont = scaled(value.linkFont, fontSize), + .linkFontOver = scaled(value.linkFontOver, fontSize), + .lineHeight = scaled(value.lineHeight), + }; +} + +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(_nameStyle, 13, 0); + _textStyle = scaled(_textStyle, 13, 0); + _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()); + + _replyBar = QRect( + scaled(1), // st::msgReplyBarPos.x(), + scaled(6) + 0,// st::msgReplyPadding.top() + st::msgReplyBarPos.y(), + scaled(2), //st::msgReplyBarSize.width(), + scaled(36)); // st::msgReplyBarSize.height(), + const auto namePosition = QPoint( + scaled(10), // st::msgReplyBarSkip + scaled(6)); // st::msgReplyPadding.top() + const auto replyPosition = QPoint( + scaled(10), // st::msgReplyBarSkip + scaled(6) + _nameStyle.font->height); // st::msgReplyPadding.top() + + const auto wantedWidth = std::max({ + namePosition.x() + _nameText.maxWidth(), + replyPosition.x() + _replyText.maxWidth(), + _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); + + _name = QRect( + namePosition, + QSize(messageWidth - namePosition.x(), _nameStyle.font->height)); + _reply = QRect( + replyPosition, + QSize(messageWidth - replyPosition.x(), _textStyle.font->height)); + _message = QRect(0, 0, messageWidth, messageHeight); + + // replyBar.bottom + st::msgReplyPadding.bottom(); + const auto replySkip = _replyBar.y() + _replyBar.height() + scaled(6); + _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 globalX) { + const auto parent = _widget.parentWidget(); + if (_window) { + const auto global = parent->mapToGlobal(QPoint()); + _localShiftLeft = globalX - global.x(); + updateWindowGlobalPosition(global, globalX); + } else { + const auto position = parent->pos(); + + QPoint(parent->width() / 2, 0) + - QPoint(_outer.width() / 2, _outer.height()); + _widget.setGeometry(QRect(position, _outer.size())); + updateOuterPosition(globalX); + } +} + +void Preview::updateGlobalPosition() { + const auto parent = _widget.parentWidget(); + const auto global = parent->mapToGlobal(QPoint()); + updateWindowGlobalPosition(global, global.x() + _localShiftLeft); +} + +void Preview::updateWindowGlobalPosition(QPoint global, int globalX) { + const auto desiredLeft = global.x() - (_minOuterSize.width() / 2); + const auto desiredRight = global.x() + + _widget.parentWidget()->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(globalX); +} + +QRect Preview::adjustByScreenGeometry(QRect geometry) const { + const auto parent = _widget.parentWidget(); + const auto screen = parent->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) { + p.setOpacity(HistoryMessageReply::kBarAlpha); + p.fillRect(_replyBar, st::msgInReplyBarColor); + p.setOpacity(1.); + + 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 parent = slider->parentWidget(); + const auto controller = window->sessionController(); + const auto user = controller + ? controller->session().user().get() + : nullptr; + auto view = user->activeUserpicView(); + 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 diff --git a/Telegram/SourceFiles/settings/settings_scale_preview.h b/Telegram/SourceFiles/settings/settings_scale_preview.h new file mode 100644 index 000000000..b7d0f3262 --- /dev/null +++ b/Telegram/SourceFiles/settings/settings_scale_preview.h @@ -0,0 +1,30 @@ +/* +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 Ui { +class RpWidget; +} // namespace Ui + +namespace Window { +class Controller; +} // namespace Window + +namespace Settings { + +enum class ScalePreviewShow { + Show, + Update, + Hide, +}; + +[[nodiscard]] Fn SetupScalePreview( + not_null window, + not_null slider); + +} // namespace Settings diff --git a/Telegram/SourceFiles/ui/chat/chat_style_radius.h b/Telegram/SourceFiles/ui/chat/chat_style_radius.h index d8c4b7d94..99c1127aa 100644 --- a/Telegram/SourceFiles/ui/chat/chat_style_radius.h +++ b/Telegram/SourceFiles/ui/chat/chat_style_radius.h @@ -17,4 +17,4 @@ namespace Ui { extern const char kOptionUseSmallMsgBubbleRadius[]; -} +} // namespace Ui diff --git a/Telegram/SourceFiles/ui/widgets/continuous_sliders.h b/Telegram/SourceFiles/ui/widgets/continuous_sliders.h index 77a0efd03..51c82883d 100644 --- a/Telegram/SourceFiles/ui/widgets/continuous_sliders.h +++ b/Telegram/SourceFiles/ui/widgets/continuous_sliders.h @@ -144,15 +144,15 @@ public: template < typename Value, typename Convert, - typename Callback, + typename Progress, typename = std::enable_if_t< - rpl::details::is_callable_plain_v + rpl::details::is_callable_plain_v && std::is_same_v()(1))>>> void setPseudoDiscrete( int valuesCount, Convert &&convert, Value current, - Callback &&callback) { + Progress &&progress) { Expects(valuesCount > 1); setAlwaysDisplayMarker(true); @@ -169,15 +169,39 @@ public: setAdjustCallback([=](float64 value) { return base::SafeRound(value * sectionsCount) / sectionsCount; }); - setChangeProgressCallback([ - =, - convert = std::forward(convert), - callback = std::forward(callback) - ](float64 value) { + setChangeProgressCallback([=](float64 value) { const auto index = int(base::SafeRound(value * sectionsCount)); - callback(convert(index)); + progress(convert(index)); }); } + + template < + typename Value, + typename Convert, + typename Progress, + typename Finished, + typename = std::enable_if_t< + rpl::details::is_callable_plain_v + && rpl::details::is_callable_plain_v + && std::is_same_v()(1))>>> + void setPseudoDiscrete( + int valuesCount, + Convert &&convert, + Value current, + Progress &&progress, + Finished &&finished) { + setPseudoDiscrete( + valuesCount, + std::forward(convert), + current, + std::forward(progress)); + setChangeFinishedCallback([=](float64 value) { + const auto sectionsCount = (valuesCount - 1); + const auto index = int(base::SafeRound(value * sectionsCount)); + finished(convert(index)); + }); + } + void setActiveFgOverride(std::optional color); void addDivider(float64 atValue, const QSize &size); diff --git a/Telegram/SourceFiles/window/section_widget.cpp b/Telegram/SourceFiles/window/section_widget.cpp index 38a8507cf..a120be093 100644 --- a/Telegram/SourceFiles/window/section_widget.cpp +++ b/Telegram/SourceFiles/window/section_widget.cpp @@ -187,20 +187,43 @@ void SectionWidget::PaintBackground( not_null theme, not_null widget, QRect clip) { - auto p = QPainter(widget); + PaintBackground( + theme, + widget, + controller->content()->height(), + controller->content()->backgroundFromY(), + clip); +} +void SectionWidget::PaintBackground( + not_null theme, + not_null widget, + int fillHeight, + int fromy, + QRect clip) { + auto p = QPainter(widget); + if (fromy) { + p.translate(0, fromy); + clip = clip.translated(0, -fromy); + } + PaintBackground(p, theme, QSize(widget->width(), fillHeight), clip); +} + +void SectionWidget::PaintBackground( + QPainter &p, + not_null theme, + QSize fill, + QRect clip) { const auto &background = theme->background(); if (background.colorForFill) { p.fillRect(clip, *background.colorForFill); return; } const auto &gradient = background.gradientForFill; - const auto fill = QSize(widget->width(), controller->content()->height()); - auto fromy = controller->content()->backgroundFromY(); auto state = theme->backgroundState(fill); const auto paintCache = [&](const Ui::CachedBackground &cache) { const auto to = QRect( - QPoint(cache.x, fromy + cache.y), + QPoint(cache.x, cache.y), cache.pixmap.size() / cIntRetinaFactor()); if (cache.waitingForNegativePattern) { // While we wait for pattern being loaded we paint just gradient. @@ -229,11 +252,15 @@ void SectionWidget::PaintBackground( const auto goodNow = hasNow && (state.now.area == fill); const auto useCache = goodNow || !gradient.isNull(); if (useCache) { - if (state.shown < 1. && !gradient.isNull()) { + const auto fade = (state.shown < 1. && !gradient.isNull()); + if (fade) { paintCache(state.was); p.setOpacity(state.shown); } paintCache(state.now); + if (fade) { + p.setOpacity(1.); + } return; } const auto &prepared = background.prepared; @@ -259,12 +286,12 @@ void SectionWidget::PaintBackground( const auto w = tiled.width() / cRetinaFactor(); const auto h = tiled.height() / cRetinaFactor(); const auto sx = qFloor(left / w); - const auto sy = qFloor((top - fromy) / h); + const auto sy = qFloor(top / h); const auto cx = qCeil(right / w); - const auto cy = qCeil((bottom - fromy) / h); + const auto cy = qCeil(bottom / h); for (auto i = sx; i < cx; ++i) { for (auto j = sy; j < cy; ++j) { - p.drawImage(QPointF(i * w, fromy + j * h), tiled); + p.drawImage(QPointF(i * w, j * h), tiled); } } } else { @@ -272,9 +299,7 @@ void SectionWidget::PaintBackground( const auto rects = Ui::ComputeChatBackgroundRects( fill, prepared.size()); - auto to = rects.to; - to.moveTop(to.top() + fromy); - p.drawImage(to, prepared, rects.from); + p.drawImage(rects.to, prepared, rects.from); } } diff --git a/Telegram/SourceFiles/window/section_widget.h b/Telegram/SourceFiles/window/section_widget.h index f42b78432..89be8819c 100644 --- a/Telegram/SourceFiles/window/section_widget.h +++ b/Telegram/SourceFiles/window/section_widget.h @@ -184,6 +184,17 @@ public: not_null theme, not_null widget, QRect clip); + static void PaintBackground( + not_null theme, + not_null widget, + int fillHeight, + int fromy, + QRect clip); + static void PaintBackground( + QPainter &p, + not_null theme, + QSize fill, + QRect clip); protected: void paintEvent(QPaintEvent *e) override; diff --git a/Telegram/lib_ui b/Telegram/lib_ui index 7882604a2..3cb645f50 160000 --- a/Telegram/lib_ui +++ b/Telegram/lib_ui @@ -1 +1 @@ -Subproject commit 7882604a29acc5d4c7deca51133180d74fb36f87 +Subproject commit 3cb645f50704b67537167e14623840a25fbdda7a