diff --git a/CMakeLists.txt b/CMakeLists.txt index f32dd98a1..b26bf087c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -36,6 +36,9 @@ PRIVATE fonts/fonts.qrc qt_conf/win.qrc + ayu/ayu_ui_settings.cpp + ayu/ayu_ui_settings.h + ui/accessible/ui_accessible_factory.cpp ui/accessible/ui_accessible_factory.h ui/accessible/ui_accessible_item.cpp diff --git a/ayu/ayu_ui_settings.cpp b/ayu/ayu_ui_settings.cpp new file mode 100644 index 000000000..8fa0e4edd --- /dev/null +++ b/ayu/ayu_ui_settings.cpp @@ -0,0 +1,58 @@ +// This is the source code of AyuGram for Desktop. +// +// We do not and cannot prevent the use of our code, +// but be respectful and credit the original author. +// +// Copyright @Radolyn, 2026 +#include "ayu_ui_settings.h" + +#include + +namespace AyuUiSettings { + +QString monoFont; +double wideMultiplier = 1.0; +bool materialSwitches; +int avatarCorners = kMaxAvatarCorners; + +void setMonoFont(QString newFont) { + monoFont = std::move(newFont); +} + +QString getMonoFont() { + return monoFont; +} + +void setWideMultiplier(double val) { + wideMultiplier = val; +} + +bool isWideMultiplied() { + return abs(wideMultiplier - 1.0) > 0.01; +} + +int getWideMultiplied(int width, double mult) { + if (!isWideMultiplied()) { + return width; + } + const auto res = width * (wideMultiplier * mult); + return std::max(width, static_cast(std::round(res))); +} + +void setMaterialSwitches(bool val) { + materialSwitches = val; +} + +bool isMaterialSwitches() { + return materialSwitches; +} + +void setAvatarCorners(int val) { + avatarCorners = val; +} + +int getAvatarCorners() { + return avatarCorners; +} + +} diff --git a/ayu/ayu_ui_settings.h b/ayu/ayu_ui_settings.h new file mode 100644 index 000000000..2c8c5383e --- /dev/null +++ b/ayu/ayu_ui_settings.h @@ -0,0 +1,27 @@ +// This is the source code of AyuGram for Desktop. +// +// We do not and cannot prevent the use of our code, +// but be respectful and credit the original author. +// +// Copyright @Radolyn, 2026 +#pragma once + +namespace AyuUiSettings { + +inline constexpr int kMaxAvatarCorners = 23; + +void setMonoFont(QString newFont); +QString getMonoFont(); + +void setWideMultiplier(double val); + +bool isWideMultiplied(); +int getWideMultiplied(int width, double mult); + +void setMaterialSwitches(bool val); +bool isMaterialSwitches(); + +void setAvatarCorners(int val); +int getAvatarCorners(); + +} diff --git a/ui/basic.style b/ui/basic.style index 377ad2580..b9b362b85 100644 --- a/ui/basic.style +++ b/ui/basic.style @@ -109,8 +109,8 @@ noContactsHeight: 100px; noContactsFont: font(fsize); noContactsColor: windowSubTextFg; -activeFadeInDuration: 500; -activeFadeOutDuration: 3000; +activeFadeInDuration: 400; +activeFadeOutDuration: 2000; smallCloseIcon: icon {{ "simple_close", smallCloseIconFg }}; smallCloseIconOver: icon {{ "simple_close", smallCloseIconFgOver }}; diff --git a/ui/colors.palette b/ui/colors.palette index a15f058e6..28a0acd0a 100644 --- a/ui/colors.palette +++ b/ui/colors.palette @@ -168,9 +168,9 @@ introBg: windowBg; // login background introTitleFg: windowBoldFg; // login title text introDescriptionFg: windowSubTextFg; // login description text -introCoverTopBg: #0f89d0; // intro gradient top (from) -introCoverBottomBg: #39b0f0; // intro gradient bottom (to) -introCoverIconsFg: #5ec6ff; // intro cloud graphics +introCoverTopBg: #2B2242; // intro gradient top (from) +introCoverBottomBg: #2B2242; // intro gradient bottom (to) +introCoverIconsFg: #FFFFFF; // intro cloud graphics introCoverPlaneTrace: #5ec6ff69; // intro plane traces introCoverPlaneInner: #c6d8e8; // intro plane part introCoverPlaneOuter: #a1bed4; // intro plane part diff --git a/ui/effects/panel_animation.cpp b/ui/effects/panel_animation.cpp index 33fc9274f..c380dbdcf 100644 --- a/ui/effects/panel_animation.cpp +++ b/ui/effects/panel_animation.cpp @@ -307,10 +307,12 @@ void PanelAnimation::setAlphaDuration() { void PanelAnimation::start() { Assert(!_finalImage.isNull()); RoundShadowAnimation::start(_finalWidth, _finalHeight, _finalImage.devicePixelRatio()); - auto checkCorner = [this](const Corner &corner) { + auto maxCornerWidth = 0; + auto maxCornerHeight = 0; + auto checkCorner = [&](const Corner &corner) { if (!corner.valid()) return; - if (_startWidth >= 0) Assert(corner.width <= _startWidth); - if (_startHeight >= 0) Assert(corner.height <= _startHeight); + maxCornerWidth = std::max(maxCornerWidth, corner.width); + maxCornerHeight = std::max(maxCornerHeight, corner.height); Assert(corner.width <= _finalInnerWidth); Assert(corner.height <= _finalInnerHeight); }; @@ -318,6 +320,12 @@ void PanelAnimation::start() { checkCorner(_topRight); checkCorner(_bottomLeft); checkCorner(_bottomRight); + if (_startWidth >= 0 && maxCornerWidth > _startWidth) { + _startWidth = -1; + } + if (_startHeight >= 0 && maxCornerHeight > _startHeight) { + _startHeight = -1; + } } auto PanelAnimation::computeState(float64 dt, float64 opacity) const diff --git a/ui/image/image_prepare.cpp b/ui/image/image_prepare.cpp index 325f5c923..19015e3a3 100644 --- a/ui/image/image_prepare.cpp +++ b/ui/image/image_prepare.cpp @@ -14,6 +14,7 @@ #include "base/bytes.h" #include "styles/palette.h" #include "styles/style_basic.h" +#include "ayu/ayu_ui_settings.h" #include #include @@ -528,7 +529,8 @@ ReadResult Read(ReadArgs &&args) { ? Option::RoundLarge : (radius == ImageRoundRadius::Small) ? Option::RoundSmall - : (radius == ImageRoundRadius::Ellipse) + : (radius == ImageRoundRadius::Ellipse + || radius == ImageRoundRadius::AyuUserpic) ? Option::RoundCircle : Option::None); } @@ -1130,6 +1132,19 @@ QImage Round( QRect target) { if (!static_cast(corners)) { return std::move(image); + } else if (radius == ImageRoundRadius::AyuUserpic) { + const auto corners_val = AyuUiSettings::getAvatarCorners(); + if (corners_val >= AyuUiSettings::kMaxAvatarCorners) { + return Circle(std::move(image), target); + } else if (corners_val <= 0) { + return std::move(image); + } + const auto size = target.isEmpty() + ? std::min(image.width(), image.height()) + : std::min(target.width(), target.height()); + const auto r = int(double(corners_val) / AyuUiSettings::kMaxAvatarCorners * size / 2.0) + / style::DevicePixelRatio(); + return Round(std::move(image), CornersMask(r), corners, target); } else if (radius == ImageRoundRadius::Ellipse) { Assert((corners & RectPart::AllCorners) == RectPart::AllCorners); return Circle(std::move(image), target); diff --git a/ui/image/image_prepare.h b/ui/image/image_prepare.h index 23555c332..f3db34c4c 100644 --- a/ui/image/image_prepare.h +++ b/ui/image/image_prepare.h @@ -21,6 +21,7 @@ enum class ImageRoundRadius { Large, Small, Ellipse, + AyuUserpic, }; namespace Images { diff --git a/ui/layers/layers.style b/ui/layers/layers.style index ebb7796ec..d3a9fe724 100644 --- a/ui/layers/layers.style +++ b/ui/layers/layers.style @@ -35,7 +35,7 @@ Box { } boxDuration: 200; -boxRadius: 8px; +boxRadius: 6px; boxButtonFont: font(boxFontSize semibold); defaultBoxButtonTextStyle: TextStyle(semiboldTextStyle) { diff --git a/ui/style/style_core_font.cpp b/ui/style/style_core_font.cpp index 3f02d5d1d..ce99d7256 100644 --- a/ui/style/style_core_font.cpp +++ b/ui/style/style_core_font.cpp @@ -21,6 +21,11 @@ #include #endif // __has_include() + +// AyuGram includes +#include "ayu/ayu_ui_settings.h" + + void style_InitFontsResource() { #ifdef Q_OS_MAC // Use resources from the .app bundle on macOS. @@ -120,7 +125,9 @@ bool LoadCustomFont(const QString &filePath) { } [[nodiscard]] QString ManualMonospaceFont() { + const auto monoFont = AyuUiSettings::getMonoFont().isEmpty() ? "Cascadia Mono"_q : AyuUiSettings::getMonoFont(); const auto kTryFirst = std::initializer_list{ + monoFont, u"Cascadia Mono"_q, u"Consolas"_q, u"Liberation Mono"_q, diff --git a/ui/text/text.cpp b/ui/text/text.cpp index fa7a09ecd..28a56b80a 100644 --- a/ui/text/text.cpp +++ b/ui/text/text.cpp @@ -29,6 +29,9 @@ const QString kQBullet = QString::fromUtf8("\xE2\x80\xA2"); } // namespace Ui namespace Ui::Text { + +bool (*RevealAllSpoilersCallback)() = nullptr; + namespace { constexpr auto kDefaultSpoilerCacheCapacity = 24; diff --git a/ui/text/text.h b/ui/text/text.h index 0d83eeba5..673d11485 100644 --- a/ui/text/text.h +++ b/ui/text/text.h @@ -89,6 +89,8 @@ struct QuotesData; struct ExtendedData; struct MarkedContext; +extern bool (*RevealAllSpoilersCallback)(); + using CustomEmojiFactory = Fn( QStringView, const MarkedContext &)>; @@ -215,6 +217,7 @@ struct QuotePaintCache { std::array outlines; QColor header; QColor bg; + QColor bg2; QColor icon; }; @@ -273,6 +276,8 @@ struct PaintContext { bool elisionMiddle = false; bool useFullWidth = false; // !(width = min(availableWidth, maxWidth())) const LinePostprocess *linePostprocess = nullptr; + bool revealSpoilers = false; + bool disableRevealAllSpoilers = false; }; class String { diff --git a/ui/text/text_entity.h b/ui/text/text_entity.h index 0781d03eb..adde06704 100644 --- a/ui/text/text_entity.h +++ b/ui/text/text_entity.h @@ -115,6 +115,12 @@ class EntityInText { [[nodiscard]] QString data() const { return _data; } + void setLocal() { + _local = true; + } + [[nodiscard]] bool isLocal() const { + return _local; + } void extendToLeft(int extent) { _offset -= extent; @@ -162,6 +168,7 @@ class EntityInText { private: EntityType _type = EntityType::Invalid; + bool _local = false; int _offset = 0; int _length = 0; QString _data; diff --git a/ui/text/text_renderer.cpp b/ui/text/text_renderer.cpp index 3207f20a2..65e0cf9d3 100644 --- a/ui/text/text_renderer.cpp +++ b/ui/text/text_renderer.cpp @@ -152,7 +152,11 @@ void Renderer::draw(QPainter &p, const PaintContext &context) { _cachedNow = context.now; _pausedEmoji = context.paused || context.pausedEmoji; _pausedSpoiler = context.paused || context.pausedSpoiler; - _spoilerOpacity = _spoiler + _revealSpoilers = context.revealSpoilers + || (Ui::Text::RevealAllSpoilersCallback + && Ui::Text::RevealAllSpoilersCallback() + && !context.disableRevealAllSpoilers); + _spoilerOpacity = (_spoiler && !_revealSpoilers) ? (1. - _spoiler->revealAnimation.value( _spoiler->revealed ? 1. : 0.)) : 0.; @@ -1392,7 +1396,7 @@ void Renderer::fillRectsFromRanges( void Renderer::paintSpoilerRects() { Expects(_p != nullptr); - if (!_spoiler) { + if (!_spoiler || _revealSpoilers) { return; } const auto opacity = _p->opacity(); @@ -1701,6 +1705,7 @@ void Renderer::applyBlockProperties( ClickHandlerPtr Renderer::lookupLink(const AbstractBlock *block) const { const auto spoilerLink = (_spoiler && !_spoiler->revealed + && !_revealSpoilers && (block->flags() & TextBlockFlag::Spoiler)) ? _spoiler->link : ClickHandlerPtr(); diff --git a/ui/text/text_renderer.h b/ui/text/text_renderer.h index b0d398bc7..c27110978 100644 --- a/ui/text/text_renderer.h +++ b/ui/text/text_renderer.h @@ -219,6 +219,7 @@ class Renderer final { bool _elisionMiddle = false; const LinePostprocess *_linePostprocess = nullptr; + bool _revealSpoilers = false; }; diff --git a/ui/text/text_utilities.cpp b/ui/text/text_utilities.cpp index 47a75de35..c70269478 100644 --- a/ui/text/text_utilities.cpp +++ b/ui/text/text_utilities.cpp @@ -110,6 +110,10 @@ TextWithEntities StrikeOut(const QString &text) { return WithSingleEntity(text, EntityType::StrikeOut); } +TextWithEntities Code(const QString &text) { + return WithSingleEntity(text, EntityType::Code); +} + TextWithEntities Link(const QString &text, const QString &url) { return WithSingleEntity(text, EntityType::CustomUrl, url); } diff --git a/ui/text/text_utilities.h b/ui/text/text_utilities.h index 867789e00..ec1fc9af7 100644 --- a/ui/text/text_utilities.h +++ b/ui/text/text_utilities.h @@ -21,6 +21,7 @@ class CustomEmoji; [[nodiscard]] TextWithEntities Italic(const QString &text); [[nodiscard]] TextWithEntities Underline(const QString &text); [[nodiscard]] TextWithEntities StrikeOut(const QString &text); +[[nodiscard]] TextWithEntities Code(const QString &text); [[nodiscard]] TextWithEntities Link( const QString &text, const QString &url = u"internal:action"_q); diff --git a/ui/widgets/checkbox.cpp b/ui/widgets/checkbox.cpp index 56ef0148e..e9ef716a7 100644 --- a/ui/widgets/checkbox.cpp +++ b/ui/widgets/checkbox.cpp @@ -16,7 +16,28 @@ #include #include +#include "ayu/ayu_ui_settings.h" + namespace Ui { +namespace { + +int SwitchShift(not_null st) { + return AyuUiSettings::isMaterialSwitches() ? st->shift : st::defaultToggleShift; +} + +int SwitchDiameter(not_null st) { + return AyuUiSettings::isMaterialSwitches() ? st->diameter : st::defaultToggleDiameter; +} + +int SwitchDiameter(not_null st) { + return AyuUiSettings::isMaterialSwitches() ? st->diameter : st::defaultToggleDiameter; +} + +int SwitchDiameter(not_null st) { + return AyuUiSettings::isMaterialSwitches() ? st->diameter : st::defaultToggleDiameter; +} + +} // namespace AbstractCheckView::AbstractCheckView(int duration, bool checked, Fn updateCallback) : _duration(duration) @@ -37,7 +58,8 @@ void AbstractCheckView::setChecked(bool checked, anim::type animated) { [=] { if (_updateCallback) _updateCallback(); }, _checked ? 0. : 1., _checked ? 1. : 0., - _duration); + AyuUiSettings::isMaterialSwitches() ? _duration : st::defaultToggleDuration, + AyuUiSettings::isMaterialSwitches() ? anim::easeOutCubic : anim::linear); } checkedChangedHook(animated); if (changed) { @@ -76,7 +98,7 @@ ToggleView::ToggleView( } QSize ToggleView::getSize() const { - return QSize(2 * _st->border + _st->diameter + _st->width, 2 * _st->border + _st->diameter); + return QSize(2 * _st->border + SwitchDiameter(_st) + _st->width, 2 * _st->border + SwitchDiameter(_st)); } void ToggleView::setStyle(const style::Toggle &st) { @@ -89,14 +111,20 @@ void ToggleView::paint(QPainter &p, int left, int top, int outerWidth) { PainterHighQualityEnabler hq(p); auto toggled = currentAnimationValue(); - auto fullWidth = _st->diameter + _st->width; - auto innerDiameter = _st->diameter - 2 * _st->shift; + auto fullWidth = SwitchDiameter(_st) + _st->width; + auto innerDiameter = SwitchDiameter(_st) - 2 * SwitchShift(_st); auto innerRadius = float64(innerDiameter) / 2.; - auto toggleLeft = left + anim::interpolate(0, fullWidth - _st->diameter, toggled); - auto bgRect = style::rtlrect(left + _st->shift, top + _st->shift, fullWidth - 2 * _st->shift, innerDiameter, outerWidth); - auto fgRect = style::rtlrect(toggleLeft, top, _st->diameter, _st->diameter, outerWidth); + auto toggleLeft = left + anim::interpolate(0, fullWidth - SwitchDiameter(_st), toggled); + auto bgRect = style::rtlrect(left + SwitchShift(_st), top + SwitchShift(_st), fullWidth - 2 * SwitchShift(_st), innerDiameter, outerWidth); + auto fgRect = style::rtlrect(toggleLeft, top, SwitchDiameter(_st), SwitchDiameter(_st), outerWidth); auto fgBrush = anim::brush(_st->untoggledFg, _st->toggledFg, toggled); + auto fgRectF = QRectF(fgRect); + if (AyuUiSettings::isMaterialSwitches()) { + const auto ayuToggleAnim = anim::interpolateToF(_st->animPadding, 0, toggled); + fgRectF.setRect(fgRectF.x() + ayuToggleAnim / 2., fgRectF.y() + ayuToggleAnim / 2., fgRectF.width() - ayuToggleAnim, fgRectF.height() - ayuToggleAnim); + } + p.setPen(Qt::NoPen); p.setBrush(fgBrush); p.drawRoundedRect(bgRect, innerRadius, innerRadius); @@ -105,7 +133,7 @@ void ToggleView::paint(QPainter &p, int left, int top, int outerWidth) { pen.setWidth(_st->border); p.setPen(pen); p.setBrush(anim::brush(_st->untoggledBg, _st->toggledBg, toggled)); - p.drawEllipse(fgRect); + p.drawEllipse(fgRectF); if (_locked || _st->xsize > 0) { p.setPen(Qt::NoPen); @@ -127,8 +155,8 @@ void ToggleView::paintXV(QPainter &p, int left, int top, int outerWidth, float64 if (toggled < 1) { // Just X or X->V. const auto xSize = 0. + _st->xsize; - const auto xLeft = left + (_st->diameter - xSize) / 2.; - const auto xTop = top + (_st->diameter - xSize) / 2.; + const auto xLeft = left + (SwitchDiameter(_st) - xSize) / 2.; + const auto xTop = top + (SwitchDiameter(_st) - xSize) / 2.; QPointF pathX[] = { { xLeft, xTop + stroke }, { xLeft + stroke, xTop }, @@ -150,7 +178,7 @@ void ToggleView::paintXV(QPainter &p, int left, int top, int outerWidth, float64 // X->V. const auto vSize = 0. + _st->vsize; const auto fSize = (xSize + vSize - 2. * stroke); - const auto vLeft = left + (_st->diameter - fSize) / 2.; + const auto vLeft = left + (SwitchDiameter(_st) - fSize) / 2.; const auto vTop = 0. + xTop + _st->vshift; QPointF pathV[] = { { vLeft, vTop + xSize - vSize + stroke }, @@ -177,10 +205,10 @@ void ToggleView::paintXV(QPainter &p, int left, int top, int outerWidth, float64 } else { // Just V. const auto xSize = 0. + _st->xsize; - const auto xTop = top + (_st->diameter - xSize) / 2.; + const auto xTop = top + (SwitchDiameter(_st) - xSize) / 2.; const auto vSize = 0. + _st->vsize; const auto fSize = (xSize + vSize - 2. * stroke); - const auto vLeft = left + (_st->diameter - (_st->xsize + _st->vsize - 2. * stroke)) / 2.; + const auto vLeft = left + (SwitchDiameter(_st) - (_st->xsize + _st->vsize - 2. * stroke)) / 2.; const auto vTop = 0. + xTop + _st->vshift; QPointF pathV[] = { { vLeft, vTop + xSize - vSize + stroke }, diff --git a/ui/widgets/widgets.style b/ui/widgets/widgets.style index 7eb3c87c3..20216fb05 100644 --- a/ui/widgets/widgets.style +++ b/ui/widgets/widgets.style @@ -123,6 +123,7 @@ Toggle { shift: pixels; diameter: pixels; width: pixels; + animPadding: pixels; xsize: pixels; vsize: pixels; vshift: pixels; @@ -867,16 +868,20 @@ defaultRadio: Radio { duration: universalDuration; rippleAreaPadding: 8px; } +defaultToggleDuration: universalDuration; +defaultToggleShift: 1px; +defaultToggleDiameter: 16px; defaultToggle: Toggle { toggledBg: windowBg; toggledFg: windowBgActive; untoggledBg: windowBg; untoggledFg: checkboxFg; - duration: universalDuration; + duration: 150; border: 2px; - shift: 1px; - diameter: 16px; + shift: -2px; + diameter: 14px; width: 14px; + animPadding: 2px; xsize: 0px; vsize: 0px; vshift: 0px; @@ -939,7 +944,7 @@ roundShadowRadius8px: Shadow { defaultPanelAnimation: PanelAnimation { startWidth: 0.5; widthDuration: 0.6; - startHeight: 0.3; + startHeight: 0.45; heightDuration: 0.9; startOpacity: 0.2; opacityDuration: 0.3; @@ -1000,7 +1005,7 @@ defaultPopupMenu: PopupMenu { menu: defaultMenu; - radius: 8px; + radius: 6px; duration: 150; showDuration: 200; } diff --git a/ui/wrap/slide_wrap.cpp b/ui/wrap/slide_wrap.cpp index 0c868a892..a70eeda3b 100644 --- a/ui/wrap/slide_wrap.cpp +++ b/ui/wrap/slide_wrap.cpp @@ -65,7 +65,7 @@ SlideWrap *SlideWrap::toggle( _toggled ? 0. : 1., _toggled ? 1. : 0., _duration, - anim::linear); + ease); if (_finishedCallback) { _animation.setFinishedCallback(_finishedCallback); } diff --git a/ui/wrap/slide_wrap.h b/ui/wrap/slide_wrap.h index 021c14a6f..ae84586cb 100644 --- a/ui/wrap/slide_wrap.h +++ b/ui/wrap/slide_wrap.h @@ -58,6 +58,9 @@ class SlideWrap : public Wrap> { QMargins getMargins() const override; + // I don't care about this being public + anim::transition ease = anim::linear; + protected: int resizeGetHeight(int newWidth) override; void wrappedSizeUpdated(QSize size) override;