From 5ccb58b1bbd6afb4295f3fc04c20b2fdecc3093d Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Tue, 24 Mar 2026 16:51:44 +0300 Subject: [PATCH] Added new class of settings button with description. --- Telegram/CMakeLists.txt | 2 + .../settings/detailed_settings_button.cpp | 299 ++++++++++++++++++ .../settings/detailed_settings_button.h | 76 +++++ Telegram/SourceFiles/settings/settings.style | 30 ++ 4 files changed, 407 insertions(+) create mode 100644 Telegram/SourceFiles/settings/detailed_settings_button.cpp create mode 100644 Telegram/SourceFiles/settings/detailed_settings_button.h diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index 4a3423535c..f0f1555cda 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -1618,6 +1618,8 @@ PRIVATE settings/settings_codes.h settings/settings_common_session.cpp settings/settings_common_session.h + settings/detailed_settings_button.cpp + settings/detailed_settings_button.h settings/sections/settings_credits.cpp settings/sections/settings_credits.h settings/settings_credits_graphics.cpp diff --git a/Telegram/SourceFiles/settings/detailed_settings_button.cpp b/Telegram/SourceFiles/settings/detailed_settings_button.cpp new file mode 100644 index 0000000000..353acb565a --- /dev/null +++ b/Telegram/SourceFiles/settings/detailed_settings_button.cpp @@ -0,0 +1,299 @@ +#include "settings/detailed_settings_button.h" + +#include "ui/painter.h" +#include "ui/ui_utility.h" +#include "ui/wrap/vertical_layout.h" +#include "styles/style_settings.h" + +DetailedSettingsButton::DetailedSettingsButton( + QWidget *parent, + rpl::producer title, + rpl::producer description, + Settings::IconDescriptor icon, + rpl::producer toggled, + const style::DetailedSettingsButtonStyle &rowStyle) +: Ui::RippleButton(parent, rowStyle.button.ripple) +, _style(rowStyle) +, _title(0) +, _description(0) +, _iconForeground(icon.icon) +, _iconBackground(icon.background) +, _iconBackgroundBrush(std::move(icon.backgroundBrush)) { + std::move( + title + ) | rpl::on_next([=](const QString &text) { + _title.setText( + _style.button.style, + text, + kDefaultTextOptions); + accessibilityNameChanged(); + refreshLayout(); + }, lifetime()); + std::move( + description + ) | rpl::on_next([=](const QString &text) { + _description.setText( + _style.description.style, + text, + kDefaultTextOptions); + refreshLayout(); + }, lifetime()); + _toggle = std::make_unique( + _style.button.toggle, + false, + [this] { rtlupdate(toggleRect()); }); + addClickHandler([=] { + _toggle->setChecked(!_toggle->checked(), anim::type::normal); + }); + std::move( + toggled + ) | rpl::on_next([=](bool checked) { + _toggle->setChecked(checked, anim::type::normal); + }, lifetime()); + _toggle->checkedChanges() | rpl::on_next([=] { + accessibilityStateChanged({ .checked = true }); + }, lifetime()); + widthValue() | rpl::on_next([=](int width) { + if (width > 0) { + refreshLayout(); + } + }, lifetime()); +} + +QString DetailedSettingsButton::accessibilityName() { + return _title.toString(); +} + +Ui::AccessibilityState DetailedSettingsButton::accessibilityState() const { + return { .checkable = true, .checked = _toggle->checked() }; +} + +#if QT_VERSION >= QT_VERSION_CHECK(6, 11, 0) +QAccessible::Role DetailedSettingsButton::accessibilityRole() { + return QAccessible::Role::Switch; +} +#endif + +bool DetailedSettingsButton::toggled() const { + return _toggle->checked(); +} + +rpl::producer DetailedSettingsButton::toggledChanges() const { + return _toggle->checkedChanges(); +} + +rpl::producer> +DetailedSettingsButton::clickAreaEvents() const { + return events(); +} + +void DetailedSettingsButton::setToggleLocked(bool locked) { + _toggle->setLocked(locked); +} + +void DetailedSettingsButton::onStateChanged( + State was, + StateChangeSource source) { + const auto wasDisabled = !!(was & StateFlag::Disabled); + const auto nowDisabled = isDisabled(); + if (!nowDisabled || !isDown()) { + RippleButton::onStateChanged(was, source); + } + _toggle->setStyle( + isOver() + ? _style.button.toggleOver + : _style.button.toggle); + if (nowDisabled != wasDisabled) { + setPointerCursor(!nowDisabled); + } + update(); +} + +void DetailedSettingsButton::paintEvent(QPaintEvent *e) { + auto p = Painter(this); + const auto over = isOver() || isDown(); + p.fillRect( + e->rect(), + over ? _style.button.textBgOver : _style.button.textBg); + paintRipple(p, 0, 0); + + paintIcon(p); + + p.setPen(over + ? _style.button.textFgOver + : _style.button.textFg); + _title.drawLeftElided( + p, + _style.button.padding.left(), + titleTop(), + _titleWidth, + width()); + p.setPen(_style.description.textFg); + if (_descriptionHeight > 0) { + _description.drawLeft( + p, + _style.button.padding.left(), + _descriptionTop, + _descriptionWidth, + width()); + } + + const auto toggle = toggleRect(); + _toggle->paint(p, toggle.left(), toggle.top(), width()); +} + +int DetailedSettingsButton::resizeGetHeight(int newWidth) { + _titleWidth = textAvailableWidth(newWidth); + _descriptionWidth + = std::max(1, _titleWidth - _style.descriptionRightSkip); + _descriptionTop = descriptionTopValue(); + _descriptionHeight = _description.countHeight(_descriptionWidth); + return _descriptionTop + + _descriptionHeight + + _style.descriptionBottomSkip; +} + +void DetailedSettingsButton::refreshLayout() { + Ui::PostponeCall(crl::guard(this, [=] { + const auto current = widthNoMargins(); + if (current > 0) { + resizeToWidth(current); + } + update(); + })); +} + +int DetailedSettingsButton::titleTop() const { + return _style.button.padding.top(); +} + +int DetailedSettingsButton::titleHeight() const { + return _style.button.height; +} + +int DetailedSettingsButton::descriptionTopValue() const { + return titleTop() + titleHeight() + _style.descriptionTopSkip; +} + +int DetailedSettingsButton::firstDescriptionLineBottom() const { + const auto top = (_descriptionTop > 0) + ? _descriptionTop + : descriptionTopValue(); + return top + qMax( + _style.description.style.lineHeight, + _style.description.style.font->height); +} + +int DetailedSettingsButton::textAvailableWidth(int outerw) const { + const auto toggle = _toggle->getSize(); + return std::max( + 1, + outerw + - _style.button.padding.left() + - _style.button.padding.right() + - _style.button.toggleSkip + - toggle.width()); +} + +QRect DetailedSettingsButton::toggleRect() const { + const auto toggle = _toggle->getSize(); + const auto anchorBottom = firstDescriptionLineBottom(); + return QRect( + width() - _style.button.toggleSkip - toggle.width(), + (anchorBottom - toggle.height()) / 2, + toggle.width(), + toggle.height()); +} + +QRect DetailedSettingsButton::iconAreaRect() const { + const auto size = iconSize(); + return QRect( + _style.button.iconLeft, + titleTop(), + size, + size); +} + +QRect DetailedSettingsButton::iconRect() const { + const auto area = iconAreaRect(); + const auto padding = _style.iconPadding; + const auto width = std::max(1, area.width() - (2 * padding)); + const auto height = std::max(1, area.height() - (2 * padding)); + return QRect( + area.x() + padding, + area.y() + padding, + width, + height); +} + +QRect DetailedSettingsButton::iconForegroundRect(QRect iconRect) const { + const auto padding = _style.iconForegroundPadding; + const auto width = std::max(1, iconRect.width() - (2 * padding)); + const auto height = std::max(1, iconRect.height() - (2 * padding)); + return QRect( + iconRect.x() + padding, + iconRect.y() + padding, + width, + height); +} + +int DetailedSettingsButton::iconSize() const { + const auto result = firstDescriptionLineBottom() - titleTop(); + return std::max(1, result); +} + +void DetailedSettingsButton::paintIcon(QPainter &p) const { + if (!_iconForeground && !_iconBackground && !_iconBackgroundBrush) { + return; + } + const auto rect = iconRect(); + if (_iconBackground) { + p.setPen(Qt::NoPen); + p.setBrush(*_iconBackground); + { + auto hq = PainterHighQualityEnabler(p); + p.drawRoundedRect(rect, _style.iconRadius, _style.iconRadius); + } + } else if (_iconBackgroundBrush) { + p.setPen(Qt::NoPen); + p.setBrush(*_iconBackgroundBrush); + { + auto hq = PainterHighQualityEnabler(p); + p.drawRoundedRect(rect, _style.iconRadius, _style.iconRadius); + } + } + if (_iconForeground) { + const auto source = _iconForeground->size(); + if (!source.isEmpty()) { + const auto foreground = iconForegroundRect(rect); + const auto scale = std::min( + double(foreground.width()) / source.width(), + double(foreground.height()) / source.height()); + const auto shiftX = (foreground.width() + - (source.width() * scale)) / 2.; + const auto shiftY = (foreground.height() + - (source.height() * scale)) / 2.; + p.save(); + p.translate(foreground.x() + shiftX, foreground.y() + shiftY); + p.scale(scale, scale); + _iconForeground->paint(p, 0, 0, source.width()); + p.restore(); + } + } +} + +not_null AddDetailedSettingsButton( + not_null container, + rpl::producer title, + rpl::producer description, + Settings::IconDescriptor icon, + rpl::producer toggled, + const style::DetailedSettingsButtonStyle &rowStyle) { + return container->add(object_ptr( + container, + std::move(title), + std::move(description), + std::move(icon), + std::move(toggled), + rowStyle)); +} diff --git a/Telegram/SourceFiles/settings/detailed_settings_button.h b/Telegram/SourceFiles/settings/detailed_settings_button.h new file mode 100644 index 0000000000..76b070fa3d --- /dev/null +++ b/Telegram/SourceFiles/settings/detailed_settings_button.h @@ -0,0 +1,76 @@ +#pragma once + +#include "settings/settings_common.h" +#include "ui/text/text.h" +#include "ui/widgets/buttons.h" +#include "ui/widgets/checkbox.h" + +namespace Ui { +class VerticalLayout; +} // namespace Ui + +namespace style { +struct DetailedSettingsButtonStyle; +} // namespace style + +class DetailedSettingsButton final : public Ui::RippleButton { +public: + DetailedSettingsButton( + QWidget *parent, + rpl::producer title, + rpl::producer description, + Settings::IconDescriptor icon, + rpl::producer toggled, + const style::DetailedSettingsButtonStyle &rowStyle); + + QString accessibilityName() override; + Ui::AccessibilityState accessibilityState() const override; +#if QT_VERSION >= QT_VERSION_CHECK(6, 11, 0) + QAccessible::Role accessibilityRole() override; +#endif + + [[nodiscard]] bool toggled() const; + [[nodiscard]] rpl::producer toggledChanges() const; + [[nodiscard]] rpl::producer> clickAreaEvents() const; + void setToggleLocked(bool locked); + +protected: + void onStateChanged(State was, StateChangeSource source) override; + void paintEvent(QPaintEvent *e) override; + int resizeGetHeight(int newWidth) override; + +private: + void refreshLayout(); + [[nodiscard]] int titleTop() const; + [[nodiscard]] int titleHeight() const; + [[nodiscard]] int descriptionTopValue() const; + [[nodiscard]] int firstDescriptionLineBottom() const; + [[nodiscard]] int textAvailableWidth(int outerw) const; + [[nodiscard]] QRect toggleRect() const; + [[nodiscard]] QRect iconAreaRect() const; + [[nodiscard]] QRect iconRect() const; + [[nodiscard]] QRect iconForegroundRect(QRect iconRect) const; + [[nodiscard]] int iconSize() const; + void paintIcon(QPainter &p) const; + + const style::DetailedSettingsButtonStyle &_style; + Ui::Text::String _title; + Ui::Text::String _description; + std::unique_ptr _toggle; + const style::icon *_iconForeground = nullptr; + const style::color *_iconBackground = nullptr; + const std::optional _iconBackgroundBrush; + int _titleWidth = 0; + int _descriptionWidth = 0; + int _descriptionTop = 0; + int _descriptionHeight = 0; + +}; + +[[nodiscard]] not_null AddDetailedSettingsButton( + not_null container, + rpl::producer title, + rpl::producer description, + Settings::IconDescriptor icon, + rpl::producer toggled, + const style::DetailedSettingsButtonStyle &rowStyle); diff --git a/Telegram/SourceFiles/settings/settings.style b/Telegram/SourceFiles/settings/settings.style index 2a5249c142..20009ddf9a 100644 --- a/Telegram/SourceFiles/settings/settings.style +++ b/Telegram/SourceFiles/settings/settings.style @@ -762,3 +762,33 @@ settingsAgeVerifyTitle: FlatLabel(boxTitle) { settingsAgeVerifyIcon: icon {{ "settings/filled_verify_age-48x48", windowFgActive }}; settingsAgeVerifyIconPadding: margins(16px, 16px, 16px, 16px); settingsAgeVerifyIconMargin: margins(0px, 24px, 0px, 14px); + +DetailedSettingsButtonStyle { + button: SettingsButton; + description: FlatLabel; + iconRadius: pixels; + iconPadding: pixels; + iconForegroundPadding: pixels; + descriptionTopSkip: pixels; + descriptionBottomSkip: pixels; + descriptionRightSkip: pixels; +} + +detailedSettingsButtonStyle: DetailedSettingsButtonStyle { + button: SettingsButton(settingsButtonNoIconLocked) { + style: defaultTextStyle; + textBg: transparent; + textBgOver: transparent; + padding: margins(68px, 8px, 22px, 8px); + iconLeft: 17px; + } + description: FlatLabel(defaultFlatLabel) { + textFg: windowSubTextFg; + } + iconRadius: 10px; + iconPadding: 6px; + iconForegroundPadding: 3px; + descriptionTopSkip: 0px; + descriptionBottomSkip: 8px; + descriptionRightSkip: 4px; +}