From ac8117a6d8aa2b6e47243a31c71c95ab4452b9f8 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Mon, 27 Nov 2023 20:08:59 +0300 Subject: [PATCH] Improved style of input field for login code. --- Telegram/SourceFiles/intro/intro.style | 6 + Telegram/SourceFiles/intro/intro_code.cpp | 93 ++---- Telegram/SourceFiles/intro/intro_code.h | 23 +- .../SourceFiles/intro/intro_code_input.cpp | 308 ++++++++++++++++++ Telegram/SourceFiles/intro/intro_code_input.h | 49 +++ Telegram/cmake/td_ui.cmake | 3 + 6 files changed, 389 insertions(+), 93 deletions(-) create mode 100644 Telegram/SourceFiles/intro/intro_code_input.cpp create mode 100644 Telegram/SourceFiles/intro/intro_code_input.h diff --git a/Telegram/SourceFiles/intro/intro.style b/Telegram/SourceFiles/intro/intro.style index e94a8fb4da..c3b078706a 100644 --- a/Telegram/SourceFiles/intro/intro.style +++ b/Telegram/SourceFiles/intro/intro.style @@ -115,6 +115,12 @@ introPassword: introCountry; introPasswordTop: 74px; introPasswordHintTop: 151px; +introCodeDigitFont: font(20px); +introCodeDigitHeight: 50px; +introCodeDigitBorderWidth: 4px; +introCodeDigitSkip: 10px; +introCodeDigitAnimatioDuration: 120; + introPasswordHint: FlatLabel(introDescription) { textFg: windowFg; } diff --git a/Telegram/SourceFiles/intro/intro_code.cpp b/Telegram/SourceFiles/intro/intro_code.cpp index d2192fdffe..e5d09140ca 100644 --- a/Telegram/SourceFiles/intro/intro_code.cpp +++ b/Telegram/SourceFiles/intro/intro_code.cpp @@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "intro/intro_code.h" #include "lang/lang_keys.h" +#include "intro/intro_code_input.h" #include "intro/intro_signup.h" #include "intro/intro_password_check.h" #include "boxes/abstract_box.h" @@ -26,67 +27,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Intro { namespace details { -CodeInput::CodeInput( - QWidget *parent, - const style::InputField &st, - rpl::producer placeholder) -: Ui::MaskedInputField(parent, st, std::move(placeholder)) { -} - -void CodeInput::setDigitsCountMax(int digitsCount) { - _digitsCountMax = digitsCount; -} - -void CodeInput::correctValue(const QString &was, int wasCursor, QString &now, int &nowCursor) { - QString newText; - int oldPos(nowCursor), newPos(-1), oldLen(now.length()), digitCount = 0; - for (int i = 0; i < oldLen; ++i) { - if (now[i].isDigit()) { - ++digitCount; - } - } - accumulate_min(digitCount, _digitsCountMax); - auto strict = (digitCount == _digitsCountMax); - - newText.reserve(oldLen); - for (int i = 0; i < oldLen; ++i) { - QChar ch(now[i]); - if (ch.isDigit()) { - if (!digitCount--) { - break; - } - newText += ch; - if (strict && !digitCount) { - break; - } - } else if (ch == '-') { - newText += ch; - } - if (i == oldPos) { - newPos = newText.length(); - } - } - if (newPos < 0 || newPos > newText.size()) { - newPos = newText.size(); - } - if (newText != now) { - now = newText; - setText(now); - startPlaceholderAnimation(); - } - if (newPos != nowCursor) { - nowCursor = newPos; - setCursorPosition(nowCursor); - } -} - CodeWidget::CodeWidget( QWidget *parent, not_null account, not_null data) : Step(parent, account, data) , _noTelegramCode(this, tr::lng_code_no_telegram(tr::now), st::introLink) -, _code(this, st::introCode, tr::lng_code_ph()) +, _code(this) , _callTimer([=] { sendCall(); }) , _callStatus(getData()->callStatus) , _callTimeout(getData()->callTimeout) @@ -97,7 +44,6 @@ CodeWidget::CodeWidget( refreshLang(); }, lifetime()); - connect(_code, &CodeInput::changed, [=] { codeChanged(); }); _noTelegramCode->addClickHandler([=] { noTelegramCode(); }); _code->setDigitsCountMax(getData()->codeLength); @@ -111,9 +57,15 @@ CodeWidget::CodeWidget( }) | rpl::flatten_latest()); account->setHandleLoginCode([=](const QString &code) { - _code->setText(code); - submitCode(); + _code->setCode(code); + _code->requestCode(); }); + + _code->codeCollected( + ) | rpl::start_with_next([=](const QString &code) { + hideError(); + submitCode(code); + }, lifetime()); } void CodeWidget::refreshLang() { @@ -210,7 +162,7 @@ void CodeWidget::showCodeError(rpl::producer text) { } void CodeWidget::setInnerFocus() { - _code->setFocusFast(); + _code->setFocus(); } void CodeWidget::activate() { @@ -233,7 +185,7 @@ void CodeWidget::finished() { cancelled(); _sentCode.clear(); - _code->setText(QString()); + _code->clear(); } void CodeWidget::cancelled() { @@ -267,6 +219,7 @@ void CodeWidget::checkRequest() { void CodeWidget::codeSubmitDone(const MTPauth_Authorization &result) { stopCheck(); + _code->setEnabled(true); _sentRequest = 0; finish(result); } @@ -274,12 +227,16 @@ void CodeWidget::codeSubmitDone(const MTPauth_Authorization &result) { void CodeWidget::codeSubmitFail(const MTP::Error &error) { if (MTP::IsFloodError(error)) { stopCheck(); + _code->setEnabled(true); + _code->setFocus(); _sentRequest = 0; showCodeError(tr::lng_flood_error()); return; } stopCheck(); + _code->setEnabled(true); + _code->setFocus(); _sentRequest = 0; auto &err = error.type(); if (err == u"PHONE_NUMBER_INVALID"_q @@ -303,11 +260,6 @@ void CodeWidget::codeSubmitFail(const MTP::Error &error) { } } -void CodeWidget::codeChanged() { - hideError(); - submitCode(); -} - void CodeWidget::sendCall() { if (_callStatus == CallStatus::Waiting) { if (--_callTimeout <= 0) { @@ -370,19 +322,13 @@ void CodeWidget::gotPassword(const MTPaccount_Password &result) { void CodeWidget::submit() { if (getData()->codeByFragmentUrl.isEmpty()) { - submitCode(); + _code->requestCode(); } else { File::OpenUrl(getData()->codeByFragmentUrl); } } -void CodeWidget::submitCode() { - const auto text = QString( - _code->getLastText() - ).remove( - TextUtilities::RegExpDigitsExclude() - ).mid(0, getData()->codeLength); - +void CodeWidget::submitCode(const QString &text) { if (_sentRequest || _sentCode == text || text.size() != getData()->codeLength) { @@ -394,6 +340,7 @@ void CodeWidget::submitCode() { _checkRequestTimer.callEach(1000); _sentCode = text; + _code->setEnabled(false); getData()->pwdState = Core::CloudPasswordState(); _sentRequest = api().request(MTPauth_SignIn( MTP_flags(MTPauth_SignIn::Flag::f_phone_code), diff --git a/Telegram/SourceFiles/intro/intro_code.h b/Telegram/SourceFiles/intro/intro_code.h index 65f39117b2..53a94b50dc 100644 --- a/Telegram/SourceFiles/intro/intro_code.h +++ b/Telegram/SourceFiles/intro/intro_code.h @@ -16,6 +16,7 @@ namespace Ui { class RoundButton; class LinkButton; class FlatLabel; +class CodeInput; } // namespace Ui namespace Intro { @@ -23,23 +24,6 @@ namespace details { enum class CallStatus; -class CodeInput final : public Ui::MaskedInputField { -public: - CodeInput( - QWidget *parent, - const style::InputField &st, - rpl::producer placeholder); - - void setDigitsCountMax(int digitsCount); - -protected: - void correctValue(const QString &was, int wasCursor, QString &now, int &nowCursor) override; - -private: - int _digitsCountMax = 5; - -}; - class CodeWidget final : public Step { public: CodeWidget( @@ -65,7 +49,6 @@ protected: private: void noTelegramCode(); - void codeChanged(); void sendCall(); void checkRequest(); @@ -85,14 +68,14 @@ private: void noTelegramCodeDone(const MTPauth_SentCode &result); void noTelegramCodeFail(const MTP::Error &result); - void submitCode(); + void submitCode(const QString &text); void stopCheck(); object_ptr _noTelegramCode; mtpRequestId _noTelegramCodeRequestId = 0; - object_ptr _code; + object_ptr _code; QString _sentCode; mtpRequestId _sentRequest = 0; diff --git a/Telegram/SourceFiles/intro/intro_code_input.cpp b/Telegram/SourceFiles/intro/intro_code_input.cpp new file mode 100644 index 0000000000..ff06c1f92d --- /dev/null +++ b/Telegram/SourceFiles/intro/intro_code_input.cpp @@ -0,0 +1,308 @@ +// This file is part of Desktop App Toolkit, +// a set of libraries for developing nice desktop applications. +// +// For license and copyright information please follow this link: +// https://github.com/desktop-app/legal/blob/master/LEGAL +// +#include "intro/intro_code_input.h" + +#include "ui/abstract_button.h" +#include "ui/effects/shake_animation.h" +#include "ui/rect.h" +#include "ui/text/text_entity.h" +#include "styles/style_intro.h" +#include "styles/style_layers.h" // boxRadius + +#include + +namespace Ui { +namespace { + +constexpr auto kDigitNone = int(-1); + +[[nodiscard]] int Circular(int left, int right) { + return ((left % right) + right) % right; +} + +class Shaker final { +public: + explicit Shaker(not_null widget); + + void shake(); + +private: + const not_null _widget; + Ui::Animations::Simple _animation; + +}; + +Shaker::Shaker(not_null widget) +: _widget(widget) { +} + +void Shaker::shake() { + if (_animation.animating()) { + return; + } + _animation.start(DefaultShakeCallback([=, x = _widget->x()](int shift) { + _widget->moveToLeft(x + shift, _widget->y()); + }), 0., 1., st::shakeDuration); +} + +} // namespace + +class CodeDigit final : public Ui::AbstractButton { +public: + explicit CodeDigit(not_null widget); + + void setDigit(int digit); + [[nodiscard]] int digit() const; + + void setBorderColor(const QBrush &brush); + void shake(); + +protected: + void paintEvent(QPaintEvent *e) override; + +private: + Shaker _shaker; + Ui::Animations::Simple _animation; + int _dataDigit = kDigitNone; + int _viewDigit = kDigitNone; + + QPen _borderPen; + +}; + +CodeDigit::CodeDigit(not_null widget) +: Ui::AbstractButton(widget) +, _shaker(this) { + setBorderColor(st::windowBgRipple); +} + +void CodeDigit::setDigit(int digit) { + if ((_dataDigit == digit) && _animation.animating()) { + return; + } + _dataDigit = digit; + if (_viewDigit != digit) { + constexpr auto kDuration = st::introCodeDigitAnimatioDuration; + _animation.stop(); + if (digit == kDigitNone) { + _animation.start([=](float64 value) { + update(); + if (!value) { + _viewDigit = digit; + } + }, 1., 0., kDuration); + } else { + _viewDigit = digit; + _animation.start([=] { update(); }, 0., 1., kDuration); + } + } +} + +int CodeDigit::digit() const { + return _dataDigit; +} + +void CodeDigit::setBorderColor(const QBrush &brush) { + _borderPen = QPen(brush, st::introCodeDigitBorderWidth); + update(); +} + +void CodeDigit::shake() { + _shaker.shake(); +} + +void CodeDigit::paintEvent(QPaintEvent *e) { + auto p = QPainter(this); + + auto clipPath = QPainterPath(); + clipPath.addRoundedRect(rect(), st::boxRadius, st::boxRadius); + p.setClipPath(clipPath); + + p.fillRect(rect(), st::windowBgOver); + p.strokePath(clipPath, _borderPen); + + if (_viewDigit == kDigitNone) { + return; + } + const auto hiding = (_dataDigit == kDigitNone); + const auto progress = _animation.value(1.); + + if (hiding) { + p.setOpacity(progress * progress); + const auto center = rect().center(); + p.setTransform(QTransform() + .translate(center.x(), center.y()) + .scale(progress, progress) + .translate(-center.x(), -center.y())); + } else { + p.setOpacity(progress); + constexpr auto kSlideDistanceRatio = 0.2; + const auto distance = rect().height() * kSlideDistanceRatio; + p.translate(0, (distance * (1. - progress))); + } + p.setFont(st::introCodeDigitFont); + p.setPen(st::windowFg); + p.drawText(rect(), QString::number(_viewDigit), style::al_center); +} + +CodeInput::CodeInput(QWidget *parent) +: Ui::RpWidget(parent) { + setFocusPolicy(Qt::StrongFocus); +} + +void CodeInput::setDigitsCountMax(int digitsCount) { + _digitsCountMax = digitsCount; + + _digits.clear(); + _currentIndex = 0; + + constexpr auto kWidthRatio = 0.8; + const auto digitWidth = st::introCodeDigitHeight * kWidthRatio; + const auto padding = Margins(st::introCodeDigitSkip); + resize( + padding.left() + + digitWidth * digitsCount + + st::introCodeDigitSkip * (digitsCount - 1) + + padding.right(), + st::introCodeDigitHeight); + + for (auto i = 0; i < digitsCount; i++) { + const auto widget = Ui::CreateChild(this); + widget->setPointerCursor(false); + widget->setClickedCallback([=] { unfocusAll(_currentIndex = i); }); + widget->resize(digitWidth, st::introCodeDigitHeight); + widget->moveToLeft( + padding.left() + (digitWidth + st::introCodeDigitSkip) * i, + 0); + _digits.emplace_back(widget); + } +} + +void CodeInput::setCode(QString code) { + using namespace TextUtilities; + code = code.remove(RegExpDigitsExclude()).mid(0, _digitsCountMax); + for (int i = 0; i < _digits.size(); i++) { + if (i >= code.size()) { + return; + } + _digits[i]->setDigit(code.at(i).digitValue()); + } +} + +void CodeInput::requestCode() { + const auto result = collectDigits(); + if (result.size() == _digitsCountMax) { + _codeCollected.fire_copy(result); + } else { + findEmptyAndPerform([&](int i) { _digits[i]->shake(); }); + } +} + +rpl::producer CodeInput::codeCollected() const { + return _codeCollected.events(); +} + +void CodeInput::clear() { + for (const auto &digit : _digits) { + digit->setDigit(kDigitNone); + } + unfocusAll(_currentIndex = 0); +} + +void CodeInput::showError() { + clear(); + for (const auto &digit : _digits) { + digit->shake(); + digit->setBorderColor(st::activeLineFgError); + } +} + +void CodeInput::focusInEvent(QFocusEvent *e) { + unfocusAll(_currentIndex); +} + +void CodeInput::focusOutEvent(QFocusEvent *e) { + unfocusAll(kDigitNone); +} + +void CodeInput::paintEvent(QPaintEvent *e) { + auto p = QPainter(this); + p.fillRect(rect(), st::windowBg); +} + +void CodeInput::keyPressEvent(QKeyEvent *e) { + const auto key = e->key(); + if (key == Qt::Key_Down || key == Qt::Key_Right || key == Qt::Key_Space) { + _currentIndex = Circular(_currentIndex + 1, _digits.size()); + unfocusAll(_currentIndex); + } else if (key == Qt::Key_Up || key == Qt::Key_Left) { + _currentIndex = Circular(_currentIndex - 1, _digits.size()); + unfocusAll(_currentIndex); + } else if (key >= Qt::Key_0 && key <= Qt::Key_9) { + const auto index = int(key - Qt::Key_0); + _digits[_currentIndex]->setDigit(index); + _currentIndex = Circular(_currentIndex + 1, _digits.size()); + if (!_currentIndex) { + const auto result = collectDigits(); + if (result.size() == _digitsCountMax) { + _codeCollected.fire_copy(result); + _currentIndex = _digits.size() - 1; + } else { + findEmptyAndPerform([&](int i) { _currentIndex = i; }); + } + } + unfocusAll(_currentIndex); + } else if (key == Qt::Key_Delete) { + _digits[_currentIndex]->setDigit(kDigitNone); + } else if (key == Qt::Key_Backspace) { + const auto wasDigit = _digits[_currentIndex]->digit(); + _digits[_currentIndex]->setDigit(kDigitNone); + _currentIndex = std::clamp(_currentIndex - 1, 0, int(_digits.size())); + if (wasDigit == kDigitNone) { + _digits[_currentIndex]->setDigit(kDigitNone); + } + unfocusAll(_currentIndex); + } else if (key == Qt::Key_Enter || key == Qt::Key_Return) { + requestCode(); + } else if (key >= Qt::Key_A && key <= Qt::Key_Z) { + _digits[_currentIndex]->shake(); + } else if (key == Qt::Key_Home || key == Qt::Key_PageUp) { + unfocusAll(_currentIndex = 0); + } else if (key == Qt::Key_End || key == Qt::Key_PageDown) { + unfocusAll(_currentIndex = (_digits.size() - 1)); + } +} + +QString CodeInput::collectDigits() const { + auto result = QString(); + for (const auto &digit : _digits) { + if (digit->digit() != kDigitNone) { + result += QString::number(digit->digit()); + } + } + return result; +} + +void CodeInput::unfocusAll(int except) { + for (auto i = 0; i < _digits.size(); i++) { + const auto focused = (i == except); + _digits[i]->setBorderColor(focused + ? st::windowActiveTextFg + : st::windowBgRipple); + } +} + +void CodeInput::findEmptyAndPerform(const Fn &callback) { + for (auto i = 0; i < _digits.size(); i++) { + if (_digits[i]->digit() == kDigitNone) { + callback(i); + break; + } + } +} + +} // namespace Ui diff --git a/Telegram/SourceFiles/intro/intro_code_input.h b/Telegram/SourceFiles/intro/intro_code_input.h new file mode 100644 index 0000000000..fc030fcde3 --- /dev/null +++ b/Telegram/SourceFiles/intro/intro_code_input.h @@ -0,0 +1,49 @@ +// This file is part of Desktop App Toolkit, +// a set of libraries for developing nice desktop applications. +// +// For license and copyright information please follow this link: +// https://github.com/desktop-app/legal/blob/master/LEGAL +// +#pragma once + +#include "ui/rp_widget.h" + +namespace Ui { + +class CodeDigit; + +class CodeInput final : public Ui::RpWidget { +public: + CodeInput(QWidget *parent); + + void setDigitsCountMax(int digitsCount); + + void setCode(QString code); + + void requestCode(); + [[nodiscard]] rpl::producer codeCollected() const; + + void clear(); + void showError(); + +protected: + void focusInEvent(QFocusEvent *e) override; + void focusOutEvent(QFocusEvent *e) override; + void paintEvent(QPaintEvent *e) override; + void keyPressEvent(QKeyEvent *e) override; + +private: + [[nodiscard]] QString collectDigits() const; + + void unfocusAll(int except); + void findEmptyAndPerform(const Fn &callback); + + int _digitsCountMax = 0; + std::vector> _digits; + int _currentIndex = 0; + + rpl::event_stream _codeCollected; + +}; + +} // namespace Ui diff --git a/Telegram/cmake/td_ui.cmake b/Telegram/cmake/td_ui.cmake index 40c38c6851..9c533fc823 100644 --- a/Telegram/cmake/td_ui.cmake +++ b/Telegram/cmake/td_ui.cmake @@ -117,6 +117,9 @@ PRIVATE info/boosts/giveaway/select_countries_box.cpp info/boosts/giveaway/select_countries_box.h + intro/intro_code_input.cpp + intro/intro_code_input.h + layout/abstract_layout_item.cpp layout/abstract_layout_item.h layout/layout_mosaic.cpp