diff --git a/CMakeLists.txt b/CMakeLists.txt index 6646223..1f9a6cf 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -139,8 +139,6 @@ PRIVATE ui/style/style_core.h ui/style/style_core_color.cpp ui/style/style_core_color.h - ui/style/style_core_custom_font.cpp - ui/style/style_core_custom_font.h ui/style/style_core_direction.cpp ui/style/style_core_direction.h ui/style/style_core_font.cpp @@ -263,6 +261,7 @@ PRIVATE ui/abstract_button.h ui/animated_icon.cpp ui/animated_icon.h + ui/arc_angles.h ui/basic_click_handlers.cpp ui/basic_click_handlers.h ui/cached_special_layer_shadow_corners.cpp diff --git a/fonts/DAOpenSansBold.ttf b/fonts/DAOpenSansBold.ttf deleted file mode 100644 index bdc55af..0000000 Binary files a/fonts/DAOpenSansBold.ttf and /dev/null differ diff --git a/fonts/DAOpenSansBoldItalic.ttf b/fonts/DAOpenSansBoldItalic.ttf deleted file mode 100644 index 47ebb8b..0000000 Binary files a/fonts/DAOpenSansBoldItalic.ttf and /dev/null differ diff --git a/fonts/DAOpenSansSemiboldAsBold.ttf b/fonts/DAOpenSansSemiboldAsBold.ttf deleted file mode 100644 index ef2eadd..0000000 Binary files a/fonts/DAOpenSansSemiboldAsBold.ttf and /dev/null differ diff --git a/fonts/DAOpenSansSemiboldItalicAsBold.ttf b/fonts/DAOpenSansSemiboldItalicAsBold.ttf deleted file mode 100644 index 1507d53..0000000 Binary files a/fonts/DAOpenSansSemiboldItalicAsBold.ttf and /dev/null differ diff --git a/fonts/DAVazirBold.ttf b/fonts/DAVazirBold.ttf deleted file mode 100644 index 28e6a07..0000000 Binary files a/fonts/DAVazirBold.ttf and /dev/null differ diff --git a/fonts/DAVazirMedium.ttf b/fonts/DAVazirMedium.ttf deleted file mode 100644 index 4e303f9..0000000 Binary files a/fonts/DAVazirMedium.ttf and /dev/null differ diff --git a/fonts/DAVazirMediumAsBold.ttf b/fonts/DAVazirMediumAsBold.ttf deleted file mode 100644 index f69068c..0000000 Binary files a/fonts/DAVazirMediumAsBold.ttf and /dev/null differ diff --git a/fonts/DAVazirRegular.ttf b/fonts/DAVazirRegular.ttf deleted file mode 100644 index 15985f8..0000000 Binary files a/fonts/DAVazirRegular.ttf and /dev/null differ diff --git a/fonts/DAOpenSansRegularItalic.ttf b/fonts/OpenSans-Italic.ttf similarity index 97% rename from fonts/DAOpenSansRegularItalic.ttf rename to fonts/OpenSans-Italic.ttf index 6c6e046..88c01b6 100644 Binary files a/fonts/DAOpenSansRegularItalic.ttf and b/fonts/OpenSans-Italic.ttf differ diff --git a/fonts/DAOpenSansRegular.ttf b/fonts/OpenSans-Regular.ttf similarity index 97% rename from fonts/DAOpenSansRegular.ttf rename to fonts/OpenSans-Regular.ttf index 705a2e5..abd6746 100644 Binary files a/fonts/DAOpenSansRegular.ttf and b/fonts/OpenSans-Regular.ttf differ diff --git a/fonts/DAOpenSansSemibold.ttf b/fonts/OpenSans-SemiBold.ttf similarity index 98% rename from fonts/DAOpenSansSemibold.ttf rename to fonts/OpenSans-SemiBold.ttf index 71d61b7..6bfb1f8 100644 Binary files a/fonts/DAOpenSansSemibold.ttf and b/fonts/OpenSans-SemiBold.ttf differ diff --git a/fonts/DAOpenSansSemiboldItalic.ttf b/fonts/OpenSans-SemiBoldItalic.ttf similarity index 98% rename from fonts/DAOpenSansSemiboldItalic.ttf rename to fonts/OpenSans-SemiBoldItalic.ttf index 7e45d7a..3ba9213 100644 Binary files a/fonts/DAOpenSansSemiboldItalic.ttf and b/fonts/OpenSans-SemiBoldItalic.ttf differ diff --git a/fonts/Vazirmatn-UI-NL-Regular.ttf b/fonts/Vazirmatn-UI-NL-Regular.ttf new file mode 100644 index 0000000..9deb47e Binary files /dev/null and b/fonts/Vazirmatn-UI-NL-Regular.ttf differ diff --git a/fonts/Vazirmatn-UI-NL-SemiBold.ttf b/fonts/Vazirmatn-UI-NL-SemiBold.ttf new file mode 100644 index 0000000..3255b93 Binary files /dev/null and b/fonts/Vazirmatn-UI-NL-SemiBold.ttf differ diff --git a/fonts/fonts.qrc b/fonts/fonts.qrc index 22a4a29..19baebb 100644 --- a/fonts/fonts.qrc +++ b/fonts/fonts.qrc @@ -1,13 +1,10 @@ - DAOpenSansRegular.ttf - DAOpenSansRegularItalic.ttf - DAOpenSansSemiboldAsBold.ttf - DAOpenSansSemiboldItalicAsBold.ttf - DAOpenSansSemibold.ttf - DAOpenSansSemiboldItalic.ttf - DAVazirRegular.ttf - DAVazirMediumAsBold.ttf - DAVazirMedium.ttf + OpenSans-Regular.ttf + OpenSans-Italic.ttf + OpenSans-SemiBold.ttf + OpenSans-SemiBoldItalic.ttf + Vazirmatn-UI-NL-Regular.ttf + Vazirmatn-UI-NL-SemiBold.ttf diff --git a/ui/arc_angles.h b/ui/arc_angles.h new file mode 100644 index 0000000..39c1ce2 --- /dev/null +++ b/ui/arc_angles.h @@ -0,0 +1,18 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +namespace arc { + +constexpr auto kFullLength = 360 * 16; +constexpr auto kHalfLength = (kFullLength / 2); +constexpr auto kQuarterLength = (kFullLength / 4); +constexpr auto kMinLength = (kFullLength / 360); +constexpr auto kAlmostFullLength = (kFullLength - kMinLength); + +} // namespace arc diff --git a/ui/basic.style b/ui/basic.style index 8d55bac..de0312c 100644 --- a/ui/basic.style +++ b/ui/basic.style @@ -118,6 +118,8 @@ defaultVerticalListSkip: 6px; shakeShift: 4px; shakeDuration: 300; +universalDuration: 120; + // floating badge colors roundedFg: radialFg; roundedBg: radialBg; // closest to #00000066 diff --git a/ui/colors.palette b/ui/colors.palette index 71081e0..699842c 100644 --- a/ui/colors.palette +++ b/ui/colors.palette @@ -670,6 +670,10 @@ statisticsChartLineIndigo: #7f79f3; // represents indigo color on statistical ch statisticsChartLinePurple: #9f79e8; // represents purple color on statistical charts statisticsChartLineCyan: #40d0ca; // represents cyan color on statistical charts +creditsBg1: #f7d05f; // credits icon gradient 1 +creditsBg2: #fcecc0; // credits icon gradient 2 +creditsStroke: #da8735; // credits icon stroke + // kotatogram ktgTopBarBg: topBarBg; // Kotatogram: top bar background ktgTopBarNameFg: dialogsNameFg; // Kotatogram: top bar name text diff --git a/ui/effects/animations.cpp b/ui/effects/animations.cpp index b0c69d9..3fb737a 100644 --- a/ui/effects/animations.cpp +++ b/ui/effects/animations.cpp @@ -6,8 +6,9 @@ // #include "ui/effects/animations.h" -#include "ui/ui_utility.h" #include "base/invoke_queued.h" +#include "ui/ui_utility.h" +#include "styles/style_basic.h" #include @@ -22,7 +23,7 @@ namespace Ui { namespace Animations { namespace { -constexpr auto kAnimationTick = crl::time(1000) / 120; +constexpr auto kAnimationTick = crl::time(1000) / st::universalDuration; constexpr auto kIgnoreUpdatesTimeout = crl::time(4); Manager *ManagerInstance = nullptr; diff --git a/ui/effects/cross_animation.cpp b/ui/effects/cross_animation.cpp index 18a9cd4..e847ab3 100644 --- a/ui/effects/cross_animation.cpp +++ b/ui/effects/cross_animation.cpp @@ -7,6 +7,7 @@ #include "ui/effects/cross_animation.h" #include "ui/effects/animation_value.h" +#include "ui/arc_angles.h" #include "ui/painter.h" #include @@ -17,7 +18,6 @@ namespace { constexpr auto kPointCount = 12; constexpr auto kStaticLoadingValue = float64(-666); -constexpr auto kFullArcLength = 360 * 16; // @@ -148,12 +148,12 @@ void CrossAnimation::paint( auto pathDeleteSize = kPointCount; const auto staticLoading = (loading == kStaticLoadingValue); - auto loadingArcLength = staticLoading ? kFullArcLength : 0; + auto loadingArcLength = staticLoading ? arc::kFullLength : 0; if (loading > 0.) { transformLoadingCross(loading, pathDelete, pathDeleteSize); auto loadingArc = (loading >= 0.5) ? (loading - 1.) : loading; - loadingArcLength = qRound(-loadingArc * 2 * kFullArcLength); + loadingArcLength = qRound(-loadingArc * 2 * arc::kFullLength); } if (!staticLoading) { @@ -184,9 +184,9 @@ void CrossAnimation::paint( if (staticLoading) { anim::DrawStaticLoading(p, roundPart, stroke, color); } else { - auto loadingArcStart = kFullArcLength / 8; + auto loadingArcStart = arc::kQuarterLength / 2; if (shown < 1.) { - loadingArcStart -= qRound(-(shown - 1.) * kFullArcLength / 4.); + loadingArcStart -= qRound(-(shown - 1.) * arc::kQuarterLength); } if (loadingArcLength < 0) { loadingArcStart += loadingArcLength; diff --git a/ui/effects/radial_animation.cpp b/ui/effects/radial_animation.cpp index 2ae1d53..8de21b6 100644 --- a/ui/effects/radial_animation.cpp +++ b/ui/effects/radial_animation.cpp @@ -6,16 +6,14 @@ // #include "ui/effects/radial_animation.h" +#include "ui/arc_angles.h" #include "ui/painter.h" #include "styles/style_widgets.h" namespace Ui { namespace { -constexpr auto kFullArcLength = 360 * 16; -constexpr auto kQuarterArcLength = (kFullArcLength / 4); -constexpr auto kMinArcLength = (kFullArcLength / 360); -constexpr auto kAlmostFullArcLength = (kFullArcLength - kMinArcLength); +constexpr auto kFullArcLength = arc::kFullLength; } // namespace @@ -23,14 +21,14 @@ const int RadialState::kFull = kFullArcLength; void RadialAnimation::start(float64 prg) { _firstStart = _lastStart = _lastTime = crl::now(); - const auto iprg = qRound(qMax(prg, 0.0001) * kAlmostFullArcLength); - const auto iprgstrict = qRound(prg * kAlmostFullArcLength); + const auto iprg = qRound(qMax(prg, 0.0001) * arc::kAlmostFullLength); + const auto iprgstrict = qRound(prg * arc::kAlmostFullLength); _arcEnd = anim::value(iprgstrict, iprg); _animation.start(); } bool RadialAnimation::update(float64 prg, bool finished, crl::time ms) { - const auto iprg = qRound(qMax(prg, 0.0001) * kAlmostFullArcLength); + const auto iprg = qRound(qMax(prg, 0.0001) * arc::kAlmostFullLength); const auto result = (iprg != qRound(_arcEnd.to())) || (_finished != finished); if (_finished != finished) { @@ -101,13 +99,13 @@ void RadialAnimation::draw( } RadialState RadialAnimation::computeState() const { - auto length = kMinArcLength + qRound(_arcEnd.current()); - auto from = kQuarterArcLength + auto length = arc::kMinLength + qRound(_arcEnd.current()); + auto from = arc::kQuarterLength - length - (anim::Disabled() ? 0 : qRound(_arcStart.current())); if (style::RightToLeft()) { - from = kQuarterArcLength - (from - kQuarterArcLength) - length; - if (from < 0) from += kFullArcLength; + from = arc::kQuarterLength - (from - arc::kQuarterLength) - length; + if (from < 0) from += arc::kFullLength; } return { _opacity, from, length }; } diff --git a/ui/gl/gl_detection.cpp b/ui/gl/gl_detection.cpp index 150eb8b..a6707af 100644 --- a/ui/gl/gl_detection.cpp +++ b/ui/gl/gl_detection.cpp @@ -33,6 +33,7 @@ namespace Ui::GL { namespace { bool ForceDisabled/* = false*/; +bool LastCheckCrashed/* = false*/; #ifdef Q_OS_WIN ANGLE ResolvedANGLE/* = ANGLE::Auto*/; @@ -59,9 +60,14 @@ void CrashCheckStart() { const char kOptionAllowLinuxNvidiaOpenGL[] = "allow-linux-nvidia-opengl"; Capabilities CheckCapabilities(QWidget *widget, bool avoidWidgetCreation) { - if (ForceDisabled) { - LOG_ONCE(("OpenGL: Force-disabled.")); - return {}; + if (!Platform::IsMac()) { + if (ForceDisabled) { + LOG_ONCE(("OpenGL: Force-disabled.")); + return {}; + } else if (LastCheckCrashed) { + LOG_ONCE(("OpenGL: Last-crashed.")); + return {}; + } } [[maybe_unused]] static const auto BugListInited = [] { @@ -243,8 +249,17 @@ Backend ChooseBackendDefault(Capabilities capabilities) { return use ? Backend::OpenGL : Backend::Raster; } +void DetectLastCheckCrash() { + [[maybe_unused]] static const auto Once = [] { + LastCheckCrashed = !Platform::IsMac() + && QFile::exists(Integration::Instance().openglCheckFilePath()); + return false; + }(); +} + bool LastCrashCheckFailed() { - return QFile::exists(Integration::Instance().openglCheckFilePath()); + DetectLastCheckCrash(); + return LastCheckCrashed; } void CrashCheckFinish() { @@ -252,7 +267,9 @@ void CrashCheckFinish() { } void ForceDisable(bool disable) { - ForceDisabled = disable; + if (!Platform::IsMac()) { + ForceDisabled = disable; + } } #ifdef Q_OS_WIN diff --git a/ui/gl/gl_detection.h b/ui/gl/gl_detection.h index 97e6606..5b23fa7 100644 --- a/ui/gl/gl_detection.h +++ b/ui/gl/gl_detection.h @@ -31,6 +31,7 @@ struct Capabilities { void ForceDisable(bool disable); +void DetectLastCheckCrash(); [[nodiscard]] bool LastCrashCheckFailed(); void CrashCheckFinish(); diff --git a/ui/integration.cpp b/ui/integration.cpp index f249c18..5306967 100644 --- a/ui/integration.cpp +++ b/ui/integration.cpp @@ -6,7 +6,6 @@ // #include "ui/integration.h" -#include "ui/style/style_core_custom_font.h" #include "ui/gl/gl_detection.h" #include "ui/text/text_entity.h" #include "ui/text/text_block.h" @@ -45,10 +44,6 @@ void Integration::textActionsUpdated() { void Integration::activationFromTopPanel() { } -style::CustomFontSettings Integration::fontSettings() { - return {}; -} - bool Integration::screenIsLocked() { return false; } diff --git a/ui/integration.h b/ui/integration.h index 7c39b0d..4b3a9b4 100644 --- a/ui/integration.h +++ b/ui/integration.h @@ -23,10 +23,6 @@ class ClickHandler; struct ClickContext; struct EntityLinkData; -namespace style { -struct CustomFontSettings; -} // namespace style - namespace Ui { namespace Emoji { class One; @@ -53,8 +49,6 @@ public: virtual void textActionsUpdated(); virtual void activationFromTopPanel(); - virtual style::CustomFontSettings fontSettings(); - [[nodiscard]] virtual bool screenIsLocked(); [[nodiscard]] virtual std::shared_ptr createLinkHandler( diff --git a/ui/layers/box_content.cpp b/ui/layers/box_content.cpp index 9ed0e96..5575885 100644 --- a/ui/layers/box_content.cpp +++ b/ui/layers/box_content.cpp @@ -263,8 +263,21 @@ RectParts BoxContent::customCornersFilling() { } void BoxContent::scrollToY(int top, int bottom) { + scrollTo({ top, bottom }); +} + +void BoxContent::scrollTo(ScrollToRequest request, anim::type animated) { if (_scroll) { - _scroll->scrollToY(top, bottom); + const auto v = _scroll->computeScrollTo(request.ymin, request.ymax); + const auto now = _scroll->scrollTop(); + if (animated == anim::type::instant || v == now) { + _scrollAnimation.stop(); + _scroll->scrollToY(v); + } else { + _scrollAnimation.start([=] { + _scroll->scrollToY(_scrollAnimation.value(v)); + }, now, v, st::slideWrapDuration, anim::sineInOut); + } } } diff --git a/ui/layers/box_content.h b/ui/layers/box_content.h index 5526b41..237d38c 100644 --- a/ui/layers/box_content.h +++ b/ui/layers/box_content.h @@ -13,6 +13,7 @@ #include "ui/widgets/labels.h" #include "ui/layers/layer_widget.h" #include "ui/layers/show.h" +#include "ui/effects/animations.h" #include "ui/effects/animation_value.h" #include "ui/text/text_entity.h" #include "ui/rp_widget.h" @@ -59,6 +60,7 @@ class ScrollArea; class FlatLabel; class FadeShadow; class BoxContent; +struct ScrollToRequest; class BoxContentDelegate { public: @@ -213,6 +215,9 @@ public: void scrollByDraggingDelta(int delta); void scrollToY(int top, int bottom = -1); + void scrollTo( + ScrollToRequest request, + anim::type animated = anim::type::instant); void sendScrollViewportEvent(not_null event); [[nodiscard]] rpl::producer<> scrolls() const; [[nodiscard]] int scrollTop() const; @@ -318,6 +323,7 @@ private: object_ptr _bottomShadow = { nullptr }; Ui::DraggingScrollManager _draggingScroll; + Ui::Animations::Simple _scrollAnimation; rpl::event_stream<> _boxClosingStream; diff --git a/ui/painter.h b/ui/painter.h index dcc3f89..f9e630a 100644 --- a/ui/painter.h +++ b/ui/painter.h @@ -22,14 +22,16 @@ public: void drawTextLeft(int x, int y, int outerw, const QString &text, int textWidth = -1) { QFontMetrics m(fontMetrics()); - auto ascent = (_ascent == 0 ? m.ascent() : _ascent); if (style::RightToLeft() && textWidth < 0) textWidth = m.horizontalAdvance(text); + const auto result = style::FindAdjustResult(font()); + const auto ascent = result ? result->iascent : m.ascent(); drawText(style::RightToLeft() ? (outerw - x - textWidth) : x, y + ascent, text); } void drawTextRight(int x, int y, int outerw, const QString &text, int textWidth = -1) { QFontMetrics m(fontMetrics()); - auto ascent = (_ascent == 0 ? m.ascent() : _ascent); if (!style::RightToLeft() && textWidth < 0) textWidth = m.horizontalAdvance(text); + const auto result = style::FindAdjustResult(font()); + const auto ascent = result ? result->iascent : m.ascent(); drawText(style::RightToLeft() ? x : (outerw - x - textWidth), y + ascent, text); } void drawPixmapLeft(int x, int y, int outerw, const QPixmap &pix, const QRect &from) { @@ -84,10 +86,6 @@ public: [[nodiscard]] bool inactive() const { return _inactive; } - void setFont(const style::font &font) { - _ascent = font->ascent; - QPainter::setFont(font->f); - } void setTextSpoilerMess(not_null mess) { _spoilerMess = mess; } @@ -102,7 +100,6 @@ private: const style::TextPalette *_textPalette = nullptr; Ui::Text::SpoilerMess *_spoilerMess = nullptr; bool _inactive = false; - int _ascent = 0; }; diff --git a/ui/platform/mac/ui_window_title_mac.mm b/ui/platform/mac/ui_window_title_mac.mm index 4b1100b..ddfe882 100644 --- a/ui/platform/mac/ui_window_title_mac.mm +++ b/ui/platform/mac/ui_window_title_mac.mm @@ -107,13 +107,13 @@ void TitleWidget::init(int height) { const auto apple = (family == u".AppleSystemUIFont"_q); setFromFont(style::font( apple ? 13 : (height * 15) / 24, - apple ? style::internal::FontBold : 0, + apple ? style::FontFlag::Bold : style::FontFlag(), family)); break; } } if (!_textStyle) { - setFromFont(style::font(13, style::internal::FontSemibold, 0)); + setFromFont(style::font(13, style::FontFlag::Semibold, 0)); } } diff --git a/ui/platform/ui_platform_window.cpp b/ui/platform/ui_platform_window.cpp index 926f51f..8ae5966 100644 --- a/ui/platform/ui_platform_window.cpp +++ b/ui/platform/ui_platform_window.cpp @@ -424,11 +424,36 @@ void DefaultWindowHelper::updateRoundingOverlay() { rect.bottomRight() - QPoint(radiusWithFix, radiusWithFix), radiusSize )) || !rect.contains(clip); - }) | rpl::start_with_next([=] { + }) | rpl::start_with_next([=](QRect clip) { Painter p(_roundingOverlay); - const auto rect = window()->rect().marginsRemoved(resizeArea()); + const auto skip = resizeArea(); + const auto outer = window()->rect(); + + const auto rect = outer.marginsRemoved(skip); p.setCompositionMode(QPainter::CompositionMode_DestinationIn); _roundRect.paint(p, rect, RectPart::AllCorners); + + p.setCompositionMode(QPainter::CompositionMode_Source); + const auto outside = std::array{ + QRect(0, 0, outer.width(), skip.top()), + QRect(0, skip.top(), skip.left(), outer.height() - skip.top()), + QRect( + outer.width() - skip.right(), + skip.top(), + skip.right(), + outer.height() - skip.top()), + QRect( + skip.left(), + outer.height() - skip.bottom(), + outer.width() - skip.left() - skip.right(), + skip.bottom()) + }; + for (const auto &part : outside) { + if (const auto fill = clip.intersected(part); !fill.isEmpty()) { + p.fillRect(fill, Qt::transparent); + } + } + p.setCompositionMode(QPainter::CompositionMode_SourceOver); Shadow::paint(p, rect, window()->width(), Shadow(), _sides, _corners); }, _roundingOverlay->lifetime()); diff --git a/ui/platform/win/ui_window_win.cpp b/ui/platform/win/ui_window_win.cpp index 4ac4d77..cedf461 100644 --- a/ui/platform/win/ui_window_win.cpp +++ b/ui/platform/win/ui_window_win.cpp @@ -576,6 +576,7 @@ bool WindowHelper::handleNativeEvent( } updateWindowFrameColors(active); window()->update(); + _title->update(); } return false; case WM_NCPAINT: { diff --git a/ui/rp_widget.cpp b/ui/rp_widget.cpp index 41b24bc..0b57704 100644 --- a/ui/rp_widget.cpp +++ b/ui/rp_widget.cpp @@ -15,9 +15,6 @@ #include #include -// Patching out this code without patching out all other private API usage -// and the Qt::{Core,Gui,Widgets}Private cmake dependency is asking -// for memory corruption class TWidgetPrivate : public QWidgetPrivate { public: #if QT_VERSION >= QT_VERSION_CHECK(6, 4, 0) diff --git a/ui/style/style_core.cpp b/ui/style/style_core.cpp index 8015bf3..e589944 100644 --- a/ui/style/style_core.cpp +++ b/ui/style/style_core.cpp @@ -29,12 +29,12 @@ auto PaletteVersion = 0; auto ShortAnimationRunning = rpl::variable(false); auto RunningShortAnimations = 0; -std::vector &StyleModules() { +[[nodiscard]] std::vector &StyleModules() { static auto result = std::vector(); return result; } -void startModules(int scale) { +void StartModules(int scale) { for (const auto module : StyleModules()) { module->start(scale); } @@ -60,14 +60,14 @@ void StopShortAnimation() { } // namespace internal -void startManager(int scale) { - internal::registerFontFamily("Open Sans"); - internal::startModules(scale); +void StartManager(int scale) { + internal::RegisterFontFamily("Open Sans"); + internal::StartModules(scale); } -void stopManager() { - internal::destroyFonts(); - internal::destroyIcons(); +void StopManager() { + internal::DestroyFonts(); + internal::DestroyIcons(); } rpl::producer<> PaletteChanged() { diff --git a/ui/style/style_core.h b/ui/style/style_core.h index 0d517f9..aed0de3 100644 --- a/ui/style/style_core.h +++ b/ui/style/style_core.h @@ -35,8 +35,8 @@ void StopShortAnimation(); } // namespace internal -void startManager(int scale); -void stopManager(); +void StartManager(int scale); +void StopManager(); [[nodiscard]] rpl::producer<> PaletteChanged(); [[nodiscard]] int PaletteVersion(); diff --git a/ui/style/style_core_color.h b/ui/style/style_core_color.h index 40a4def..1d7a592 100644 --- a/ui/style/style_core_color.h +++ b/ui/style/style_core_color.h @@ -57,27 +57,28 @@ public: _data->set(r, g, b, a); } - operator const QBrush &() const { + [[nodiscard]] operator const QBrush &() const { return _data->b; } - operator const QPen &() const { + [[nodiscard]] operator const QPen &() const { return _data->p; } - ColorData *operator->() const { + [[nodiscard]] ColorData *operator->() const { return _data; } - ColorData *v() const { + [[nodiscard]] ColorData *get() const { return _data; } - explicit operator bool() const { + [[nodiscard]] explicit operator bool() const { return !!_data; } class Proxy; - Proxy operator[](const style::palette &paletteOverride) const; + [[nodiscard]] Proxy operator[]( + const style::palette &paletteOverride) const; private: friend class OwnedColor; @@ -164,12 +165,12 @@ public: } Proxy(const Proxy &other) = default; - operator const QBrush &() const { return _color; } - operator const QPen &() const { return _color; } - ColorData *operator->() const { return _color.v(); } - ColorData *v() const { return _color.v(); } - explicit operator bool() const { return _color ? true : false; } - Color clone() const { return _color; } + [[nodiscard]] operator const QBrush &() const { return _color; } + [[nodiscard]] operator const QPen &() const { return _color; } + [[nodiscard]] ColorData *operator->() const { return _color.get(); } + [[nodiscard]] ColorData *get() const { return _color.get(); } + [[nodiscard]] explicit operator bool() const { return !!_color; } + [[nodiscard]] Color clone() const { return _color; } private: Color _color; diff --git a/ui/style/style_core_custom_font.cpp b/ui/style/style_core_custom_font.cpp deleted file mode 100644 index 6c279a1..0000000 --- a/ui/style/style_core_custom_font.cpp +++ /dev/null @@ -1,95 +0,0 @@ -// 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/style/style_core_custom_font.h" - -#include "ui/style/style_core_font.h" -#include "ui/style/style_core_scale.h" -#include "ui/integration.h" - -#include -#include - -namespace style { -namespace { - -using namespace internal; - -auto RegularFont = CustomFont(); -auto BoldFont = CustomFont(); - -} // namespace - -void SetCustomFonts(const CustomFont ®ular, const CustomFont &bold) { - RegularFont = regular; - BoldFont = bold; -} - -QFont ResolveFont(const QString &familyOverride, uint32 flags, int size) { - static auto Database = QFontDatabase(); - - const auto fontSettings = Ui::Integration::Instance().fontSettings(); - const auto overrideIsEmpty = GetPossibleEmptyOverride(flags).isEmpty(); - - const auto bold = ((flags & FontBold) || (flags & FontSemibold)); - const auto italic = (flags & FontItalic); - const auto &custom = bold ? BoldFont : RegularFont; - const auto useCustom = !custom.family.isEmpty(); - - auto result = QFont(QGuiApplication::font().family()); - if (!familyOverride.isEmpty()) { - result.setFamily(familyOverride); - if (bold) { - result.setBold(true); - } - } else if (flags & FontMonospace) { - result.setFamily(MonospaceFont()); - } else if (useCustom) { - const auto sizes = Database.smoothSizes(custom.family, custom.style); - const auto good = sizes.isEmpty() - ? Database.pointSizes(custom.family, custom.style) - : sizes; - const auto point = good.isEmpty() ? size : good.front(); - result = Database.font(custom.family, custom.style, point); - } else { - if (!fontSettings.useSystemFont || !overrideIsEmpty) { - result.setFamily(GetFontOverride(flags)); - } - if (bold) { - if (fontSettings.semiboldIsBold) { - result.setBold(true); -#ifdef LIB_UI_USE_PACKAGED_FONTS - } else { - result.setWeight(QFont::DemiBold); -#else // LIB_UI_USE_PACKAGED_FONTS - } else if (fontSettings.useSystemFont) { - result.setWeight(QFont::DemiBold); - } else { - result.setBold(true); -#endif // !LIB_UI_USE_PACKAGED_FONTS - } - - if (!fontSettings.semiboldIsBold) { - if (flags & FontItalic) { - result.setStyleName("Semibold Italic"); - } else { - result.setStyleName("Semibold"); - } - } - } - } - if (italic) { - result.setItalic(true); - } - - result.setUnderline(flags & FontUnderline); - result.setStrikeOut(flags & FontStrikeOut); - result.setPixelSize(size + ConvertScale(fontSettings.fontSize)); - - return result; -} - -} // namespace style diff --git a/ui/style/style_core_custom_font.h b/ui/style/style_core_custom_font.h deleted file mode 100644 index 289168b..0000000 --- a/ui/style/style_core_custom_font.h +++ /dev/null @@ -1,55 +0,0 @@ -// 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 - -namespace style { - -struct CustomFont { - QString family; - QString style; -}; - -struct CustomFontSettings { - QString mainFont; - QString semiboldFont; - QString monospaceFont; - int fontSize = 0; - bool semiboldIsBold = false; - bool useSystemFont = false; - bool useOriginalMetrics = false; -}; - -inline bool operator==(const CustomFont &a, const CustomFont &b) { - return (a.family == b.family) && (a.style == b.style); -} - -inline bool operator<(const CustomFont &a, const CustomFont &b) { - return (a.family < b.family) - || (a.family == b.family && a.style < b.style); -} - -inline bool operator!=(const CustomFont &a, const CustomFont &b) { - return !(a == b); -} - -inline bool operator>(const CustomFont &a, const CustomFont &b) { - return (b < a); -} - -inline bool operator<=(const CustomFont &a, const CustomFont &b) { - return !(b < a); -} - -inline bool operator>=(const CustomFont &a, const CustomFont &b) { - return !(a < b); -} - -void SetCustomFonts(const CustomFont ®ular, const CustomFont &bold); - -[[nodiscard]] QFont ResolveFont(const QString &familyOverride, uint32 flags, int size); - -} // namespace style diff --git a/ui/style/style_core_font.cpp b/ui/style/style_core_font.cpp index 3e58c5f..db336ea 100644 --- a/ui/style/style_core_font.cpp +++ b/ui/style/style_core_font.cpp @@ -8,19 +8,15 @@ #include "base/algorithm.h" #include "base/debug_log.h" +#include "base/variant.h" #include "base/base_file_utilities.h" -#include "ui/style/style_core_custom_font.h" #include "ui/integration.h" +#include "ui/style/style_core_scale.h" #include #include #include #include -#include - -#if __has_include() -#include -#endif void style_InitFontsResource() { #ifdef Q_OS_MAC // Use resources from the .app bundle on macOS. @@ -40,80 +36,118 @@ void style_InitFontsResource() { } namespace style { -namespace internal { namespace { -QMap fontFamilyMap; -QVector fontFamilies; -QMap fontsMap; +QString Custom; +CustomFontSettings CustomSettings; -uint32 fontKey(int size, uint32 flags, int family) { - return (((uint32(family) << 12) | uint32(size)) << 6) | flags; +} // namespace + +const QString &SystemFontTag() { + static const auto result = u"(system)"_q + QChar(0); + return result; } -bool ValidateFont(const QString &familyName, int flags = 0) { - QFont checkFont(familyName); - checkFont.setBold(flags & style::internal::FontBold); - checkFont.setItalic(flags & style::internal::FontItalic); - checkFont.setUnderline(flags & style::internal::FontUnderline); - auto realFamily = QFontInfo(checkFont).family(); - if (realFamily.trimmed().compare(familyName, Qt::CaseInsensitive)) { - LOG(("Font Error: could not resolve '%1' font, got '%2'.").arg(familyName, realFamily)); - return false; - } +void SetCustomFont(const QString &font) { + Custom = font; +} - auto metrics = QFontMetrics(checkFont); - if (!metrics.height()) { - LOG(("Font Error: got a zero height in '%1'.").arg(familyName)); - return false; - } +void SetCustomFontSettings(const CustomFontSettings &settings) { + CustomSettings = settings; +} - return true; +namespace internal { + +struct ResolvedFont { + ResolvedFont(FontResolveResult result, FontVariants *modified); + + FontResolveResult result; + FontData data; +}; + +ResolvedFont::ResolvedFont(FontResolveResult result, FontVariants *modified) +: result(std::move(result)) +, data(this->result, modified) { +} + +namespace { + +#ifndef LIB_UI_USE_PACKAGED_FONTS +const auto FontTypes = std::array{ + u"OpenSans-Regular"_q, + u"OpenSans-Italic"_q, + u"OpenSans-SemiBold"_q, + u"OpenSans-SemiBoldItalic"_q, +}; +const auto PersianFontTypes = std::array{ + u"Vazirmatn-UI-NL-Regular"_q, + u"Vazirmatn-UI-NL-SemiBold"_q, +}; +#endif // !LIB_UI_USE_PACKAGED_FONTS + +bool Started = false; + +base::flat_map FontFamilyIndices; +std::vector FontFamilies; +base::flat_map> FontsByKey; +base::flat_map QtFontsKeys; + +[[nodiscard]] uint32 FontKey(int size, FontFlags flags, int family) { + return (uint32(family) << 18) + | (uint32(size) << 6) + | uint32(flags.value()); +} + +[[nodiscard]] uint64 QtFontKey(const QFont &font) { + static auto Families = base::flat_map(); + + const auto family = font.family(); + auto i = Families.find(family); + if (i == end(Families)) { + i = Families.emplace(family, Families.size()).first; + } + return (uint64(i->second) << 24) + | (uint64(font.weight()) << 16) + | (uint64(font.bold() ? 1 : 0) << 15) + | (uint64(font.italic() ? 1 : 0) << 14) + | (uint64(font.underline() ? 1 : 0) << 13) + | (uint64(font.strikeOut() ? 1 : 0) << 12) + | (uint64(font.pixelSize())); } #ifndef LIB_UI_USE_PACKAGED_FONTS -bool LoadCustomFont(const QString &filePath, const QString &familyName, int flags = 0) { +bool LoadCustomFont(const QString &filePath) { auto regularId = QFontDatabase::addApplicationFont(filePath); if (regularId < 0) { LOG(("Font Error: could not add '%1'.").arg(filePath)); return false; } - const auto found = [&] { - for (auto &family : QFontDatabase::applicationFontFamilies(regularId)) { - LOG(("Font: from '%1' loaded '%2'").arg(filePath, family)); - if (!family.trimmed().compare(familyName, Qt::CaseInsensitive)) { - return true; - } - } - return false; - }(); - if (!found) { - LOG(("Font Error: could not locate '%1' font in '%2'.").arg(familyName, filePath)); - return false; + for (auto &family : QFontDatabase::applicationFontFamilies(regularId)) { + LOG(("Font: from '%1' loaded '%2'").arg(filePath, family)); } - return ValidateFont(familyName, flags); + return true; } #endif // !LIB_UI_USE_PACKAGED_FONTS -[[nodiscard]] QString SystemMonospaceFont() { - const auto type = QFontDatabase::FixedFont; - return QFontDatabase::systemFont(type).family(); -} - bool TryFont(const QString &attempt) { const auto resolved = QFontInfo(QFont(attempt)).family(); return !resolved.trimmed().compare(attempt, Qt::CaseInsensitive); } +[[nodiscard]] QString SystemMonospaceFont() { + const auto type = QFontDatabase::FixedFont; + return QFontDatabase::systemFont(type).family(); +} + [[nodiscard]] QString ManualMonospaceFont() { const auto kTryFirst = std::initializer_list{ - "Cascadia Mono", - "Consolas", - "Liberation Mono", - "Menlo", - "Courier" + u"Cascadia Mono"_q, + u"Consolas"_q, + u"Liberation Mono"_q, + u"Menlo"_q, + u"Courier"_q, }; for (const auto &family : kTryFirst) { if (TryFont(family)) { @@ -123,188 +157,10 @@ bool TryFont(const QString &attempt) { return QString(); } -QFontMetrics GetFontMetrics(int size) { -#ifdef DESKTOP_APP_USE_PACKAGED_FONTS - QFont originalFont("Open Sans"); -#else // !DESKTOP_APP_USE_PACKAGED_FONTS - QFont originalFont("DAOpenSansRegular"); -#endif // !DESKTOP_APP_USE_PACKAGED_FONTS - originalFont.setPixelSize(size); - return QFontMetrics(originalFont); -} - -enum { - FontTypeRegular = 0, - FontTypeRegularItalic, - FontTypeBold, - FontTypeBoldItalic, - FontTypeSemibold, - FontTypeSemiboldItalic, - - FontTypesCount, -}; -#ifndef LIB_UI_USE_PACKAGED_FONTS -QString FontTypeFiles[FontTypesCount] = { - "DAOpenSansRegular", - "DAOpenSansRegularItalic", - "DAOpenSansSemiboldAsBold", - "DAOpenSansSemiboldItalicAsBold", - "DAOpenSansSemiboldAsBold", - "DAOpenSansSemiboldItalicAsBold", -}; -QString FontTypeNames[FontTypesCount] = { - "DAOpenSansRegular", - "DAOpenSansRegularItalic", - "DAOpenSansSemibold", - "DAOpenSansSemiboldItalic", - "DAOpenSansSemibold", - "DAOpenSansSemiboldItalic", -}; -QString FontTypePersianFallbackFiles[FontTypesCount] = { - "DAVazirRegular", - "DAVazirRegular", - "DAVazirMediumAsBold", - "DAVazirMediumAsBold", - "DAVazirMediumAsBold", - "DAVazirMediumAsBold", -}; -QString FontTypePersianFallback[FontTypesCount] = { - "DAVazirRegular", - "DAVazirRegular", - "DAVazirMedium", - "DAVazirMedium", - "DAVazirMedium", - "DAVazirMedium", -}; -int32 FontTypeFlags[FontTypesCount] = { - 0, - FontItalic, - FontBold, - FontBold | FontItalic, - FontSemibold, - FontSemibold | FontItalic, -}; -#endif // !LIB_UI_USE_PACKAGED_FONTS - -bool Started = false; -QString Overrides[FontTypesCount]; - -} // namespace - -void StartFonts() { - if (Started) { - return; - } - Started = true; - - style_InitFontsResource(); - - const auto fontSettings = Ui::Integration::Instance().fontSettings(); - -#ifndef LIB_UI_USE_PACKAGED_FONTS - if (!fontSettings.useSystemFont) { - [[maybe_unused]] bool areGood[FontTypesCount] = { false }; - for (auto i = 0; i != FontTypesCount; ++i) { - const auto file = FontTypeFiles[i]; - const auto name = FontTypeNames[i]; - const auto flags = FontTypeFlags[i]; - areGood[i] = LoadCustomFont(":/gui/fonts/" + file + ".ttf", name, flags); - Overrides[i] = name; - - const auto persianFallbackFile = FontTypePersianFallbackFiles[i]; - const auto persianFallback = FontTypePersianFallback[i]; - LoadCustomFont(":/gui/fonts/" + persianFallbackFile + ".ttf", persianFallback, flags); - -#ifdef Q_OS_WIN - // Attempt to workaround a strange font bug with Open Sans Semibold not loading. - // See https://github.com/telegramdesktop/tdesktop/issues/3276 for details. - // Crash happens on "options.maxh / _t->_st->font->height" with "division by zero". - // In that place "_t->_st->font" is "semiboldFont" is "font(13 "Open Sans Semibold"). - const auto fallback = "Segoe UI"; - if (!areGood[i]) { - if (ValidateFont(fallback, flags)) { - Overrides[i] = fallback; - LOG(("Fonts Info: Using '%1' instead of '%2'.").arg(fallback).arg(name)); - } - } - // Disable default fallbacks to Segoe UI, see: - // https://github.com/telegramdesktop/tdesktop/issues/5368 - // - //QFont::insertSubstitution(name, fallback); -#endif // Q_OS_WIN - - QFont::insertSubstitution(name, persianFallback); - } - -#ifdef Q_OS_MAC - auto list = QStringList(); - list.append("STIXGeneral"); - list.append(".SF NS Text"); - list.append("Helvetica Neue"); - list.append("Lucida Grande"); - for (const auto &name : FontTypeNames) { - QFont::insertSubstitutions(name, list); - } -#endif // Q_OS_MAC - } -#elif __has_include() // !LIB_UI_USE_PACKAGED_FONTS - g_warning( - "Unable to load patched fonts with Qt workarounds, " - "expect font issues."); -#endif // LIB_UI_USE_PACKAGED_FONTS - - if (!fontSettings.mainFont.isEmpty() && ValidateFont(fontSettings.mainFont)) { - Overrides[FontTypeRegular] = fontSettings.mainFont; - Overrides[FontTypeRegularItalic] = fontSettings.mainFont; - } - if (!fontSettings.semiboldFont.isEmpty() && ValidateFont(fontSettings.semiboldFont)) { - Overrides[FontTypeBold] = fontSettings.semiboldFont; - Overrides[FontTypeBoldItalic] = fontSettings.semiboldFont; - Overrides[FontTypeSemibold] = fontSettings.semiboldFont; - Overrides[FontTypeSemiboldItalic] = fontSettings.semiboldFont; - } else if (!fontSettings.mainFont.isEmpty() && ValidateFont(fontSettings.mainFont)) { - Overrides[FontTypeBold] = fontSettings.mainFont; - Overrides[FontTypeBoldItalic] = fontSettings.mainFont; - Overrides[FontTypeSemibold] = fontSettings.mainFont; - Overrides[FontTypeSemiboldItalic] = fontSettings.mainFont; - } - - auto appFont = QApplication::font(); - appFont.setStyleStrategy(QFont::PreferQuality); - QApplication::setFont(appFont); -} - -QString GetPossibleEmptyOverride(int32 flags) { - flags = flags & (FontBold | FontSemibold | FontItalic); - int32 flagsBold = flags & (FontBold | FontItalic); - int32 flagsSemibold = flags & (FontSemibold | FontItalic); - if (flagsSemibold == (FontSemibold | FontItalic)) { - return Overrides[FontTypeSemiboldItalic]; - } else if (flagsSemibold == FontSemibold) { - return Overrides[FontTypeSemibold]; - } else if (flagsBold == (FontBold | FontItalic)) { - return Overrides[FontTypeBoldItalic]; - } else if (flagsBold == FontBold) { - return Overrides[FontTypeBold]; - } else if (flags == FontItalic) { - return Overrides[FontTypeRegularItalic]; - } else if (flags == 0) { - return Overrides[FontTypeRegular]; - } - return QString(); -} - -QString GetFontOverride(int32 flags) { - const auto result = GetPossibleEmptyOverride(flags); - return result.isEmpty() ? "Open Sans" : result; -} - -QString MonospaceFont() { +[[nodiscard]] QString MonospaceFont() { static const auto family = [&]() -> QString { - const auto fontSettings = Ui::Integration::Instance().fontSettings(); - - if (TryFont(fontSettings.monospaceFont)) { - return fontSettings.monospaceFont; + if (TryFont(CustomSettings.monospaceFont)) { + return CustomSettings.monospaceFont; } const auto manual = ManualMonospaceFont(); @@ -319,86 +175,306 @@ QString MonospaceFont() { const auto useSystem = manual.isEmpty() || (metrics.horizontalAdvance(QChar('i')) == metrics.horizontalAdvance(QChar('W'))); #endif // Q_OS_WIN - return (useSystem || fontSettings.useSystemFont) ? system : manual; + return useSystem ? system : manual; }(); return family; } -void destroyFonts() { - for (auto fontData : std::as_const(fontsMap)) { - delete fontData; +struct Metrics { + int pixelSize = 0; + float64 ascent = 0.; + float64 height = 0.; +}; +[[nodiscard]] Metrics ComputeMetrics(QFont font, bool adjust) { + constexpr auto kMaxSizeShift = 8; + + const auto startSize = font.pixelSize(); + const auto metrics = QFontMetricsF(font); + const auto simple = [&] { + return Metrics{ + .pixelSize = startSize, + .ascent = metrics.ascent(), + .height = metrics.height(), + }; + }; + + const auto family = font.family(); + const auto basic = u"Open Sans"_q; + if (family == basic || !adjust) { + return simple(); } - fontsMap.clear(); -} -int registerFontFamily(const QString &family) { - auto result = fontFamilyMap.value(family, -1); - if (result < 0) { - result = fontFamilies.size(); - fontFamilyMap.insert(family, result); - fontFamilies.push_back(family); + auto copy = font; + copy.setFamily(basic); + const auto basicMetrics = QFontMetricsF(copy); + + static const auto Full = u"bdfghijklpqtyBDFGHIJKLPQTY1234567890[]{}()"_q; + + // I tried to choose height in such way that + // - normal fonts won't be too large, + // - some exotic fonts, like Symbol (Greek), won't be too small, + // - some other exotic fonts, like Segoe Script, won't be too large. + const auto Height = [](const QFontMetricsF &metrics) { + //static const auto Test = u"acemnorsuvwxz"_q; + //return metrics.tightBoundingRect(Test).height(); + + //static const auto Test = u"acemnorsuvwxz"_q; + //auto result = metrics.boundingRect(Test[0]).height(); + //for (const auto &ch : Test | ranges::views::drop(1)) { + // const auto single = metrics.boundingRect(ch).height(); + // if (result > single) { + // result = single; + // } + //} + //return result; + + //static const auto Test = u"acemnorsuvwxz"_q; + //auto result = 0.; + //for (const auto &ch : Test) { + // result -= metrics.boundingRect(ch).y(); + //} + //return result / Test.size(); + + static const char16_t Test[] = u"acemnorsuvwxz"; + constexpr auto kCount = int(std::size(Test)) - 1; + + auto heights = std::array{}; + for (auto i = 0; i != kCount; ++i) { + heights[i] = -metrics.boundingRect(QChar(Test[i])).y(); + } + ranges::sort(heights); + //return heights[kCount / 2]; + + // Average the middle third. + const auto from = kCount / 3; + const auto till = kCount - from; + auto result = 0.; + for (auto i = from; i != till; ++i) { + result += heights[i]; + } + return result / (till - from); + }; + + const auto desired = Height(basicMetrics); + const auto desiredFull = basicMetrics.tightBoundingRect(Full); + if (desired < 1. || desiredFull.height() < desired) { + return simple(); } - return result; -} -FontData::FontData(int size, uint32 flags, int family, Font *other) -: f(ResolveFont(family ? fontFamilies[family] : QString(), flags, size)) -, _m(f) -, _size(size) -, _flags(flags) -, _family(family) { - if (other) { - memcpy(_modified, other, sizeof(_modified)); + const auto adjusted = [&](int size, const QFontMetricsF &metrics) { + const auto full = metrics.tightBoundingRect(Full); + const auto desiredTightHeight = desiredFull.height(); + const auto heightAdd = basicMetrics.height() - desiredTightHeight; + const auto tightHeight = full.height(); + return Metrics{ + .pixelSize = size, + .ascent = basicMetrics.ascent(), + .height = tightHeight + heightAdd, + }; + }; + + auto current = Height(metrics); + if (current < 1.) { + return simple(); + } else if (std::abs(current - desired) < 0.2) { + return adjusted(startSize, metrics); } - _modified[_flags] = Font(this); - const auto fontSettings = Ui::Integration::Instance().fontSettings(); - if (fontSettings.useOriginalMetrics && !(_flags & FontMonospace)) { - const auto mOrig = GetFontMetrics(size); - - height = int(base::SafeRound(mOrig.height())); - ascent = int(base::SafeRound(mOrig.ascent())); - descent = int(base::SafeRound(mOrig.descent())); + const auto adjustedByFont = [&](const QFont &font) { + return adjusted(font.pixelSize(), QFontMetricsF(font)); + }; + const auto max = std::min(kMaxSizeShift, startSize - 1); + if (current < desired) { + for (auto i = 0; i != max; ++i) { + const auto shift = i + 1; + font.setPixelSize(startSize + shift); + const auto metrics = QFontMetricsF(font); + const auto now = Height(metrics); + if (now > desired) { + const auto better = (now - desired) < (desired - current); + if (better) { + return adjusted(startSize + shift, metrics); + } + font.setPixelSize(startSize + shift - 1); + return adjustedByFont(font); + } + current = now; + } + font.setPixelSize(startSize + max); + return adjustedByFont(font); } else { - height = int(base::SafeRound(_m.height())); - ascent = int(base::SafeRound(_m.ascent())); - descent = int(base::SafeRound(_m.descent())); + for (auto i = 0; i != max; ++i) { + const auto shift = i + 1; + font.setPixelSize(startSize - shift); + const auto metrics = QFontMetricsF(font); + const auto now = Height(metrics); + if (now < desired) { + const auto better = (desired - now) < (current - desired); + if (better) { + return adjusted(startSize - shift, metrics); + } + font.setPixelSize(startSize - shift + 1); + return adjustedByFont(font); + } + current = now; + } + font.setPixelSize(startSize - max); + return adjustedByFont(font); + } +} + +[[nodiscard]] FontResolveResult ResolveFont( + const QString &family, + FontFlags flags, + int size) { + auto font = QFont(QFont().family()); + + const auto monospace = (flags & FontFlag::Monospace) != 0; + const auto system = !monospace && (family == SystemFontTag()); + const auto overriden = !monospace && !system && !family.isEmpty(); + if (monospace) { + font.setFamily(MonospaceFont()); + } else if (system) { + } else if (overriden) { + font.setFamily(family); + } else { + font.setFamily("Open Sans"_q); +#if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0) + font.setFeature("ss03", true); +#endif // Qt >= 6.7.0 + } + font.setPixelSize(size + ConvertScale(CustomSettings.fontSize)); + + const auto adjust = (overriden || system); + const auto metrics = ComputeMetrics(font, adjust); + font.setPixelSize(metrics.pixelSize); + + font.setWeight((flags & (FontFlag::Bold | FontFlag::Semibold)) + ? QFont::DemiBold + : QFont::Normal); + if (font.bold()) { + const auto style = QFontInfo(font).styleName(); + if (CustomSettings.semiboldIsBold + || (!style.isEmpty() && !style.startsWith( + "Semibold", + Qt::CaseInsensitive))) { + font.setBold(true); + } } + font.setItalic(flags & FontFlag::Italic); + font.setUnderline(flags & FontFlag::Underline); + font.setStrikeOut(flags & FontFlag::StrikeOut); + + const auto index = (family == Custom) ? 0 : RegisterFontFamily(family); + return { + .font = font, + .ascent = metrics.ascent, + .height = metrics.height, + .iascent = int(base::SafeRound(metrics.ascent)), + .iheight = int(base::SafeRound(metrics.height)), + .requestedFamily = index, + .requestedSize = size, + .requestedFlags = flags, + }; +} + +} // namespace + +void StartFonts() { + if (Started) { + return; + } + Started = true; + + style_InitFontsResource(); + +#ifndef LIB_UI_USE_PACKAGED_FONTS + const auto base = u":/gui/fonts/"_q; + const auto name = u"Open Sans"_q; + + for (const auto &file : FontTypes) { + LoadCustomFont(base + file + u".ttf"_q); + } + + for (const auto &file : PersianFontTypes) { + LoadCustomFont(base + file + u".ttf"_q); + } + QFont::insertSubstitution(name, u"Vazirmatn UI NL"_q); + +#ifdef Q_OS_MAC + const auto list = QStringList{ + u"STIXGeneral"_q, + u".SF NS Text"_q, + u"Helvetica Neue"_q, + u"Lucida Grande"_q, + }; + QFont::insertSubstitutions(name, list); +#endif // Q_OS_MAC +#endif // !LIB_UI_USE_PACKAGED_FONTS +} + +void DestroyFonts() { + base::take(FontsByKey); +} + +int RegisterFontFamily(const QString &family) { + auto i = FontFamilyIndices.find(family); + if (i == end(FontFamilyIndices)) { + i = FontFamilyIndices.emplace(family, FontFamilies.size()).first; + FontFamilies.push_back(family); + } + return i->second; +} + +FontData::FontData(const FontResolveResult &result, FontVariants *modified) +: f(result.font) +, _m(f) +, _size(result.requestedSize) +, _family(result.requestedFamily) +, _flags(result.requestedFlags) { + if (modified) { + memcpy(&_modified, modified, sizeof(_modified)); + } + _modified[int(_flags)] = Font(this); + + height = int(base::SafeRound(result.height)); + ascent = int(base::SafeRound(result.ascent)); + descent = height - ascent; spacew = width(QLatin1Char(' ')); - elidew = width("..."); + elidew = width(u"..."_q); } Font FontData::bold(bool set) const { - return otherFlagsFont(FontBold, set); + return otherFlagsFont(FontFlag::Bold, set); } Font FontData::italic(bool set) const { - return otherFlagsFont(FontItalic, set); + return otherFlagsFont(FontFlag::Italic, set); } Font FontData::underline(bool set) const { - return otherFlagsFont(FontUnderline, set); + return otherFlagsFont(FontFlag::Underline, set); } Font FontData::strikeout(bool set) const { - return otherFlagsFont(FontStrikeOut, set); + return otherFlagsFont(FontFlag::StrikeOut, set); } Font FontData::semibold(bool set) const { - return otherFlagsFont(FontSemibold, set); + return otherFlagsFont(FontFlag::Semibold, set); } Font FontData::monospace(bool set) const { - return otherFlagsFont(FontMonospace, set); + return otherFlagsFont(FontFlag::Monospace, set); } int FontData::size() const { return _size; } -uint32 FontData::flags() const { +FontFlags FontData::flags() const { return _flags; } @@ -406,45 +482,60 @@ int FontData::family() const { return _family; } -Font FontData::otherFlagsFont(uint32 flag, bool set) const { - int32 newFlags = set ? (_flags | flag) : (_flags & ~flag); - if (!_modified[newFlags].v()) { - _modified[newFlags] = Font(_size, newFlags, _family, _modified); +Font FontData::otherFlagsFont(FontFlag flag, bool set) const { + const auto newFlags = set ? (_flags | flag) : (_flags & ~flag); + if (!_modified[newFlags]) { + _modified[newFlags] = Font(_size, newFlags, _family, &_modified); } return _modified[newFlags]; } -Font::Font(int size, uint32 flags, const QString &family) { - if (fontFamilyMap.isEmpty()) { - for (uint32 i = 0, s = fontFamilies.size(); i != s; ++i) { - fontFamilyMap.insert(fontFamilies.at(i), i); - } - } - - auto i = fontFamilyMap.constFind(family); - if (i == fontFamilyMap.cend()) { - fontFamilies.push_back(family); - i = fontFamilyMap.insert(family, fontFamilies.size() - 1); - } - init(size, flags, i.value(), 0); +Font::Font(int size, FontFlags flags, const QString &family) { + init(size, flags, RegisterFontFamily(family), 0); } -Font::Font(int size, uint32 flags, int family) { +Font::Font(int size, FontFlags flags, int family) { init(size, flags, family, 0); } -Font::Font(int size, uint32 flags, int family, Font *modified) { +Font::Font(int size, FontFlags flags, int family, FontVariants *modified) { init(size, flags, family, modified); } -void Font::init(int size, uint32 flags, int family, Font *modified) { - uint32 key = fontKey(size, flags, family); - auto i = fontsMap.constFind(key); - if (i == fontsMap.cend()) { - i = fontsMap.insert(key, new FontData(size, flags, family, modified)); +void Font::init( + int size, + FontFlags flags, + int family, + FontVariants *modified) { + const auto key = FontKey(size, flags, family); + auto i = FontsByKey.find(key); + if (i == end(FontsByKey)) { + i = FontsByKey.emplace( + key, + std::make_unique( + ResolveFont( + family ? FontFamilies[family] : Custom, + flags, + size), + modified)).first; + QtFontsKeys.emplace(QtFontKey(i->second->data.f), key); } - ptr = i.value(); + _data = &i->second->data; +} + +OwnedFont::OwnedFont(const QString &custom, FontFlags flags, int size) +: _data(ResolveFont(custom, flags, size), nullptr) { + _font._data = &_data; } } // namespace internal + +const FontResolveResult *FindAdjustResult(const QFont &font) { + const auto key = internal::QtFontKey(font); + const auto i = internal::QtFontsKeys.find(key); + return (i != end(internal::QtFontsKeys)) + ? &internal::FontsByKey[i->second]->result + : nullptr; +} + } // namespace style diff --git a/ui/style/style_core_font.h b/ui/style/style_core_font.h index f26c125..ad43c02 100644 --- a/ui/style/style_core_font.h +++ b/ui/style/style_core_font.h @@ -7,6 +7,7 @@ #pragma once #include "base/basic_types.h" +#include "base/flags.h" #include #include @@ -14,59 +15,86 @@ #include namespace style { + +struct CustomFontSettings { + QString monospaceFont; + int fontSize = 0; + bool semiboldIsBold = false; +}; + +[[nodiscard]] const QString &SystemFontTag(); +void SetCustomFont(const QString &font); +void SetCustomFontSettings(const CustomFontSettings &settings); + +enum class FontFlag : uchar { + Bold = 0x01, + Italic = 0x02, + Underline = 0x04, + StrikeOut = 0x08, + Semibold = 0x10, + Monospace = 0x20, +}; +inline constexpr bool is_flag_type(FontFlag) { return true; } +using FontFlags = base::flags; + +struct FontResolveResult { + QFont font; + float64 ascent = 0.; + float64 height = 0.; + int iascent = 0; + int iheight = 0; + int requestedFamily = 0; + int requestedSize = 0; + FontFlags requestedFlags; +}; +[[nodiscard]] const FontResolveResult *FindAdjustResult(const QFont &font); + namespace internal { void StartFonts(); -[[nodiscard]] QString GetPossibleEmptyOverride(int32 flags = 0); -[[nodiscard]] QString GetFontOverride(int32 flags = 0); -[[nodiscard]] QString MonospaceFont(); -void destroyFonts(); -int registerFontFamily(const QString &family); +void DestroyFonts(); +int RegisterFontFamily(const QString &family); + +inline constexpr auto kFontVariants = 0x40; + +class Font; +using FontVariants = std::array; class FontData; -class Font { +class Font final { public: Font(Qt::Initialization = Qt::Uninitialized) { } - Font(int size, uint32 flags, const QString &family); - Font(int size, uint32 flags, int family); + Font(int size, FontFlags flags, const QString &family); + Font(int size, FontFlags flags, int family); - FontData *operator->() const { - return ptr; + [[nodiscard]] FontData *operator->() const { + return _data; } - FontData *v() const { - return ptr; + [[nodiscard]] FontData *get() const { + return _data; } - operator bool() const { - return !!ptr; + [[nodiscard]] operator bool() const { + return _data != nullptr; } - operator const QFont &() const; + [[nodiscard]] operator const QFont &() const; private: - FontData *ptr = nullptr; - - void init(int size, uint32 flags, int family, Font *modified); - friend void startManager(); - - Font(FontData *p) : ptr(p) { - } - Font(int size, uint32 flags, int family, Font *modified); friend class FontData; + friend class OwnedFont; -}; + FontData *_data = nullptr; -enum FontFlags { - FontBold = 0x01, - FontItalic = 0x02, - FontUnderline = 0x04, - FontStrikeOut = 0x08, - FontSemibold = 0x10, - FontMonospace = 0x20, + void init(int size, FontFlags flags, int family, FontVariants *modified); + friend void StartManager(); + + explicit Font(FontData *data) : _data(data) { + } + Font(int size, FontFlags flags, int family, FontVariants *modified); - FontDifferentFlags = 0x40, }; class FontData { @@ -94,37 +122,74 @@ public: [[nodiscard]] Font semibold(bool set = true) const; [[nodiscard]] Font monospace(bool set = true) const; - int size() const; - uint32 flags() const; - int family() const; + [[nodiscard]] int size() const; + [[nodiscard]] FontFlags flags() const; + [[nodiscard]] int family() const; QFont f; - int32 height, ascent, descent, spacew, elidew; + int height = 0; + int ascent = 0; + int descent = 0; + int spacew = 0; + int elidew = 0; private: - mutable Font _modified[FontDifferentFlags]; + friend class OwnedFont; + friend struct ResolvedFont; - Font otherFlagsFont(uint32 flag, bool set) const; - FontData(int size, uint32 flags, int family, Font *other); + mutable FontVariants _modified; + + [[nodiscard]] Font otherFlagsFont(FontFlag flag, bool set) const; + FontData(const FontResolveResult &data, FontVariants *modified); - friend class Font; QFontMetricsF _m; - int _size; - uint32 _flags; - int _family; + int _size = 0; + int _family = 0; + FontFlags _flags = 0; }; inline bool operator==(const Font &a, const Font &b) { - return a.v() == b.v(); + return a.get() == b.get(); } inline bool operator!=(const Font &a, const Font &b) { - return a.v() != b.v(); + return a.get() != b.get(); } inline Font::operator const QFont &() const { - return ptr->f; + Expects(_data != nullptr); + + return _data->f; } +class OwnedFont final { +public: + OwnedFont(const QString &custom, FontFlags flags, int size); + OwnedFont(const OwnedFont &other) + : _data(other._data) { + _font._data = &_data; + } + + OwnedFont &operator=(const OwnedFont &other) { + _data = other._data; + return *this; + } + + [[nodiscard]] const Font &font() const { + return _font; + } + [[nodiscard]] FontData *operator->() const { + return _font.get(); + } + [[nodiscard]] FontData *get() const { + return _font.get(); + } + +private: + FontData _data; + Font _font; + +}; + } // namespace internal } // namespace style diff --git a/ui/style/style_core_icon.cpp b/ui/style/style_core_icon.cpp index 3ddc9ee..3f00b90 100644 --- a/ui/style/style_core_icon.cpp +++ b/ui/style/style_core_icon.cpp @@ -393,14 +393,14 @@ Icon Icon::withPalette(const style::palette &palette) const { return result; } -void resetIcons() { +void ResetIcons() { iconPixmaps.clear(); for (const auto data : iconData) { data->reset(); } } -void destroyIcons() { +void DestroyIcons() { iconData.clear(); iconPixmaps.clear(); diff --git a/ui/style/style_core_icon.h b/ui/style/style_core_icon.h index 9afd9ee..398f176 100644 --- a/ui/style/style_core_icon.h +++ b/ui/style/style_core_icon.h @@ -295,8 +295,8 @@ private: }; -void resetIcons(); -void destroyIcons(); +void ResetIcons(); +void DestroyIcons(); } // namespace internal } // namespace style diff --git a/ui/style/style_core_palette.cpp b/ui/style/style_core_palette.cpp index 687c764..bad3e65 100644 --- a/ui/style/style_core_palette.cpp +++ b/ui/style/style_core_palette.cpp @@ -226,7 +226,7 @@ QByteArray save() { bool load(const QByteArray &cache) { if (GetMutable().load(cache)) { - style::internal::resetIcons(); + style::internal::ResetIcons(); return true; } return false; @@ -242,17 +242,17 @@ palette::SetResult setColor(QLatin1String name, QLatin1String from) { void apply(const palette &other) { GetMutable() = other; - style::internal::resetIcons(); + style::internal::ResetIcons(); } void reset() { GetMutable().reset(); - style::internal::resetIcons(); + style::internal::ResetIcons(); } void reset(const colorizer &with) { GetMutable().reset(with); - style::internal::resetIcons(); + style::internal::ResetIcons(); } int indexOfColor(color c) { diff --git a/ui/style/style_core_types.h b/ui/style/style_core_types.h index c30adae..4dd34c0 100644 --- a/ui/style/style_core_types.h +++ b/ui/style/style_core_types.h @@ -27,6 +27,7 @@ using cursor = Qt::CursorShape; using align = Qt::Alignment; using margins = QMargins; using font = internal::Font; +using owned_font = internal::OwnedFont; using color = internal::Color; using owned_color = internal::OwnedColor; using complex_color = internal::ComplexColor; diff --git a/ui/text/text.cpp b/ui/text/text.cpp index 367134c..c3d20dc 100644 --- a/ui/text/text.cpp +++ b/ui/text/text.cpp @@ -984,6 +984,8 @@ void String::enumerateLines( return withElided(true); } lineHeight = 0; + last_rBearing = 0;// b->f_rbearing(); (0 for newline) + last_rPadding = 0;// b->f_rpadding(); (0 for newline) initNextParagraph(i + 1, index); longWordLine = true; @@ -1810,8 +1812,7 @@ bool IsSpace(QChar ch) { || (ch == QChar::LineSeparator) || (ch == QChar::ObjectReplacementCharacter) || (ch == QChar::CarriageReturn) - || (ch == QChar::Tabulation) - || (ch == QChar(8203)/*Zero width space.*/); + || (ch == QChar::Tabulation); } bool IsDiacritic(QChar ch) { // diacritic and variation selectors @@ -1821,6 +1822,9 @@ bool IsDiacritic(QChar ch) { // diacritic and variation selectors } bool IsReplacedBySpace(QChar ch) { + // Those symbols are replaced by space on the Telegram server, + // so we replace them as well, for sent / received consistency. + // // \xe2\x80[\xa8 - \xac\xad] // 8232 - 8237 // QString from1 = QString::fromUtf8("\xe2\x80\xa8"), to1 = QString::fromUtf8("\xe2\x80\xad"); // \xcc[\xb3\xbf\x8a] // 819, 831, 778 @@ -1836,7 +1840,9 @@ bool IsReplacedBySpace(QChar ch) { } bool IsTrimmed(QChar ch) { - return (IsSpace(ch) || IsBad(ch)); + return IsSpace(ch) + || IsBad(ch) + || (ch == QChar(8203)); // zero width space } } // namespace Ui::Text diff --git a/ui/text/text_block.cpp b/ui/text/text_block.cpp index afe8fb7..9403e8b 100644 --- a/ui/text/text_block.cpp +++ b/ui/text/text_block.cpp @@ -373,28 +373,31 @@ bool BlockParser::isSpaceBreak( style::font WithFlags( const style::font &font, TextBlockFlags flags, - uint32 fontFlags) { + style::FontFlags fontFlags) { using namespace style::internal; + using Flag = style::FontFlag; if (!flags && !fontFlags) { return font; - } else if (IsMono(flags) || (fontFlags & FontMonospace)) { + } else if (IsMono(flags) || (fontFlags & Flag::Monospace)) { return font->monospace(); } auto result = font; - if ((flags & TextBlockFlag::Bold) || (fontFlags & FontBold)) { + if ((flags & TextBlockFlag::Bold) || (fontFlags & Flag::Bold)) { result = result->bold(); } else if ((flags & TextBlockFlag::Semibold) - || (fontFlags & FontSemibold)) { + || (fontFlags & Flag::Semibold)) { result = result->semibold(); } - if ((flags & TextBlockFlag::Italic) || (fontFlags & FontItalic)) { + if ((flags & TextBlockFlag::Italic) || (fontFlags & Flag::Italic)) { result = result->italic(); } - if ((flags & TextBlockFlag::Underline) || (fontFlags & FontUnderline)) { + if ((flags & TextBlockFlag::Underline) + || (fontFlags & Flag::Underline)) { result = result->underline(); } - if ((flags & TextBlockFlag::StrikeOut) || (fontFlags & FontStrikeOut)) { + if ((flags & TextBlockFlag::StrikeOut) + || (fontFlags & Flag::StrikeOut)) { result = result->strikeout(); } if (flags & TextBlockFlag::Tilde) { // Tilde fix in OpenSans. diff --git a/ui/text/text_block.h b/ui/text/text_block.h index aa51882..0eee5e7 100644 --- a/ui/text/text_block.h +++ b/ui/text/text_block.h @@ -47,7 +47,7 @@ using TextBlockFlags = base::flags; [[nodiscard]] style::font WithFlags( const style::font &font, TextBlockFlags flags, - uint32 fontFlags = 0); + style::FontFlags fontFlags = 0); [[nodiscard]] Qt::LayoutDirection UnpackParagraphDirection( bool ltr, diff --git a/ui/text/text_parser.cpp b/ui/text/text_parser.cpp index 96a045d..34876de 100644 --- a/ui/text/text_parser.cpp +++ b/ui/text/text_parser.cpp @@ -78,7 +78,17 @@ constexpr auto kMaxDiacAfterSymbol = 2; const auto &font = st.font; return (font->size() * style::DevicePixelRatio() == 13) && (font->flags() == 0) - && (font->f.family() == qstr("DAOpenSansRegular")); + && (font->f.family() == qstr("Open Sans")); +} + +[[nodiscard]] bool IsDiacriticAllowedAfter(QChar ch) { + const auto code = ch.unicode(); + const auto category = ch.category(); + return (code > 32) + && (category != QChar::Other_Control) + && (category != QChar::Other_Format) + && (category != QChar::Other_PrivateUse) + && (category != QChar::Other_NotAssigned); } } // namespace @@ -567,7 +577,7 @@ void Parser::parseCurrentChar() { createBlock(-_emojiLookback); } _t->_text.push_back(_ch); - _allowDiacritic = true; + _allowDiacritic = IsDiacriticAllowedAfter(_ch); } if (!isDiacritic) { _diacritics = 0; diff --git a/ui/text/text_renderer.cpp b/ui/text/text_renderer.cpp index 58e72bd..397c22a 100644 --- a/ui/text/text_renderer.cpp +++ b/ui/text/text_renderer.cpp @@ -156,6 +156,12 @@ bool Distinct(FixedRange a, FixedRange b) { Renderer::Renderer(const Ui::Text::String &t) : _t(&t) , _spoiler(_t->_extended ? _t->_extended->spoiler.get() : nullptr) { + [[maybe_unused]] static const auto Once = [] { + // Running with a Qt version other than the one built upon is + // dangerous due to excessive private API usage in this class + Assert(QLatin1String(qVersion()) == QLatin1String(QT_VERSION_STR)); + return true; + }(); } Renderer::~Renderer() { diff --git a/ui/ui_utility.h b/ui/ui_utility.h index 5e2dd81..5601942 100644 --- a/ui/ui_utility.h +++ b/ui/ui_utility.h @@ -56,16 +56,23 @@ inline base::unique_qptr CreateObject(Args &&...args) { template inline Value *CreateChild( - Parent *parent, + Parent parent, Args &&...args) { - Expects(parent != nullptr); + if constexpr (std::is_pointer_v) { + Expects(parent != nullptr); - if constexpr (std::is_base_of_v) { - return new Value(parent, std::forward(args)...); + if constexpr (std::is_base_of_v) { + return new Value(parent, std::forward(args)...); + } else { + return CreateChild>( + parent, + std::forward(args)...)->value(); + } + } else if constexpr (requires(const Parent & t) { t.get(); }) { + return new Value(parent.get(), std::forward(args)...); } else { - return CreateChild>( - parent, - std::forward(args)...)->value(); + static_assert(requires(const Parent &t) { t.data(); }); + return new Value(parent.data(), std::forward(args)...); } } diff --git a/ui/widgets/buttons.cpp b/ui/widgets/buttons.cpp index 389f957..bc1e439 100644 --- a/ui/widgets/buttons.cpp +++ b/ui/widgets/buttons.cpp @@ -25,9 +25,7 @@ LinkButton::LinkButton( , _st(st) , _text(text) , _textWidth(st.font->width(_text)) { - resize( - naturalWidth(), - _st.padding.top() + _st.font->height + _st.padding.bottom()); + resizeToText(); setCursor(style::cur_pointer); } @@ -59,10 +57,16 @@ void LinkButton::paintEvent(QPaintEvent *e) { void LinkButton::setText(const QString &text) { _text = text; _textWidth = _st.font->width(_text); - resize(naturalWidth(), _st.font->height); + resizeToText(); update(); } +void LinkButton::resizeToText() { + resize( + naturalWidth(), + _st.padding.top() + _st.font->height + _st.padding.bottom()); +} + void LinkButton::setColorOverride(std::optional textFg) { _textFgOverride = textFg; update(); diff --git a/ui/widgets/buttons.h b/ui/widgets/buttons.h index d39f65e..9f57206 100644 --- a/ui/widgets/buttons.h +++ b/ui/widgets/buttons.h @@ -37,6 +37,8 @@ protected: void onStateChanged(State was, StateChangeSource source) override; private: + void resizeToText(); + const style::LinkButton &_st; QString _text; int _textWidth = 0; diff --git a/ui/widgets/elastic_scroll.cpp b/ui/widgets/elastic_scroll.cpp index ec5c2a0..e9838c3 100644 --- a/ui/widgets/elastic_scroll.cpp +++ b/ui/widgets/elastic_scroll.cpp @@ -381,6 +381,14 @@ ElasticScroll::ElasticScroll( }, _bar->lifetime()); } +ElasticScroll::~ElasticScroll() { + // Destroy the _bar cleanly (keeping _bar == nullptr) to avoid a crash: + // + // _bar destructor may send LeaveEvent to ElasticScroll, + // which will try to toggle(false) the _bar in leaveEventHook. + base::take(_bar); +} + void ElasticScroll::setHandleTouch(bool handle) { if (_touchDisabled != handle) { return; @@ -1071,13 +1079,15 @@ void ElasticScroll::keyPressEvent(QKeyEvent *e) { } void ElasticScroll::enterEventHook(QEnterEvent *e) { - if (!_disabled) { + if (_bar && !_disabled) { _bar->toggle(true); } } void ElasticScroll::leaveEventHook(QEvent *e) { - _bar->toggle(false); + if (_bar) { + _bar->toggle(false); + } } void ElasticScroll::scrollTo(ScrollToRequest request) { diff --git a/ui/widgets/elastic_scroll.h b/ui/widgets/elastic_scroll.h index 8404c98..e905341 100644 --- a/ui/widgets/elastic_scroll.h +++ b/ui/widgets/elastic_scroll.h @@ -113,6 +113,7 @@ public: QWidget *parent, const style::ScrollArea &st = st::defaultScrollArea, Qt::Orientation orientation = Qt::Vertical); + ~ElasticScroll(); void setHandleTouch(bool handle); bool viewportEvent(QEvent *e); diff --git a/ui/widgets/fields/input_field.cpp b/ui/widgets/fields/input_field.cpp index eba736f..95b6d4e 100644 --- a/ui/widgets/fields/input_field.cpp +++ b/ui/widgets/fields/input_field.cpp @@ -6,12 +6,12 @@ // #include "ui/widgets/fields/input_field.h" -#include "ui/widgets/popup_menu.h" #include "ui/text/text.h" +#include "ui/widgets/labels.h" +#include "ui/widgets/popup_menu.h" #include "ui/emoji_config.h" #include "ui/ui_utility.h" #include "ui/painter.h" -#include "ui/style/style_core_custom_font.h" #include "base/invoke_queued.h" #include "base/random.h" #include "base/platform/base_platform_info.h" @@ -138,7 +138,7 @@ bool IsNewline(QChar ch) { } [[nodiscard]] bool IsCustomEmojiLink(QStringView link) { - return link.startsWith(Ui::InputField::kCustomEmojiTagStart); + return link.startsWith(InputField::kCustomEmojiTagStart); } [[nodiscard]] QString MakeUniqueCustomEmojiLink(QStringView link) { @@ -157,7 +157,7 @@ bool IsNewline(QChar ch) { } [[nodiscard]] uint64 CustomEmojiIdFromLink(QStringView link) { - const auto skip = Ui::InputField::kCustomEmojiTagStart.size(); + const auto skip = InputField::kCustomEmojiTagStart.size(); const auto index = link.indexOf('?', skip + 1); return base::StringViewMid( link, @@ -702,16 +702,16 @@ QString AccumulateText(Iterator begin, Iterator end) { return result; } -QTextImageFormat PrepareEmojiFormat(EmojiPtr emoji, const QFont &font) { +QTextImageFormat PrepareEmojiFormat(EmojiPtr emoji, int lineHeight) { const auto factor = style::DevicePixelRatio(); const auto size = Emoji::GetSizeNormal(); const auto width = size + st::emojiPadding * factor * 2; - const auto height = std::max(QFontMetrics(font).height() * factor, size); + const auto height = std::max(lineHeight * factor, size); auto result = QTextImageFormat(); result.setWidth(width / factor); result.setHeight(height / factor); result.setName(emoji->toUrl()); - result.setVerticalAlignment(QTextCharFormat::AlignBottom); + result.setVerticalAlignment(QTextCharFormat::AlignTop); return result; } @@ -751,6 +751,7 @@ QTextCharFormat PrepareTagFormat( result.setProperty( kCustomEmojiId, CustomEmojiIdFromLink(replaceWith)); + result.setVerticalAlignment(QTextCharFormat::AlignTop); } else if (IsValidMarkdownLink(tag)) { color = st::defaultTextPalette.linkFg; } else if (tag == kTagBold) { @@ -923,6 +924,7 @@ struct FormattingAction { RemoveTag, RemoveNewline, ClearInstantReplace, + FixLineHeight, }; Type type = Type::Invalid; @@ -1026,7 +1028,12 @@ private: void InsertEmojiAtCursor(QTextCursor cursor, EmojiPtr emoji) { const auto currentFormat = cursor.charFormat(); - auto format = PrepareEmojiFormat(emoji, currentFormat.font()); + const auto blockFormat = cursor.blockFormat(); + const auto type = blockFormat.lineHeightType(); + const auto height = (type == QTextBlockFormat::FixedHeight) + ? blockFormat.lineHeight() + : QFontMetrics(cursor.charFormat().font()).height(); + auto format = PrepareEmojiFormat(emoji, height); ApplyTagFormat(format, currentFormat); cursor.insertText(kObjectReplacement, format); } @@ -1043,7 +1050,7 @@ void InsertCustomEmojiAtCursor( format.setProperty(kCustomEmojiText, text); format.setProperty(kCustomEmojiLink, unique); format.setProperty(kCustomEmojiId, CustomEmojiIdFromLink(link)); - format.setVerticalAlignment(QTextCharFormat::AlignBottom); + format.setVerticalAlignment(QTextCharFormat::AlignTop); format.setFont(field->st().font); format.setForeground(field->st().textFg); format.setBackground(QBrush()); @@ -1122,8 +1129,12 @@ const InstantReplaces &InstantReplaces::Custom() { return result; } -CustomEmojiObject::CustomEmojiObject(Factory factory, Fn paused) -: _factory(std::move(factory)) +CustomEmojiObject::CustomEmojiObject( + const style::font &font, + Factory factory, + Fn paused) +: _font(font) +, _factory(std::move(factory)) , _paused(std::move(paused)) , _now(crl::now()) { } @@ -1143,10 +1154,9 @@ QSizeF CustomEmojiObject::intrinsicSize( const QTextFormat &format) { const auto size = st::emojiSize * 1.; const auto width = size + st::emojiPadding * 2.; - const auto font = format.toCharFormat().font(); - const auto height = std::min(QFontMetrics(font).height() * 1., size); + const auto height = std::max(_font->height * 1., size); if (!_skip) { - const auto emoji = Ui::Text::AdjustCustomEmojiSize(st::emojiSize); + const auto emoji = Text::AdjustCustomEmojiSize(st::emojiSize); _skip = (st::emojiSize - emoji) / 2; } return { width, height }; @@ -1236,6 +1246,30 @@ InputField::InputField( _inner->setAcceptRichText(false); resize(_st.width, _minHeight); + { // In case of default fonts all those should be zero. + const auto metrics = QFontMetricsF(_st.font->f); + const auto realAscent = int(base::SafeRound(metrics.ascent())); + const auto ascentAdd = _st.font->ascent - realAscent; + //const auto realHeight = int(base::SafeRound(metrics.height())); + //const auto heightAdd = _st.font->height - realHeight - ascentAdd; + //_customFontMargins = QMargins(0, ascentAdd, 0, heightAdd); + _customFontMargins = QMargins(0, ascentAdd, 0, -ascentAdd); + // We move _inner down by ascentAdd for the first line to look + // at the same vertical position as in the default font. + // + // But we don't want to get vertical scroll in case the field + // fits pixel-perfect with the default font, so we allow the + // bottom margin to be the same shift, but negative. + + if (_mode != Mode::SingleLine) { + const auto metrics = QFontMetricsF(_st.font->f); + const auto leading = qMax(metrics.leading(), qreal(0.0)); + const auto adjustment = (metrics.ascent() + leading) + - ((_st.font->height * 4) / 5); + _placeholderCustomFontSkip = int(base::SafeRound(-adjustment)); + } + } + if (_st.textBg->c.alphaF() >= 1. && !_st.borderRadius) { setAttribute(Qt::WA_OpaquePaintEvent); @@ -1256,11 +1290,21 @@ InputField::InputField( ) | rpl::start_with_next([=] { updatePalette(); }, lifetime()); + { + auto cursor = _inner->textCursor(); - _defaultCharFormat = _inner->textCursor().charFormat(); - updatePalette(); - _inner->textCursor().setCharFormat(_defaultCharFormat); + _defaultCharFormat = cursor.charFormat(); + updatePalette(); + cursor.setCharFormat(_defaultCharFormat); + _defaultBlockFormat = cursor.blockFormat(); + _defaultBlockFormat.setLineHeight( + _st.font->height, + QTextBlockFormat::FixedHeight); + cursor.setBlockFormat(_defaultBlockFormat); + + _inner->setTextCursor(cursor); + } _inner->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); _inner->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); @@ -1268,14 +1312,6 @@ InputField::InputField( _inner->viewport()->setAutoFillBackground(false); _inner->setContentsMargins(0, 0, 0, 0); - const auto fontSettings = Integration::Instance().fontSettings(); - if (fontSettings.useOriginalMetrics) { - const auto metrics = QFontMetricsF(_st.font->f); - const auto heightDelta = (_st.font->height - metrics.height()); - const auto topMargin = (_st.font->ascent - metrics.ascent()) - + (heightDelta ? heightDelta / 2 : 0); - _inner->setViewportMargins(0, topMargin, 0, 0); - } _inner->document()->setDocumentMargin(0); setAttribute(Qt::WA_AcceptTouchEvents); @@ -1310,6 +1346,7 @@ InputField::InputField( auto cursor = textCursor(); if (!cursor.hasSelection() && !cursor.position()) { cursor.setCharFormat(_defaultCharFormat); + cursor.setBlockFormat(_defaultBlockFormat); setTextCursor(cursor); } }, lifetime()); @@ -1329,8 +1366,8 @@ InputField::InputField( setCursor(style::cur_text); heightAutoupdated(); - if (!_lastTextWithTags.text.isEmpty()) { - setTextWithTags(_lastTextWithTags, HistoryAction::Clear); + if (!value.text.isEmpty()) { + setTextWithTags(value, HistoryAction::Clear); } startBorderAnimation(); @@ -1476,7 +1513,7 @@ void InputField::setTagMimeProcessor(Fn processor) { void InputField::setCustomEmojiFactory( CustomEmojiFactory factory, Fn paused) { - _customEmojiObject = std::make_unique([=]( + _customEmojiObject = std::make_unique(_st.font, [=]( QStringView data) { return factory(data, [=] { customEmojiRepaint(); }); }, std::move(paused)); @@ -1781,6 +1818,13 @@ void InputField::paintEvent(QPaintEvent *e) { const auto focusedDegree = _a_focused.value(_focused ? 1. : 0.); paintSurrounding(p, r, errorDegree, focusedDegree); + const auto skip = int(base::SafeRound(_inner->document()->documentMargin())); + const auto margins = _st.textMargins + + _st.placeholderMargins + + QMargins(skip, skip + _placeholderCustomFontSkip, skip, 0) + + _additionalMargins + + _customFontMargins; + if (_st.placeholderScale > 0. && !_placeholderPath.isEmpty()) { auto placeholderShiftDegree = _a_placeholderShifted.value(_placeholderShifted ? 1. : 0.); p.save(); @@ -1788,7 +1832,7 @@ void InputField::paintEvent(QPaintEvent *e) { auto placeholderTop = anim::interpolate(0, _st.placeholderShift, placeholderShiftDegree); - QRect r(rect().marginsRemoved(_st.textMargins + _st.placeholderMargins)); + QRect r(rect().marginsRemoved(margins)); r.moveTop(r.top() + placeholderTop); if (style::RightToLeft()) r.moveLeft(width() - r.left() - r.width()); @@ -1815,29 +1859,16 @@ void InputField::paintEvent(QPaintEvent *e) { p.setFont(_st.placeholderFont); p.setPen(anim::pen(_st.placeholderFg, _st.placeholderFgActive, focusedDegree)); - if (_st.placeholderAlign == style::al_topleft && _placeholderAfterSymbols > 0) { const auto skipWidth = placeholderSkipWidth(); p.drawText( - _st.textMargins.left() + _st.placeholderMargins.left() + skipWidth, - _st.textMargins.top() + _st.placeholderMargins.top() + _st.placeholderFont->ascent, + margins.left() + skipWidth, + margins.top() + _st.placeholderFont->ascent, _placeholder); } else { - auto r = rect().marginsRemoved(_st.textMargins + _st.placeholderMargins); + auto r = rect().marginsRemoved(margins); r.moveLeft(r.left() + placeholderLeft); if (style::RightToLeft()) r.moveLeft(width() - r.left() - r.width()); - const auto fontSettings = Integration::Instance().fontSettings(); - if (fontSettings.useOriginalMetrics) { - const auto metrics = QFontMetricsF(_st.placeholderFont->f); - if (_mode == Mode::SingleLine) { - const auto heightDelta = (_st.placeholderFont->height - metrics.height()); - const auto topMargin = (_st.placeholderFont->ascent - metrics.ascent()) - + (heightDelta ? heightDelta / 2 : 0); - r.moveTop(r.top() + topMargin); - } else { - r.moveTop(r.top() + _st.placeholderFont->ascent - metrics.ascent()); - } - } p.drawText(r, _placeholder, _st.placeholderAlign); } @@ -1880,7 +1911,7 @@ void InputField::focusInEvent(QFocusEvent *e) { ? mapFromGlobal(QCursor::pos()).x() : (width() / 2); InvokeQueued(this, [=] { - if (hasFocus()) { + if (QWidget::hasFocus()) { focusInner(); } }); @@ -2139,7 +2170,7 @@ bool InputField::isRedoAvailable() const { void InputField::processFormatting(int insertPosition, int insertEnd) { // Tilde formatting. const auto tildeFormatting = (_st.font->f.pixelSize() * style::DevicePixelRatio() == 13) - && (_st.font->f.family() == qstr("DAOpenSansRegular")); + && (_st.font->f.family() == qstr("Open Sans")); auto isTildeFragment = false; auto tildeFixedFont = _st.font->semibold()->f; @@ -2170,6 +2201,10 @@ void InputField::processFormatting(int insertPosition, int insertEnd) { if (tillBlock.isValid()) tillBlock = tillBlock.next(); for (auto block = fromBlock; block != tillBlock; block = block.next()) { + if (block.blockFormat().lineHeightType() != QTextBlockFormat::FixedHeight) { + action.type = ActionType::FixLineHeight; + break; + } for (auto fragmentIt = block.begin(); !fragmentIt.atEnd(); ++fragmentIt) { auto fragment = fragmentIt.fragment(); Assert(fragment.isValid()); @@ -2351,6 +2386,11 @@ void InputField::processFormatting(int insertPosition, int insertEnd) { PrepareFormattingOptimization(document); auto cursor = QTextCursor(document); + if (action.type == ActionType::FixLineHeight) { + cursor.select(QTextCursor::Document); + cursor.setBlockFormat(_defaultBlockFormat); + continue; + } cursor.setPosition(action.intervalStart); cursor.setPosition(action.intervalEnd, QTextCursor::KeepAnchor); if (action.type == ActionType::InsertEmoji @@ -2420,7 +2460,6 @@ void InputField::documentContentsChanged( if (_correcting) { return; } - // In case of input method events Qt emits // document content change signals for a whole // text block where the even took place. @@ -2466,7 +2505,7 @@ void InputField::documentContentsChanged( QTextCursor(document).endEditBlock(); handleContentsChanged(); const auto added = charsAdded - _emojiSurrogateAmount; - _documentContentsChanges.fire({position, charsRemoved, added}); + _documentContentsChanges.fire({ position, charsRemoved, added }); _emojiSurrogateAmount = 0; }); @@ -2680,6 +2719,7 @@ void InputField::setTextWithTags( _realCharsAdded = textWithTags.text.size(); const auto document = _inner->document(); auto cursor = QTextCursor(document); + cursor.setBlockFormat(_defaultBlockFormat); if (historyAction == HistoryAction::Clear) { document->setUndoRedoEnabled(false); cursor.beginEditBlock(); @@ -2927,21 +2967,32 @@ void InputField::keyPressEventInner(QKeyEvent *e) { #endif // Q_OS_MAC } else { const auto text = e->text(); - const auto oldPosition = textCursor().position(); + const auto old = textCursor(); + const auto oldPosition = old.position(); + const auto oldSelection = old.hasSelection(); const auto oldModifiers = e->modifiers(); const auto allowedModifiers = (enter && ctrl) ? (~Qt::ControlModifier) : (enter && shift) ? (~Qt::ShiftModifier) - // Qt bug workaround https://bugreports.qt.io/browse/QTBUG-49771 - : (backspace && Platform::IsX11()) - ? (Qt::ControlModifier) : oldModifiers; const auto changeModifiers = (oldModifiers & ~allowedModifiers) != 0; if (changeModifiers) { e->setModifiers(oldModifiers & allowedModifiers); } - _inner->QTextEdit::keyPressEvent(e); + if (e == QKeySequence::InsertParagraphSeparator) { + // qtbase commit dbb9579566f3accd8aa5fe61db9692991117afd3 introduced + // special logic for repeated 'Enter' key presses, which drops the + // block format instead of inserting a newline in case the block format + // is non-trivial. For custom fonts we use non-trivial block formats + // always for the entire QTextEdit, so we revert that logic and simply + // insert a newline as it was before Qt 6.X.Y where this was added. + textCursor().insertBlock(); + _inner->ensureCursorVisible(); + e->accept(); + } else { + _inner->QTextEdit::keyPressEvent(e); + } if (changeModifiers) { e->setModifiers(oldModifiers); } @@ -2954,7 +3005,10 @@ void InputField::keyPressEventInner(QKeyEvent *e) { } else if (e->key() == Qt::Key_PageDown || e->key() == Qt::Key_Down) { cursor.movePosition(QTextCursor::End, e->modifiers().testFlag(Qt::ShiftModifier) ? QTextCursor::KeepAnchor : QTextCursor::MoveAnchor); check = true; - } else if (e->key() == Qt::Key_Left || e->key() == Qt::Key_Right || e->key() == Qt::Key_Backspace) { + } else if (!oldSelection + && (e->key() == Qt::Key_Left + || e->key() == Qt::Key_Right + || e->key() == Qt::Key_Backspace)) { e->ignore(); } if (check) { @@ -3185,7 +3239,7 @@ void InputField::inputMethodEventInner(QInputMethodEvent *e) { } _inputMethodCommit = e->commitString(); - const auto weak = Ui::MakeWeak(this); + const auto weak = MakeWeak(this); _inner->QTextEdit::inputMethodEvent(e); if (weak && _inputMethodCommit.has_value()) { @@ -3356,7 +3410,7 @@ void InputField::commitInstantReplacement( result.setProperty(kCustomEmojiText, with); result.setProperty(kCustomEmojiLink, unique); result.setProperty(kCustomEmojiId, CustomEmojiIdFromLink(link)); - result.setVerticalAlignment(QTextCharFormat::AlignBottom); + result.setVerticalAlignment(QTextCharFormat::AlignTop); return result; } const auto use = Integration::Instance().defaultEmojiVariant( @@ -3619,8 +3673,13 @@ void InputField::commitMarkdownLinkEdit( _insertedTags.clear(); _reverseMarkdownReplacement = false; + _correcting = true; + cursor.joinPreviousEditBlock(); cursor.setCharFormat(_defaultCharFormat); + cursor.setBlockFormat(_defaultBlockFormat); + cursor.endEditBlock(); _inner->setTextCursor(cursor); + _correcting = false; } void InputField::toggleSelectionMarkdown(const QString &tag) { @@ -3950,14 +4009,18 @@ void InputField::insertFromMimeDataInner(const QMimeData *source) { void InputField::resizeEvent(QResizeEvent *e) { refreshPlaceholder(_placeholderFull.current()); _inner->setGeometry(rect().marginsRemoved( - _st.textMargins + _additionalMargins)); + _st.textMargins + _additionalMargins + _customFontMargins)); _borderAnimationStart = width() / 2; RpWidget::resizeEvent(e); checkContentHeight(); } void InputField::refreshPlaceholder(const QString &text) { - const auto availableWidth = width() - _st.textMargins.left() - _st.textMargins.right() - _st.placeholderMargins.left() - _st.placeholderMargins.right(); + const auto margins = _st.textMargins + + _st.placeholderMargins + + _additionalMargins + + _customFontMargins; + const auto availableWidth = rect().marginsRemoved(margins).width(); if (_st.placeholderScale > 0.) { auto placeholderFont = _st.placeholderFont->f; placeholderFont.setStyleStrategy(QFont::PreferMatch); @@ -3965,7 +4028,9 @@ void InputField::refreshPlaceholder(const QString &text) { _placeholder = metrics.elidedText(text, Qt::ElideRight, availableWidth); _placeholderPath = QPainterPath(); if (!_placeholder.isEmpty()) { - _placeholderPath.addText(0, QFontMetrics(placeholderFont).ascent(), placeholderFont, _placeholder); + const auto result = style::FindAdjustResult(placeholderFont); + const auto ascent = result ? result->iascent : metrics.ascent(); + _placeholderPath.addText(0, ascent, placeholderFont, _placeholder); } } else { _placeholder = _st.placeholderFont->elided(text, availableWidth); @@ -4054,4 +4119,52 @@ int FieldCharacterCount(not_null field) { return field->lastTextSizeWithoutSurrogatePairsCount(); } +void AddLengthLimitLabel(not_null field, int limit) { + struct State { + rpl::variable length; + }; + const auto state = field->lifetime().make_state(); + state->length = rpl::single( + rpl::empty + ) | rpl::then(field->changes()) | rpl::map([=] { + return int(field->getLastText().size()); + }); + const auto allowExceed = std::max(limit / 2, 9); + field->setMaxLength(limit + allowExceed); + const auto threshold = std::min(limit / 2, 9); + auto warningText = state->length.value() | rpl::map([=](int count) { + const auto left = limit - count; + return (left < threshold) ? QString::number(left) : QString(); + }); + const auto warning = CreateChild( + field.get(), + std::move(warningText), + st::defaultInputFieldLimit); + + const auto maxSize = st::defaultInputFieldLimit.style.font->width( + QString::number(-allowExceed)); + const auto add = std::max(maxSize - field->st().textMargins.right(), 0); + if (add) { + field->setAdditionalMargins({ 0, 0, add, 0 }); + } + state->length.value() | rpl::map( + rpl::mappers::_1 > limit + ) | rpl::start_with_next([=](bool exceeded) { + warning->setTextColorOverride(exceeded + ? st::attentionButtonFg->c + : std::optional()); + }, warning->lifetime()); + rpl::combine( + field->sizeValue(), + warning->sizeValue() + ) | rpl::start_with_next([=] { + // Baseline alignment. + const auto top = field->st().textMargins.top() + + field->st().font->ascent + - st::defaultInputFieldLimit.style.font->ascent; + warning->moveToRight(0, top); + }, warning->lifetime()); + warning->setAttribute(Qt::WA_TransparentForMouseEvents); +} + } // namespace Ui diff --git a/ui/widgets/fields/input_field.h b/ui/widgets/fields/input_field.h index 8f15f44..957509c 100644 --- a/ui/widgets/fields/input_field.h +++ b/ui/widgets/fields/input_field.h @@ -80,7 +80,10 @@ class CustomEmojiObject : public QObject, public QTextObjectInterface { public: using Factory = Fn(QStringView)>; - CustomEmojiObject(Factory factory, Fn paused); + CustomEmojiObject( + const style::font &font, + Factory factory, + Fn paused); ~CustomEmojiObject(); void *qt_metacast(const char *iid) override; @@ -100,6 +103,7 @@ public: void clear(); private: + const style::font _font; Factory _factory; Fn _paused; base::flat_map> _emoji; @@ -499,6 +503,8 @@ private: std::optional _inputMethodCommit; QMargins _additionalMargins; + QMargins _customFontMargins; + int _placeholderCustomFontSkip = 0; bool _forcePlaceholderHidden = false; bool _reverseMarkdownReplacement = false; @@ -561,6 +567,7 @@ private: base::unique_qptr _contextMenu; QTextCharFormat _defaultCharFormat; + QTextBlockFormat _defaultBlockFormat; rpl::variable _scrollTop; @@ -583,4 +590,6 @@ void PrepareFormattingOptimization(not_null document); [[nodiscard]] int FieldCharacterCount(not_null field); +void AddLengthLimitLabel(not_null field, int limit); + } // namespace Ui diff --git a/ui/widgets/fields/masked_input_field.cpp b/ui/widgets/fields/masked_input_field.cpp index 2a9112d..dde671a 100644 --- a/ui/widgets/fields/masked_input_field.cpp +++ b/ui/widgets/fields/masked_input_field.cpp @@ -390,7 +390,9 @@ void MaskedInputField::refreshPlaceholder(const QString &text) { _placeholder = metrics.elidedText(text, Qt::ElideRight, availableWidth); _placeholderPath = QPainterPath(); if (!_placeholder.isEmpty()) { - _placeholderPath.addText(0, QFontMetrics(placeholderFont).ascent(), placeholderFont, _placeholder); + const auto result = style::FindAdjustResult(placeholderFont); + const auto ascent = result ? result->iascent : metrics.ascent(); + _placeholderPath.addText(0, ascent, placeholderFont, _placeholder); } } else { _placeholder = _st.placeholderFont->elided(text, availableWidth); diff --git a/ui/widgets/fields/time_part_input.cpp b/ui/widgets/fields/time_part_input.cpp index 5ca80a8..a2e7b40 100644 --- a/ui/widgets/fields/time_part_input.cpp +++ b/ui/widgets/fields/time_part_input.cpp @@ -42,18 +42,32 @@ rpl::producer<> TimePart::erasePrevious() const { return _erasePrevious.events(); } +rpl::producer<> TimePart::jumpToPrevious() const { + return _jumpToPrevious.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); + const auto position = cursorPosition(); + const auto selection = hasSelectedText(); + if (!selection && !position) { + if (e->key() == Qt::Key_Backspace) { + _erasePrevious.fire({}); + return; + } else if (e->key() == Qt::Key_Left) { + _jumpToPrevious.fire({}); + return; + } + } else if (!selection && position == getLastText().size()) { + if (e->key() == Qt::Key_Right) { + _putNext.fire(QChar(0)); + return; + } } + MaskedInputField::keyPressEvent(e); } void TimePart::wheelEvent(QWheelEvent *e) { @@ -78,10 +92,10 @@ void TimePart::correctValue( int wasCursor, QString &now, int &nowCursor) { - auto newText = QString(); - auto newCursor = -1; const auto oldCursor = nowCursor; const auto oldLength = now.size(); + auto newCursor = (oldCursor > 0) ? -1 : 0; + auto newText = QString(); auto accumulated = 0; auto limit = 0; for (; limit != oldLength; ++limit) { diff --git a/ui/widgets/fields/time_part_input.h b/ui/widgets/fields/time_part_input.h index 306a471..0ca4dcf 100644 --- a/ui/widgets/fields/time_part_input.h +++ b/ui/widgets/fields/time_part_input.h @@ -18,6 +18,7 @@ public: void setWheelStep(int value); [[nodiscard]] rpl::producer<> erasePrevious() const; + [[nodiscard]] rpl::producer<> jumpToPrevious() const; [[nodiscard]] rpl::producer putNext() const; [[nodiscard]] std::optional number(); @@ -37,6 +38,7 @@ private: int _maxDigits = 0; int _wheelStep = 0; rpl::event_stream<> _erasePrevious; + rpl::event_stream<> _jumpToPrevious; rpl::event_stream _putNext; }; diff --git a/ui/widgets/labels.cpp b/ui/widgets/labels.cpp index 8b75997..a1aaeb2 100644 --- a/ui/widgets/labels.cpp +++ b/ui/widgets/labels.cpp @@ -687,7 +687,7 @@ void FlatLabel::showContextMenu(QContextMenuEvent *e, ContextMenuReason reason) const auto request = ContextMenuRequest{ .menu = _contextMenu.get(), .link = ClickHandler::getActive(), - .hasSelection = hasSelection, + .selection = _selectable ? _selection : TextSelection(), .uponSelection = uponSelection, .fullSelection = _selectable && _text.isFullSelection(_selection), }; @@ -716,7 +716,7 @@ void FlatLabel::fillContextMenu(ContextMenuRequest request) { Integration::Instance().phraseContextCopySelected(), [=] { copySelectedText(); }); } else if (_selectable - && !request.hasSelection + && request.selection.empty() && !_contextCopyText.isEmpty()) { request.menu->addAction( _contextCopyText, diff --git a/ui/widgets/labels.h b/ui/widgets/labels.h index cb66f5c..d43e9ac 100644 --- a/ui/widgets/labels.h +++ b/ui/widgets/labels.h @@ -117,6 +117,10 @@ public: const style::FlatLabel &st = st::defaultFlatLabel, const style::PopupMenu &stMenu = st::defaultPopupMenu); + [[nodiscard]] const style::FlatLabel &st() const { + return _st; + } + void setOpacity(float64 o); void setTextColorOverride(std::optional color); @@ -154,7 +158,7 @@ public: struct ContextMenuRequest { not_null menu; ClickHandlerPtr link; - bool hasSelection = false; + TextSelection selection; bool uponSelection = false; bool fullSelection = false; }; diff --git a/ui/widgets/menu/menu_add_action_callback.h b/ui/widgets/menu/menu_add_action_callback.h index e1cde37..a0e833a 100644 --- a/ui/widgets/menu/menu_add_action_callback.h +++ b/ui/widgets/menu/menu_add_action_callback.h @@ -8,6 +8,10 @@ #include "ui/style/style_core.h" +namespace style { +struct MenuSeparator; +} // namespace style + namespace Ui { class PopupMenu; } // namespace Ui @@ -20,6 +24,7 @@ public: QString text; Fn handler; const style::icon *icon; + const style::MenuSeparator *separatorSt = nullptr; Fn)> fillSubmenu; int addTopShift = 0; bool isSeparator = false; diff --git a/ui/widgets/menu/menu_add_action_callback_factory.cpp b/ui/widgets/menu/menu_add_action_callback_factory.cpp index 5429d61..e120843 100644 --- a/ui/widgets/menu/menu_add_action_callback_factory.cpp +++ b/ui/widgets/menu/menu_add_action_callback_factory.cpp @@ -27,8 +27,8 @@ MenuCallback CreateAddActionCallback(not_null menu) { action->setMenu(Ui::CreateChild(menu->menu().get())); a.fillSubmenu(menu->ensureSubmenu(action, menu->st())); return action; - } else if (a.isSeparator) { - return menu->addSeparator(); + } else if (a.separatorSt || a.isSeparator) { + return menu->addSeparator(a.separatorSt); } else if (a.isAttention) { return menu->addAction(base::make_unique_q( menu, diff --git a/ui/widgets/popup_menu.cpp b/ui/widgets/popup_menu.cpp index 3a79351..62531a7 100644 --- a/ui/widgets/popup_menu.cpp +++ b/ui/widgets/popup_menu.cpp @@ -900,12 +900,6 @@ void PopupMenu::deleteOnHide(bool del) { } void PopupMenu::popup(const QPoint &p) { - if (_clearLastSeparator) { - _menu->clearLastSeparator(); - for (const auto &[action, submenu] : _submenus) { - submenu->menu()->clearLastSeparator(); - } - } if (prepareGeometryFor(p)) { popupPrepared(); return; @@ -957,10 +951,15 @@ bool PopupMenu::prepareGeometryFor(const QPoint &p) { } bool PopupMenu::prepareGeometryFor(const QPoint &p, PopupMenu *parent) { + if (_clearLastSeparator) { + _menu->clearLastSeparator(); + for (const auto &[action, submenu] : _submenus) { + submenu->menu()->clearLastSeparator(); + } + } + const auto usingScreenGeometry = !::Platform::IsWayland(); - const auto screen = usingScreenGeometry - ? QGuiApplication::screenAt(p) - : nullptr; + const auto screen = QGuiApplication::screenAt(p); if ((usingScreenGeometry && !screen) || (!parent && ::Platform::IsMac() @@ -1002,7 +1001,7 @@ bool PopupMenu::prepareGeometryFor(const QPoint &p, PopupMenu *parent) { _additionalMenuPadding.left() - _st.shadow.extend.left(), 0), _padding.top() - _topShift); - auto r = screen ? screen->availableGeometry() : QRect(); + auto r = usingScreenGeometry ? screen->availableGeometry() : QRect(); const auto parentWidth = _parent ? _parent->inner().width() : 0; if (style::RightToLeft()) { const auto badLeft = !r.isNull() && w.x() - width() < r.x() - _margins.left(); diff --git a/ui/widgets/scroll_area.cpp b/ui/widgets/scroll_area.cpp index 81fc8bf..c8acf08 100644 --- a/ui/widgets/scroll_area.cpp +++ b/ui/widgets/scroll_area.cpp @@ -20,10 +20,13 @@ namespace Ui { // flick scroll taken from http://qt-project.org/doc/qt-4.8/demos-embedded-anomaly-src-flickcharm-cpp.html -ScrollShadow::ScrollShadow(ScrollArea *parent, const style::ScrollArea *st) : QWidget(parent), _st(st) { +ScrollShadow::ScrollShadow(ScrollArea *parent, const style::ScrollArea *st) +: QWidget(parent) +, _st(st) { + Expects(_st != nullptr); + Expects(_st->shColor.get() != nullptr); + setVisible(false); - Assert(_st != nullptr); - Assert(_st->shColor.v() != nullptr); } void ScrollShadow::paintEvent(QPaintEvent *e) { @@ -706,32 +709,48 @@ void ScrollArea::scrollToWidget(not_null widget) { } } -void ScrollArea::scrollToY(int toTop, int toBottom) { +int ScrollArea::computeScrollTo(int toTop, int toBottom) { if (const auto inner = widget()) { SendPendingMoveResizeEvents(inner); } SendPendingMoveResizeEvents(this); - int toMin = 0, toMax = scrollTopMax(); + const auto toMin = 0; + const auto toMax = scrollTopMax(); if (toTop < toMin) { toTop = toMin; } else if (toTop > toMax) { toTop = toMax; } - bool exact = (toBottom < 0); + const auto exact = (toBottom < 0); - int curTop = scrollTop(), curHeight = height(), curBottom = curTop + curHeight, scToTop = toTop; + const auto curTop = scrollTop(); + const auto curHeight = height(); + const auto curBottom = curTop + curHeight; + auto scToTop = toTop; if (!exact && toTop >= curTop) { - if (toBottom < toTop) toBottom = toTop; - if (toBottom <= curBottom) return; + if (toBottom < toTop) { + toBottom = toTop; + } + if (toBottom <= curBottom) { + return curTop; + } scToTop = toBottom - curHeight; - if (scToTop > toTop) scToTop = toTop; - if (scToTop == curTop) return; + if (scToTop > toTop) { + scToTop = toTop; + } + if (scToTop == curTop) { + return curTop; + } } else { scToTop = toTop; } - verticalScrollBar()->setValue(scToTop); + return scToTop; +} + +void ScrollArea::scrollToY(int toTop, int toBottom) { + verticalScrollBar()->setValue(computeScrollTo(toTop, toBottom)); } void ScrollArea::doSetOwnedWidget(object_ptr w) { diff --git a/ui/widgets/scroll_area.h b/ui/widgets/scroll_area.h index b1d03a2..25f7360 100644 --- a/ui/widgets/scroll_area.h +++ b/ui/widgets/scroll_area.h @@ -166,6 +166,7 @@ public: void scrollTo(ScrollToRequest request); void scrollToWidget(not_null widget); + [[nodiscard]] int computeScrollTo(int toTop, int toBottom); void scrollToY(int toTop, int toBottom = -1); void disableScroll(bool dis); diff --git a/ui/widgets/separate_panel.cpp b/ui/widgets/separate_panel.cpp index 1ee1437..01bf57a 100644 --- a/ui/widgets/separate_panel.cpp +++ b/ui/widgets/separate_panel.cpp @@ -676,6 +676,12 @@ void SeparatePanel::initGeometry(QSize size) { }(); move(rect.topLeft()); setFixedSize(rect.size()); + createWinId(); + if (_useTransparency) { + Platform::SetWindowMargins(this, _padding); + } else { + Platform::UnsetWindowMargins(this); + } updateControlsGeometry(); } diff --git a/ui/widgets/time_input.cpp b/ui/widgets/time_input.cpp index 66041e6..b21f54e 100644 --- a/ui/widgets/time_input.cpp +++ b/ui/widgets/time_input.cpp @@ -110,6 +110,10 @@ TimeInput::TimeInput( _minute->erasePrevious() | rpl::start_with_next([=] { erasePrevious(_hour); }, lifetime()); + _minute->jumpToPrevious() | rpl::start_with_next([=] { + _hour->setCursorPosition(_hour->getLastText().size()); + _hour->setFocus(); + }, lifetime()); _separator1->setAttribute(Qt::WA_TransparentForMouseEvents); setMouseTracking(true); diff --git a/ui/widgets/tooltip.cpp b/ui/widgets/tooltip.cpp index 73af93a..44dcd93 100644 --- a/ui/widgets/tooltip.cpp +++ b/ui/widgets/tooltip.cpp @@ -77,7 +77,7 @@ Tooltip::~Tooltip() { void Tooltip::popup(const QPoint &m, const QString &text, const style::Tooltip *st) { const auto usingScreenGeometry = !::Platform::IsWayland(); - const auto screen = usingScreenGeometry ? QGuiApplication::screenAt(m) : nullptr; + const auto screen = QGuiApplication::screenAt(m); if (usingScreenGeometry && !screen) { Hide(); return; @@ -118,10 +118,13 @@ void Tooltip::popup(const QPoint &m, const QString &text, const style::Tooltip * p.setX(m.x() - (s.width() / 2)); } - // adjust tooltip position if (screen) { createWinId(); windowHandle()->setScreen(screen); + } + + // adjust tooltip position + if (usingScreenGeometry) { const auto r = screen->availableGeometry(); if (r.x() + r.width() - _st->skip < p.x() + s.width() && p.x() + s.width() > m.x()) { p.setX(qMax(r.x() + r.width() - int32(_st->skip) - s.width(), m.x() - s.width())); diff --git a/ui/widgets/widgets.style b/ui/widgets/widgets.style index 55c6a4c..8dbd1fe 100644 --- a/ui/widgets/widgets.style +++ b/ui/widgets/widgets.style @@ -744,7 +744,7 @@ defaultCheck: Check { diameter: 22px; thickness: 2px; icon: defaultCheckboxIcon; - duration: 120; + duration: universalDuration; rippleAreaPadding: 8px; } defaultRadio: Radio { @@ -755,7 +755,7 @@ defaultRadio: Radio { thickness: 2px; outerSkip: 10px; // * 0.1 skip: 60px; // * 0.1 - duration: 120; + duration: universalDuration; rippleAreaPadding: 8px; } defaultToggle: Toggle { @@ -763,7 +763,7 @@ defaultToggle: Toggle { toggledFg: windowBgActive; untoggledBg: windowBg; untoggledFg: checkboxFg; - duration: 120; + duration: universalDuration; border: 2px; shift: 1px; diameter: 16px; @@ -910,6 +910,9 @@ defaultInputField: InputField { heightMin: 55px; heightMax: 148px; } +defaultInputFieldLimit: FlatLabel(defaultFlatLabel) { + textFg: windowSubTextFg; +} defaultIconButton: IconButton { iconPosition: point(-1px, -1px); @@ -1016,6 +1019,7 @@ SettingsSlider { labelStyle: TextStyle; labelFg: color; labelFgActive: color; + strictSkip: pixels; duration: int; rippleBottomSkip: pixels; rippleBg: color;