From 8ff42a86d9cdff1a902ddebfccb38bf69f5180ed Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Fri, 6 Aug 2021 04:07:34 +0300 Subject: [PATCH 01/18] Moved time input widgets to lib_ui. --- CMakeLists.txt | 2 + ui/ui_utility.cpp | 12 + ui/ui_utility.h | 3 + ui/widgets/time_input.cpp | 468 ++++++++++++++++++++++++++++++++++++++ ui/widgets/time_input.h | 78 +++++++ 5 files changed, 563 insertions(+) create mode 100644 ui/widgets/time_input.cpp create mode 100644 ui/widgets/time_input.h diff --git a/CMakeLists.txt b/CMakeLists.txt index f463fed..6119c75 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -188,6 +188,8 @@ PRIVATE ui/widgets/side_bar_button.h ui/widgets/shadow.cpp ui/widgets/shadow.h + ui/widgets/time_input.cpp + ui/widgets/time_input.h ui/widgets/tooltip.cpp ui/widgets/tooltip.h ui/wrap/fade_wrap.cpp diff --git a/ui/ui_utility.cpp b/ui/ui_utility.cpp index f37e607..6388228 100644 --- a/ui/ui_utility.cpp +++ b/ui/ui_utility.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include @@ -220,4 +221,15 @@ void DisableCustomScaling() { } } +int WheelDirection(not_null e) { + // Only a mouse wheel is accepted. + constexpr auto step = static_cast(QWheelEvent::DefaultDeltasPerStep); + const auto delta = e->angleDelta().y(); + const auto absDelta = std::abs(delta); + if (absDelta != step) { + return 0; + } + return (delta / absDelta); +} + } // namespace Ui diff --git a/ui/ui_utility.h b/ui/ui_utility.h index ece602d..f7f6fb1 100644 --- a/ui/ui_utility.h +++ b/ui/ui_utility.h @@ -14,6 +14,7 @@ class QPixmap; class QImage; +class QWheelEvent; enum class RectPart; using RectParts = base::flags; @@ -193,4 +194,6 @@ QPointer MakeWeak(not_null object) { void DisableCustomScaling(); +int WheelDirection(not_null e); + } // namespace Ui diff --git a/ui/widgets/time_input.cpp b/ui/widgets/time_input.cpp new file mode 100644 index 0000000..318e1a1 --- /dev/null +++ b/ui/widgets/time_input.cpp @@ -0,0 +1,468 @@ +// 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 "ui/widgets/time_input.h" + +#include "ui/widgets/input_fields.h" +#include "ui/ui_utility.h" + +#include +#include + +namespace Ui { +namespace { + +QTime ValidateTime(const QString &value) { + const auto match = QRegularExpression( + "^(\\d{1,2})\\:(\\d\\d)$").match(value); + if (!match.hasMatch()) { + return QTime(); + } + const auto readInt = [](const QString &value) { + auto ref = value.midRef(0); + while (!ref.isEmpty() && ref.at(0) == '0') { + ref = ref.mid(1); + } + return ref.toInt(); + }; + return QTime(readInt(match.captured(1)), readInt(match.captured(2))); +} + +QString GetHour(const QString &value) { + if (const auto time = ValidateTime(value); time.isValid()) { + return QString::number(time.hour()); + } + return QString(); +} + +QString GetMinute(const QString &value) { + if (const auto time = ValidateTime(value); time.isValid()) { + return QString("%1").arg(time.minute(), 2, 10, QChar('0')); + } + return QString(); +} + +} // namespace + +class TimePart final : public MaskedInputField { +public: + using MaskedInputField::MaskedInputField; + + void setMaxValue(int value); + void setWheelStep(int value); + + rpl::producer<> erasePrevious() const; + rpl::producer putNext() const; + +protected: + void keyPressEvent(QKeyEvent *e) override; + void wheelEvent(QWheelEvent *e) override; + + void correctValue( + const QString &was, + int wasCursor, + QString &now, + int &nowCursor) override; + +private: + int _maxValue = 0; + int _maxDigits = 0; + int _wheelStep = 0; + rpl::event_stream<> _erasePrevious; + rpl::event_stream _putNext; + +}; + +int Number(not_null field) { + const auto text = field->getLastText(); + auto ref = text.midRef(0); + while (!ref.isEmpty() && ref.at(0) == '0') { + ref = ref.mid(1); + } + return ref.toInt(); +} + +void TimePart::setMaxValue(int value) { + _maxValue = value; + _maxDigits = 0; + while (value > 0) { + ++_maxDigits; + value /= 10; + } +} + +void TimePart::setWheelStep(int value) { + _wheelStep = value; +} + +rpl::producer<> TimePart::erasePrevious() const { + return _erasePrevious.events(); +} + +rpl::producer TimePart::putNext() const { + return _putNext.events(); +} + +void TimePart::keyPressEvent(QKeyEvent *e) { + const auto isBackspace = (e->key() == Qt::Key_Backspace); + const auto isBeginning = (cursorPosition() == 0); + if (isBackspace && isBeginning && !hasSelectedText()) { + _erasePrevious.fire({}); + } else { + MaskedInputField::keyPressEvent(e); + } +} + +void TimePart::wheelEvent(QWheelEvent *e) { + const auto direction = WheelDirection(e); + auto time = Number(this) + (direction * _wheelStep); + const auto max = _maxValue + 1; + if (time < 0) { + time += max; + } else if (time >= max) { + time -= max; + } + setText(QString::number(time)); +} + +void TimePart::correctValue( + const QString &was, + int wasCursor, + QString &now, + int &nowCursor) { + auto newText = QString(); + auto newCursor = -1; + const auto oldCursor = nowCursor; + const auto oldLength = now.size(); + auto accumulated = 0; + auto limit = 0; + for (; limit != oldLength; ++limit) { + if (now[limit].isDigit()) { + accumulated *= 10; + accumulated += (now[limit].unicode() - '0'); + if (accumulated > _maxValue || limit == _maxDigits) { + break; + } + } + } + for (auto i = 0; i != limit;) { + if (now[i].isDigit()) { + newText += now[i]; + } + if (++i == oldCursor) { + newCursor = newText.size(); + } + } + if (newCursor < 0) { + newCursor = newText.size(); + } + if (newText != now) { + now = newText; + setText(now); + startPlaceholderAnimation(); + } + if (newCursor != nowCursor) { + nowCursor = newCursor; + setCursorPosition(nowCursor); + } + if (accumulated > _maxValue + || (limit == _maxDigits && oldLength > _maxDigits)) { + if (oldCursor > limit) { + _putNext.fire('0' + (accumulated % 10)); + } else { + _putNext.fire(0); + } + } +} + +TimeInput::TimeInput( + QWidget *parent, + const QString &value, + const style::InputField &stField, + const style::InputField &stDateField, + const style::FlatLabel &stSeparator, + const style::margins &stSeparatorPadding) +: RpWidget(parent) +, _stField(stField) +, _stDateField(stDateField) +, _stSeparator(stSeparator) +, _stSeparatorPadding(stSeparatorPadding) +, _hour( + this, + _stField, + rpl::never(), + GetHour(value)) +, _separator1( + this, + object_ptr( + this, + QString(":"), + _stSeparator), + _stSeparatorPadding) +, _minute( + this, + _stField, + rpl::never(), + GetMinute(value)) +, _value(valueCurrent()) { + const auto focused = [=](const object_ptr &field) { + return [this, pointer = MakeWeak(field.data())]{ + _borderAnimationStart = pointer->borderAnimationStart() + + pointer->x() + - _hour->x(); + setFocused(true); + }; + }; + const auto blurred = [=] { + setFocused(false); + }; + const auto changed = [=] { + _value = valueCurrent(); + }; + connect(_hour, &MaskedInputField::focused, focused(_hour)); + connect(_minute, &MaskedInputField::focused, focused(_minute)); + connect(_hour, &MaskedInputField::blurred, blurred); + connect(_minute, &MaskedInputField::blurred, blurred); + connect(_hour, &MaskedInputField::changed, changed); + connect(_minute, &MaskedInputField::changed, changed); + _hour->setMaxValue(23); + _hour->setWheelStep(1); + _hour->putNext() | rpl::start_with_next([=](QChar ch) { + putNext(_minute, ch); + }, lifetime()); + _minute->setMaxValue(59); + _minute->setWheelStep(10); + _minute->erasePrevious() | rpl::start_with_next([=] { + erasePrevious(_hour); + }, lifetime()); + _separator1->setAttribute(Qt::WA_TransparentForMouseEvents); + setMouseTracking(true); + + _value.changes( + ) | rpl::start_with_next([=] { + setErrorShown(false); + }, lifetime()); + + const auto submitHour = [=] { + if (hour()) { + _minute->setFocus(); + } + }; + const auto submitMinute = [=] { + if (minute()) { + if (hour()) { + _submitRequests.fire({}); + } else { + _hour->setFocus(); + } + } + }; + connect( + _hour, + &MaskedInputField::submitted, + submitHour); + connect( + _minute, + &MaskedInputField::submitted, + submitMinute); +} + +void TimeInput::putNext(const object_ptr &field, QChar ch) { + field->setCursorPosition(0); + if (ch.unicode()) { + field->setText(ch + field->getLastText()); + field->setCursorPosition(1); + } + field->setFocus(); +} + +void TimeInput::erasePrevious(const object_ptr &field) { + const auto text = field->getLastText(); + if (!text.isEmpty()) { + field->setCursorPosition(text.size() - 1); + field->setText(text.mid(0, text.size() - 1)); + } + field->setFocus(); +} + +bool TimeInput::setFocusFast() { + if (hour()) { + _minute->setFocusFast(); + } else { + _hour->setFocusFast(); + } + return true; +} + +int TimeInput::hour() const { + return Number(_hour); +} + +int TimeInput::minute() const { + return Number(_minute); +} + +QString TimeInput::valueCurrent() const { + const auto result = QString("%1:%2" + ).arg(hour() + ).arg(minute(), 2, 10, QChar('0')); + return ValidateTime(result).isValid() ? result : QString(); +} + +rpl::producer TimeInput::value() const { + return _value.value(); +} + +rpl::producer<> TimeInput::submitRequests() const { + return _submitRequests.events(); +} + +void TimeInput::paintEvent(QPaintEvent *e) { + Painter p(this); + + const auto &_st = _stDateField; + const auto height = _st.heightMin; + if (_st.border) { + p.fillRect(0, height - _st.border, width(), _st.border, _st.borderFg); + } + auto errorDegree = _a_error.value(_error ? 1. : 0.); + auto borderShownDegree = _a_borderShown.value(1.); + auto borderOpacity = _a_borderOpacity.value(_borderVisible ? 1. : 0.); + if (_st.borderActive && (borderOpacity > 0.)) { + auto borderStart = std::clamp(_borderAnimationStart, 0, width()); + auto borderFrom = qRound(borderStart * (1. - borderShownDegree)); + auto borderTo = borderStart + qRound((width() - borderStart) * borderShownDegree); + if (borderTo > borderFrom) { + auto borderFg = anim::brush(_st.borderFgActive, _st.borderFgError, errorDegree); + p.setOpacity(borderOpacity); + p.fillRect(borderFrom, height - _st.borderActive, borderTo - borderFrom, _st.borderActive, borderFg); + p.setOpacity(1); + } + } +} + +template +bool TimeInput::insideSeparator(QPoint position, const Widget &widget) const { + const auto x = position.x(); + const auto y = position.y(); + return (x >= widget->x() && x < widget->x() + widget->width()) + && (y >= _hour->y() && y < _hour->y() + _hour->height()); +} + +void TimeInput::mouseMoveEvent(QMouseEvent *e) { + const auto cursor = insideSeparator(e->pos(), _separator1) + ? style::cur_text + : style::cur_default; + if (_cursor != cursor) { + _cursor = cursor; + setCursor(_cursor); + } +} + +void TimeInput::mousePressEvent(QMouseEvent *e) { + const auto x = e->pos().x(); + const auto focus1 = [&] { + if (_hour->getLastText().size() > 1) { + _minute->setFocus(); + } else { + _hour->setFocus(); + } + }; + if (insideSeparator(e->pos(), _separator1)) { + focus1(); + _borderAnimationStart = x - _hour->x(); + } +} + +int TimeInput::resizeGetHeight(int width) { + const auto &_st = _stField; + const auto &font = _st.placeholderFont; + const auto addToWidth = _stSeparatorPadding.left(); + const auto hourWidth = _st.textMargins.left() + + _st.placeholderMargins.left() + + font->width(QString("23")) + + _st.placeholderMargins.right() + + _st.textMargins.right() + + addToWidth; + const auto minuteWidth = _st.textMargins.left() + + _st.placeholderMargins.left() + + font->width(QString("59")) + + _st.placeholderMargins.right() + + _st.textMargins.right() + + addToWidth; + const auto full = hourWidth + - addToWidth + + _separator1->width() + + minuteWidth + - addToWidth; + auto left = (width - full) / 2; + auto top = 0; + _hour->setGeometry(left, top, hourWidth, _hour->height()); + left += hourWidth - addToWidth; + _separator1->resizeToNaturalWidth(width); + _separator1->move(left, top); + left += _separator1->width(); + _minute->setGeometry(left, top, minuteWidth, _minute->height()); + return _stDateField.heightMin; +} + +void TimeInput::showError() { + setErrorShown(true); + if (!_focused) { + setInnerFocus(); + } +} + +void TimeInput::setInnerFocus() { + if (hour()) { + _minute->setFocus(); + } else { + _hour->setFocus(); + } +} + +void TimeInput::setErrorShown(bool error) { + if (_error != error) { + _error = error; + _a_error.start( + [=] { update(); }, + _error ? 0. : 1., + _error ? 1. : 0., + _stDateField.duration); + startBorderAnimation(); + } +} + +void TimeInput::setFocused(bool focused) { + if (_focused != focused) { + _focused = focused; + _a_focused.start( + [=] { update(); }, + _focused ? 0. : 1., + _focused ? 1. : 0., + _stDateField.duration); + startBorderAnimation(); + } +} + +void TimeInput::startBorderAnimation() { + auto borderVisible = (_error || _focused); + if (_borderVisible != borderVisible) { + _borderVisible = borderVisible; + const auto duration = _stDateField.duration; + if (_borderVisible) { + if (_a_borderOpacity.animating()) { + _a_borderOpacity.start([=] { update(); }, 0., 1., duration); + } else { + _a_borderShown.start([=] { update(); }, 0., 1., duration); + } + } else { + _a_borderOpacity.start([=] { update(); }, 1., 0., duration); + } + } +} + +} // namespace Ui diff --git a/ui/widgets/time_input.h b/ui/widgets/time_input.h new file mode 100644 index 0000000..1724a65 --- /dev/null +++ b/ui/widgets/time_input.h @@ -0,0 +1,78 @@ +// 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" + +#include "ui/effects/animations.h" +#include "ui/widgets/labels.h" + +namespace Ui { + +class TimePart; + +class TimeInput final : public RpWidget { +public: + TimeInput( + QWidget *parent, + const QString &value, + const style::InputField &stField, + const style::InputField &stDateField, + const style::FlatLabel &stSeparator, + const style::margins &stSeparatorPadding); + + bool setFocusFast(); + rpl::producer value() const; + rpl::producer<> submitRequests() const; + QString valueCurrent() const; + void showError(); + + int resizeGetHeight(int width) override; + +protected: + void paintEvent(QPaintEvent *e) override; + void mousePressEvent(QMouseEvent *e) override; + void mouseMoveEvent(QMouseEvent *e) override; + +private: + void setInnerFocus(); + void putNext(const object_ptr &field, QChar ch); + void erasePrevious(const object_ptr &field); + void setErrorShown(bool error); + void setFocused(bool focused); + void startBorderAnimation(); + template + bool insideSeparator(QPoint position, const Widget &widget) const; + + int hour() const; + int minute() const; + + const style::InputField &_stField; + const style::InputField &_stDateField; + const style::FlatLabel &_stSeparator; + const style::margins &_stSeparatorPadding; + + object_ptr _hour; + object_ptr> _separator1; + object_ptr _minute; + rpl::variable _value; + rpl::event_stream<> _submitRequests; + + style::cursor _cursor = style::cur_default; + Animations::Simple _a_borderShown; + int _borderAnimationStart = 0; + Animations::Simple _a_borderOpacity; + bool _borderVisible = false; + + Animations::Simple _a_error; + bool _error = false; + Animations::Simple _a_focused; + bool _focused = false; + +}; + +} // namespace Ui From 4fcddb5bfeb15350a4e749e4ead302a89b283106 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Wed, 11 Aug 2021 08:58:42 +0300 Subject: [PATCH 02/18] Added focus rpl event to time input. --- ui/widgets/time_input.cpp | 5 +++++ ui/widgets/time_input.h | 2 ++ 2 files changed, 7 insertions(+) diff --git a/ui/widgets/time_input.cpp b/ui/widgets/time_input.cpp index 318e1a1..ee80060 100644 --- a/ui/widgets/time_input.cpp +++ b/ui/widgets/time_input.cpp @@ -214,6 +214,7 @@ TimeInput::TimeInput( + pointer->x() - _hour->x(); setFocused(true); + _focuses.fire({}); }; }; const auto blurred = [=] { @@ -320,6 +321,10 @@ rpl::producer<> TimeInput::submitRequests() const { return _submitRequests.events(); } +rpl::producer<> TimeInput::focuses() const { + return _focuses.events(); +} + void TimeInput::paintEvent(QPaintEvent *e) { Painter p(this); diff --git a/ui/widgets/time_input.h b/ui/widgets/time_input.h index 1724a65..bc8a187 100644 --- a/ui/widgets/time_input.h +++ b/ui/widgets/time_input.h @@ -28,6 +28,7 @@ public: bool setFocusFast(); rpl::producer value() const; rpl::producer<> submitRequests() const; + rpl::producer<> focuses() const; QString valueCurrent() const; void showError(); @@ -61,6 +62,7 @@ private: object_ptr _minute; rpl::variable _value; rpl::event_stream<> _submitRequests; + rpl::event_stream<> _focuses; style::cursor _cursor = style::cur_default; Animations::Simple _a_borderShown; From a2d1114a93c094f9b4919ddba06d42eca40a8655 Mon Sep 17 00:00:00 2001 From: Ilya Fedin Date: Wed, 11 Aug 2021 06:24:27 +0400 Subject: [PATCH 03/18] Get rid of osx and linux32 special targets --- ui/emoji_config.cpp | 12 ++++++------ ui/emoji_config.h | 2 +- ui/style/style_core.cpp | 4 ---- ui/text/text.cpp | 14 +------------- ui/text/text_entity.cpp | 2 -- ui/ui_utility.cpp | 2 -- 6 files changed, 8 insertions(+), 28 deletions(-) diff --git a/ui/emoji_config.cpp b/ui/emoji_config.cpp index 6680854..148feeb 100644 --- a/ui/emoji_config.cpp +++ b/ui/emoji_config.cpp @@ -35,7 +35,7 @@ constexpr auto kSetVersion = uint32(2); constexpr auto kCacheVersion = uint32(6); constexpr auto kMaxId = uint32(1 << 8); -#if defined Q_OS_MAC && !defined OS_MAC_OLD +#ifdef Q_OS_MAC constexpr auto kScaleForTouchBar = 150; #endif @@ -80,7 +80,7 @@ auto CanClearUniversal = false; auto WaitingToSwitchBackToId = 0; auto Updates = rpl::event_stream<>(); -#if defined Q_OS_MAC && !defined OS_MAC_OLD +#ifdef Q_OS_MAC auto TouchbarSize = -1; auto TouchbarInstance = std::unique_ptr(); auto TouchbarEmoji = (Instance*)nullptr; @@ -498,7 +498,7 @@ void Init() { InstanceNormal = std::make_unique(SizeNormal); InstanceLarge = std::make_unique(SizeLarge); -#if defined Q_OS_MAC && !defined OS_MAC_OLD +#ifdef Q_OS_MAC if (style::Scale() != kScaleForTouchBar) { TouchbarSize = int(style::ConvertScale(18 * 4 / 3., kScaleForTouchBar * style::DevicePixelRatio())); @@ -516,7 +516,7 @@ void Clear() { InstanceNormal = nullptr; InstanceLarge = nullptr; -#if defined Q_OS_MAC && !defined OS_MAC_OLD +#ifdef Q_OS_MAC TouchbarInstance = nullptr; TouchbarEmoji = nullptr; #endif @@ -620,7 +620,7 @@ int GetSizeLarge() { return SizeLarge; } -#if defined Q_OS_MAC && !defined OS_MAC_OLD +#ifdef Q_OS_MAC int GetSizeTouchbar() { return (style::Scale() == kScaleForTouchBar) ? GetSizeLarge() @@ -763,7 +763,7 @@ const QPixmap &SinglePixmap(EmojiPtr emoji, int fontHeight) { } void Draw(QPainter &p, EmojiPtr emoji, int size, int x, int y) { -#if defined Q_OS_MAC && !defined OS_MAC_OLD +#ifdef Q_OS_MAC const auto s = (style::Scale() == kScaleForTouchBar) ? SizeLarge : TouchbarSize; diff --git a/ui/emoji_config.h b/ui/emoji_config.h index 75f10e1..3183258 100644 --- a/ui/emoji_config.h +++ b/ui/emoji_config.h @@ -40,7 +40,7 @@ void ClearNeedSwitchToId(); [[nodiscard]] int GetSizeNormal(); [[nodiscard]] int GetSizeLarge(); -#if defined Q_OS_MAC && !defined OS_MAC_OLD +#ifdef Q_OS_MAC [[nodiscard]] int GetSizeTouchbar(); #endif diff --git a/ui/style/style_core.cpp b/ui/style/style_core.cpp index 9914e50..48d1dd4 100644 --- a/ui/style/style_core.cpp +++ b/ui/style/style_core.cpp @@ -144,11 +144,7 @@ namespace internal { QImage createCircleMask(int size, QColor bg, QColor fg) { int realSize = size * DevicePixelRatio(); -#ifndef OS_MAC_OLD auto result = QImage(realSize, realSize, QImage::Format::Format_Grayscale8); -#else // OS_MAC_OLD - auto result = QImage(realSize, realSize, QImage::Format::Format_RGB32); -#endif // OS_MAC_OLD { QPainter p(&result); PainterHighQualityEnabler hq(p); diff --git a/ui/text/text.cpp b/ui/text/text.cpp index 95e22f9..6db3e73 100644 --- a/ui/text/text.cpp +++ b/ui/text/text.cpp @@ -134,14 +134,7 @@ bool IsBad(QChar ch) { || (ch >= 127 && ch < 160 && ch != 156) // qt harfbuzz crash see https://github.com/telegramdesktop/tdesktop/issues/4551 - || (Platform::IsMac() && ch == 6158) - - // tmp hack see https://bugreports.qt.io/browse/QTBUG-48910 - || (Platform::IsMac10_11OrGreater() - && !Platform::IsMac10_12OrGreater() - && ch >= 0x0B00 - && ch <= 0x0B7F - && IsDiac(ch)); + || (Platform::IsMac() && ch == 6158); } } // namespace @@ -1982,12 +1975,7 @@ private: if (item == -1) return; -#ifdef OS_MAC_OLD - auto end = _e->findItem(line.from + line.length - 1); -#else // OS_MAC_OLD auto end = _e->findItem(line.from + line.length - 1, item); -#endif // OS_MAC_OLD - auto blockIndex = _lineStartBlock; auto currentBlock = _t->_blocks[blockIndex].get(); auto nextBlock = (++blockIndex < _blocksSize) ? _t->_blocks[blockIndex].get() : nullptr; diff --git a/ui/text/text_entity.cpp b/ui/text/text_entity.cpp index b535f4e..cd4453e 100644 --- a/ui/text/text_entity.cpp +++ b/ui/text/text_entity.cpp @@ -88,9 +88,7 @@ QRegularExpression CreateRegExp(const QString &expression) { auto result = QRegularExpression( expression, QRegularExpression::UseUnicodePropertiesOption); -#ifndef OS_MAC_OLD result.optimize(); -#endif // OS_MAC_OLD return result; } diff --git a/ui/ui_utility.cpp b/ui/ui_utility.cpp index f37e607..7e5b7d8 100644 --- a/ui/ui_utility.cpp +++ b/ui/ui_utility.cpp @@ -169,9 +169,7 @@ void SendSynteticMouseEvent(QWidget *widget, QEvent::Type type, Qt::MouseButton , button , QGuiApplication::mouseButtons() | button , QGuiApplication::keyboardModifiers() -#ifndef OS_MAC_OLD , Qt::MouseEventSynthesizedByApplication -#endif // OS_MAC_OLD ); ev.setTimestamp(crl::now()); QGuiApplication::sendEvent(windowHandle, &ev); From 2bd63281b58d54aa129da78f05f0e6e73e5d63c9 Mon Sep 17 00:00:00 2001 From: John Preston Date: Wed, 11 Aug 2021 17:53:02 +0300 Subject: [PATCH 04/18] Add Images::Read method. --- ui/image/image_prepare.cpp | 48 ++++++++++++++++++++++++++++++++++++++ ui/image/image_prepare.h | 20 ++++++++++++++++ 2 files changed, 68 insertions(+) diff --git a/ui/image/image_prepare.cpp b/ui/image/image_prepare.cpp index aaa4469..1b25717 100644 --- a/ui/image/image_prepare.cpp +++ b/ui/image/image_prepare.cpp @@ -13,6 +13,10 @@ #include "styles/palette.h" #include "styles/style_basic.h" +#include +#include +#include + namespace Images { namespace { @@ -111,6 +115,50 @@ std::array PrepareCorners( return result; } +ReadResult Read(ReadArgs &&args) { + if (args.content.isEmpty()) { + auto file = QFile(args.path); + if (file.size() > kReadBytesLimit + || !file.open(QIODevice::ReadOnly)) { + return {}; + } + args.content = file.readAll(); + } + if (args.content.isEmpty()) { + return {}; + } + auto result = ReadResult(); + { + auto buffer = QBuffer(&args.content); + auto reader = QImageReader(&buffer); + reader.setAutoTransform(true); + if (!reader.canRead()) { + return {}; + } + const auto size = reader.size(); + if (size.width() * size.height() > kReadMaxArea) { + return {}; + } + if (!reader.read(&result.image) || result.image.isNull()) { + return {}; + } + result.animated = reader.supportsAnimation() + && (reader.imageCount() > 1); + result.format = reader.format().toLower(); + } + if (args.returnContent) { + result.content = args.content; + } else { + args.content = QByteArray(); + } + if (args.forceOpaque + && result.format != qstr("jpg") + && result.format != qstr("jpeg")) { + result.image = prepareOpaque(std::move(result.image)); + } + return result; +} + QImage prepareBlur(QImage img) { if (img.isNull()) { return img; diff --git a/ui/image/image_prepare.h b/ui/image/image_prepare.h index 60261b7..5683027 100644 --- a/ui/image/image_prepare.h +++ b/ui/image/image_prepare.h @@ -39,6 +39,26 @@ namespace Images { int radius, const style::color &color); + +// Try to read images up to 64MB. +inline constexpr auto kReadBytesLimit = 64 * 1024 * 1024; +inline constexpr auto kReadMaxArea = 12'032 * 9'024; + +struct ReadArgs { + QString path; + QByteArray content; + QSize maxSize; + bool forceOpaque = false; + bool returnContent = false; +}; +struct ReadResult { + QImage image; + QByteArray content; + QByteArray format; + bool animated = false; +}; +[[nodiscard]] ReadResult Read(ReadArgs &&args); + QImage prepareBlur(QImage image); void prepareRound( QImage &image, From dd88f8fa41a06bdf3128276d8084cfa4f087dee7 Mon Sep 17 00:00:00 2001 From: John Preston Date: Wed, 11 Aug 2021 19:54:59 +0300 Subject: [PATCH 05/18] Add UnpackGzip and reading gzip-ed SVGs. --- CMakeLists.txt | 2 + ui/image/image_prepare.cpp | 127 +++++++++++++++++++++++++++++++------ ui/image/image_prepare.h | 2 + 3 files changed, 111 insertions(+), 20 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index f463fed..235e37c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -266,4 +266,6 @@ PUBLIC target_link_libraries(lib_ui PUBLIC desktop-app::lib_base +PRIVATE + desktop-app::external_zlib ) diff --git a/ui/image/image_prepare.cpp b/ui/image/image_prepare.cpp index 1b25717..660a20f 100644 --- a/ui/image/image_prepare.cpp +++ b/ui/image/image_prepare.cpp @@ -10,16 +10,22 @@ #include "ui/style/style_core.h" #include "ui/painter.h" #include "base/flat_map.h" +#include "base/debug_log.h" #include "styles/palette.h" #include "styles/style_basic.h" +#include "zlib.h" #include #include #include +#include namespace Images { namespace { +// They should be smaller. +constexpr auto kMaxGzipFileSize = 5 * 1024 * 1024; + TG_FORCE_INLINE uint64 blurGetColors(const uchar *p) { return (uint64)p[0] + ((uint64)p[1] << 16) + ((uint64)p[2] << 32) + ((uint64)p[3] << 48); } @@ -115,6 +121,96 @@ std::array PrepareCorners( return result; } +[[nodiscard]] QByteArray UnpackGzip(const QByteArray &bytes) { + z_stream stream; + stream.zalloc = nullptr; + stream.zfree = nullptr; + stream.opaque = nullptr; + stream.avail_in = 0; + stream.next_in = nullptr; + int res = inflateInit2(&stream, 16 + MAX_WBITS); + if (res != Z_OK) { + return bytes; + } + const auto guard = gsl::finally([&] { inflateEnd(&stream); }); + + auto result = QByteArray(kMaxGzipFileSize + 1, char(0)); + stream.avail_in = bytes.size(); + stream.next_in = reinterpret_cast(const_cast(bytes.data())); + stream.avail_out = 0; + while (!stream.avail_out) { + stream.avail_out = result.size(); + stream.next_out = reinterpret_cast(result.data()); + int res = inflate(&stream, Z_NO_FLUSH); + if (res != Z_OK && res != Z_STREAM_END) { + return bytes; + } else if (!stream.avail_out) { + return bytes; + } + } + result.resize(result.size() - stream.avail_out); + return result; +} + +[[nodiscard]] ReadResult ReadGzipSvg(const ReadArgs &args) { + const auto bytes = UnpackGzip(args.content); + if (bytes.isEmpty()) { + LOG(("Svg Error: Couldn't unpack gzip-ed content.")); + return {}; + } + auto renderer = QSvgRenderer(bytes); + if (!renderer.isValid()) { + LOG(("Svg Error: Invalid data.")); + return {}; + } + auto size = renderer.defaultSize(); + if (!args.maxSize.isEmpty() + && (size.width() > args.maxSize.width() + || size.height() > args.maxSize.height())) { + size = size.scaled(args.maxSize, Qt::KeepAspectRatio); + } + if (size.isEmpty()) { + LOG(("Svg Error: Bad size %1x%2." + ).arg(renderer.defaultSize().width() + ).arg(renderer.defaultSize().height())); + return {}; + } + auto result = ReadResult(); + result.image = QImage(size, QImage::Format_ARGB32_Premultiplied); + result.image.fill(Qt::transparent); + { + QPainter p(&result.image); + renderer.render(&p, QRect(QPoint(), size)); + } + result.format = "svg"; + return result; +} + +[[nodiscard]] ReadResult ReadOther(const ReadArgs &args) { + auto bytes = args.content; + if (bytes.isEmpty()) { + return {}; + } + auto buffer = QBuffer(&bytes); + auto reader = QImageReader(&buffer); + reader.setAutoTransform(true); + if (!reader.canRead()) { + return {}; + } + const auto size = reader.size(); + if (size.width() * size.height() > kReadMaxArea) { + return {}; + } + auto result = ReadResult(); + if (!reader.read(&result.image) || result.image.isNull()) { + return {}; + } + result.animated = reader.supportsAnimation() + && (reader.imageCount() > 1); + result.format = reader.format().toLower(); + return result; +} + ReadResult Read(ReadArgs &&args) { if (args.content.isEmpty()) { auto file = QFile(args.path); @@ -124,33 +220,24 @@ ReadResult Read(ReadArgs &&args) { } args.content = file.readAll(); } - if (args.content.isEmpty()) { + auto result = args.gzipSvg ? ReadGzipSvg(args) : ReadOther(args); + if (result.image.isNull()) { + args = ReadArgs(); return {}; } - auto result = ReadResult(); - { - auto buffer = QBuffer(&args.content); - auto reader = QImageReader(&buffer); - reader.setAutoTransform(true); - if (!reader.canRead()) { - return {}; - } - const auto size = reader.size(); - if (size.width() * size.height() > kReadMaxArea) { - return {}; - } - if (!reader.read(&result.image) || result.image.isNull()) { - return {}; - } - result.animated = reader.supportsAnimation() - && (reader.imageCount() > 1); - result.format = reader.format().toLower(); - } if (args.returnContent) { result.content = args.content; } else { args.content = QByteArray(); } + if (!args.maxSize.isEmpty() + && (result.image.width() > args.maxSize.width() + || result.image.height() > args.maxSize.height())) { + result.image = result.image.scaled( + args.maxSize, + Qt::KeepAspectRatio, + Qt::SmoothTransformation); + } if (args.forceOpaque && result.format != qstr("jpg") && result.format != qstr("jpeg")) { diff --git a/ui/image/image_prepare.h b/ui/image/image_prepare.h index 5683027..512e3a3 100644 --- a/ui/image/image_prepare.h +++ b/ui/image/image_prepare.h @@ -39,6 +39,7 @@ namespace Images { int radius, const style::color &color); +[[nodiscard]] QByteArray UnpackGzip(const QByteArray &bytes); // Try to read images up to 64MB. inline constexpr auto kReadBytesLimit = 64 * 1024 * 1024; @@ -48,6 +49,7 @@ struct ReadArgs { QString path; QByteArray content; QSize maxSize; + bool gzipSvg = false; bool forceOpaque = false; bool returnContent = false; }; From 6de724bc1cadbabe6338fb0874a907dc7ab1e1cf Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Fri, 6 Aug 2021 04:07:34 +0300 Subject: [PATCH 06/18] Moved time input widgets to lib_ui. --- CMakeLists.txt | 2 + ui/ui_utility.cpp | 12 + ui/ui_utility.h | 3 + ui/widgets/time_input.cpp | 468 ++++++++++++++++++++++++++++++++++++++ ui/widgets/time_input.h | 78 +++++++ 5 files changed, 563 insertions(+) create mode 100644 ui/widgets/time_input.cpp create mode 100644 ui/widgets/time_input.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 235e37c..d68eb4a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -188,6 +188,8 @@ PRIVATE ui/widgets/side_bar_button.h ui/widgets/shadow.cpp ui/widgets/shadow.h + ui/widgets/time_input.cpp + ui/widgets/time_input.h ui/widgets/tooltip.cpp ui/widgets/tooltip.h ui/wrap/fade_wrap.cpp diff --git a/ui/ui_utility.cpp b/ui/ui_utility.cpp index 7e5b7d8..a2e55e6 100644 --- a/ui/ui_utility.cpp +++ b/ui/ui_utility.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include @@ -218,4 +219,15 @@ void DisableCustomScaling() { } } +int WheelDirection(not_null e) { + // Only a mouse wheel is accepted. + constexpr auto step = static_cast(QWheelEvent::DefaultDeltasPerStep); + const auto delta = e->angleDelta().y(); + const auto absDelta = std::abs(delta); + if (absDelta != step) { + return 0; + } + return (delta / absDelta); +} + } // namespace Ui diff --git a/ui/ui_utility.h b/ui/ui_utility.h index ece602d..f7f6fb1 100644 --- a/ui/ui_utility.h +++ b/ui/ui_utility.h @@ -14,6 +14,7 @@ class QPixmap; class QImage; +class QWheelEvent; enum class RectPart; using RectParts = base::flags; @@ -193,4 +194,6 @@ QPointer MakeWeak(not_null object) { void DisableCustomScaling(); +int WheelDirection(not_null e); + } // namespace Ui diff --git a/ui/widgets/time_input.cpp b/ui/widgets/time_input.cpp new file mode 100644 index 0000000..318e1a1 --- /dev/null +++ b/ui/widgets/time_input.cpp @@ -0,0 +1,468 @@ +// 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 "ui/widgets/time_input.h" + +#include "ui/widgets/input_fields.h" +#include "ui/ui_utility.h" + +#include +#include + +namespace Ui { +namespace { + +QTime ValidateTime(const QString &value) { + const auto match = QRegularExpression( + "^(\\d{1,2})\\:(\\d\\d)$").match(value); + if (!match.hasMatch()) { + return QTime(); + } + const auto readInt = [](const QString &value) { + auto ref = value.midRef(0); + while (!ref.isEmpty() && ref.at(0) == '0') { + ref = ref.mid(1); + } + return ref.toInt(); + }; + return QTime(readInt(match.captured(1)), readInt(match.captured(2))); +} + +QString GetHour(const QString &value) { + if (const auto time = ValidateTime(value); time.isValid()) { + return QString::number(time.hour()); + } + return QString(); +} + +QString GetMinute(const QString &value) { + if (const auto time = ValidateTime(value); time.isValid()) { + return QString("%1").arg(time.minute(), 2, 10, QChar('0')); + } + return QString(); +} + +} // namespace + +class TimePart final : public MaskedInputField { +public: + using MaskedInputField::MaskedInputField; + + void setMaxValue(int value); + void setWheelStep(int value); + + rpl::producer<> erasePrevious() const; + rpl::producer putNext() const; + +protected: + void keyPressEvent(QKeyEvent *e) override; + void wheelEvent(QWheelEvent *e) override; + + void correctValue( + const QString &was, + int wasCursor, + QString &now, + int &nowCursor) override; + +private: + int _maxValue = 0; + int _maxDigits = 0; + int _wheelStep = 0; + rpl::event_stream<> _erasePrevious; + rpl::event_stream _putNext; + +}; + +int Number(not_null field) { + const auto text = field->getLastText(); + auto ref = text.midRef(0); + while (!ref.isEmpty() && ref.at(0) == '0') { + ref = ref.mid(1); + } + return ref.toInt(); +} + +void TimePart::setMaxValue(int value) { + _maxValue = value; + _maxDigits = 0; + while (value > 0) { + ++_maxDigits; + value /= 10; + } +} + +void TimePart::setWheelStep(int value) { + _wheelStep = value; +} + +rpl::producer<> TimePart::erasePrevious() const { + return _erasePrevious.events(); +} + +rpl::producer TimePart::putNext() const { + return _putNext.events(); +} + +void TimePart::keyPressEvent(QKeyEvent *e) { + const auto isBackspace = (e->key() == Qt::Key_Backspace); + const auto isBeginning = (cursorPosition() == 0); + if (isBackspace && isBeginning && !hasSelectedText()) { + _erasePrevious.fire({}); + } else { + MaskedInputField::keyPressEvent(e); + } +} + +void TimePart::wheelEvent(QWheelEvent *e) { + const auto direction = WheelDirection(e); + auto time = Number(this) + (direction * _wheelStep); + const auto max = _maxValue + 1; + if (time < 0) { + time += max; + } else if (time >= max) { + time -= max; + } + setText(QString::number(time)); +} + +void TimePart::correctValue( + const QString &was, + int wasCursor, + QString &now, + int &nowCursor) { + auto newText = QString(); + auto newCursor = -1; + const auto oldCursor = nowCursor; + const auto oldLength = now.size(); + auto accumulated = 0; + auto limit = 0; + for (; limit != oldLength; ++limit) { + if (now[limit].isDigit()) { + accumulated *= 10; + accumulated += (now[limit].unicode() - '0'); + if (accumulated > _maxValue || limit == _maxDigits) { + break; + } + } + } + for (auto i = 0; i != limit;) { + if (now[i].isDigit()) { + newText += now[i]; + } + if (++i == oldCursor) { + newCursor = newText.size(); + } + } + if (newCursor < 0) { + newCursor = newText.size(); + } + if (newText != now) { + now = newText; + setText(now); + startPlaceholderAnimation(); + } + if (newCursor != nowCursor) { + nowCursor = newCursor; + setCursorPosition(nowCursor); + } + if (accumulated > _maxValue + || (limit == _maxDigits && oldLength > _maxDigits)) { + if (oldCursor > limit) { + _putNext.fire('0' + (accumulated % 10)); + } else { + _putNext.fire(0); + } + } +} + +TimeInput::TimeInput( + QWidget *parent, + const QString &value, + const style::InputField &stField, + const style::InputField &stDateField, + const style::FlatLabel &stSeparator, + const style::margins &stSeparatorPadding) +: RpWidget(parent) +, _stField(stField) +, _stDateField(stDateField) +, _stSeparator(stSeparator) +, _stSeparatorPadding(stSeparatorPadding) +, _hour( + this, + _stField, + rpl::never(), + GetHour(value)) +, _separator1( + this, + object_ptr( + this, + QString(":"), + _stSeparator), + _stSeparatorPadding) +, _minute( + this, + _stField, + rpl::never(), + GetMinute(value)) +, _value(valueCurrent()) { + const auto focused = [=](const object_ptr &field) { + return [this, pointer = MakeWeak(field.data())]{ + _borderAnimationStart = pointer->borderAnimationStart() + + pointer->x() + - _hour->x(); + setFocused(true); + }; + }; + const auto blurred = [=] { + setFocused(false); + }; + const auto changed = [=] { + _value = valueCurrent(); + }; + connect(_hour, &MaskedInputField::focused, focused(_hour)); + connect(_minute, &MaskedInputField::focused, focused(_minute)); + connect(_hour, &MaskedInputField::blurred, blurred); + connect(_minute, &MaskedInputField::blurred, blurred); + connect(_hour, &MaskedInputField::changed, changed); + connect(_minute, &MaskedInputField::changed, changed); + _hour->setMaxValue(23); + _hour->setWheelStep(1); + _hour->putNext() | rpl::start_with_next([=](QChar ch) { + putNext(_minute, ch); + }, lifetime()); + _minute->setMaxValue(59); + _minute->setWheelStep(10); + _minute->erasePrevious() | rpl::start_with_next([=] { + erasePrevious(_hour); + }, lifetime()); + _separator1->setAttribute(Qt::WA_TransparentForMouseEvents); + setMouseTracking(true); + + _value.changes( + ) | rpl::start_with_next([=] { + setErrorShown(false); + }, lifetime()); + + const auto submitHour = [=] { + if (hour()) { + _minute->setFocus(); + } + }; + const auto submitMinute = [=] { + if (minute()) { + if (hour()) { + _submitRequests.fire({}); + } else { + _hour->setFocus(); + } + } + }; + connect( + _hour, + &MaskedInputField::submitted, + submitHour); + connect( + _minute, + &MaskedInputField::submitted, + submitMinute); +} + +void TimeInput::putNext(const object_ptr &field, QChar ch) { + field->setCursorPosition(0); + if (ch.unicode()) { + field->setText(ch + field->getLastText()); + field->setCursorPosition(1); + } + field->setFocus(); +} + +void TimeInput::erasePrevious(const object_ptr &field) { + const auto text = field->getLastText(); + if (!text.isEmpty()) { + field->setCursorPosition(text.size() - 1); + field->setText(text.mid(0, text.size() - 1)); + } + field->setFocus(); +} + +bool TimeInput::setFocusFast() { + if (hour()) { + _minute->setFocusFast(); + } else { + _hour->setFocusFast(); + } + return true; +} + +int TimeInput::hour() const { + return Number(_hour); +} + +int TimeInput::minute() const { + return Number(_minute); +} + +QString TimeInput::valueCurrent() const { + const auto result = QString("%1:%2" + ).arg(hour() + ).arg(minute(), 2, 10, QChar('0')); + return ValidateTime(result).isValid() ? result : QString(); +} + +rpl::producer TimeInput::value() const { + return _value.value(); +} + +rpl::producer<> TimeInput::submitRequests() const { + return _submitRequests.events(); +} + +void TimeInput::paintEvent(QPaintEvent *e) { + Painter p(this); + + const auto &_st = _stDateField; + const auto height = _st.heightMin; + if (_st.border) { + p.fillRect(0, height - _st.border, width(), _st.border, _st.borderFg); + } + auto errorDegree = _a_error.value(_error ? 1. : 0.); + auto borderShownDegree = _a_borderShown.value(1.); + auto borderOpacity = _a_borderOpacity.value(_borderVisible ? 1. : 0.); + if (_st.borderActive && (borderOpacity > 0.)) { + auto borderStart = std::clamp(_borderAnimationStart, 0, width()); + auto borderFrom = qRound(borderStart * (1. - borderShownDegree)); + auto borderTo = borderStart + qRound((width() - borderStart) * borderShownDegree); + if (borderTo > borderFrom) { + auto borderFg = anim::brush(_st.borderFgActive, _st.borderFgError, errorDegree); + p.setOpacity(borderOpacity); + p.fillRect(borderFrom, height - _st.borderActive, borderTo - borderFrom, _st.borderActive, borderFg); + p.setOpacity(1); + } + } +} + +template +bool TimeInput::insideSeparator(QPoint position, const Widget &widget) const { + const auto x = position.x(); + const auto y = position.y(); + return (x >= widget->x() && x < widget->x() + widget->width()) + && (y >= _hour->y() && y < _hour->y() + _hour->height()); +} + +void TimeInput::mouseMoveEvent(QMouseEvent *e) { + const auto cursor = insideSeparator(e->pos(), _separator1) + ? style::cur_text + : style::cur_default; + if (_cursor != cursor) { + _cursor = cursor; + setCursor(_cursor); + } +} + +void TimeInput::mousePressEvent(QMouseEvent *e) { + const auto x = e->pos().x(); + const auto focus1 = [&] { + if (_hour->getLastText().size() > 1) { + _minute->setFocus(); + } else { + _hour->setFocus(); + } + }; + if (insideSeparator(e->pos(), _separator1)) { + focus1(); + _borderAnimationStart = x - _hour->x(); + } +} + +int TimeInput::resizeGetHeight(int width) { + const auto &_st = _stField; + const auto &font = _st.placeholderFont; + const auto addToWidth = _stSeparatorPadding.left(); + const auto hourWidth = _st.textMargins.left() + + _st.placeholderMargins.left() + + font->width(QString("23")) + + _st.placeholderMargins.right() + + _st.textMargins.right() + + addToWidth; + const auto minuteWidth = _st.textMargins.left() + + _st.placeholderMargins.left() + + font->width(QString("59")) + + _st.placeholderMargins.right() + + _st.textMargins.right() + + addToWidth; + const auto full = hourWidth + - addToWidth + + _separator1->width() + + minuteWidth + - addToWidth; + auto left = (width - full) / 2; + auto top = 0; + _hour->setGeometry(left, top, hourWidth, _hour->height()); + left += hourWidth - addToWidth; + _separator1->resizeToNaturalWidth(width); + _separator1->move(left, top); + left += _separator1->width(); + _minute->setGeometry(left, top, minuteWidth, _minute->height()); + return _stDateField.heightMin; +} + +void TimeInput::showError() { + setErrorShown(true); + if (!_focused) { + setInnerFocus(); + } +} + +void TimeInput::setInnerFocus() { + if (hour()) { + _minute->setFocus(); + } else { + _hour->setFocus(); + } +} + +void TimeInput::setErrorShown(bool error) { + if (_error != error) { + _error = error; + _a_error.start( + [=] { update(); }, + _error ? 0. : 1., + _error ? 1. : 0., + _stDateField.duration); + startBorderAnimation(); + } +} + +void TimeInput::setFocused(bool focused) { + if (_focused != focused) { + _focused = focused; + _a_focused.start( + [=] { update(); }, + _focused ? 0. : 1., + _focused ? 1. : 0., + _stDateField.duration); + startBorderAnimation(); + } +} + +void TimeInput::startBorderAnimation() { + auto borderVisible = (_error || _focused); + if (_borderVisible != borderVisible) { + _borderVisible = borderVisible; + const auto duration = _stDateField.duration; + if (_borderVisible) { + if (_a_borderOpacity.animating()) { + _a_borderOpacity.start([=] { update(); }, 0., 1., duration); + } else { + _a_borderShown.start([=] { update(); }, 0., 1., duration); + } + } else { + _a_borderOpacity.start([=] { update(); }, 1., 0., duration); + } + } +} + +} // namespace Ui diff --git a/ui/widgets/time_input.h b/ui/widgets/time_input.h new file mode 100644 index 0000000..1724a65 --- /dev/null +++ b/ui/widgets/time_input.h @@ -0,0 +1,78 @@ +// 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" + +#include "ui/effects/animations.h" +#include "ui/widgets/labels.h" + +namespace Ui { + +class TimePart; + +class TimeInput final : public RpWidget { +public: + TimeInput( + QWidget *parent, + const QString &value, + const style::InputField &stField, + const style::InputField &stDateField, + const style::FlatLabel &stSeparator, + const style::margins &stSeparatorPadding); + + bool setFocusFast(); + rpl::producer value() const; + rpl::producer<> submitRequests() const; + QString valueCurrent() const; + void showError(); + + int resizeGetHeight(int width) override; + +protected: + void paintEvent(QPaintEvent *e) override; + void mousePressEvent(QMouseEvent *e) override; + void mouseMoveEvent(QMouseEvent *e) override; + +private: + void setInnerFocus(); + void putNext(const object_ptr &field, QChar ch); + void erasePrevious(const object_ptr &field); + void setErrorShown(bool error); + void setFocused(bool focused); + void startBorderAnimation(); + template + bool insideSeparator(QPoint position, const Widget &widget) const; + + int hour() const; + int minute() const; + + const style::InputField &_stField; + const style::InputField &_stDateField; + const style::FlatLabel &_stSeparator; + const style::margins &_stSeparatorPadding; + + object_ptr _hour; + object_ptr> _separator1; + object_ptr _minute; + rpl::variable _value; + rpl::event_stream<> _submitRequests; + + style::cursor _cursor = style::cur_default; + Animations::Simple _a_borderShown; + int _borderAnimationStart = 0; + Animations::Simple _a_borderOpacity; + bool _borderVisible = false; + + Animations::Simple _a_error; + bool _error = false; + Animations::Simple _a_focused; + bool _focused = false; + +}; + +} // namespace Ui From 49899091aafe71f8b4233d09f555a0a746673fbd Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Wed, 11 Aug 2021 08:58:42 +0300 Subject: [PATCH 07/18] Added focus rpl event to time input. --- ui/widgets/time_input.cpp | 5 +++++ ui/widgets/time_input.h | 2 ++ 2 files changed, 7 insertions(+) diff --git a/ui/widgets/time_input.cpp b/ui/widgets/time_input.cpp index 318e1a1..ee80060 100644 --- a/ui/widgets/time_input.cpp +++ b/ui/widgets/time_input.cpp @@ -214,6 +214,7 @@ TimeInput::TimeInput( + pointer->x() - _hour->x(); setFocused(true); + _focuses.fire({}); }; }; const auto blurred = [=] { @@ -320,6 +321,10 @@ rpl::producer<> TimeInput::submitRequests() const { return _submitRequests.events(); } +rpl::producer<> TimeInput::focuses() const { + return _focuses.events(); +} + void TimeInput::paintEvent(QPaintEvent *e) { Painter p(this); diff --git a/ui/widgets/time_input.h b/ui/widgets/time_input.h index 1724a65..bc8a187 100644 --- a/ui/widgets/time_input.h +++ b/ui/widgets/time_input.h @@ -28,6 +28,7 @@ public: bool setFocusFast(); rpl::producer value() const; rpl::producer<> submitRequests() const; + rpl::producer<> focuses() const; QString valueCurrent() const; void showError(); @@ -61,6 +62,7 @@ private: object_ptr _minute; rpl::variable _value; rpl::event_stream<> _submitRequests; + rpl::event_stream<> _focuses; style::cursor _cursor = style::cur_default; Animations::Simple _a_borderShown; From 3d5fcdb7ddab336ee2fadc3ceeced420e2c1f504 Mon Sep 17 00:00:00 2001 From: John Preston Date: Fri, 13 Aug 2021 15:58:13 +0300 Subject: [PATCH 08/18] Add Images::DitherImage. --- ui/image/image_prepare.cpp | 72 ++++++++++++++++++++++++++++++++++++++ ui/image/image_prepare.h | 1 + 2 files changed, 73 insertions(+) diff --git a/ui/image/image_prepare.cpp b/ui/image/image_prepare.cpp index 660a20f..f4c7408 100644 --- a/ui/image/image_prepare.cpp +++ b/ui/image/image_prepare.cpp @@ -11,6 +11,7 @@ #include "ui/painter.h" #include "base/flat_map.h" #include "base/debug_log.h" +#include "base/bytes.h" #include "styles/palette.h" #include "styles/style_basic.h" @@ -582,6 +583,77 @@ QImage BlurLargeImage(QImage image, int radius) { return image; } +[[nodiscard]] QImage DitherImage(QImage image) { + Expects(image.bytesPerLine() == image.width() * 4); + + const auto width = image.width(); + const auto height = image.height(); + + if (width < 16 || height < 16) { + return image; + } + + const auto area = width * height; + const auto shifts = std::make_unique(area); + bytes::set_random(bytes::make_span(shifts.get(), area)); + + // shiftx = int(shift & 0x0F) - 8; shifty = int(shift >> 4) - 8; + // Clamp shifts close to edges. + for (auto y = 0; y != 8; ++y) { + const auto min = 8 - y; + const auto shifted = (min << 4); + auto shift = shifts.get() + y * width; + for (const auto till = shift + width; shift != till; ++shift) { + if ((*shift >> 4) < min) { + *shift = shifted | (*shift & 0x0F); + } + } + } + for (auto y = height - 7; y != height; ++y) { + const auto max = 8 + (height - y - 1); + const auto shifted = (max << 4); + auto shift = shifts.get() + y * width; + for (const auto till = shift + width; shift != till; ++shift) { + if ((*shift >> 4) > max) { + *shift = shifted | (*shift & 0x0F); + } + } + } + for (auto shift = shifts.get(), ytill = shift + area + ; shift != ytill + ; shift += width - 8) { + for (const auto till = shift + 8; shift != till; ++shift) { + const auto min = (till - shift); + if ((*shift & 0x0F) < min) { + *shift = (*shift & 0xF0) | min; + } + } + } + for (auto shift = shifts.get(), ytill = shift + area; shift != ytill;) { + shift += width - 7; + for (const auto till = shift + 7; shift != till; ++shift) { + const auto max = 8 + (till - shift - 1); + if ((*shift & 0x0F) > max) { + *shift = (*shift & 0xF0) | max; + } + } + } + + auto result = image; + result.detach(); + + const auto src = reinterpret_cast(image.constBits()); + const auto dst = reinterpret_cast(result.bits()); + for (auto index = 0; index != area; ++index) { + const auto shift = shifts[index]; + const auto shiftx = int(shift & 0x0F) - 8; + const auto shifty = int(shift >> 4) - 8; + dst[index] = src[index + (shifty * width) + shiftx]; + } + + return result; +} + void prepareCircle(QImage &img) { Assert(!img.isNull()); diff --git a/ui/image/image_prepare.h b/ui/image/image_prepare.h index 512e3a3..9abad9e 100644 --- a/ui/image/image_prepare.h +++ b/ui/image/image_prepare.h @@ -27,6 +27,7 @@ namespace Images { [[nodiscard]] QPixmap PixmapFast(QImage &&image); [[nodiscard]] QImage BlurLargeImage(QImage image, int radius); +[[nodiscard]] QImage DitherImage(QImage image); [[nodiscard]] const std::array &CornersMask( ImageRoundRadius radius); From 3b45189923554a401683b8be39d8b0d25314dcf0 Mon Sep 17 00:00:00 2001 From: John Preston Date: Mon, 16 Aug 2021 12:29:30 +0300 Subject: [PATCH 09/18] Use dither radius based on image size. --- ui/image/image_prepare.cpp | 66 +++++++++++++++++++++++++------------- 1 file changed, 43 insertions(+), 23 deletions(-) diff --git a/ui/image/image_prepare.cpp b/ui/image/image_prepare.cpp index f4c7408..d60194f 100644 --- a/ui/image/image_prepare.cpp +++ b/ui/image/image_prepare.cpp @@ -583,57 +583,58 @@ QImage BlurLargeImage(QImage image, int radius) { return image; } -[[nodiscard]] QImage DitherImage(QImage image) { - Expects(image.bytesPerLine() == image.width() * 4); +template // 4 means 16x16, 3 means 8x8 +[[nodiscard]] QImage DitherGeneric(const QImage &image) { + static_assert(kBits >= 1 && kBits <= 4); + + constexpr auto kSquareSide = (1 << kBits); + constexpr auto kShift = kSquareSide / 2; + constexpr auto kMask = (kSquareSide - 1); const auto width = image.width(); const auto height = image.height(); - - if (width < 16 || height < 16) { - return image; - } - const auto area = width * height; const auto shifts = std::make_unique(area); bytes::set_random(bytes::make_span(shifts.get(), area)); - // shiftx = int(shift & 0x0F) - 8; shifty = int(shift >> 4) - 8; + // shiftx = int(shift & kMask) - kShift; + // shifty = int((shift >> 4) & kMask) - kShift; // Clamp shifts close to edges. - for (auto y = 0; y != 8; ++y) { - const auto min = 8 - y; + for (auto y = 0; y != kShift; ++y) { + const auto min = kShift - y; const auto shifted = (min << 4); auto shift = shifts.get() + y * width; for (const auto till = shift + width; shift != till; ++shift) { - if ((*shift >> 4) < min) { + if (((*shift >> 4) & kMask) < min) { *shift = shifted | (*shift & 0x0F); } } } - for (auto y = height - 7; y != height; ++y) { - const auto max = 8 + (height - y - 1); + for (auto y = height - (kShift - 1); y != height; ++y) { + const auto max = kShift + (height - y - 1); const auto shifted = (max << 4); auto shift = shifts.get() + y * width; for (const auto till = shift + width; shift != till; ++shift) { - if ((*shift >> 4) > max) { + if (((*shift >> 4) & kMask) > max) { *shift = shifted | (*shift & 0x0F); } } } for (auto shift = shifts.get(), ytill = shift + area ; shift != ytill - ; shift += width - 8) { - for (const auto till = shift + 8; shift != till; ++shift) { + ; shift += width - kShift) { + for (const auto till = shift + kShift; shift != till; ++shift) { const auto min = (till - shift); - if ((*shift & 0x0F) < min) { + if ((*shift & kMask) < min) { *shift = (*shift & 0xF0) | min; } } } for (auto shift = shifts.get(), ytill = shift + area; shift != ytill;) { - shift += width - 7; - for (const auto till = shift + 7; shift != till; ++shift) { - const auto max = 8 + (till - shift - 1); - if ((*shift & 0x0F) > max) { + shift += width - (kShift - 1); + for (const auto till = shift + (kShift - 1); shift != till; ++shift) { + const auto max = kShift + (till - shift - 1); + if ((*shift & kMask) > max) { *shift = (*shift & 0xF0) | max; } } @@ -646,14 +647,33 @@ QImage BlurLargeImage(QImage image, int radius) { const auto dst = reinterpret_cast(result.bits()); for (auto index = 0; index != area; ++index) { const auto shift = shifts[index]; - const auto shiftx = int(shift & 0x0F) - 8; - const auto shifty = int(shift >> 4) - 8; + const auto shiftx = int(shift & kMask) - kShift; + const auto shifty = int((shift >> 4) & kMask) - kShift; dst[index] = src[index + (shifty * width) + shiftx]; } return result; } +[[nodiscard]] QImage DitherImage(QImage image) { + Expects(image.bytesPerLine() == image.width() * 4); + + const auto width = image.width(); + const auto height = image.height(); + const auto min = std::min(width, height); + const auto max = std::max(width, height); + if (max >= 1024 && min >= 512) { + return DitherGeneric<4>(image); + } else if (max >= 512 && min >= 256) { + return DitherGeneric<3>(image); + } else if (max >= 256 && min >= 128) { + return DitherGeneric<2>(image); + } else if (min >= 32) { + return DitherGeneric<1>(image); + } + return image; +} + void prepareCircle(QImage &img) { Assert(!img.isNull()); From 669767855a197976183684d25b2753899ee95e5c Mon Sep 17 00:00:00 2001 From: John Preston Date: Mon, 16 Aug 2021 13:23:48 +0300 Subject: [PATCH 10/18] Add gradient generating methods. --- ui/image/image_prepare.cpp | 323 ++++++++++++++++++++++++++++--------- ui/image/image_prepare.h | 6 + 2 files changed, 257 insertions(+), 72 deletions(-) diff --git a/ui/image/image_prepare.cpp b/ui/image/image_prepare.cpp index d60194f..72e3891 100644 --- a/ui/image/image_prepare.cpp +++ b/ui/image/image_prepare.cpp @@ -79,6 +79,210 @@ std::array PrepareCornersMask(int radius) { return result; } +template // 4 means 16x16, 3 means 8x8 +[[nodiscard]] QImage DitherGeneric(const QImage &image) { + static_assert(kBits >= 1 && kBits <= 4); + + constexpr auto kSquareSide = (1 << kBits); + constexpr auto kShift = kSquareSide / 2; + constexpr auto kMask = (kSquareSide - 1); + + const auto width = image.width(); + const auto height = image.height(); + const auto area = width * height; + const auto shifts = std::make_unique(area); + bytes::set_random(bytes::make_span(shifts.get(), area)); + + // shiftx = int(shift & kMask) - kShift; + // shifty = int((shift >> 4) & kMask) - kShift; + // Clamp shifts close to edges. + for (auto y = 0; y != kShift; ++y) { + const auto min = kShift - y; + const auto shifted = (min << 4); + auto shift = shifts.get() + y * width; + for (const auto till = shift + width; shift != till; ++shift) { + if (((*shift >> 4) & kMask) < min) { + *shift = shifted | (*shift & 0x0F); + } + } + } + for (auto y = height - (kShift - 1); y != height; ++y) { + const auto max = kShift + (height - y - 1); + const auto shifted = (max << 4); + auto shift = shifts.get() + y * width; + for (const auto till = shift + width; shift != till; ++shift) { + if (((*shift >> 4) & kMask) > max) { + *shift = shifted | (*shift & 0x0F); + } + } + } + for (auto shift = shifts.get(), ytill = shift + area + ; shift != ytill + ; shift += width - kShift) { + for (const auto till = shift + kShift; shift != till; ++shift) { + const auto min = (till - shift); + if ((*shift & kMask) < min) { + *shift = (*shift & 0xF0) | min; + } + } + } + for (auto shift = shifts.get(), ytill = shift + area; shift != ytill;) { + shift += width - (kShift - 1); + for (const auto till = shift + (kShift - 1); shift != till; ++shift) { + const auto max = kShift + (till - shift - 1); + if ((*shift & kMask) > max) { + *shift = (*shift & 0xF0) | max; + } + } + } + + auto result = image; + result.detach(); + + const auto src = reinterpret_cast(image.constBits()); + const auto dst = reinterpret_cast(result.bits()); + for (auto index = 0; index != area; ++index) { + const auto shift = shifts[index]; + const auto shiftx = int(shift & kMask) - kShift; + const auto shifty = int((shift >> 4) & kMask) - kShift; + dst[index] = src[index + (shifty * width) + shiftx]; + } + + return result; +} + +[[nodiscard]] QImage GenerateSmallComplexGradient( + const std::vector &colors, + int rotation, + float progress) { + const auto positions = std::vector>{ + { 0.80f, 0.10f }, + { 0.60f, 0.20f }, + { 0.35f, 0.25f }, + { 0.25f, 0.60f }, + { 0.20f, 0.90f }, + { 0.40f, 0.80f }, + { 0.65f, 0.75f }, + { 0.75f, 0.40f }, + }; + const auto positionsForPhase = [&](int phase) { + auto result = std::vector>(4); + for (auto i = 0; i != 4; ++i) { + result[i] = positions[(phase + i * 2) % 8]; + result[i].second = 1.f - result[i].second; + } + return result; + }; + const auto phase = std::clamp(rotation, 0, 315) / 45; + const auto previousPhase = (phase + 1) % 8; + const auto previous = positionsForPhase(previousPhase); + const auto current = positionsForPhase(phase); + + constexpr auto kWidth = 64; + constexpr auto kHeight = 64; + static const auto pixelCache = [&] { + auto result = std::make_unique(kWidth * kHeight * 2); + const auto invwidth = 1.f / kWidth; + const auto invheight = 1.f / kHeight; + auto floats = result.get(); + for (auto y = 0; y != kHeight; ++y) { + const auto directPixelY = y * invheight; + const auto centerDistanceY = directPixelY - 0.5f; + const auto centerDistanceY2 = centerDistanceY * centerDistanceY; + for (auto x = 0; x != kWidth; ++x) { + const auto directPixelX = x * invwidth; + const auto centerDistanceX = directPixelX - 0.5f; + const auto centerDistance = sqrtf( + centerDistanceX * centerDistanceX + centerDistanceY2); + + const auto swirlFactor = 0.35f * centerDistance; + const auto theta = swirlFactor * swirlFactor * 0.8f * 8.0f; + const auto sinTheta = sinf(theta); + const auto cosTheta = cosf(theta); + *floats++ = std::max( + 0.0f, + std::min( + 1.0f, + (0.5f + + centerDistanceX * cosTheta + - centerDistanceY * sinTheta))); + *floats++ = std::max( + 0.0f, + std::min( + 1.0f, + (0.5f + + centerDistanceX * sinTheta + + centerDistanceY * cosTheta))); + } + } + return result; + }(); + const auto colorsCount = int(colors.size()); + auto colorsFloat = std::vector>(colorsCount); + for (auto i = 0; i != colorsCount; ++i) { + colorsFloat[i] = { + float(colors[i].red()), + float(colors[i].green()), + float(colors[i].blue()), + }; + } + auto result = QImage( + kWidth, + kHeight, + QImage::Format_ARGB32_Premultiplied); + Assert(result.bytesPerLine() == kWidth * 4); + + auto cache = pixelCache.get(); + auto pixels = reinterpret_cast(result.bits()); + for (auto y = 0; y != kHeight; ++y) { + for (auto x = 0; x != kWidth; ++x) { + const auto pixelX = *cache++; + const auto pixelY = *cache++; + + auto distanceSum = 0.f; + auto r = 0.f; + auto g = 0.f; + auto b = 0.f; + for (auto i = 0; i != colorsCount; ++i) { + const auto colorX = previous[i].first + + (current[i].first - previous[i].first) * progress; + const auto colorY = previous[i].second + + (current[i].second - previous[i].second) * progress; + + const auto dx = pixelX - colorX; + const auto dy = pixelY - colorY; + const auto distance = std::max( + 0.0f, + 0.9f - sqrtf(dx * dx + dy * dy)); + const auto square = distance * distance; + const auto fourth = square * square; + distanceSum += fourth; + + r += fourth * colorsFloat[i][0]; + g += fourth * colorsFloat[i][1]; + b += fourth * colorsFloat[i][2]; + } + + const auto red = uint32(r / distanceSum); + const auto green = uint32(g / distanceSum); + const auto blue = uint32(b / distanceSum); + *pixels++ = 0xFF000000U | (red << 16) | (green << 8) | blue; + } + } + return result; +} + +[[nodiscard]] QImage GenerateComplexGradient( + QSize size, + const std::vector &colors, + int rotation, + float progress) { + auto exact = GenerateSmallComplexGradient(colors, rotation, progress); + return (exact.size() == size) + ? exact + : exact.scaled(size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation); +} + } // namespace QPixmap PixmapFast(QImage &&image) { @@ -583,78 +787,6 @@ QImage BlurLargeImage(QImage image, int radius) { return image; } -template // 4 means 16x16, 3 means 8x8 -[[nodiscard]] QImage DitherGeneric(const QImage &image) { - static_assert(kBits >= 1 && kBits <= 4); - - constexpr auto kSquareSide = (1 << kBits); - constexpr auto kShift = kSquareSide / 2; - constexpr auto kMask = (kSquareSide - 1); - - const auto width = image.width(); - const auto height = image.height(); - const auto area = width * height; - const auto shifts = std::make_unique(area); - bytes::set_random(bytes::make_span(shifts.get(), area)); - - // shiftx = int(shift & kMask) - kShift; - // shifty = int((shift >> 4) & kMask) - kShift; - // Clamp shifts close to edges. - for (auto y = 0; y != kShift; ++y) { - const auto min = kShift - y; - const auto shifted = (min << 4); - auto shift = shifts.get() + y * width; - for (const auto till = shift + width; shift != till; ++shift) { - if (((*shift >> 4) & kMask) < min) { - *shift = shifted | (*shift & 0x0F); - } - } - } - for (auto y = height - (kShift - 1); y != height; ++y) { - const auto max = kShift + (height - y - 1); - const auto shifted = (max << 4); - auto shift = shifts.get() + y * width; - for (const auto till = shift + width; shift != till; ++shift) { - if (((*shift >> 4) & kMask) > max) { - *shift = shifted | (*shift & 0x0F); - } - } - } - for (auto shift = shifts.get(), ytill = shift + area - ; shift != ytill - ; shift += width - kShift) { - for (const auto till = shift + kShift; shift != till; ++shift) { - const auto min = (till - shift); - if ((*shift & kMask) < min) { - *shift = (*shift & 0xF0) | min; - } - } - } - for (auto shift = shifts.get(), ytill = shift + area; shift != ytill;) { - shift += width - (kShift - 1); - for (const auto till = shift + (kShift - 1); shift != till; ++shift) { - const auto max = kShift + (till - shift - 1); - if ((*shift & kMask) > max) { - *shift = (*shift & 0xF0) | max; - } - } - } - - auto result = image; - result.detach(); - - const auto src = reinterpret_cast(image.constBits()); - const auto dst = reinterpret_cast(result.bits()); - for (auto index = 0; index != area; ++index) { - const auto shift = shifts[index]; - const auto shiftx = int(shift & kMask) - kShift; - const auto shifty = int((shift >> 4) & kMask) - kShift; - dst[index] = src[index + (shifty * width) + shiftx]; - } - - return result; -} - [[nodiscard]] QImage DitherImage(QImage image) { Expects(image.bytesPerLine() == image.width() * 4); @@ -674,6 +806,53 @@ template // 4 means 16x16, 3 means 8x8 return image; } +[[nodiscard]] QImage GenerateGradient( + QSize size, + const std::vector &colors, + int rotation, + float progress) { + Expects(!colors.empty()); + Expects(colors.size() <= 4); + + if (size.isEmpty()) { + return QImage(); + } else if (colors.size() > 2) { + return GenerateComplexGradient(size, colors, rotation, progress); + } + auto result = QImage(size, QImage::Format_ARGB32_Premultiplied); + if (colors.size() == 1) { + result.fill(colors.front()); + return result; + } + + auto p = QPainter(&result); + const auto width = size.width(); + const auto height = size.height(); + const auto [start, finalStop] = [&]() -> std::pair { + const auto type = std::clamp(rotation, 0, 315) / 45; + switch (type) { + case 0: return { { 0, 0 }, { 0, height } }; + case 1: return { { width, 0 }, { 0, height } }; + case 2: return { { width, 0 }, { 0, 0 } }; + case 3: return { { width, height }, { 0, 0 } }; + case 4: return { { 0, height }, { 0, 0 } }; + case 5: return { { 0, height }, { width, 0 } }; + case 6: return { { 0, 0 }, { width, 0 } }; + case 7: return { { 0, 0 }, { width, height } }; + } + Unexpected("Rotation value in GenerateDitheredGradient."); + }(); + auto gradient = QLinearGradient(start, finalStop); + gradient.setStops(QGradientStops{ + { 0.0, colors[0] }, + { 1.0, colors[1] } + }); + p.fillRect(QRect(QPoint(), size), QBrush(std::move(gradient))); + p.end(); + + return result; +} + void prepareCircle(QImage &img) { Assert(!img.isNull()); diff --git a/ui/image/image_prepare.h b/ui/image/image_prepare.h index 9abad9e..e697e04 100644 --- a/ui/image/image_prepare.h +++ b/ui/image/image_prepare.h @@ -29,6 +29,12 @@ namespace Images { [[nodiscard]] QImage BlurLargeImage(QImage image, int radius); [[nodiscard]] QImage DitherImage(QImage image); +[[nodiscard]] QImage GenerateGradient( + QSize size, + const std::vector &colors, // colors.size() <= 4. + int rotation = 0, + float progress = 1.f); + [[nodiscard]] const std::array &CornersMask( ImageRoundRadius radius); [[nodiscard]] std::array PrepareCorners( From 38d65bf16e707682cae8763061050fa7b08a8322 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Sun, 15 Aug 2021 07:50:16 +0300 Subject: [PATCH 11/18] Added separate entity type for media timestamp. --- ui/text/text_entity.h | 1 + 1 file changed, 1 insertion(+) diff --git a/ui/text/text_entity.h b/ui/text/text_entity.h index 20be4ce..ad5c624 100644 --- a/ui/text/text_entity.h +++ b/ui/text/text_entity.h @@ -24,6 +24,7 @@ enum class EntityType : uchar { Mention, MentionName, BotCommand, + MediaTimestamp, Bold, Semibold, From 6b320a99da1d1a4430c8168998f62e1e5ec919ab Mon Sep 17 00:00:00 2001 From: John Preston Date: Tue, 17 Aug 2021 17:33:41 +0300 Subject: [PATCH 12/18] Generate gradients in QImage::Format_RGB32. --- ui/image/image_prepare.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/image/image_prepare.cpp b/ui/image/image_prepare.cpp index 72e3891..9ef543e 100644 --- a/ui/image/image_prepare.cpp +++ b/ui/image/image_prepare.cpp @@ -229,7 +229,7 @@ template // 4 means 16x16, 3 means 8x8 auto result = QImage( kWidth, kHeight, - QImage::Format_ARGB32_Premultiplied); + QImage::Format_RGB32); Assert(result.bytesPerLine() == kWidth * 4); auto cache = pixelCache.get(); @@ -819,7 +819,7 @@ QImage BlurLargeImage(QImage image, int radius) { } else if (colors.size() > 2) { return GenerateComplexGradient(size, colors, rotation, progress); } - auto result = QImage(size, QImage::Format_ARGB32_Premultiplied); + auto result = QImage(size, QImage::Format_RGB32); if (colors.size() == 1) { result.fill(colors.front()); return result; From 5938cb012fd78f5128fdefa3794fbae6c8f1dcf6 Mon Sep 17 00:00:00 2001 From: John Preston Date: Fri, 20 Aug 2021 20:10:21 +0300 Subject: [PATCH 13/18] Don't try to open files by empty paths. --- ui/image/image_prepare.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ui/image/image_prepare.cpp b/ui/image/image_prepare.cpp index 9ef543e..8ba2e77 100644 --- a/ui/image/image_prepare.cpp +++ b/ui/image/image_prepare.cpp @@ -418,6 +418,9 @@ std::array PrepareCorners( ReadResult Read(ReadArgs &&args) { if (args.content.isEmpty()) { + if (args.path.isEmpty()) { + return {}; + } auto file = QFile(args.path); if (file.size() > kReadBytesLimit || !file.open(QIODevice::ReadOnly)) { From 15ffd051d605be310051645412c54c2b9f87ff01 Mon Sep 17 00:00:00 2001 From: John Preston Date: Thu, 26 Aug 2021 16:24:25 +0300 Subject: [PATCH 14/18] Use flat_map for iconPixmaps. --- ui/style/style_core_icon.cpp | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/ui/style/style_core_icon.cpp b/ui/style/style_core_icon.cpp index c250beb..246da6c 100644 --- a/ui/style/style_core_icon.cpp +++ b/ui/style/style_core_icon.cpp @@ -20,7 +20,7 @@ uint32 colorKey(QColor c) { } base::flat_map iconMasks; -QMap, QPixmap> iconPixmaps; +base::flat_map, QPixmap> iconPixmaps; OrderedSet iconData; QImage createIconMask(const IconMask *mask, int scale) { @@ -268,12 +268,14 @@ void MonoIcon::ensureColorizedImage(QColor color) const { void MonoIcon::createCachedPixmap() const { auto key = qMakePair(_mask, colorKey(_color->c)); - auto j = iconPixmaps.constFind(key); - if (j == iconPixmaps.cend()) { + auto j = iconPixmaps.find(key); + if (j == end(iconPixmaps)) { auto image = colorizeImage(_maskImage, _color); - j = iconPixmaps.insert(key, QPixmap::fromImage(std::move(image))); + j = iconPixmaps.emplace( + key, + QPixmap::fromImage(std::move(image))).first; } - _pixmap = j.value(); + _pixmap = j->second; _size = _pixmap.size() / DevicePixelRatio(); } From 3c95a9187194c07fda409f1ad1e142232bb82cbc Mon Sep 17 00:00:00 2001 From: John Preston Date: Fri, 27 Aug 2021 23:43:50 +0300 Subject: [PATCH 15/18] Allow duplicating icons with different palettes. --- ui/style/style_core_icon.cpp | 37 +++++++++++++++++++++++++++++++++--- ui/style/style_core_icon.h | 20 ++++++++++++++++--- 2 files changed, 51 insertions(+), 6 deletions(-) diff --git a/ui/style/style_core_icon.cpp b/ui/style/style_core_icon.cpp index 246da6c..09b7c21 100644 --- a/ui/style/style_core_icon.cpp +++ b/ui/style/style_core_icon.cpp @@ -8,6 +8,7 @@ #include "ui/style/style_core.h" #include "base/basic_types.h" +#include "styles/palette.h" #include @@ -21,7 +22,7 @@ uint32 colorKey(QColor c) { base::flat_map iconMasks; base::flat_map, QPixmap> iconPixmaps; -OrderedSet iconData; +base::flat_set iconData; QImage createIconMask(const IconMask *mask, int scale) { auto maskImage = QImage::fromData(mask->data(), mask->size(), "PNG"); @@ -86,6 +87,14 @@ QSize readGeneratedSize(const IconMask *mask, int scale) { } // namespace +MonoIcon::MonoIcon(const MonoIcon &other, const style::palette &palette) +: _mask(other._mask) +, _color( + palette.colorAtIndex( + style::main_palette::indexOfColor(other._color))) +, _offset(other._offset) { +} + MonoIcon::MonoIcon(const IconMask *mask, Color color, QPoint offset) : _mask(mask) , _color(std::move(color)) @@ -279,8 +288,20 @@ void MonoIcon::createCachedPixmap() const { _size = _pixmap.size() / DevicePixelRatio(); } +IconData::IconData(const IconData &other, const style::palette &palette) { + created(); + _parts.reserve(other._parts.size()); + for (const auto &part : other._parts) { + _parts.push_back(MonoIcon(part, palette)); + } +} + void IconData::created() { - iconData.insert(this); + iconData.emplace(this); +} + +IconData::~IconData() { + iconData.remove(this); } void IconData::fill(QPainter &p, const QRect &rect) const { @@ -306,7 +327,8 @@ void IconData::fill(QPainter &p, const QRect &rect, QColor colorOverride) const } QImage IconData::instance(QColor colorOverride, int scale) const { - Assert(_parts.size() == 1); + Expects(_parts.size() == 1); + auto &part = _parts[0]; Assert(part.offset() == QPoint(0, 0)); return part.instance(colorOverride, scale); @@ -332,6 +354,15 @@ int IconData::height() const { return _height; } +Icon Icon::withPalette(const style::palette &palette) const { + Expects(_data != nullptr); + + auto result = Icon(Qt::Uninitialized); + result._data = new IconData(*_data, palette); + result._owner = true; + return result; +} + void resetIcons() { iconPixmaps.clear(); for (const auto data : iconData) { diff --git a/ui/style/style_core_icon.h b/ui/style/style_core_icon.h index f5c3f59..a9707ca 100644 --- a/ui/style/style_core_icon.h +++ b/ui/style/style_core_icon.h @@ -43,6 +43,7 @@ public: MonoIcon &operator=(const MonoIcon &other) = delete; MonoIcon(MonoIcon &&other) = default; MonoIcon &operator=(MonoIcon &&other) = default; + MonoIcon(const MonoIcon &other, const style::palette &palette); MonoIcon(const IconMask *mask, Color color, QPoint offset); void reset() const; @@ -82,13 +83,18 @@ private: class IconData { public: + struct FromIcons { + }; template - IconData(MonoIcons &&...icons) { + IconData(FromIcons, MonoIcons &&...icons) { created(); _parts.reserve(sizeof...(MonoIcons)); addIcons(std::forward(icons)...); } + IconData(const IconData &other, const style::palette &palette); + ~IconData(); + void reset() { for (const auto &part : _parts) { part.reset(); @@ -149,11 +155,17 @@ public: } template - Icon(MonoIcons&&... icons) : _data(new IconData(std::forward(icons)...)), _owner(true) { + Icon(MonoIcons&&... icons) + : _data(new IconData( + IconData::FromIcons{}, + std::forward(icons)...)) + , _owner(true) { } Icon(const Icon &other) : _data(other._data) { } - Icon(Icon &&other) : _data(base::take(other._data)), _owner(base::take(other._owner)) { + Icon(Icon &&other) + : _data(base::take(other._data)) + , _owner(base::take(other._owner)) { } Icon &operator=(const Icon &other) { Expects(!_owner); @@ -247,6 +259,8 @@ public: return Proxy(*this, paletteOverride); } + Icon withPalette(const style::palette &palette) const; + ~Icon() { if (auto data = base::take(_data)) { if (_owner) { From 2e3eef52f7eac3b804164bf91990baa75fffb818 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Mon, 30 Aug 2021 18:08:41 +0300 Subject: [PATCH 16/18] Added new send action for choosing sticker. --- ui/widgets/widgets.style | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/ui/widgets/widgets.style b/ui/widgets/widgets.style index 2b7aab5..5354be8 100644 --- a/ui/widgets/widgets.style +++ b/ui/widgets/widgets.style @@ -1188,6 +1188,12 @@ historySendActionUploadStrokeNumerator: 16px; historySendActionUploadSizeNumerator: 32px; historySendActionUploadDenominator: 8.; +historySendActionChooseStickerDuration: 2000; +historySendActionChooseStickerPosition: point(1px, -10px); +historySendActionChooseStickerEyeWidth: 7px; +historySendActionChooseStickerEyeHeight: 11px; +historySendActionChooseStickerEyeStep: 2px; + MediaPlayerButton { playPosition: point; playOuter: size; From ec3af38ad964aeb1ed64160f5042a75df7b9e4a6 Mon Sep 17 00:00:00 2001 From: John Preston Date: Wed, 1 Sep 2021 18:50:00 +0300 Subject: [PATCH 17/18] Attempt to fix a possible crash in InputField. --- ui/widgets/input_fields.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/ui/widgets/input_fields.cpp b/ui/widgets/input_fields.cpp index 2dd8674..c841cc6 100644 --- a/ui/widgets/input_fields.cpp +++ b/ui/widgets/input_fields.cpp @@ -1295,7 +1295,6 @@ InputField::InputField( , _lastTextWithTags(value) , _placeholderFull(std::move(placeholder)) { _inner->setDocument(CreateChild(_inner.get(), _st)); - _inner->setAcceptRichText(false); resize(_st.width, _minHeight); @@ -2976,7 +2975,7 @@ void InputField::inputMethodEventInner(QInputMethodEvent *e) { const auto weak = Ui::MakeWeak(this); _inner->QTextEdit::inputMethodEvent(e); - if (weak) { + if (weak && _inputMethodCommit.has_value()) { const auto text = *base::take(_inputMethodCommit); if (!processMarkdownReplaces(text)) { processInstantReplaces(text); From ea570c07b6feef698f103a2876991f05f3ea1eb3 Mon Sep 17 00:00:00 2001 From: John Preston Date: Wed, 1 Sep 2021 19:08:07 +0300 Subject: [PATCH 18/18] Attempt to fix a crash in event loop nesting tracking. --- ui/platform/win/ui_window_win.cpp | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/ui/platform/win/ui_window_win.cpp b/ui/platform/win/ui_window_win.cpp index 2c72ddb..130d63e 100644 --- a/ui/platform/win/ui_window_win.cpp +++ b/ui/platform/win/ui_window_win.cpp @@ -10,6 +10,7 @@ #include "ui/platform/win/ui_window_title_win.h" #include "base/platform/base_platform_info.h" #include "base/platform/win/base_windows_safe_library.h" +#include "base/integration.h" #include "base/debug_log.h" #include "styles/palette.h" @@ -150,15 +151,19 @@ bool WindowHelper::NativeFilter::nativeEventFilter( const QByteArray &eventType, void *message, long *result) { + auto filtered = false; const auto msg = static_cast(message); const auto i = _windowByHandle.find(msg->hwnd); - return (i != end(_windowByHandle)) - ? i->second->handleNativeEvent( - msg->message, - msg->wParam, - msg->lParam, - reinterpret_cast(result)) - : false; + if (i != end(_windowByHandle)) { + base::Integration::Instance().enterFromEventLoop([&] { + filtered = i->second->handleNativeEvent( + msg->message, + msg->wParam, + msg->lParam, + reinterpret_cast(result)); + }); + } + return filtered; } WindowHelper::WindowHelper(not_null window)