diff --git a/CMakeLists.txt b/CMakeLists.txt index ecf3f2b..9860dbe 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -42,8 +42,11 @@ PRIVATE ui/effects/animations.h ui/effects/cross_animation.cpp ui/effects/cross_animation.h + ui/effects/cross_line.cpp + ui/effects/cross_line.h ui/effects/fade_animation.cpp ui/effects/fade_animation.h + ui/effects/gradient.h ui/effects/numbers_animation.cpp ui/effects/numbers_animation.h ui/effects/panel_animation.cpp @@ -66,6 +69,12 @@ PRIVATE ui/layers/layer_manager.h ui/layers/layer_widget.cpp ui/layers/layer_widget.h + ui/paint/blob.cpp + ui/paint/blob.h + ui/paint/blobs.cpp + ui/paint/blobs.h + ui/paint/blobs_linear.cpp + ui/paint/blobs_linear.h ui/platform/linux/ui_window_linux.cpp ui/platform/linux/ui_window_linux.h ui/platform/linux/ui_utility_linux.cpp @@ -123,6 +132,10 @@ PRIVATE ui/widgets/box_content_divider.h ui/widgets/buttons.cpp ui/widgets/buttons.h + ui/widgets/call_button.cpp + ui/widgets/call_button.h + ui/widgets/call_mute_button.cpp + ui/widgets/call_mute_button.h ui/widgets/checkbox.cpp ui/widgets/checkbox.h ui/widgets/dropdown_menu.cpp @@ -187,6 +200,9 @@ PRIVATE ui/ui_log.h ui/ui_utility.cpp ui/ui_utility.h + + ui/ui_pch.h + emoji_suggestions/emoji_suggestions.cpp emoji_suggestions/emoji_suggestions.h emoji_suggestions/emoji_suggestions_helper.h diff --git a/icons/calls/voice_muted_large.png b/icons/calls/voice_muted_large.png new file mode 100644 index 0000000..ae5a118 Binary files /dev/null and b/icons/calls/voice_muted_large.png differ diff --git a/icons/calls/voice_muted_large@2x.png b/icons/calls/voice_muted_large@2x.png new file mode 100644 index 0000000..cd1161c Binary files /dev/null and b/icons/calls/voice_muted_large@2x.png differ diff --git a/icons/calls/voice_muted_large@3x.png b/icons/calls/voice_muted_large@3x.png new file mode 100644 index 0000000..3907746 Binary files /dev/null and b/icons/calls/voice_muted_large@3x.png differ diff --git a/icons/calls/voice_unmuted_large.png b/icons/calls/voice_unmuted_large.png new file mode 100644 index 0000000..c2355a3 Binary files /dev/null and b/icons/calls/voice_unmuted_large.png differ diff --git a/icons/calls/voice_unmuted_large@2x.png b/icons/calls/voice_unmuted_large@2x.png new file mode 100644 index 0000000..c2e70b2 Binary files /dev/null and b/icons/calls/voice_unmuted_large@2x.png differ diff --git a/icons/calls/voice_unmuted_large@3x.png b/icons/calls/voice_unmuted_large@3x.png new file mode 100644 index 0000000..d15a5fc Binary files /dev/null and b/icons/calls/voice_unmuted_large@3x.png differ diff --git a/icons/title_button_close@2x.png b/icons/title_button_close@2x.png index e43b4d8..b281004 100644 Binary files a/icons/title_button_close@2x.png and b/icons/title_button_close@2x.png differ diff --git a/icons/title_button_close@3x.png b/icons/title_button_close@3x.png index 18c8496..c4bf819 100644 Binary files a/icons/title_button_close@3x.png and b/icons/title_button_close@3x.png differ diff --git a/ui/basic.style b/ui/basic.style index 3f0832e..c8b4703 100644 --- a/ui/basic.style +++ b/ui/basic.style @@ -73,7 +73,6 @@ roundRadiusLarge: 6px; roundRadiusSmall: 3px; dateRadius: roundRadiusLarge; -buttonRadius: roundRadiusSmall; setLittleSkip: 9px; diff --git a/ui/click_handler.h b/ui/click_handler.h index 5f87046..4c13c47 100644 --- a/ui/click_handler.h +++ b/ui/click_handler.h @@ -105,16 +105,20 @@ protected: class LambdaClickHandler : public ClickHandler { public: - LambdaClickHandler(Fn handler) : _handler(std::move(handler)) { + LambdaClickHandler(Fn handler) + : _handler([handler = std::move(handler)](ClickContext) { handler(); }) { + } + LambdaClickHandler(Fn handler) + : _handler(std::move(handler)) { } void onClick(ClickContext context) const override final { if (context.button == Qt::LeftButton && _handler) { - _handler(); + _handler(context); } } private: - Fn _handler; + Fn _handler; }; diff --git a/ui/colors.palette b/ui/colors.palette index f097422..eeb5d4c 100644 --- a/ui/colors.palette +++ b/ui/colors.palette @@ -554,6 +554,35 @@ callHangupBg: #d75a5a; // phone call popup hangup button background callHangupRipple: #c04646; // phone call popup hangup button ripple effect callMuteRipple: #ffffff12; // phone call popup mute mic and camera ripple effect +groupCallBg: #1a2026; // group call popup background +groupCallActiveFg: #4db8ff; // group call active controls text +groupCallMembersBg: #2c333d; // group call members list background +groupCallMembersBgOver: #323a45; // group call members list row with mouse over +groupCallMembersBgRipple: #39424f; // group call member row ripple effect +groupCallMembersFg: #ffffff; // group call member name text +groupCallMemberActiveIcon: #8deb90; // group call active member icon +groupCallMemberActiveStatus: #8deb90; // group call active member status text +groupCallMemberInactiveIcon: #84888f; // group call inactive member icon +groupCallMemberInactiveStatus: #61c0ff; // group call inactive member status text +groupCallMemberMutedIcon: #ed7372; // group call muted by admin member icon +groupCallMemberNotJoinedStatus: #91979e; // group call non joined member status text +groupCallIconFg: #ffffff; // group call mute / settings / leave icon +groupCallLive1: #0dcc39; // group call live button color1 +groupCallLive2: #0bb6bd; // group call live button color2 +groupCallMuted1: #0992ef; // group call muted button color1 +groupCallMuted2: #16ccfb; // group call muted button color2 +groupCallForceMutedBar1: #c65493; // group call force muted top bar color1 +groupCallForceMutedBar2: #7a6af1; // group call force muted top bar color2 +groupCallForceMutedBar3: #5f95e8; // group call force muted top bar color3 +groupCallForceMuted1: #4f9cff; // group call force muted button color1 +groupCallForceMuted2: #9b52e9; // group call force muted button color2 +groupCallForceMuted3: #eb5353; // group call force muted button color3 +groupCallMenuBg: #292d33; // group call popup menu background +groupCallMenuBgOver: #343940; // group call popup menu with mouse over +groupCallMenuBgRipple: #3a4047; // group call popup menu ripple effect +groupCallLeaveBg: #f75c5c7f; // group call leave button background +groupCallLeaveBgRipple: #f75c5c9e; // group call leave button ripple effect + callBarBg: dialogsBgActive; // active phone call bar background callBarMuteRipple: dialogsRippleBgActive; // active phone call bar mute and hangup button ripple effect callBarBgMuted: #8f8f8f | dialogsUnreadBgMuted; // phone call bar with muted mic background diff --git a/ui/effects/animation_value.cpp b/ui/effects/animation_value.cpp index 2018c7f..f56c851 100644 --- a/ui/effects/animation_value.cpp +++ b/ui/effects/animation_value.cpp @@ -13,7 +13,7 @@ namespace anim { namespace { -bool AnimationsDisabled = false; +rpl::variable AnimationsDisabled = false; } // namespace @@ -64,8 +64,12 @@ transition easeOutQuint = [](const float64 &delta, const float64 &dt) { return delta * (t2 * t2 * t + 1); }; +rpl::producer Disables() { + return AnimationsDisabled.value(); +}; + bool Disabled() { - return AnimationsDisabled; + return AnimationsDisabled.current(); } void SetDisabled(bool disabled) { diff --git a/ui/effects/animation_value.h b/ui/effects/animation_value.h index 0a7d226..faf308a 100644 --- a/ui/effects/animation_value.h +++ b/ui/effects/animation_value.h @@ -94,8 +94,12 @@ private: }; +TG_FORCE_INLINE float64 interpolateF(int a, int b, float64 b_ratio) { + return a + float64(b - a) * b_ratio; +} + TG_FORCE_INLINE int interpolate(int a, int b, float64 b_ratio) { - return qRound(a + float64(b - a) * b_ratio); + return std::round(interpolateF(a, b, b_ratio)); } #ifdef ARCH_CPU_32_BITS @@ -344,6 +348,7 @@ QPainterPath path(QPointF (&from)[N]) { return result; } +rpl::producer Disables(); bool Disabled(); void SetDisabled(bool disabled); @@ -354,4 +359,50 @@ void DrawStaticLoading( QPen pen, QBrush brush = Qt::NoBrush); +class continuous_value { +public: + continuous_value() = default; + continuous_value(float64 duration) : _duration(duration) { + } + void start(float64 to, float64 duration) { + _to = to; + _delta = (_to - _cur) / duration; + } + void start(float64 to) { + start(to, _duration); + } + void reset() { + _to = _cur = _delta = 0.; + } + + float64 current() const { + return _cur; + } + float64 to() const { + return _to; + } + float64 delta() const { + return _delta; + } + void update(crl::time dt, Fn &&callback = nullptr) { + if (_to != _cur) { + _cur += _delta * dt; + if ((_to != _cur) && ((_delta > 0) == (_cur > _to))) { + _cur = _to; + } + if (callback) { + callback(_cur); + } + } + } + +private: + float64 _duration = 0.; + float64 _to = 0.; + + float64 _cur = 0.; + float64 _delta = 0.; + }; + +} // namespace anim diff --git a/ui/effects/cross_line.cpp b/ui/effects/cross_line.cpp new file mode 100644 index 0000000..4c70455 --- /dev/null +++ b/ui/effects/cross_line.cpp @@ -0,0 +1,99 @@ +// 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/effects/cross_line.h" + +#include "ui/painter.h" + +namespace Ui { + +CrossLineAnimation::CrossLineAnimation( + const style::CrossLineAnimation &st, + bool reversed, + float angle) +: _st(st) +, _reversed(reversed) +, _transparentPen(Qt::transparent, st.stroke, Qt::SolidLine, Qt::RoundCap) +, _strokePen(st.fg, st.stroke, Qt::SolidLine, Qt::RoundCap) +, _line(st.startPosition, st.endPosition) { + _line.setAngle(angle); +} + +void CrossLineAnimation::paint( + Painter &p, + QPoint position, + float64 progress, + std::optional colorOverride) { + paint(p, position.x(), position.y(), progress, colorOverride); +} + +void CrossLineAnimation::paint( + Painter &p, + int left, + int top, + float64 progress, + std::optional colorOverride) { + if (progress == 0.) { + if (colorOverride) { + _st.icon.paint(p, left, top, _st.icon.width(), *colorOverride); + } else { + _st.icon.paint(p, left, top, _st.icon.width()); + } + } else if (progress == 1.) { + if (_completeCross.isNull()) { + fillFrame(progress, colorOverride); + _completeCross = _frame; + } + p.drawImage(left, top, _completeCross); + } else { + fillFrame(progress, colorOverride); + p.drawImage(left, top, _frame); + } +} + +void CrossLineAnimation::fillFrame( + float64 progress, + std::optional colorOverride) { + const auto ratio = style::DevicePixelRatio(); + if (_frame.isNull()) { + _frame = QImage( + _st.icon.size() * ratio, + QImage::Format_ARGB32_Premultiplied); + _frame.setDevicePixelRatio(ratio); + } + _frame.fill(Qt::transparent); + + auto topLine = _line; + topLine.setLength(topLine.length() * progress); + auto bottomLine = topLine.translated(0, _strokePen.widthF() + 1); + + Painter q(&_frame); + PainterHighQualityEnabler hq(q); + if (colorOverride) { + _st.icon.paint(q, 0, 0, _st.icon.width(), *colorOverride); + } else { + _st.icon.paint(q, 0, 0, _st.icon.width()); + } + + if (colorOverride) { + auto pen = _strokePen; + pen.setColor(*colorOverride); + q.setPen(pen); + } else { + q.setPen(_strokePen); + } + q.drawLine(_reversed ? topLine : bottomLine); + + q.setCompositionMode(QPainter::CompositionMode_Source); + q.setPen(_transparentPen); + q.drawLine(_reversed ? bottomLine : topLine); +} + +void CrossLineAnimation::invalidate() { + _completeCross = QImage(); +} + +} // namespace Ui diff --git a/ui/effects/cross_line.h b/ui/effects/cross_line.h new file mode 100644 index 0000000..d1336c7 --- /dev/null +++ b/ui/effects/cross_line.h @@ -0,0 +1,49 @@ +// This file is part of Desktop App Toolkit, +// a set of libraries for developing nice desktop applications. +// +// For license and copyright information please follow this link: +// https://github.com/desktop-app/legal/blob/master/LEGAL +// +#pragma once + +#include "styles/style_widgets.h" + +class Painter; + +namespace Ui { + +class CrossLineAnimation { +public: + CrossLineAnimation( + const style::CrossLineAnimation &st, + bool reversed = false, + float angle = 315); + + void paint( + Painter &p, + QPoint position, + float64 progress, + std::optional colorOverride = std::nullopt); + void paint( + Painter &p, + int left, + int top, + float64 progress, + std::optional colorOverride = std::nullopt); + + void invalidate(); + +private: + void fillFrame(float64 progress, std::optional colorOverride); + + const style::CrossLineAnimation &_st; + const bool _reversed; + const QPen _transparentPen; + const QPen _strokePen; + QLineF _line; + QImage _frame; + QImage _completeCross; + +}; + +} // namespace Ui diff --git a/ui/effects/gradient.h b/ui/effects/gradient.h new file mode 100644 index 0000000..f6988c5 --- /dev/null +++ b/ui/effects/gradient.h @@ -0,0 +1,244 @@ +// This file is part of Desktop App Toolkit, +// a set of libraries for developing nice desktop applications. +// +// For license and copyright information please follow this link: +// https://github.com/desktop-app/legal/blob/master/LEGAL +// +#pragma once + +#include "base/flat_map.h" +#include "ui/effects/animation_value.h" + +#include +#include + +namespace anim { + +struct gradient_colors { + explicit gradient_colors(QColor color) { + stops.push_back({ 0., color }); + stops.push_back({ 1., color }); + } + explicit gradient_colors(std::vector colors) { + if (colors.size() == 1) { + gradient_colors(colors.front()); + return; + } + const auto last = float(colors.size() - 1); + for (auto i = 0; i < colors.size(); i++) { + stops.push_back({ i / last, std::move(colors[i]) }); + } + } + explicit gradient_colors(QGradientStops colors) + : stops(std::move(colors)) { + } + + QGradientStops stops; +}; + +namespace details { + +template +class gradients { +public: + gradients(base::flat_map> colors) { + Expects(colors.size() > 0); + + for (const auto &[key, value] : colors) { + auto c = gradient_colors(std::move(value)); + _gradients.emplace(key, gradient_with_stops(std::move(c.stops))); + } + } + gradients(base::flat_map colors) { + Expects(colors.size() > 0); + + for (const auto &[key, c] : colors) { + _gradients.emplace(key, gradient_with_stops(std::move(c.stops))); + } + } + + QGradient gradient(T state1, T state2, float64 b_ratio) const { + if (b_ratio == 0.) { + return _gradients.find(state1)->second; + } else if (b_ratio == 1.) { + return _gradients.find(state2)->second; + } + + auto gradient = empty_gradient(); + const auto gradient1 = _gradients.find(state1); + const auto gradient2 = _gradients.find(state2); + + Assert(gradient1 != end(_gradients)); + Assert(gradient2 != end(_gradients)); + + const auto stopsFrom = gradient1->second.stops(); + const auto stopsTo = gradient2->second.stops(); + + if ((stopsFrom.size() == stopsTo.size()) + && ranges::equal( + stopsFrom, + stopsTo, + ranges::equal_to(), + &QGradientStop::first, + &QGradientStop::first)) { + + const auto size = stopsFrom.size(); + const auto &p = b_ratio; + for (auto i = 0; i < size; i++) { + auto c = color(stopsFrom[i].second, stopsTo[i].second, p); + gradient.setColorAt(stopsTo[i].first, std::move(c)); + } + return gradient; + } + + const auto invert = (stopsFrom.size() > stopsTo.size()); + if (invert) { + b_ratio = 1. - b_ratio; + } + + const auto &stops1 = invert ? stopsTo : stopsFrom; + const auto &stops2 = invert ? stopsFrom : stopsTo; + + const auto size1 = stops1.size(); + const auto size2 = stops2.size(); + + for (auto i = 0; i < size1; i++) { + const auto point1 = stops1[i].first; + const auto previousPoint1 = i ? stops1[i - 1].first : -1.; + + for (auto n = 0; n < size2; n++) { + const auto point2 = stops2[n].first; + + if ((point2 <= previousPoint1) || (point2 > point1)) { + continue; + } + const auto color2 = stops2[n].second; + QColor result; + if (point2 < point1) { + const auto pointRatio2 = (point2 - previousPoint1) + / (point1 - previousPoint1); + const auto color1 = color( + stops1[i - 1].second, + stops1[i].second, + pointRatio2); + + result = color(color1, color2, b_ratio); + } else { + // point2 == point1 + result = color(stops1[i].second, color2, b_ratio); + } + gradient.setColorAt(point2, std::move(result)); + } + } + return gradient; + } + +protected: + void cache_gradients() { + auto copy = std::move(_gradients); + for (const auto &[key, value] : copy) { + _gradients.emplace(key, gradient_with_stops(value.stops())); + } + } + +private: + QGradient empty_gradient() const { + return static_cast(this)->empty_gradient(); + } + QGradient gradient_with_stops(QGradientStops stops) const { + auto gradient = empty_gradient(); + gradient.setStops(std::move(stops)); + return gradient; + } + + base::flat_map _gradients; + +}; + +} // namespace details + +template +class linear_gradients final + : public details::gradients> { + using parent = details::gradients>; + +public: + linear_gradients( + base::flat_map> colors, + QPointF point1, + QPointF point2) + : parent(std::move(colors)) { + set_points(point1, point2); + } + linear_gradients( + base::flat_map colors, + QPointF point1, + QPointF point2) + : parent(std::move(colors)) { + set_points(point1, point2); + } + + void set_points(QPointF point1, QPointF point2) { + if (_point1 == point1 && _point2 == point2) { + return; + } + _point1 = point1; + _point2 = point2; + parent::cache_gradients(); + } + +private: + friend class details::gradients>; + + QGradient empty_gradient() const { + return QLinearGradient(_point1, _point2); + } + + QPointF _point1; + QPointF _point2; + +}; + +template +class radial_gradients final + : public details::gradients> { + using parent = details::gradients>; + +public: + radial_gradients( + base::flat_map> colors, + QPointF center, + float radius) + : parent(std::move(colors)) { + set_points(center, radius); + } + radial_gradients( + base::flat_map colors, + QPointF center, + float radius) + : parent(std::move(colors)) { + set_points(center, radius); + } + + void set_points(QPointF center, float radius) { + if (_center == center && _radius == radius) { + return; + } + _center = center; + _radius = radius; + parent::cache_gradients(); + } + +private: + friend class details::gradients>; + + QGradient empty_gradient() const { + return QRadialGradient(_center, _radius); + } + + QPointF _center; + float _radius = 0.; + +}; + +} // namespace anim diff --git a/ui/effects/numbers_animation.cpp b/ui/effects/numbers_animation.cpp index b490374..eb0f5d4 100644 --- a/ui/effects/numbers_animation.cpp +++ b/ui/effects/numbers_animation.cpp @@ -19,7 +19,7 @@ NumbersAnimation::NumbersAnimation( : _font(font) , _animationCallback(std::move(animationCallback)) { for (auto ch = '0'; ch != '9'; ++ch) { - accumulate_max(_digitWidth, _font->m.width(ch)); + accumulate_max(_digitWidth, _font->m.horizontalAdvance(ch)); } } @@ -67,7 +67,7 @@ void NumbersAnimation::realSetText(QString text, int value) { digit.from = digit.to; digit.fromWidth = digit.toWidth; digit.to = (newSize + i < size) ? QChar(0) : text[newSize + i - size]; - digit.toWidth = digit.to.unicode() ? _font->m.width(digit.to) : 0; + digit.toWidth = digit.to.unicode() ? _font->m.horizontalAdvance(digit.to) : 0; if (digit.from != digit.to) { animating = true; } diff --git a/ui/layers/box_content.cpp b/ui/layers/box_content.cpp index bb6d3e2..cd6a406 100644 --- a/ui/layers/box_content.cpp +++ b/ui/layers/box_content.cpp @@ -29,7 +29,7 @@ QPointer BoxContent::addButton( return addButton( std::move(text), std::move(clickCallback), - st::defaultBoxButton); + getDelegate()->style().button); } QPointer BoxContent::addLeftButton( @@ -38,7 +38,7 @@ QPointer BoxContent::addLeftButton( return getDelegate()->addLeftButton( std::move(text), std::move(clickCallback), - st::defaultBoxButton); + getDelegate()->style().button); } void BoxContent::setInner(object_ptr inner) { @@ -245,8 +245,9 @@ void BoxContent::paintEvent(QPaintEvent *e) { Painter p(this); if (testAttribute(Qt::WA_OpaquePaintEvent)) { - for (auto rect : e->region().rects()) { - p.fillRect(rect, st::boxBg); + const auto &color = getDelegate()->style().bg; + for (const auto rect : e->region()) { + p.fillRect(rect, color); } } } diff --git a/ui/layers/box_content.h b/ui/layers/box_content.h index f293fe4..8c738d1 100644 --- a/ui/layers/box_content.h +++ b/ui/layers/box_content.h @@ -45,6 +45,7 @@ class BoxContentDelegate { public: virtual void setLayerType(bool layerType) = 0; virtual void setStyle(const style::Box &st) = 0; + virtual const style::Box &style() = 0; virtual void setTitle(rpl::producer title) = 0; virtual void setAdditionalTitle(rpl::producer additional) = 0; virtual void setCloseByOutsideClick(bool close) = 0; @@ -125,6 +126,8 @@ public: void scrollToWidget(not_null widget); + virtual void showFinished() { + } void clearButtons() { getDelegate()->clearButtons(); } diff --git a/ui/layers/box_layer_widget.cpp b/ui/layers/box_layer_widget.cpp index 92adb6d..a7075ec 100644 --- a/ui/layers/box_layer_widget.cpp +++ b/ui/layers/box_layer_widget.cpp @@ -42,7 +42,7 @@ BoxLayerWidget::BoxLayerWidget( : LayerWidget(layer) , _layer(layer) , _content(std::move(content)) -, _roundRect(ImageRoundRadius::Small, st::boxBg) { +, _roundRect(ImageRoundRadius::Small, st().bg) { _content->setParent(this); _content->setDelegate(this); @@ -74,12 +74,21 @@ const style::Box &BoxLayerWidget::st() const { return _st ? *_st : _layerType - ? st::layerBox - : st::defaultBox; + ? (_layer->boxStyleOverrideLayer() + ? *_layer->boxStyleOverrideLayer() + : st::layerBox) + : (_layer->boxStyleOverride() + ? *_layer->boxStyleOverride() + : st::defaultBox); } void BoxLayerWidget::setStyle(const style::Box &st) { _st = &st; + _roundRect.setColor(st.bg); +} + +const style::Box &BoxLayerWidget::style() { + return st(); } int BoxLayerWidget::buttonsHeight() const { @@ -117,8 +126,8 @@ void BoxLayerWidget::paintEvent(QPaintEvent *e) { } auto other = e->region().intersected(QRect(0, st::boxRadius, width(), height() - 2 * st::boxRadius)); if (!other.isEmpty()) { - for (auto rect : other.rects()) { - p.fillRect(rect, st::boxBg); + for (const auto rect : other) { + p.fillRect(rect, st().bg); } } if (!_additionalTitle.current().isEmpty() @@ -137,21 +146,29 @@ void BoxLayerWidget::paintEvent(QPaintEvent *e) { void BoxLayerWidget::paintAdditionalTitle(Painter &p) { p.setFont(st::boxTitleAdditionalFont); - p.setPen(st::boxTitleAdditionalFg); - p.drawTextLeft(_titleLeft + (_title ? _title->width() : 0) + st::boxTitleAdditionalSkip, _titleTop + st::boxTitleFont->ascent - st::boxTitleAdditionalFont->ascent, width(), _additionalTitle.current()); + p.setPen(st().titleAdditionalFg); + p.drawTextLeft( + _titleLeft + (_title ? _title->width() : 0) + st::boxTitleAdditionalSkip, + _titleTop + st::boxTitleFont->ascent - st::boxTitleAdditionalFont->ascent, + width(), + _additionalTitle.current()); } void BoxLayerWidget::parentResized() { auto newHeight = countRealHeight(); auto parentSize = parentWidget()->size(); - setGeometry((parentSize.width() - width()) / 2, (parentSize.height() - newHeight) / 2, width(), newHeight); + setGeometry( + (parentSize.width() - width()) / 2, + (parentSize.height() - newHeight) / 2, + width(), + newHeight); update(); } void BoxLayerWidget::setTitle(rpl::producer title) { const auto wasTitle = hasTitle(); if (title) { - _title.create(this, rpl::duplicate(title), st::boxTitle); + _title.create(this, rpl::duplicate(title), st().title); _title->show(); std::move( title @@ -323,9 +340,10 @@ void BoxLayerWidget::setDimensions(int newWidth, int maxHeight, bool forceCenter resize(newWidth, countRealHeight()); auto newGeometry = geometry(); auto parentHeight = parentWidget()->height(); - if (newGeometry.top() + newGeometry.height() + st::boxVerticalMargin > parentHeight + const auto bottomMargin = st().margin.bottom(); + if (newGeometry.top() + newGeometry.height() + bottomMargin > parentHeight || forceCenterPosition) { - const auto top1 = parentHeight - int(st::boxVerticalMargin) - newGeometry.height(); + const auto top1 = parentHeight - bottomMargin - newGeometry.height(); const auto top2 = (parentHeight - newGeometry.height()) / 2; const auto newTop = forceCenterPosition ? std::min(top1, top2) @@ -343,7 +361,10 @@ void BoxLayerWidget::setDimensions(int newWidth, int maxHeight, bool forceCenter } int BoxLayerWidget::countRealHeight() const { - return qMin(_fullHeight, parentWidget()->height() - 2 * st::boxVerticalMargin); + const auto &margin = st().margin; + return std::min( + _fullHeight, + parentWidget()->height() - margin.top() - margin.bottom()); } int BoxLayerWidget::countFullHeight() const { diff --git a/ui/layers/box_layer_widget.h b/ui/layers/box_layer_widget.h index 0313b66..a26c5ad 100644 --- a/ui/layers/box_layer_widget.h +++ b/ui/layers/box_layer_widget.h @@ -45,6 +45,7 @@ public: void setLayerType(bool layerType) override; void setStyle(const style::Box &st) override; + const style::Box &style() override; void setTitle(rpl::producer title) override; void setAdditionalTitle(rpl::producer additional) override; void showBox( @@ -52,6 +53,10 @@ public: LayerOptions options, anim::type animated) override; + void showFinished() override { + _content->showFinished(); + } + void clearButtons() override; QPointer addButton( rpl::producer text, diff --git a/ui/layers/generic_box.h b/ui/layers/generic_box.h index 058f05c..e0bceca 100644 --- a/ui/layers/generic_box.h +++ b/ui/layers/generic_box.h @@ -38,6 +38,9 @@ public: void setFocusCallback(Fn callback) { _focus = callback; } + void setShowFinishedCallback(Fn callback) { + _showFinished = callback; + } int rowsCount() const { return _content->count(); @@ -76,6 +79,11 @@ public: BoxContent::setInnerFocus(); } } + void showFinished() override { + if (_showFinished) { + _showFinished(); + } + } [[nodiscard]] not_null verticalLayout(); @@ -111,6 +119,7 @@ private: FnMut)> _init; Fn _focus; + Fn _showFinished; object_ptr _content; int _width = 0; diff --git a/ui/layers/layer_manager.cpp b/ui/layers/layer_manager.cpp index 6ebbaa0..b5ca389 100644 --- a/ui/layers/layer_manager.cpp +++ b/ui/layers/layer_manager.cpp @@ -11,6 +11,16 @@ namespace Ui { LayerManager::LayerManager(not_null widget) : _widget(widget) { } +void LayerManager::setStyleOverrides( + const style::Box *boxSt, + const style::Box *layerSt) { + _boxSt = boxSt; + _layerSt = layerSt; + if (_layer) { + _layer->setStyleOverrides(_boxSt, _layerSt); + } +} + void LayerManager::setHideByBackgroundClick(bool hide) { _hideByBackgroundClick = hide; if (_layer) { @@ -55,6 +65,7 @@ void LayerManager::ensureLayerCreated() { } _layer.emplace(_widget); _layer->setHideByBackgroundClick(_hideByBackgroundClick); + _layer->setStyleOverrides(_boxSt, _layerSt); _layer->hideFinishEvents( ) | rpl::filter([=] { diff --git a/ui/layers/layer_manager.h b/ui/layers/layer_manager.h index 1f0d31e..4f53169 100644 --- a/ui/layers/layer_manager.h +++ b/ui/layers/layer_manager.h @@ -10,6 +10,10 @@ #include +namespace style { +struct Box; +} // namespace style + namespace Ui { class BoxContent; @@ -19,6 +23,10 @@ class LayerManager final { public: explicit LayerManager(not_null widget); + void setStyleOverrides( + const style::Box *boxSt, + const style::Box *layerSt); + void setHideByBackgroundClick(bool hide); void showBox( object_ptr box, @@ -34,6 +42,9 @@ private: const not_null _widget; base::unique_qptr _layer; + + const style::Box *_boxSt = nullptr; + const style::Box *_layerSt = nullptr; bool _hideByBackgroundClick = false; }; diff --git a/ui/layers/layer_widget.cpp b/ui/layers/layer_widget.cpp index 74a32e1..c0ffe93 100644 --- a/ui/layers/layer_widget.cpp +++ b/ui/layers/layer_widget.cpp @@ -280,8 +280,8 @@ void LayerStackWidget::BackgroundWidget::paintEvent(QPaintEvent *e) { // (alpha_final - alpha_current) / (1 - alpha_current) so we won't get glitches // in the transparent special layer cache corners after filling special layer // rect above its cache with alpha_current opacity. - auto region = QRegion(bg) - specialLayerBox; - for (auto rect : region.rects()) { + const auto region = QRegion(bg) - specialLayerBox; + for (const auto rect : region) { p.fillRect(rect, st::layerBg); } p.setOpacity((bgOpacity - overSpecialOpacity) / (1. - (overSpecialOpacity * st::layerBg->c.alphaF()))); @@ -458,6 +458,13 @@ bool LayerStackWidget::layerShown() const { return _specialLayer || currentLayer() || _mainMenu; } +void LayerStackWidget::setStyleOverrides( + const style::Box *boxSt, + const style::Box *layerSt) { + _boxSt = boxSt; + _layerSt = layerSt; +} + void LayerStackWidget::setCacheImages() { auto bodyCache = QPixmap(), mainMenuCache = QPixmap(); auto specialLayerCache = QPixmap(); diff --git a/ui/layers/layer_widget.h b/ui/layers/layer_widget.h index 94344a0..f673678 100644 --- a/ui/layers/layer_widget.h +++ b/ui/layers/layer_widget.h @@ -16,6 +16,10 @@ class SectionMemento; struct SectionShow; } // namespace Window +namespace style { +struct Box; +} // namespace style + namespace Ui { class BoxContent; @@ -93,6 +97,16 @@ public: void finishAnimating(); rpl::producer<> hideFinishEvents() const; + void setStyleOverrides( + const style::Box *boxSt, + const style::Box *layerSt); + [[nodiscard]] const style::Box *boxStyleOverrideLayer() const { + return _layerSt; + } + [[nodiscard]] const style::Box *boxStyleOverride() const { + return _boxSt; + } + void showBox( object_ptr box, LayerOptions options, @@ -201,6 +215,9 @@ private: class BackgroundWidget; object_ptr _background; + + const style::Box *_boxSt = nullptr; + const style::Box *_layerSt = nullptr; bool _hideByBackgroundClick = true; rpl::event_stream<> _hideFinishStream; diff --git a/ui/layers/layers.style b/ui/layers/layers.style index ddb32ea..10046f9 100644 --- a/ui/layers/layers.style +++ b/ui/layers/layers.style @@ -24,6 +24,11 @@ ServiceCheck { Box { buttonPadding: margins; buttonHeight: pixels; + button: RoundButton; + margin: margins; + title: FlatLabel; + bg: color; + titleAdditionalFg: color; } boxDuration: 200; @@ -36,12 +41,6 @@ defaultBoxButton: RoundButton(defaultLightButton) { font: boxButtonFont; } -boxTextStyle: TextStyle(defaultTextStyle) { - font: font(boxFontSize); - linkFont: font(boxFontSize); - linkFontOver: font(boxFontSize underline); -} - boxLabelStyle: TextStyle(boxTextStyle) { lineHeight: 22px; } @@ -118,7 +117,6 @@ boxOptionListPadding: margins(0px, 0px, 0px, 0px); boxOptionListSkip: 20px; boxOptionInputSkip: 6px; -boxVerticalMargin: 10px; boxWidth: 320px; boxWideWidth: 364px; boxPadding: margins(22px, 30px, 22px, 8px); @@ -129,6 +127,11 @@ boxMediumSkip: 20px; defaultBox: Box { buttonPadding: margins(8px, 12px, 13px, 12px); buttonHeight: 36px; + button: defaultBoxButton; + margin: margins(0px, 10px, 0px, 10px); + bg: boxBg; + title: boxTitle; + titleAdditionalFg: boxTitleAdditionalFg; } layerBox: Box(defaultBox) { buttonPadding: margins(8px, 8px, 8px, 8px); diff --git a/ui/paint/blob.cpp b/ui/paint/blob.cpp new file mode 100644 index 0000000..b7f783e --- /dev/null +++ b/ui/paint/blob.cpp @@ -0,0 +1,243 @@ +// 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/paint/blob.h" + +#include "base/openssl_help.h" +#include "ui/painter.h" + +#include +#include + +namespace Ui::Paint { + +namespace { + +constexpr auto kMaxSpeed = 8.2; +constexpr auto kMinSpeed = 0.8; + +constexpr auto kMinSegmentSpeed = 0.017; +constexpr auto kSegmentSpeedDiff = 0.003; + +float64 RandomAdditional() { + return (openssl::RandomValue() % 100 / 100.); +} + +} // namespace + +Blob::Blob(int n, float minSpeed, float maxSpeed) +: _segmentsCount(n) +, _minSpeed(minSpeed ? minSpeed : kMinSpeed) +, _maxSpeed(maxSpeed ? maxSpeed : kMaxSpeed) +, _pen(Qt::NoBrush, 0, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin) { +} + +void Blob::generateBlob() { + for (auto i = 0; i < _segmentsCount; i++) { + generateSingleValues(i); + // Fill nexts. + generateTwoValues(i); + // Fill currents. + generateTwoValues(i); + } +} + +void Blob::generateSingleValues(int i) { + auto &segment = segmentAt(i); + segment.progress = 0.; + segment.speed = kMinSegmentSpeed + + kSegmentSpeedDiff * std::abs(RandomAdditional()); +} + +void Blob::update(float level, float speedScale) { + for (auto i = 0; i < _segmentsCount; i++) { + auto &segment = segmentAt(i); + segment.progress += (segment.speed * _minSpeed) + + level * segment.speed * _maxSpeed * speedScale; + if (segment.progress >= 1) { + generateSingleValues(i); + generateTwoValues(i); + } + } +} + +void Blob::setRadiuses(Radiuses values) { + _radiuses = values; +} + +Blob::Radiuses Blob::radiuses() const { + return _radiuses; +} + +RadialBlob::RadialBlob(int n, float minScale, float minSpeed, float maxSpeed) +: Blob(n, minSpeed, maxSpeed) +, _segmentLength((4.0 / 3.0) * std::tan(M_PI / (2 * n))) +, _minScale(minScale) +, _segmentAngle(360. / n) +, _angleDiff(_segmentAngle * 0.05) +, _segments(n) { +} + +void RadialBlob::paint(Painter &p, const QBrush &brush, float outerScale) { + auto path = QPainterPath(); + auto m = QMatrix(); + + p.save(); + const auto scale = (_minScale + (1. - _minScale) * _scale) * outerScale; + if (scale == 0.) { + p.restore(); + return; + } else if (scale != 1.) { + p.scale(scale, scale); + } + + for (auto i = 0; i < _segmentsCount; i++) { + const auto &segment = _segments[i]; + + const auto nextIndex = i + 1 < _segmentsCount ? (i + 1) : 0; + const auto nextSegment = _segments[nextIndex]; + + const auto progress = segment.progress; + const auto progressNext = nextSegment.progress; + + const auto r1 = segment.radius.current * (1. - progress) + + segment.radius.next * progress; + const auto r2 = nextSegment.radius.current * (1. - progressNext) + + nextSegment.radius.next * progressNext; + const auto angle1 = segment.angle.current * (1. - progress) + + segment.angle.next * progress; + const auto angle2 = nextSegment.angle.current * (1. - progressNext) + + nextSegment.angle.next * progressNext; + + const auto l = _segmentLength * (std::min(r1, r2) + + (std::max(r1, r2) - std::min(r1, r2)) / 2.); + + m.reset(); + m.rotate(angle1); + + const auto pointStart1 = m.map(QPointF(0, -r1)); + const auto pointStart2 = m.map(QPointF(l, -r1)); + + m.reset(); + m.rotate(angle2); + const auto pointEnd1 = m.map(QPointF(0, -r2)); + const auto pointEnd2 = m.map(QPointF(-l, -r2)); + + if (i == 0) { + path.moveTo(pointStart1); + } + + path.cubicTo(pointStart2, pointEnd2, pointEnd1); + } + + p.setBrush(Qt::NoBrush); + + p.setPen(_pen); + p.fillPath(path, brush); + p.drawPath(path); + + p.restore(); +} + +void RadialBlob::generateTwoValues(int i) { + auto &radius = _segments[i].radius; + auto &angle = _segments[i].angle; + + const auto radDiff = _radiuses.max - _radiuses.min; + + angle.setNext(_segmentAngle * i + RandomAdditional() * _angleDiff); + radius.setNext(_radiuses.min + std::abs(RandomAdditional()) * radDiff); +} + +void RadialBlob::update(float level, float speedScale) { + _scale = level; + Blob::update(level, speedScale); +} + +Blob::Segment &RadialBlob::segmentAt(int i) { + return _segments[i]; +}; + +LinearBlob::LinearBlob( + int n, + Direction direction, + float minSpeed, + float maxSpeed) +: Blob(n + 1) +, _topDown(direction == Direction::TopDown ? 1 : -1) +, _segments(_segmentsCount) { +} + +void LinearBlob::paint(Painter &p, const QBrush &brush, int width) { + if (!width) { + return; + } + + auto path = QPainterPath(); + + const auto left = 0; + const auto right = width; + + path.moveTo(right, 0); + path.lineTo(left, 0); + + const auto n = float(_segmentsCount - 1); + + p.save(); + + for (auto i = 0; i < _segmentsCount; i++) { + const auto &segment = _segments[i]; + + if (!i) { + const auto &progress = segment.progress; + const auto r1 = segment.radius.current * (1. - progress) + + segment.radius.next * progress; + const auto y = r1 * _topDown; + path.lineTo(left, y); + } else { + const auto &prevSegment = _segments[i - 1]; + const auto &progress = prevSegment.progress; + const auto r1 = prevSegment.radius.current * (1. - progress) + + prevSegment.radius.next * progress; + + const auto &progressNext = segment.progress; + const auto r2 = segment.radius.current * (1. - progressNext) + + segment.radius.next * progressNext; + + const auto x1 = (right - left) / n * (i - 1); + const auto x2 = (right - left) / n * i; + const auto cx = x1 + (x2 - x1) / 2; + + const auto y1 = r1 * _topDown; + const auto y2 = r2 * _topDown; + path.cubicTo( + QPointF(cx, y1), + QPointF(cx, y2), + QPointF(x2, y2) + ); + } + } + path.lineTo(right, 0); + + p.setBrush(Qt::NoBrush); + p.setPen(_pen); + p.fillPath(path, brush); + p.drawPath(path); + + p.restore(); +} + +void LinearBlob::generateTwoValues(int i) { + auto &radius = _segments[i].radius; + const auto radDiff = _radiuses.max - _radiuses.min; + radius.setNext(_radiuses.min + std::abs(RandomAdditional()) * radDiff); +} + +Blob::Segment &LinearBlob::segmentAt(int i) { + return _segments[i]; +}; + +} // namespace Ui::Paint diff --git a/ui/paint/blob.h b/ui/paint/blob.h new file mode 100644 index 0000000..e48e621 --- /dev/null +++ b/ui/paint/blob.h @@ -0,0 +1,113 @@ +// 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 + +class Painter; + +namespace Ui::Paint { + +class Blob { +public: + struct Radiuses { + float min = 0.; + float max = 0.; + }; + + Blob(int n, float minSpeed = 0, float maxSpeed = 0); + virtual ~Blob() = default; + + void update(float level, float speedScale); + void generateBlob(); + + void setRadiuses(Radiuses values); + [[nodiscard]] Radiuses radiuses() const; + +protected: + struct TwoValues { + float current = 0.; + float next = 0.; + void setNext(float v) { + current = next; + next = v; + } + }; + + struct Segment { + float progress = 0.; + float speed = 0.; + }; + + void generateSingleValues(int i); + virtual void generateTwoValues(int i) = 0; + virtual Segment &segmentAt(int i) = 0; + + const int _segmentsCount; + const float _minSpeed; + const float _maxSpeed; + const QPen _pen; + + Radiuses _radiuses; + +}; + +class RadialBlob final : public Blob { +public: + RadialBlob(int n, float minScale, float minSpeed = 0, float maxSpeed = 0); + + void paint(Painter &p, const QBrush &brush, float outerScale = 1.); + void update(float level, float speedScale); + +private: + struct Segment : Blob::Segment { + Blob::TwoValues radius; + Blob::TwoValues angle; + }; + + void generateTwoValues(int i) override; + Blob::Segment &segmentAt(int i) override; + + const float64 _segmentLength; + const float _minScale; + const float _segmentAngle; + const float _angleDiff; + + std::vector _segments; + + float64 _scale = 0; + +}; + +class LinearBlob final : public Blob { +public: + enum class Direction { + TopDown, + BottomUp, + }; + + LinearBlob( + int n, + Direction direction = Direction::TopDown, + float minSpeed = 0, + float maxSpeed = 0); + + void paint(Painter &p, const QBrush &brush, int width); + +private: + struct Segment : Blob::Segment { + Blob::TwoValues radius; + }; + + void generateTwoValues(int i) override; + Blob::Segment &segmentAt(int i) override; + + const int _topDown; + + std::vector _segments; + +}; + +} // namespace Ui::Paint diff --git a/ui/paint/blobs.cpp b/ui/paint/blobs.cpp new file mode 100644 index 0000000..6bce7f6 --- /dev/null +++ b/ui/paint/blobs.cpp @@ -0,0 +1,99 @@ +// 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/paint/blobs.h" + +#include "ui/painter.h" + +namespace Ui::Paint { + +Blobs::Blobs( + std::vector blobDatas, + float levelDuration, + float maxLevel) +: _maxLevel(maxLevel) +, _blobDatas(std::move(blobDatas)) +, _levelValue(levelDuration) { + init(); +} + +void Blobs::init() { + for (const auto &data : _blobDatas) { + auto blob = Paint::RadialBlob( + data.segmentsCount, + data.minScale, + data.minSpeed, + data.maxSpeed); + blob.setRadiuses({ data.minRadius, data.maxRadius }); + blob.generateBlob(); + _blobs.push_back(std::move(blob)); + } +} + +float Blobs::maxRadius() const { + const auto maxOfRadiuses = [](const BlobData data) { + return std::max(data.maxRadius, data.minRadius); + }; + const auto max = *ranges::max_element( + _blobDatas, + std::less<>(), + maxOfRadiuses); + return maxOfRadiuses(max); +} + +int Blobs::size() const { + return _blobs.size(); +} + +void Blobs::setRadiusesAt( + rpl::producer &&radiuses, + int index) { + Expects(index >= 0 && index < size()); + std::move( + radiuses + ) | rpl::start_with_next([=](Blob::Radiuses r) { + _blobs[index].setRadiuses(std::move(r)); + }, _lifetime); +} + +Blob::Radiuses Blobs::radiusesAt(int index) { + Expects(index >= 0 && index < size()); + return _blobs[index].radiuses(); +} + +void Blobs::setLevel(float value) { + const auto to = std::min(_maxLevel, value) / _maxLevel; + _levelValue.start(to); +} + +void Blobs::resetLevel() { + _levelValue.reset(); +} + +void Blobs::paint(Painter &p, const QBrush &brush, float outerScale) { + const auto opacity = p.opacity(); + for (auto i = 0; i < _blobs.size(); i++) { + _blobs[i].update(_levelValue.current(), _blobDatas[i].speedScale); + const auto alpha = _blobDatas[i].alpha; + if (alpha != 1.) { + p.setOpacity(opacity * alpha); + } + _blobs[i].paint(p, brush, outerScale); + if (alpha != 1.) { + p.setOpacity(opacity); + } + } +} + +void Blobs::updateLevel(crl::time dt) { + _levelValue.update((dt > 20) ? 17 : dt); +} + +float64 Blobs::currentLevel() const { + return _levelValue.current(); +} + +} // namespace Ui::Paint diff --git a/ui/paint/blobs.h b/ui/paint/blobs.h new file mode 100644 index 0000000..c93f419 --- /dev/null +++ b/ui/paint/blobs.h @@ -0,0 +1,64 @@ +// This file is part of Desktop App Toolkit, +// a set of libraries for developing nice desktop applications. +// +// For license and copyright information please follow this link: +// https://github.com/desktop-app/legal/blob/master/LEGAL +// +#pragma once + +#include "ui/effects/animation_value.h" +#include "ui/paint/blob.h" + +class Painter; + +namespace Ui::Paint { + +class Blobs final { +public: + struct BlobData { + int segmentsCount = 0; + float minScale = 0; + float minRadius = 0; + float maxRadius = 0; + float speedScale = 0; + float alpha = 0; + float minSpeed = 0; + float maxSpeed = 0; + }; + + Blobs( + std::vector blobDatas, + float levelDuration, + float maxLevel); + + void setRadiusesAt( + rpl::producer &&radiuses, + int index); + Blob::Radiuses radiusesAt(int index); + + void setLevel(float value); + void resetLevel(); + void paint(Painter &p, const QBrush &brush, float outerScale = 1.); + void updateLevel(crl::time dt); + + [[nodiscard]] float maxRadius() const; + [[nodiscard]] int size() const; + [[nodiscard]] float64 currentLevel() const; + + static constexpr auto kHideBlobsDuration = 2000; + +private: + void init(); + + const float _maxLevel; + + std::vector _blobDatas; + std::vector _blobs; + + anim::continuous_value _levelValue; + + rpl::lifetime _lifetime; + +}; + +} // namespace Ui::Paint diff --git a/ui/paint/blobs_linear.cpp b/ui/paint/blobs_linear.cpp new file mode 100644 index 0000000..37d92ec --- /dev/null +++ b/ui/paint/blobs_linear.cpp @@ -0,0 +1,107 @@ +// 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/paint/blobs_linear.h" + +#include "ui/painter.h" + +namespace Ui::Paint { + +LinearBlobs::LinearBlobs( + std::vector blobDatas, + float levelDuration, + float maxLevel, + LinearBlob::Direction direction) +: _maxLevel(maxLevel) +, _direction(direction) +, _blobDatas(std::move(blobDatas)) +, _levelValue(levelDuration) { + init(); +} + +void LinearBlobs::init() { + for (const auto &data : _blobDatas) { + auto blob = Paint::LinearBlob( + data.segmentsCount, + _direction, + data.minSpeed, + data.maxSpeed); + blob.setRadiuses({ data.minRadius, data.idleRadius }); + blob.generateBlob(); + _blobs.push_back(std::move(blob)); + } +} + +float LinearBlobs::maxRadius() const { + const auto maxOfRadiuses = [](const BlobData &d) { + return std::max(d.idleRadius, std::max(d.maxRadius, d.minRadius)); + }; + const auto max = *ranges::max_element( + _blobDatas, + std::less<>(), + maxOfRadiuses); + return maxOfRadiuses(max); +} + +int LinearBlobs::size() const { + return _blobs.size(); +} + +void LinearBlobs::setRadiusesAt( + rpl::producer &&radiuses, + int index) { + Expects(index >= 0 && index < size()); + std::move( + radiuses + ) | rpl::start_with_next([=](Blob::Radiuses r) { + _blobs[index].setRadiuses(std::move(r)); + }, _lifetime); +} + +Blob::Radiuses LinearBlobs::radiusesAt(int index) { + Expects(index >= 0 && index < size()); + return _blobs[index].radiuses(); +} + +void LinearBlobs::setLevel(float value) { + const auto to = std::min(_maxLevel, value) / _maxLevel; + _levelValue.start(to); +} + +void LinearBlobs::paint(Painter &p, const QBrush &brush, int width) { + PainterHighQualityEnabler hq(p); + const auto opacity = p.opacity(); + for (auto i = 0; i < _blobs.size(); i++) { + _blobs[i].update(_levelValue.current(), _blobDatas[i].speedScale); + const auto alpha = _blobDatas[i].alpha; + if (alpha != 1.) { + p.setOpacity(opacity * alpha); + } + _blobs[i].paint(p, brush, width); + if (alpha != 1.) { + p.setOpacity(opacity); + } + } +} + +void LinearBlobs::updateLevel(crl::time dt) { + const auto d = (dt > 20) ? 17 : dt; + _levelValue.update(d); + + const auto level = (float)currentLevel(); + for (auto i = 0; i < _blobs.size(); i++) { + const auto &data = _blobDatas[i]; + _blobs[i].setRadiuses({ + data.minRadius, + data.idleRadius + (data.maxRadius - data.idleRadius) * level }); + } +} + +float64 LinearBlobs::currentLevel() const { + return _levelValue.current(); +} + +} // namespace Ui::Paint diff --git a/ui/paint/blobs_linear.h b/ui/paint/blobs_linear.h new file mode 100644 index 0000000..e175870 --- /dev/null +++ b/ui/paint/blobs_linear.h @@ -0,0 +1,65 @@ +// This file is part of Desktop App Toolkit, +// a set of libraries for developing nice desktop applications. +// +// For license and copyright information please follow this link: +// https://github.com/desktop-app/legal/blob/master/LEGAL +// +#pragma once + +#include "ui/effects/animation_value.h" +#include "ui/paint/blob.h" + +class Painter; + +namespace Ui::Paint { + +class LinearBlobs final { +public: + struct BlobData { + int segmentsCount = 0; + float minRadius = 0; + float maxRadius = 0; + float idleRadius = 0; + float speedScale = 0; + float alpha = 0; + float minSpeed = 0; + float maxSpeed = 0; + }; + + LinearBlobs( + std::vector blobDatas, + float levelDuration, + float maxLevel, + LinearBlob::Direction direction); + + void setRadiusesAt( + rpl::producer &&radiuses, + int index); + Blob::Radiuses radiusesAt(int index); + + void setLevel(float value); + void paint(Painter &p, const QBrush &brush, int width); + void updateLevel(crl::time dt); + + [[nodiscard]] float maxRadius() const; + [[nodiscard]] int size() const; + [[nodiscard]] float64 currentLevel() const; + + static constexpr auto kHideBlobsDuration = 2000; + +private: + void init(); + + const float _maxLevel; + const LinearBlob::Direction _direction; + + std::vector _blobDatas; + std::vector _blobs; + + anim::continuous_value _levelValue; + + rpl::lifetime _lifetime; + +}; + +} // namespace Ui::Paint diff --git a/ui/painter.h b/ui/painter.h index f497058..1cbc6e2 100644 --- a/ui/painter.h +++ b/ui/painter.h @@ -19,13 +19,13 @@ 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.width(text); + if (style::RightToLeft() && textWidth < 0) textWidth = m.horizontalAdvance(text); 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.width(text); + if (!style::RightToLeft() && textWidth < 0) textWidth = m.horizontalAdvance(text); drawText(style::RightToLeft() ? x : (outerw - x - textWidth), y + ascent, text); } void drawPixmapLeft(int x, int y, int outerw, const QPixmap &pix, const QRect &from) { @@ -91,8 +91,7 @@ public: static constexpr QPainter::RenderHint Hints[] = { QPainter::Antialiasing, QPainter::SmoothPixmapTransform, - QPainter::TextAntialiasing, - QPainter::HighQualityAntialiasing + QPainter::TextAntialiasing }; const auto hints = _painter.renderHints(); @@ -119,6 +118,6 @@ public: private: QPainter &_painter; - QPainter::RenderHints _hints = 0; + QPainter::RenderHints _hints; }; diff --git a/ui/platform/linux/ui_utility_linux.cpp b/ui/platform/linux/ui_utility_linux.cpp index bab2e97..01566c5 100644 --- a/ui/platform/linux/ui_utility_linux.cpp +++ b/ui/platform/linux/ui_utility_linux.cpp @@ -6,13 +6,14 @@ // #include "ui/platform/linux/ui_utility_linux.h" -#include "base/flat_set.h" #include "ui/ui_log.h" #include "base/platform/base_platform_info.h" +#include "base/qt_adapters.h" +#include "base/flat_set.h" #include +#include #include -#include #include namespace Ui { @@ -28,19 +29,18 @@ bool TranslucentWindowsSupported(QPoint globalPosition) { } if (const auto native = QGuiApplication::platformNativeInterface()) { if (const auto desktop = QApplication::desktop()) { - const auto index = desktop->screenNumber(globalPosition); - const auto screens = QGuiApplication::screens(); - if (const auto screen = (index >= 0 && index < screens.size()) ? screens[index] : QGuiApplication::primaryScreen()) { + if (const auto screen = base::QScreenNearestTo(globalPosition)) { if (native->nativeResourceForScreen(QByteArray("compositingEnabled"), screen)) { return true; } + const auto index = QGuiApplication::screens().indexOf(screen); static auto WarnedAbout = base::flat_set(); if (!WarnedAbout.contains(index)) { WarnedAbout.emplace(index); UI_LOG(("WARNING: Compositing is disabled for screen index %1 (for position %2,%3)").arg(index).arg(globalPosition.x()).arg(globalPosition.y())); } } else { - UI_LOG(("WARNING: Could not get screen for index %1 (for position %2,%3)").arg(index).arg(globalPosition.x()).arg(globalPosition.y())); + UI_LOG(("WARNING: Could not get screen for position %1,%2").arg(globalPosition.x()).arg(globalPosition.y())); } } } diff --git a/ui/platform/linux/ui_utility_linux.h b/ui/platform/linux/ui_utility_linux.h index 73bfff9..c9081fc 100644 --- a/ui/platform/linux/ui_utility_linux.h +++ b/ui/platform/linux/ui_utility_linux.h @@ -12,7 +12,7 @@ class QPaintEvent; namespace Ui { namespace Platform { -inline void StartTranslucentPaint(QPainter &p, gsl::span rects) { +inline void StartTranslucentPaint(QPainter &p, const QRegion ®ion) { } inline void InitOnTopPanel(not_null panel) { diff --git a/ui/platform/mac/ui_utility_mac.mm b/ui/platform/mac/ui_utility_mac.mm index 3478157..46aeb11 100644 --- a/ui/platform/mac/ui_utility_mac.mm +++ b/ui/platform/mac/ui_utility_mac.mm @@ -68,10 +68,10 @@ void ReInitOnTopPanel(not_null panel) { [platformPanel setCollectionBehavior:newBehavior]; } -void StartTranslucentPaint(QPainter &p, gsl::span rects) { +void StartTranslucentPaint(QPainter &p, const QRegion ®ion) { p.setCompositionMode(QPainter::CompositionMode_Source); - for (const auto &r : rects) { - p.fillRect(r, Qt::transparent); + for (const auto rect : region) { + p.fillRect(rect, Qt::transparent); } p.setCompositionMode(QPainter::CompositionMode_SourceOver); } diff --git a/ui/platform/mac/ui_window_mac.h b/ui/platform/mac/ui_window_mac.h index 717c5e3..284b7ae 100644 --- a/ui/platform/mac/ui_window_mac.h +++ b/ui/platform/mac/ui_window_mac.h @@ -33,11 +33,12 @@ private: void setupBodyTitleAreaEvents() override; void init(); - void toggleCustomTitle(bool visible); + void updateCustomTitleVisibility(bool force = false); const std::unique_ptr _private; const not_null _title; const not_null _body; + bool _titleVisible = true; #ifdef OS_OSX struct WindowDrag { diff --git a/ui/platform/mac/ui_window_mac.mm b/ui/platform/mac/ui_window_mac.mm index 550584e..433bd73 100644 --- a/ui/platform/mac/ui_window_mac.mm +++ b/ui/platform/mac/ui_window_mac.mm @@ -207,7 +207,8 @@ void WindowHelper::Private::close() { Fn WindowHelper::Private::toggleCustomTitleCallback() { return crl::guard(_owner->window(), [=](bool visible) { - _owner->toggleCustomTitle(visible); + _owner->_titleVisible = visible; + _owner->updateCustomTitleVisibility(true); }); } @@ -286,7 +287,7 @@ WindowHelper::WindowHelper(not_null window) : nullptr) , _body(Ui::CreateChild(window.get())) { if (_title->shouldBeHidden()) { - toggleCustomTitle(false); + updateCustomTitleVisibility(); } init(); } @@ -303,27 +304,25 @@ void WindowHelper::setTitle(const QString &title) { _title->setText(title); } window()->setWindowTitle( - (!_title || _title->isHidden()) ? title : QString()); + (!_title || !_titleVisible) ? title : QString()); } void WindowHelper::setTitleStyle(const style::WindowTitle &st) { if (_title) { _title->setStyle(st); if (_title->shouldBeHidden()) { - toggleCustomTitle(false); + updateCustomTitleVisibility(); } } } -void WindowHelper::toggleCustomTitle(bool visible) { - if (_title->shouldBeHidden()) { - visible = false; - } - if (!_title || _title->isHidden() != visible) { +void WindowHelper::updateCustomTitleVisibility(bool force) { + auto visible = !_title->shouldBeHidden() && _titleVisible; + if (!_title || (!force && _title->isHidden() != visible)) { return; } _title->setVisible(visible); - window()->setWindowTitle(visible ? QString() : _title->text()); + window()->setWindowTitle(_titleVisible ? QString() : _title->text()); } void WindowHelper::setMinimumSize(QSize size) { diff --git a/ui/platform/ui_platform_utility.h b/ui/platform/ui_platform_utility.h index 0e23059..a8eaedd 100644 --- a/ui/platform/ui_platform_utility.h +++ b/ui/platform/ui_platform_utility.h @@ -16,7 +16,7 @@ namespace Platform { [[nodiscard]] bool IsApplicationActive(); [[nodiscard]] bool TranslucentWindowsSupported(QPoint globalPosition); -void StartTranslucentPaint(QPainter &p, gsl::span rects); +void StartTranslucentPaint(QPainter &p, const QRegion ®ion); void InitOnTopPanel(not_null panel); void DeInitOnTopPanel(not_null panel); diff --git a/ui/platform/win/ui_utility_win.h b/ui/platform/win/ui_utility_win.h index dcd82b2..a7a1a14 100644 --- a/ui/platform/win/ui_utility_win.h +++ b/ui/platform/win/ui_utility_win.h @@ -27,7 +27,7 @@ inline void DeInitOnTopPanel(not_null panel) { inline void ReInitOnTopPanel(not_null panel) { } -inline void StartTranslucentPaint(QPainter &p, gsl::span rects) { +inline void StartTranslucentPaint(QPainter &p, const QRegion ®ion) { } inline void ShowOverAll(not_null widget, bool canFocus) { diff --git a/ui/round_rect.cpp b/ui/round_rect.cpp index eb42a51..8d19b22 100644 --- a/ui/round_rect.cpp +++ b/ui/round_rect.cpp @@ -67,22 +67,25 @@ RoundRect::RoundRect( ImageRoundRadius radius, const style::color &color) : _color(color) -, _corners(Images::PrepareCorners(radius, color)) { +, _refresh([=] { _corners = Images::PrepareCorners(radius, _color); }) { + _refresh(); style::PaletteChanged( - ) | rpl::start_with_next([=] { - _corners = Images::PrepareCorners(radius, _color); - }, _lifetime); + ) | rpl::start_with_next(_refresh, _lifetime); } RoundRect::RoundRect( int radius, const style::color &color) : _color(color) -, _corners(Images::PrepareCorners(radius, color)) { +, _refresh([=] { _corners = Images::PrepareCorners(radius, _color); }) { + _refresh(); style::PaletteChanged( - ) | rpl::start_with_next([=] { - _corners = Images::PrepareCorners(radius, _color); - }, _lifetime); + ) | rpl::start_with_next(_refresh, _lifetime); +} + +void RoundRect::setColor(const style::color &color) { + _color = color; + _refresh(); } const style::color &RoundRect::color() const { diff --git a/ui/round_rect.h b/ui/round_rect.h index 920c3dd..e2c184b 100644 --- a/ui/round_rect.h +++ b/ui/round_rect.h @@ -27,6 +27,7 @@ public: RoundRect(int radius, const style::color &color); [[nodiscard]] const style::color &color() const; + void setColor(const style::color &color); void paint( QPainter &p, const QRect &rect, @@ -39,6 +40,7 @@ public: private: style::color _color; std::array _corners; + Fn _refresh; rpl::lifetime _lifetime; diff --git a/ui/style/style_core_font.cpp b/ui/style/style_core_font.cpp index 2040b5e..4c47130 100644 --- a/ui/style/style_core_font.cpp +++ b/ui/style/style_core_font.cpp @@ -348,7 +348,7 @@ QString MonospaceFont() { // Prefer system monospace font. const auto metrics = QFontMetrics(QFont(system)); const auto useSystem = manual.isEmpty() - || (metrics.charWidth("i", 0) == metrics.charWidth("W", 0)); + || (metrics.horizontalAdvance(QChar('i')) == metrics.horizontalAdvance(QChar('W'))); #endif // Q_OS_WIN || Q_OS_MAC return (useSystem || UseSystemFont) ? system : manual; }(); diff --git a/ui/style/style_core_font.h b/ui/style/style_core_font.h index d017ef0..43708f6 100644 --- a/ui/style/style_core_font.h +++ b/ui/style/style_core_font.h @@ -79,13 +79,13 @@ enum FontFlags { class FontData { public: int width(const QString &str) const { - return m.width(str); + return m.horizontalAdvance(str); } int width(const QString &str, int32 from, int32 to) const { return width(str.mid(from, to)); } int width(QChar ch) const { - return m.width(ch); + return m.horizontalAdvance(ch); } QString elided( const QString &str, diff --git a/ui/text/text_entity.cpp b/ui/text/text_entity.cpp index 4d6de85..9bdfc43 100644 --- a/ui/text/text_entity.cpp +++ b/ui/text/text_entity.cpp @@ -9,6 +9,7 @@ #include "base/qthelp_url.h" #include "base/qthelp_regex.h" #include "base/crc32hash.h" +#include "base/qt_adapters.h" #include "ui/text/text.h" #include "ui/widgets/input_fields.h" #include "ui/emoji_config.h" @@ -1403,7 +1404,7 @@ QStringList PrepareSearchWords( auto list = clean.split(SplitterOverride ? *SplitterOverride : RegExpWordSplit(), - QString::SkipEmptyParts); + base::QStringSkipEmptyParts); auto size = list.size(); result.reserve(list.size()); for (const auto &word : std::as_const(list)) { @@ -1949,6 +1950,20 @@ void Trim(TextWithEntities &result) { } } +int SerializeTagsSize(const TextWithTags::Tags &tags) { + auto result = qint32(0); + if (tags.isEmpty()) { + return result; + } + result += sizeof(qint32); + for (const auto &tag : tags) { + result += 2 * sizeof(qint32) // offset, length + + sizeof(quint32) // id.size + + tag.id.size() * sizeof(ushort); + } + return result; +} + QByteArray SerializeTags(const TextWithTags::Tags &tags) { if (tags.isEmpty()) { return QByteArray(); diff --git a/ui/text/text_entity.h b/ui/text/text_entity.h index 83f4384..aa04175 100644 --- a/ui/text/text_entity.h +++ b/ui/text/text_entity.h @@ -344,10 +344,13 @@ inline QString PrepareForSending(const QString &text, PrepareTextOption option = // Replace bad symbols with space and remove '\r'. void ApplyServerCleaning(TextWithEntities &result); -QByteArray SerializeTags(const TextWithTags::Tags &tags); -TextWithTags::Tags DeserializeTags(QByteArray data, int textLength); -QString TagsMimeType(); -QString TagsTextMimeType(); +[[nodiscard]] int SerializeTagsSize(const TextWithTags::Tags &tags); +[[nodiscard]] QByteArray SerializeTags(const TextWithTags::Tags &tags); +[[nodiscard]] TextWithTags::Tags DeserializeTags( + QByteArray data, + int textLength); +[[nodiscard]] QString TagsMimeType(); +[[nodiscard]] QString TagsTextMimeType(); inline const auto kMentionTagStart = qstr("mention://user."); diff --git a/ui/widgets/buttons.cpp b/ui/widgets/buttons.cpp index 7d7a0c3..d770d0e 100644 --- a/ui/widgets/buttons.cpp +++ b/ui/widgets/buttons.cpp @@ -236,8 +236,8 @@ RoundButton::RoundButton( : RippleButton(parent, st.ripple) , _textFull(std::move(text)) , _st(st) -, _roundRect(ImageRoundRadius::Small, _st.textBg) -, _roundRectOver(ImageRoundRadius::Small, _st.textBgOver) { +, _roundRect(st::buttonRadius, _st.textBg) +, _roundRectOver(st::buttonRadius, _st.textBgOver) { _textFull.value( ) | rpl::start_with_next([=](const QString &text) { resizeToText(text); diff --git a/ui/widgets/call_button.cpp b/ui/widgets/call_button.cpp new file mode 100644 index 0000000..25bf401 --- /dev/null +++ b/ui/widgets/call_button.cpp @@ -0,0 +1,232 @@ +// This file is part of Desktop App Toolkit, +// a set of libraries for developing nice desktop applications. +// +// For license and copyright information please follow this link: +// https://github.com/desktop-app/legal/blob/master/LEGAL +// +#include "ui/widgets/call_button.h" + +#include "ui/effects/ripple_animation.h" +#include "ui/painter.h" +#include "ui/widgets/labels.h" +#include "styles/style_widgets.h" +#include "styles/palette.h" + +namespace Ui { +namespace { + +constexpr auto kOuterBounceDuration = crl::time(100); + +} // namespace + +CallButton::CallButton( + QWidget *parent, + const style::CallButton &stFrom, + const style::CallButton *stTo) +: RippleButton(parent, stFrom.button.ripple) +, _stFrom(&stFrom) +, _stTo(stTo) { + resize(_stFrom->button.width, _stFrom->button.height); + + _bgMask = RippleAnimation::ellipseMask(QSize(_stFrom->bgSize, _stFrom->bgSize)); + _bgFrom = Ui::PixmapFromImage(style::colorizeImage(_bgMask, _stFrom->bg)); + if (_stTo) { + Assert(_stFrom->button.width == _stTo->button.width); + Assert(_stFrom->button.height == _stTo->button.height); + Assert(_stFrom->bgPosition == _stTo->bgPosition); + Assert(_stFrom->bgSize == _stTo->bgSize); + + _bg = QImage(_bgMask.size(), QImage::Format_ARGB32_Premultiplied); + _bg.setDevicePixelRatio(style::DevicePixelRatio()); + _bgTo = Ui::PixmapFromImage(style::colorizeImage(_bgMask, _stTo->bg)); + _iconMixedMask = QImage(_bgMask.size(), QImage::Format_ARGB32_Premultiplied); + _iconMixedMask.setDevicePixelRatio(style::DevicePixelRatio()); + _iconFrom = QImage(_bgMask.size(), QImage::Format_ARGB32_Premultiplied); + _iconFrom.setDevicePixelRatio(style::DevicePixelRatio()); + _iconFrom.fill(Qt::black); + { + QPainter p(&_iconFrom); + p.drawImage( + (_stFrom->bgSize + - _stFrom->button.icon.width()) / 2, + (_stFrom->bgSize + - _stFrom->button.icon.height()) / 2, + _stFrom->button.icon.instance(Qt::white)); + } + _iconTo = QImage(_bgMask.size(), QImage::Format_ARGB32_Premultiplied); + _iconTo.setDevicePixelRatio(style::DevicePixelRatio()); + _iconTo.fill(Qt::black); + { + QPainter p(&_iconTo); + p.drawImage( + (_stTo->bgSize + - _stTo->button.icon.width()) / 2, + (_stTo->bgSize + - _stTo->button.icon.height()) / 2, + _stTo->button.icon.instance(Qt::white)); + } + _iconMixed = QImage(_bgMask.size(), QImage::Format_ARGB32_Premultiplied); + _iconMixed.setDevicePixelRatio(style::DevicePixelRatio()); + } +} + +void CallButton::setOuterValue(float64 value) { + if (_outerValue != value) { + _outerAnimation.start([this] { + if (_progress == 0. || _progress == 1.) { + update(); + } + }, _outerValue, value, kOuterBounceDuration); + _outerValue = value; + } +} + +void CallButton::setText(rpl::producer text) { + _label.create(this, std::move(text), _stFrom->label); + _label->show(); + rpl::combine( + sizeValue(), + _label->sizeValue() + ) | rpl::start_with_next([=](QSize my, QSize label) { + _label->moveToLeft( + (my.width() - label.width()) / 2, + my.height() - label.height(), + my.width()); + }, _label->lifetime()); +} + +void CallButton::setProgress(float64 progress) { + _progress = progress; + update(); +} + +void CallButton::paintEvent(QPaintEvent *e) { + QPainter p(this); + + auto bgPosition = myrtlpoint(_stFrom->bgPosition); + auto paintFrom = (_progress == 0.) || !_stTo; + auto paintTo = !paintFrom && (_progress == 1.); + + auto outerValue = _outerAnimation.value(_outerValue); + if (outerValue > 0.) { + auto outerRadius = paintFrom ? _stFrom->outerRadius : paintTo ? _stTo->outerRadius : (_stFrom->outerRadius * (1. - _progress) + _stTo->outerRadius * _progress); + auto outerPixels = outerValue * outerRadius; + auto outerRect = QRectF(myrtlrect(bgPosition.x(), bgPosition.y(), _stFrom->bgSize, _stFrom->bgSize)); + outerRect = outerRect.marginsAdded(QMarginsF(outerPixels, outerPixels, outerPixels, outerPixels)); + + PainterHighQualityEnabler hq(p); + if (paintFrom) { + p.setBrush(_stFrom->outerBg); + } else if (paintTo) { + p.setBrush(_stTo->outerBg); + } else { + p.setBrush(anim::brush(_stFrom->outerBg, _stTo->outerBg, _progress)); + } + p.setPen(Qt::NoPen); + p.drawEllipse(outerRect); + } + + if (_bgOverride) { + const auto &s = _stFrom->bgSize; + p.setPen(Qt::NoPen); + p.setBrush(*_bgOverride); + + PainterHighQualityEnabler hq(p); + p.drawEllipse(QRect(_stFrom->bgPosition, QSize(s, s))); + } else if (paintFrom) { + p.drawPixmap(bgPosition, _bgFrom); + } else if (paintTo) { + p.drawPixmap(bgPosition, _bgTo); + } else { + style::colorizeImage(_bgMask, anim::color(_stFrom->bg, _stTo->bg, _progress), &_bg); + p.drawImage(bgPosition, _bg); + } + + auto rippleColorInterpolated = QColor(); + auto rippleColorOverride = &rippleColorInterpolated; + if (_rippleOverride) { + rippleColorOverride = &(*_rippleOverride); + } else if (paintFrom) { + rippleColorOverride = nullptr; + } else if (paintTo) { + rippleColorOverride = &_stTo->button.ripple.color->c; + } else { + rippleColorInterpolated = anim::color(_stFrom->button.ripple.color, _stTo->button.ripple.color, _progress); + } + paintRipple(p, _stFrom->button.rippleAreaPosition.x(), _stFrom->button.rippleAreaPosition.y(), rippleColorOverride); + + auto positionFrom = iconPosition(_stFrom); + if (paintFrom) { + const auto icon = &_stFrom->button.icon; + icon->paint(p, positionFrom, width()); + } else { + auto positionTo = iconPosition(_stTo); + if (paintTo) { + _stTo->button.icon.paint(p, positionTo, width()); + } else { + mixIconMasks(); + style::colorizeImage(_iconMixedMask, st::callIconFg->c, &_iconMixed); + p.drawImage(myrtlpoint(_stFrom->bgPosition), _iconMixed); + } + } +} + +QPoint CallButton::iconPosition(not_null st) const { + auto result = st->button.iconPosition; + if (result.x() < 0) { + result.setX((width() - st->button.icon.width()) / 2); + } + if (result.y() < 0) { + result.setY((height() - st->button.icon.height()) / 2); + } + return result; +} + +void CallButton::mixIconMasks() { + _iconMixedMask.fill(Qt::black); + + Painter p(&_iconMixedMask); + PainterHighQualityEnabler hq(p); + auto paintIconMask = [this, &p](const QImage &mask, float64 angle) { + auto skipFrom = _stFrom->bgSize / 2; + p.translate(skipFrom, skipFrom); + p.rotate(angle); + p.translate(-skipFrom, -skipFrom); + p.drawImage(0, 0, mask); + }; + p.save(); + paintIconMask(_iconFrom, (_stFrom->angle - _stTo->angle) * _progress); + p.restore(); + p.setOpacity(_progress); + paintIconMask(_iconTo, (_stTo->angle - _stFrom->angle) * (1. - _progress)); +} + +void CallButton::onStateChanged(State was, StateChangeSource source) { + RippleButton::onStateChanged(was, source); + + auto over = isOver(); + auto wasOver = static_cast(was & StateFlag::Over); + if (over != wasOver) { + update(); + } +} + +void CallButton::setColorOverrides(rpl::producer &&colors) { + std::move( + colors + ) | rpl::start_with_next([=](const CallButtonColors &c) { + _bgOverride = c.bg; + _rippleOverride = c.ripple; + update(); + }, lifetime()); +} + +QPoint CallButton::prepareRippleStartPosition() const { + return mapFromGlobal(QCursor::pos()) - _stFrom->button.rippleAreaPosition; +} + +QImage CallButton::prepareRippleMask() const { + return RippleAnimation::ellipseMask(QSize(_stFrom->button.rippleAreaSize, _stFrom->button.rippleAreaSize)); +} + +} // namespace Ui diff --git a/ui/widgets/call_button.h b/ui/widgets/call_button.h new file mode 100644 index 0000000..cb65c57 --- /dev/null +++ b/ui/widgets/call_button.h @@ -0,0 +1,64 @@ +// This file is part of Desktop App Toolkit, +// a set of libraries for developing nice desktop applications. +// +// For license and copyright information please follow this link: +// https://github.com/desktop-app/legal/blob/master/LEGAL +// +#pragma once + +#include "base/object_ptr.h" +#include "ui/widgets/buttons.h" +#include "ui/effects/animations.h" + +namespace Ui { + +class FlatLabel; + +struct CallButtonColors { + std::optional bg; + std::optional ripple; +}; + +class CallButton final : public RippleButton { +public: + CallButton( + QWidget *parent, + const style::CallButton &stFrom, + const style::CallButton *stTo = nullptr); + + void setProgress(float64 progress); + void setOuterValue(float64 value); + void setText(rpl::producer text); + void setColorOverrides(rpl::producer &&colors); + +protected: + void paintEvent(QPaintEvent *e) override; + + void onStateChanged(State was, StateChangeSource source) override; + + QImage prepareRippleMask() const override; + QPoint prepareRippleStartPosition() const override; + +private: + QPoint iconPosition(not_null st) const; + void mixIconMasks(); + + not_null _stFrom; + const style::CallButton *_stTo = nullptr; + float64 _progress = 0.; + + object_ptr _label = { nullptr }; + + std::optional _bgOverride; + std::optional _rippleOverride; + + QImage _bgMask, _bg; + QPixmap _bgFrom, _bgTo; + QImage _iconMixedMask, _iconFrom, _iconTo, _iconMixed; + + float64 _outerValue = 0.; + Animations::Simple _outerAnimation; + +}; + +} // namespace Ui diff --git a/ui/widgets/call_mute_button.cpp b/ui/widgets/call_mute_button.cpp new file mode 100644 index 0000000..15e3820 --- /dev/null +++ b/ui/widgets/call_mute_button.cpp @@ -0,0 +1,937 @@ +// This file is part of Desktop App Toolkit, +// a set of libraries for developing nice desktop applications. +// +// For license and copyright information please follow this link: +// https://github.com/desktop-app/legal/blob/master/LEGAL +// +#include "ui/widgets/call_mute_button.h" + +#include "base/flat_map.h" +#include "ui/abstract_button.h" +#include "ui/paint/blobs.h" +#include "ui/painter.h" +#include "ui/widgets/call_button.h" +#include "ui/widgets/labels.h" + +#include "styles/palette.h" +#include "styles/style_widgets.h" + +#include + +namespace Ui { +namespace { + +using Radiuses = Paint::Blob::Radiuses; + +constexpr auto kMaxLevel = 1.; + +constexpr auto kLevelDuration = 100. + 500. * 0.33; + +constexpr auto kScaleBig = 0.807 - 0.1; +constexpr auto kScaleSmall = 0.704 - 0.1; + +constexpr auto kScaleBigMin = 0.878; +constexpr auto kScaleSmallMin = 0.926; + +constexpr auto kScaleBigMax = (float)(kScaleBigMin + kScaleBig); +constexpr auto kScaleSmallMax = (float)(kScaleSmallMin + kScaleSmall); + +constexpr auto kMainRadiusFactor = (float)(50. / 57.); + +constexpr auto kGlowPaddingFactor = 1.2; +constexpr auto kGlowMinScale = 0.6; +constexpr auto kGlowAlpha = 150; + +constexpr auto kOverrideColorBgAlpha = 76; +constexpr auto kOverrideColorRippleAlpha = 50; + +constexpr auto kShiftDuration = crl::time(300); +constexpr auto kSwitchStateDuration = crl::time(120); +constexpr auto kSwitchLabelDuration = crl::time(180); + +// Switch state from Connecting animation. +constexpr auto kSwitchRadialDuration = crl::time(350); +constexpr auto kSwitchCirclelDuration = crl::time(275); +constexpr auto kBlobsScaleEnterDuration = crl::time(400); +constexpr auto kSwitchStateFromConnectingDuration = kSwitchRadialDuration + + kSwitchCirclelDuration + + kBlobsScaleEnterDuration; + +constexpr auto kRadialEndPartAnimation = float(kSwitchRadialDuration) + / kSwitchStateFromConnectingDuration; +constexpr auto kBlobsWidgetPartAnimation = 1. - kRadialEndPartAnimation; +constexpr auto kFillCirclePartAnimation = float(kSwitchCirclelDuration) + / (kSwitchCirclelDuration + kBlobsScaleEnterDuration); +constexpr auto kBlobPartAnimation = float(kBlobsScaleEnterDuration) + / (kSwitchCirclelDuration + kBlobsScaleEnterDuration); + +constexpr auto kOverlapProgressRadialHide = 1.2; + +constexpr auto kRadialFinishArcShift = 1200; + +auto MuteBlobs() { + return std::vector{ + { + .segmentsCount = 9, + .minScale = kScaleSmallMin / kScaleSmallMax, + .minRadius = st::callMuteMinorBlobMinRadius + * kScaleSmallMax + * kMainRadiusFactor, + .maxRadius = st::callMuteMinorBlobMaxRadius + * kScaleSmallMax + * kMainRadiusFactor, + .speedScale = 1., + .alpha = (76. / 255.), + }, + { + .segmentsCount = 12, + .minScale = kScaleBigMin / kScaleBigMax, + .minRadius = st::callMuteMajorBlobMinRadius + * kScaleBigMax + * kMainRadiusFactor, + .maxRadius = st::callMuteMajorBlobMaxRadius + * kScaleBigMax + * kMainRadiusFactor, + .speedScale = 1., + .alpha = (76. / 255.), + }, + }; +} + +auto Colors() { + using Vector = std::vector; + using Colors = anim::gradient_colors; + return base::flat_map{ + { + CallMuteButtonType::ForceMuted, + Colors(QGradientStops{ + { .0, st::groupCallForceMuted1->c }, + { .5, st::groupCallForceMuted2->c }, + { 1., st::groupCallForceMuted3->c } }) + }, + { + CallMuteButtonType::Active, + Colors(Vector{ st::groupCallLive1->c, st::groupCallLive2->c }) + }, + { + CallMuteButtonType::Connecting, + Colors(st::callIconBg->c) + }, + { + CallMuteButtonType::Muted, + Colors(Vector{ st::groupCallMuted1->c, st::groupCallMuted2->c }) + }, + }; +} + +bool IsMuted(CallMuteButtonType type) { + return (type != CallMuteButtonType::Active); +} + +bool IsConnecting(CallMuteButtonType type) { + return (type == CallMuteButtonType::Connecting); +} + +bool IsInactive(CallMuteButtonType type) { + return IsConnecting(type) || (type == CallMuteButtonType::ForceMuted); +} + +auto Clamp(float64 value) { + return std::clamp(value, 0., 1.); +} + +void ComputeRadialFinish( + int &value, + float64 progress, + int to = -RadialState::kFull) { + value = anim::interpolate(value, to, Clamp(progress)); +} + +} // namespace + +class AnimatedLabel final : public RpWidget { +public: + AnimatedLabel( + QWidget *parent, + rpl::producer &&text, + crl::time duration, + int additionalHeight, + const style::FlatLabel &st = st::defaultFlatLabel); + + int height() const; + +private: + int realHeight() const; + + void setText(const QString &text); + + const style::FlatLabel &_st; + const crl::time _duration; + const int _additionalHeight; + const TextParseOptions _options; + + Text::String _text; + Text::String _previousText; + + Animations::Simple _animation; + +}; + +AnimatedLabel::AnimatedLabel( + QWidget *parent, + rpl::producer &&text, + crl::time duration, + int additionalHeight, + const style::FlatLabel &st) +: RpWidget(parent) +, _st(st) +, _duration(duration) +, _additionalHeight(additionalHeight) +, _options({ 0, 0, 0, Qt::LayoutDirectionAuto }) { + std::move( + text + ) | rpl::start_with_next([=](const QString &value) { + setText(value); + }, lifetime()); + + paintRequest( + ) | rpl::start_with_next([=] { + Painter p(this); + const auto progress = _animation.value(1.); + + p.setFont(_st.style.font); + p.setPen(_st.textFg); + p.setTextPalette(_st.palette); + + const auto textHeight = height(); + const auto diffHeight = realHeight() - textHeight; + const auto center = (diffHeight) / 2; + + p.setOpacity(1. - progress); + if (p.opacity()) { + _previousText.draw( + p, + 0, + anim::interpolate(center, diffHeight, progress), + width(), + style::al_center); + } + + p.setOpacity(progress); + if (p.opacity()) { + _text.draw( + p, + 0, + anim::interpolate(0, center, progress), + width(), + style::al_center); + } + }, lifetime()); +} + +int AnimatedLabel::height() const { + return _st.style.font->height; +} + +int AnimatedLabel::realHeight() const { + return RpWidget::height(); +} + +void AnimatedLabel::setText(const QString &text) { + if (_text.toString() == text) { + return; + } + _previousText = _text; + _text.setText(_st.style, text, _options); + + const auto width = std::max( + _st.style.font->width(_text.toString()), + _st.style.font->width(_previousText.toString())); + resize(width + _additionalHeight, height() + _additionalHeight * 2); + + _animation.stop(); + _animation.start([=] { update(); }, 0., 1., _duration); +} + +class BlobsWidget final : public RpWidget { +public: + BlobsWidget( + not_null parent, + rpl::producer &&hideBlobs); + + void setLevel(float level); + void setBlobBrush(QBrush brush); + void setGlowBrush(QBrush brush); + + [[nodiscard]] QRectF innerRect() const; + + [[nodiscard]] float64 switchConnectingProgress() const; + void setSwitchConnectingProgress(float64 progress); + +private: + void init(); + + Paint::Blobs _blobs; + + const float _circleRadius; + QBrush _blobBrush; + QBrush _glowBrush; + int _center = 0; + QRectF _circleRect; + + float64 _switchConnectingProgress = 0.; + + crl::time _blobsLastTime = 0; + crl::time _blobsHideLastTime = 0; + + float64 _blobsScaleEnter = 0.; + crl::time _blobsScaleLastTime = 0; + + bool _hideBlobs = true; + + Animations::Basic _animation; + +}; + +BlobsWidget::BlobsWidget( + not_null parent, + rpl::producer &&hideBlobs) +: RpWidget(parent) +, _blobs(MuteBlobs(), kLevelDuration, kMaxLevel) +, _circleRadius(st::callMuteButtonActive.bgSize / 2.) +, _blobBrush(Qt::transparent) +, _glowBrush(Qt::transparent) +, _blobsLastTime(crl::now()) +, _blobsScaleLastTime(crl::now()) { + init(); + + std::move( + hideBlobs + ) | rpl::start_with_next([=](bool hide) { + if (_hideBlobs != hide) { + const auto now = crl::now(); + if ((now - _blobsScaleLastTime) >= kBlobsScaleEnterDuration) { + _blobsScaleLastTime = now; + } + _hideBlobs = hide; + } + if (hide) { + setLevel(0.); + } + _blobsHideLastTime = hide ? crl::now() : 0; + if (!hide && !_animation.animating()) { + _animation.start(); + } + }, lifetime()); +} + +void BlobsWidget::init() { + setAttribute(Qt::WA_TransparentForMouseEvents); + + { + const auto s = _blobs.maxRadius() * 2 * kGlowPaddingFactor; + resize(s, s); + } + + sizeValue( + ) | rpl::start_with_next([=](QSize size) { + _center = size.width() / 2; + + { + const auto &r = _circleRadius; + const auto left = (size.width() - r * 2.) / 2.; + const auto add = st::callConnectingRadial.thickness / 2; + _circleRect = QRectF(left, left, r * 2, r * 2).marginsAdded( + style::margins(add, add, add, add)); + } + }, lifetime()); + + paintRequest( + ) | rpl::start_with_next([=] { + Painter p(this); + PainterHighQualityEnabler hq(p); + + // Glow. + const auto s = kGlowMinScale + + (1. - kGlowMinScale) * _blobs.currentLevel(); + p.translate(_center, _center); + p.scale(s, s); + p.translate(-_center, -_center); + p.fillRect(rect(), _glowBrush); + p.resetTransform(); + + // Blobs. + p.translate(_center, _center); + const auto scale = (_switchConnectingProgress > 0.) + ? anim::easeOutBack( + 1., + _blobsScaleEnter * (1. - Clamp( + _switchConnectingProgress / kBlobPartAnimation))) + : _blobsScaleEnter; + _blobs.paint(p, _blobBrush, scale); + + // Main circle. + p.translate(-_center, -_center); + p.setPen(Qt::NoPen); + p.setBrush(_blobBrush); + p.drawEllipse(_circleRect); + + if (_switchConnectingProgress > 0.) { + p.resetTransform(); + + const auto circleProgress = + Clamp(_switchConnectingProgress - kBlobPartAnimation) + / kFillCirclePartAnimation; + + const auto mF = (_circleRect.width() / 2) * (1. - circleProgress); + const auto cutOutRect = _circleRect.marginsRemoved( + QMarginsF(mF, mF, mF, mF)); + + p.setPen(Qt::NoPen); + p.setBrush(st::callConnectingRadial.color); + p.setOpacity(circleProgress); + p.drawEllipse(_circleRect); + + p.setOpacity(1.); + p.setBrush(st::callIconBg); + + p.save(); + p.setCompositionMode(QPainter::CompositionMode_Source); + p.drawEllipse(cutOutRect); + p.restore(); + + p.drawEllipse(cutOutRect); + } + }, lifetime()); + + _animation.init([=](crl::time now) { + if (const auto &last = _blobsHideLastTime; (last > 0) + && (now - last >= kBlobsScaleEnterDuration)) { + _animation.stop(); + return false; + } + _blobs.updateLevel(now - _blobsLastTime); + _blobsLastTime = now; + + const auto dt = Clamp( + (now - _blobsScaleLastTime) / float64(kBlobsScaleEnterDuration)); + _blobsScaleEnter = _hideBlobs + ? (1. - anim::easeInCirc(1., dt)) + : anim::easeOutBack(1., dt); + + update(); + return true; + }); + shownValue( + ) | rpl::start_with_next([=](bool shown) { + if (shown) { + _animation.start(); + } else { + _animation.stop(); + } + }, lifetime()); +} + +QRectF BlobsWidget::innerRect() const { + return _circleRect; +} + +void BlobsWidget::setBlobBrush(QBrush brush) { + if (_blobBrush == brush) { + return; + } + _blobBrush = brush; +} + +void BlobsWidget::setGlowBrush(QBrush brush) { + if (_glowBrush == brush) { + return; + } + _glowBrush = brush; +} + +void BlobsWidget::setLevel(float level) { + if (_blobsHideLastTime) { + return; + } + _blobs.setLevel(level); +} + +float64 BlobsWidget::switchConnectingProgress() const { + return _switchConnectingProgress; +} + +void BlobsWidget::setSwitchConnectingProgress(float64 progress) { + _switchConnectingProgress = progress; +} + +CallMuteButton::CallMuteButton( + not_null parent, + rpl::producer &&hideBlobs, + CallMuteButtonState initial) +: _state(initial) +, _st(st::callMuteButtonActive) +, _blobs(base::make_unique_q( + parent, + rpl::combine( + rpl::single(anim::Disabled()) | rpl::then(anim::Disables()), + std::move(hideBlobs), + _state.value( + ) | rpl::map([](const CallMuteButtonState &state) { + return IsInactive(state.type); + }) + ) | rpl::map([](bool animDisabled, bool hide, bool isBadState) { + return isBadState || !(!animDisabled && !hide); + }))) +, _content(base::make_unique_q(parent)) +, _centerLabel(base::make_unique_q( + parent, + _state.value( + ) | rpl::map([](const CallMuteButtonState &state) { + return state.subtext.isEmpty() ? state.text : QString(); + }), + kSwitchLabelDuration, + st::callMuteButtonLabelAdditional, + _st.label)) +, _label(base::make_unique_q( + parent, + _state.value( + ) | rpl::map([](const CallMuteButtonState &state) { + return state.subtext.isEmpty() ? QString() : state.text; + }), + kSwitchLabelDuration, + st::callMuteButtonLabelAdditional, + _st.label)) +, _sublabel(base::make_unique_q( + parent, + _state.value( + ) | rpl::map([](const CallMuteButtonState &state) { + return state.subtext; + }), + kSwitchLabelDuration, + st::callMuteButtonLabelAdditional, + st::callMuteButtonSublabel)) +, _radial(nullptr) +, _colors(Colors()) +, _crossLineMuteAnimation(st::callMuteCrossLine) { + init(); +} + +void CallMuteButton::init() { + _content->resize(_st.button.width, _st.button.height); + + style::PaletteChanged( + ) | rpl::start_with_next([=] { + _crossLineMuteAnimation.invalidate(); + }, lifetime()); + + // Label text. + _label->show(); + rpl::combine( + _content->geometryValue(), + _label->sizeValue() + ) | rpl::start_with_next([=](QRect my, QSize size) { + updateLabelGeometry(my, size); + }, _label->lifetime()); + _label->setAttribute(Qt::WA_TransparentForMouseEvents); + + _sublabel->show(); + rpl::combine( + _content->geometryValue(), + _sublabel->sizeValue() + ) | rpl::start_with_next([=](QRect my, QSize size) { + updateSublabelGeometry(my, size); + }, _sublabel->lifetime()); + _sublabel->setAttribute(Qt::WA_TransparentForMouseEvents); + + _centerLabel->show(); + rpl::combine( + _content->geometryValue(), + _centerLabel->sizeValue() + ) | rpl::start_with_next([=](QRect my, QSize size) { + updateCenterLabelGeometry(my, size); + }, _centerLabel->lifetime()); + _centerLabel->setAttribute(Qt::WA_TransparentForMouseEvents); + + _radialInfo.rawShowProgress.value( + ) | rpl::start_with_next([=](float64 value) { + auto &info = _radialInfo; + info.realShowProgress = (1. - value) / kRadialEndPartAnimation; + + if (((value == 0.) || anim::Disabled()) && _radial) { + _radial->stop(); + _radial = nullptr; + return; + } + if ((value > 0.) && !anim::Disabled() && !_radial) { + _radial = std::make_unique( + [=] { _content->update(); }, + _radialInfo.st); + _radial->start(); + } + if ((info.realShowProgress < 1.) && !info.isDirectionToShow) { + _radial->stop(anim::type::instant); + _radial->start(); + info.state = std::nullopt; + return; + } + + if (value == 1.) { + info.state = std::nullopt; + } else { + if (_radial && !info.state.has_value()) { + info.state = _radial->computeState(); + } + } + }, lifetime()); + + // State type. + const auto previousType = + lifetime().make_state(_state.current().type); + setHandleMouseState(HandleMouseState::Disabled); + + const auto blobsInner = [&] { + // The point of the circle at 45 degrees. + const auto w = _blobs->innerRect().width(); + const auto mF = (1 - std::cos(M_PI / 4.)) * (w / 2.); + return _blobs->innerRect().marginsRemoved(QMarginsF(mF, mF, mF, mF)); + }(); + + auto linearGradients = anim::linear_gradients( + _colors, + QPointF(blobsInner.x() + blobsInner.width(), blobsInner.y()), + QPointF(blobsInner.x(), blobsInner.y() + blobsInner.height())); + + auto glowColors = [&] { + auto copy = _colors; + for (auto &[type, stops] : copy) { + auto firstColor = IsInactive(type) + ? st::groupCallBg->c + : stops.stops[0].second; + firstColor.setAlpha(kGlowAlpha); + stops.stops = QGradientStops{ + { 0., std::move(firstColor) }, + { 1., QColor(Qt::transparent) } + }; + } + return copy; + }(); + auto glows = anim::radial_gradients( + std::move(glowColors), + blobsInner.center(), + _blobs->width() / 2); + + _state.value( + ) | rpl::map([](const CallMuteButtonState &state) { + return state.type; + }) | rpl::start_with_next([=](CallMuteButtonType type) { + const auto previous = *previousType; + *previousType = type; + + const auto mouseState = HandleMouseStateFromType(type); + setHandleMouseState(HandleMouseState::Disabled); + if (mouseState != HandleMouseState::Enabled) { + setHandleMouseState(mouseState); + } + + const auto fromConnecting = IsConnecting(previous); + const auto toConnecting = IsConnecting(type); + + const auto crossFrom = IsMuted(previous) ? 0. : 1.; + const auto crossTo = IsMuted(type) ? 0. : 1.; + + const auto radialShowFrom = fromConnecting ? 1. : 0.; + const auto radialShowTo = toConnecting ? 1. : 0.; + + const auto from = (_switchAnimation.animating() && !fromConnecting) + ? (1. - _switchAnimation.value(0.)) + : 0.; + const auto to = 1.; + + _radialInfo.isDirectionToShow = fromConnecting; + + auto callback = [=](float64 value) { + const auto brushProgress = fromConnecting ? 1. : value; + _blobs->setBlobBrush(QBrush( + linearGradients.gradient(previous, type, brushProgress))); + _blobs->setGlowBrush(QBrush( + glows.gradient(previous, type, value))); + _blobs->update(); + + const auto crossProgress = (crossFrom == crossTo) + ? crossTo + : anim::interpolateF(crossFrom, crossTo, value); + if (crossProgress != _crossLineProgress) { + _crossLineProgress = crossProgress; + _content->update(_muteIconRect); + } + + const auto radialShowProgress = (radialShowFrom == radialShowTo) + ? radialShowTo + : anim::interpolateF(radialShowFrom, radialShowTo, value); + if (radialShowProgress != _radialInfo.rawShowProgress.current()) { + _radialInfo.rawShowProgress = radialShowProgress; + _blobs->setSwitchConnectingProgress(Clamp( + radialShowProgress / kBlobsWidgetPartAnimation)); + } + + overridesColors(previous, type, value); + + if (value == to) { + setHandleMouseState(mouseState); + } + }; + + _switchAnimation.stop(); + const auto duration = (1. - from) * ((fromConnecting || toConnecting) + ? kSwitchStateFromConnectingDuration + : kSwitchStateDuration); + _switchAnimation.start(std::move(callback), from, to, duration); + }, lifetime()); + + // Icon rect. + _content->sizeValue( + ) | rpl::start_with_next([=](QSize size) { + const auto &icon = _st.button.icon; + const auto &pos = _st.button.iconPosition; + + _muteIconRect = QRect( + (pos.x() < 0) ? ((size.width() - icon.width()) / 2) : pos.x(), + (pos.y() < 0) ? ((size.height() - icon.height()) / 2) : pos.y(), + icon.width(), + icon.height()); + }, lifetime()); + + // Paint. + _content->paintRequest( + ) | rpl::start_with_next([=](QRect clip) { + Painter p(_content); + + _crossLineMuteAnimation.paint( + p, + _muteIconRect.topLeft(), + 1. - _crossLineProgress); + + if (_radialInfo.state.has_value() && _switchAnimation.animating()) { + const auto radialProgress = _radialInfo.realShowProgress; + + auto r = *_radialInfo.state; + r.shown = 1.; + if (_radialInfo.isDirectionToShow) { + const auto to = r.arcFrom - kRadialFinishArcShift; + ComputeRadialFinish(r.arcFrom, radialProgress, to); + ComputeRadialFinish(r.arcLength, radialProgress); + } + + const auto opacity = (radialProgress > kOverlapProgressRadialHide) + ? 0. + : _blobs->switchConnectingProgress(); + p.setOpacity(opacity); + InfiniteRadialAnimation::Draw( + p, + r, + _st.bgPosition, + _radialInfo.st.size, + _content->width(), + QPen(_radialInfo.st.color), + _radialInfo.st.thickness); + } else if (_radial) { + auto state = _radial->computeState(); + state.shown = 1.; + + InfiniteRadialAnimation::Draw( + p, + std::move(state), + _st.bgPosition, + _radialInfo.st.size, + _content->width(), + QPen(_radialInfo.st.color), + _radialInfo.st.thickness); + } + }, _content->lifetime()); +} + +void CallMuteButton::updateLabelsGeometry() { + updateLabelGeometry(_content->geometry(), _label->size()); + updateCenterLabelGeometry(_content->geometry(), _centerLabel->size()); + updateSublabelGeometry(_content->geometry(), _sublabel->size()); +} + +void CallMuteButton::updateLabelGeometry(QRect my, QSize size) { + const auto skip = st::callMuteButtonSublabelSkip + + st::callMuteButtonLabelsSkip; + _label->moveToLeft( + my.x() + (my.width() - size.width()) / 2 + _labelShakeShift, + my.y() + my.height() - _label->height() - skip, + my.width()); +} + +void CallMuteButton::updateCenterLabelGeometry(QRect my, QSize size) { + const auto skip = (st::callMuteButtonSublabelSkip / 2) + + st::callMuteButtonLabelsSkip; + _centerLabel->moveToLeft( + my.x() + (my.width() - size.width()) / 2 + _labelShakeShift, + my.y() + my.height() - _centerLabel->height() - skip, + my.width()); +} + +void CallMuteButton::updateSublabelGeometry(QRect my, QSize size) { + const auto skip = st::callMuteButtonLabelsSkip; + _sublabel->moveToLeft( + my.x() + (my.width() - size.width()) / 2 + _labelShakeShift, + my.y() + my.height() - _sublabel->height() - skip, + my.width()); +} + +void CallMuteButton::shake() { + if (_shakeAnimation.animating()) { + return; + } + const auto update = [=] { + const auto fullProgress = _shakeAnimation.value(1.) * 6; + const auto segment = std::clamp(int(std::floor(fullProgress)), 0, 5); + const auto part = fullProgress - segment; + const auto from = (segment == 0) + ? 0. + : (segment == 1 || segment == 3 || segment == 5) + ? 1. + : -1.; + const auto to = (segment == 0 || segment == 2 || segment == 4) + ? 1. + : (segment == 1 || segment == 3) + ? -1. + : 0.; + const auto shift = from * (1. - part) + to * part; + _labelShakeShift = int(std::round(shift * st::shakeShift)); + updateLabelsGeometry(); + }; + _shakeAnimation.start( + update, + 0., + 1., + kShiftDuration); +} + +CallMuteButton::HandleMouseState CallMuteButton::HandleMouseStateFromType( + CallMuteButtonType type) { + switch (type) { + case CallMuteButtonType::Active: + case CallMuteButtonType::Muted: + return HandleMouseState::Enabled; + case CallMuteButtonType::Connecting: + return HandleMouseState::Disabled; + case CallMuteButtonType::ForceMuted: + return HandleMouseState::Blocked; + } + Unexpected("Type in HandleMouseStateFromType."); +} + +void CallMuteButton::setState(const CallMuteButtonState &state) { + _state = state; +} + +void CallMuteButton::setLevel(float level) { + _level = level; + _blobs->setLevel(level); +} + +rpl::producer CallMuteButton::clicks() const { + return _content->clicks(); +} + +QSize CallMuteButton::innerSize() const { + return innerGeometry().size(); +} + +QRect CallMuteButton::innerGeometry() const { + const auto &skip = _st.outerRadius; + return QRect( + _content->x(), + _content->y(), + _content->width() - 2 * skip, + _content->width() - 2 * skip); +} + +void CallMuteButton::moveInner(QPoint position) { + const auto &skip = _st.outerRadius; + _content->move(position - QPoint(skip, skip)); + + { + const auto offset = QPoint( + (_blobs->width() - _content->width()) / 2, + (_blobs->height() - _content->width()) / 2); + _blobs->move(_content->pos() - offset); + } +} + +void CallMuteButton::setVisible(bool visible) { + _content->setVisible(visible); + _blobs->setVisible(visible); +} + +void CallMuteButton::raise() { + _blobs->raise(); + _content->raise(); +} + +void CallMuteButton::lower() { + _content->lower(); + _blobs->lower(); +} + +void CallMuteButton::setHandleMouseState(HandleMouseState state) { + if (_handleMouseState == state) { + return; + } + _handleMouseState = state; + const auto handle = (_handleMouseState != HandleMouseState::Disabled); + const auto pointer = (_handleMouseState == HandleMouseState::Enabled); + _content->setAttribute(Qt::WA_TransparentForMouseEvents, !handle); + _content->setPointerCursor(pointer); +} + +void CallMuteButton::overridesColors( + CallMuteButtonType fromType, + CallMuteButtonType toType, + float64 progress) { + const auto forceMutedToConnecting = [](CallMuteButtonType &type) { + if (type == CallMuteButtonType::ForceMuted) { + type = CallMuteButtonType::Connecting; + } + }; + forceMutedToConnecting(toType); + forceMutedToConnecting(fromType); + const auto toInactive = IsInactive(toType); + const auto fromInactive = IsInactive(fromType); + if (toInactive && (progress == 1)) { + _colorOverrides.fire({ std::nullopt, std::nullopt }); + return; + } + auto from = _colors.find(fromType)->second.stops[0].second; + auto to = _colors.find(toType)->second.stops[0].second; + auto fromRipple = from; + auto toRipple = to; + if (!toInactive) { + toRipple.setAlpha(kOverrideColorRippleAlpha); + to.setAlpha(kOverrideColorBgAlpha); + } + if (!fromInactive) { + fromRipple.setAlpha(kOverrideColorRippleAlpha); + from.setAlpha(kOverrideColorBgAlpha); + } + const auto resultBg = anim::color(from, to, progress); + const auto resultRipple = anim::color(fromRipple, toRipple, progress); + _colorOverrides.fire({ resultBg, resultRipple }); +} + +rpl::producer CallMuteButton::colorOverrides() const { + return _colorOverrides.events(); +} + +rpl::lifetime &CallMuteButton::lifetime() { + return _blobs->lifetime(); +} + +CallMuteButton::~CallMuteButton() = default; + +} // namespace Ui diff --git a/ui/widgets/call_mute_button.h b/ui/widgets/call_mute_button.h new file mode 100644 index 0000000..154bfb3 --- /dev/null +++ b/ui/widgets/call_mute_button.h @@ -0,0 +1,126 @@ +// This file is part of Desktop App Toolkit, +// a set of libraries for developing nice desktop applications. +// +// For license and copyright information please follow this link: +// https://github.com/desktop-app/legal/blob/master/LEGAL +// +#pragma once + +#include "base/unique_qptr.h" +#include "ui/effects/animations.h" +#include "ui/effects/cross_line.h" +#include "ui/effects/gradient.h" +#include "ui/effects/radial_animation.h" + +namespace Ui { + +class BlobsWidget; + +class AbstractButton; +class FlatLabel; +class RpWidget; +class AnimatedLabel; + +struct CallButtonColors; + +enum class CallMuteButtonType { + Connecting, + Active, + Muted, + ForceMuted, +}; + +struct CallMuteButtonState { + QString text; + QString subtext; + CallMuteButtonType type = CallMuteButtonType::Connecting; +}; + +class CallMuteButton final { +public: + explicit CallMuteButton( + not_null parent, + rpl::producer &&hideBlobs, + CallMuteButtonState initial = CallMuteButtonState()); + ~CallMuteButton(); + + void setState(const CallMuteButtonState &state); + void setLevel(float level); + [[nodiscard]] rpl::producer clicks() const; + + [[nodiscard]] QSize innerSize() const; + [[nodiscard]] QRect innerGeometry() const; + void moveInner(QPoint position); + + void shake(); + + void setVisible(bool visible); + void show() { + setVisible(true); + } + void hide() { + setVisible(false); + } + void raise(); + void lower(); + + [[nodiscard]] rpl::producer colorOverrides() const; + + [[nodiscard]] rpl::lifetime &lifetime(); + +private: + enum class HandleMouseState { + Enabled, + Blocked, + Disabled, + }; + struct RadialInfo { + std::optional state = std::nullopt; + bool isDirectionToShow = false; + rpl::variable rawShowProgress = 0.; + float64 realShowProgress = 0.; + const style::InfiniteRadialAnimation &st = st::callConnectingRadial; + }; + void init(); + void overridesColors( + CallMuteButtonType fromType, + CallMuteButtonType toType, + float64 progress); + + void setHandleMouseState(HandleMouseState state); + void updateCenterLabelGeometry(QRect my, QSize size); + void updateLabelGeometry(QRect my, QSize size); + void updateSublabelGeometry(QRect my, QSize size); + void updateLabelsGeometry(); + + [[nodiscard]] static HandleMouseState HandleMouseStateFromType( + CallMuteButtonType type); + + rpl::variable _state; + float _level = 0.; + float64 _crossLineProgress = 0.; + QRect _muteIconRect; + HandleMouseState _handleMouseState = HandleMouseState::Enabled; + + const style::CallButton &_st; + + const base::unique_qptr _blobs; + const base::unique_qptr _content; + const base::unique_qptr _centerLabel; + const base::unique_qptr _label; + const base::unique_qptr _sublabel; + int _labelShakeShift = 0; + + RadialInfo _radialInfo; + std::unique_ptr _radial; + const base::flat_map _colors; + + CrossLineAnimation _crossLineMuteAnimation; + Animations::Simple _switchAnimation; + Animations::Simple _shakeAnimation; + + rpl::event_stream _colorOverrides; + +}; + +} // namespace Ui diff --git a/ui/widgets/checkbox.cpp b/ui/widgets/checkbox.cpp index 3822d50..eefca13 100644 --- a/ui/widgets/checkbox.cpp +++ b/ui/widgets/checkbox.cpp @@ -262,7 +262,7 @@ void CheckView::paint(Painter &p, int left, int top, int outerWidth) { { PainterHighQualityEnabler hq(p); - p.drawRoundedRect(style::rtlrect(QRectF(left, top, _st->diameter, _st->diameter).marginsRemoved(QMarginsF(_st->thickness / 2., _st->thickness / 2., _st->thickness / 2., _st->thickness / 2.)), outerWidth), st::buttonRadius - (_st->thickness / 2.), st::buttonRadius - (_st->thickness / 2.)); + p.drawRoundedRect(style::rtlrect(QRectF(left, top, _st->diameter, _st->diameter).marginsRemoved(QMarginsF(_st->thickness / 2., _st->thickness / 2., _st->thickness / 2., _st->thickness / 2.)), outerWidth), st::roundRadiusSmall - (_st->thickness / 2.), st::roundRadiusSmall - (_st->thickness / 2.)); } if (toggled > 0) { diff --git a/ui/widgets/input_fields.cpp b/ui/widgets/input_fields.cpp index 3b87e6d..ab9416b 100644 --- a/ui/widgets/input_fields.cpp +++ b/ui/widgets/input_fields.cpp @@ -1058,7 +1058,7 @@ void FlatInput::paintEvent(QPaintEvent *e) { p.setBrush(anim::brush(_st.bgColor, _st.bgActive, placeholderFocused)); { PainterHighQualityEnabler hq(p); - p.drawRoundedRect(QRectF(0, 0, width(), height()).marginsRemoved(QMarginsF(_st.borderWidth / 2., _st.borderWidth / 2., _st.borderWidth / 2., _st.borderWidth / 2.)), st::buttonRadius - (_st.borderWidth / 2.), st::buttonRadius - (_st.borderWidth / 2.)); + p.drawRoundedRect(QRectF(0, 0, width(), height()).marginsRemoved(QMarginsF(_st.borderWidth / 2., _st.borderWidth / 2., _st.borderWidth / 2., _st.borderWidth / 2.)), st::roundRadiusSmall - (_st.borderWidth / 2.), st::roundRadiusSmall - (_st.borderWidth / 2.)); } if (!_st.icon.empty()) { @@ -3370,7 +3370,7 @@ bool InputField::revertFormatReplace() { void InputField::contextMenuEventInner(QContextMenuEvent *e, QMenu *m) { if (const auto menu = m ? m : _inner->createStandardContextMenu()) { addMarkdownActions(menu, e); - _contextMenu = base::make_unique_q(this, menu); + _contextMenu = base::make_unique_q(this, menu, _st.menu); _contextMenu->popup(e->globalPos()); } } diff --git a/ui/widgets/labels.cpp b/ui/widgets/labels.cpp index a5c58fd..ac5c900 100644 --- a/ui/widgets/labels.cpp +++ b/ui/widgets/labels.cpp @@ -454,13 +454,12 @@ Text::StateResult FlatLabel::dragActionFinish(const QPoint &p, Qt::MouseButton b } } -#if defined Q_OS_UNIX && !defined Q_OS_MAC - if (!_selection.empty()) { + if (QGuiApplication::clipboard()->supportsSelection() + && !_selection.empty()) { TextUtilities::SetClipboardText( _text.toTextForMimeData(_selection), QClipboard::Selection); } -#endif // Q_OS_UNIX && !Q_OS_MAC return state; } diff --git a/ui/widgets/popup_menu.cpp b/ui/widgets/popup_menu.cpp index 164f8af..de300ec 100644 --- a/ui/widgets/popup_menu.cpp +++ b/ui/widgets/popup_menu.cpp @@ -12,11 +12,12 @@ #include "ui/ui_utility.h" #include "ui/delayed_activation.h" #include "base/platform/base_platform_info.h" +#include "base/qt_adapters.h" #include #include +#include #include -#include namespace Ui { @@ -448,7 +449,9 @@ void PopupMenu::popup(const QPoint &p) { } void PopupMenu::showMenu(const QPoint &p, PopupMenu *parent, TriggeredSource source) { - if (!parent && ::Platform::IsMac() && !Platform::IsApplicationActive()) { + const auto screen = base::QScreenNearestTo(p); + if (!screen + || (!parent && ::Platform::IsMac() && !Platform::IsApplicationActive())) { _hiding = false; _a_opacity.stop(); _a_show.stop(); @@ -476,7 +479,7 @@ void PopupMenu::showMenu(const QPoint &p, PopupMenu *parent, TriggeredSource sou && (*_forcedOrigin == Origin::BottomLeft || *_forcedOrigin == Origin::BottomRight); auto w = p - QPoint(0, _padding.top()); - auto r = QApplication::desktop()->screenGeometry(p); + auto r = screen->availableGeometry(); _useTransparency = Platform::TranslucentWindowsSupported(p); setAttribute(Qt::WA_OpaquePaintEvent, !_useTransparency); handleCompositingUpdate(); @@ -535,7 +538,9 @@ PopupMenu::~PopupMenu() { delete submenu; } if (const auto parent = parentWidget()) { - if (QApplication::focusWidget() != nullptr + const auto focused = QApplication::focusWidget(); + if (_reactivateParent + && focused != nullptr && Ui::InFocusChain(parent->window())) { ActivateWindowDelayed(parent); } diff --git a/ui/widgets/popup_menu.h b/ui/widgets/popup_menu.h index 03f8583..ca08f98 100644 --- a/ui/widgets/popup_menu.h +++ b/ui/widgets/popup_menu.h @@ -37,6 +37,9 @@ public: void setDestroyedCallback(Fn callback) { _destroyedCallback = std::move(callback); } + void discardParentReActivate() { + _reactivateParent = false; + } ~PopupMenu(); @@ -122,6 +125,7 @@ private: bool _deleteOnHide = true; bool _triggering = false; bool _deleteLater = false; + bool _reactivateParent = true; Fn _destroyedCallback; diff --git a/ui/widgets/tooltip.cpp b/ui/widgets/tooltip.cpp index 04d0b95..788756c 100644 --- a/ui/widgets/tooltip.cpp +++ b/ui/widgets/tooltip.cpp @@ -9,10 +9,11 @@ #include "ui/ui_utility.h" #include "ui/platform/ui_platform_utility.h" #include "base/invoke_queued.h" +#include "base/qt_adapters.h" #include "styles/style_widgets.h" +#include #include -#include namespace Ui { @@ -72,6 +73,12 @@ Tooltip::~Tooltip() { } void Tooltip::popup(const QPoint &m, const QString &text, const style::Tooltip *st) { + const auto screen = base::QScreenNearestTo(m); + if (!screen) { + Hide(); + return; + } + if (!_isEventFilter) { _isEventFilter = true; QCoreApplication::instance()->installEventFilter(this); @@ -108,7 +115,7 @@ void Tooltip::popup(const QPoint &m, const QString &text, const style::Tooltip * } // adjust tooltip position - QRect r(QApplication::desktop()->screenGeometry(m)); + 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())); } @@ -135,7 +142,7 @@ void Tooltip::paintEvent(QPaintEvent *e) { p.setPen(_st->textBorder); p.setBrush(_st->textBg); PainterHighQualityEnabler hq(p); - p.drawRoundedRect(QRectF(0.5, 0.5, width() - 1., height() - 1.), st::buttonRadius, st::buttonRadius); + p.drawRoundedRect(QRectF(0.5, 0.5, width() - 1., height() - 1.), st::roundRadiusSmall, st::roundRadiusSmall); } else { p.fillRect(rect(), _st->textBg); diff --git a/ui/widgets/widgets.style b/ui/widgets/widgets.style index e69bcf8..737e70f 100644 --- a/ui/widgets/widgets.style +++ b/ui/widgets/widgets.style @@ -182,6 +182,74 @@ ScrollArea { hiding: int; } +Shadow { + left: icon; + topLeft: icon; + top: icon; + topRight: icon; + right: icon; + bottomRight: icon; + bottom: icon; + bottomLeft: icon; + extend: margins; + fallback: color; +} + +PanelAnimation { + startWidth: double; + widthDuration: double; + startHeight: double; + heightDuration: double; + startOpacity: double; + opacityDuration: double; + startFadeTop: double; + fadeHeight: double; + fadeOpacity: double; + fadeBg: color; + shadow: Shadow; +} + +Menu { + skip: pixels; + + itemBg: color; + itemBgOver: color; + itemFg: color; + itemFgOver: color; + itemFgDisabled: color; + itemFgShortcut: color; + itemFgShortcutOver: color; + itemFgShortcutDisabled: color; + itemPadding: margins; + itemIconPosition: point; + itemStyle: TextStyle; + itemToggle: Toggle; + itemToggleOver: Toggle; + itemToggleShift: pixels; + + separatorPadding: margins; + separatorWidth: pixels; + separatorFg: color; + + arrow: icon; + + widthMin: pixels; + widthMax: pixels; + + ripple: RippleAnimation; +} + +PopupMenu { + shadow: Shadow; + scrollPadding: margins; + animation: PanelAnimation; + + menu: Menu; + + duration: int; + showDuration: int; +} + FlatInput { textColor: color; bgColor: color; @@ -231,6 +299,7 @@ InputField { borderActive: pixels; font: font; + menu: PopupMenu; width: pixels; heightMin: pixels; @@ -265,19 +334,6 @@ IconButton { ripple: RippleAnimation; } -Shadow { - left: icon; - topLeft: icon; - top: icon; - topRight: icon; - right: icon; - bottomRight: icon; - bottom: icon; - bottomLeft: icon; - extend: margins; - fallback: color; -} - MediaSlider { width: pixels; activeFg: color; @@ -344,6 +400,14 @@ CrossButton { ripple: RippleAnimation; } +CrossLineAnimation { + fg: color; + icon: icon; + startPosition: point; + endPosition: point; + stroke: pixels; +} + MultiSelectItem { padding: margins; maxWidth: pixels; @@ -379,67 +443,14 @@ MultiSelect { CallButton { button: IconButton; bg: color; + bgSize: pixels; + bgPosition: point; angle: double; outerRadius: pixels; outerBg: color; label: FlatLabel; } -Menu { - skip: pixels; - - itemBg: color; - itemBgOver: color; - itemFg: color; - itemFgOver: color; - itemFgDisabled: color; - itemFgShortcut: color; - itemFgShortcutOver: color; - itemFgShortcutDisabled: color; - itemPadding: margins; - itemIconPosition: point; - itemStyle: TextStyle; - itemToggle: Toggle; - itemToggleOver: Toggle; - itemToggleShift: pixels; - - separatorPadding: margins; - separatorWidth: pixels; - separatorFg: color; - - arrow: icon; - - widthMin: pixels; - widthMax: pixels; - - ripple: RippleAnimation; -} - -PanelAnimation { - startWidth: double; - widthDuration: double; - startHeight: double; - heightDuration: double; - startOpacity: double; - opacityDuration: double; - startFadeTop: double; - fadeHeight: double; - fadeOpacity: double; - fadeBg: color; - shadow: Shadow; -} - -PopupMenu { - shadow: Shadow; - scrollPadding: margins; - animation: PanelAnimation; - - menu: Menu; - - duration: int; - showDuration: int; -} - InnerDropdown { padding: margins; shadow: Shadow; @@ -493,6 +504,7 @@ SettingsButton { textBgOver: color; font: font; + rightLabel: FlatLabel; height: pixels; padding: margins; @@ -614,6 +626,8 @@ defaultRippleAnimation: RippleAnimation { emptyRippleAnimation: RippleAnimation { } +buttonRadius: 4px; + defaultActiveButton: RoundButton { textFg: activeButtonFg; textFgOver: activeButtonFgOver; @@ -709,35 +723,6 @@ defaultFlatInput: FlatInput { phDuration: 100; } -defaultInputField: InputField { - textBg: windowBg; - textFg: windowFg; - textMargins: margins(0px, 26px, 0px, 4px); - textAlign: align(topleft); - - placeholderFg: windowSubTextFg; - placeholderFgActive: windowActiveTextFg; - placeholderFgError: attentionButtonFg; - placeholderMargins: margins(0px, 0px, 0px, 0px); - placeholderAlign: align(topleft); - placeholderScale: 0.9; - placeholderShift: -20px; - placeholderFont: font(semibold 14px); - duration: 150; - - borderFg: inputBorderFg; - borderFgActive: activeLineFg; - borderFgError: activeLineFgError; - - border: 1px; - borderActive: 2px; - - font: boxTextFont; - - heightMin: 52px; - heightMax: 148px; -} - defaultCheckboxIcon: icon {{ "default_checkbox_check", overviewCheckFgActive, point(4px, 7px) }}; defaultCheck: Check { @@ -778,6 +763,7 @@ defaultToggle: Toggle { } defaultCheckbox: Checkbox { textFg: windowFg; + textFgActive: windowFg; width: -44px; margin: margins(8px, 8px, 8px, 8px); @@ -795,83 +781,6 @@ defaultCheckbox: Checkbox { disabledOpacity: 0.5; } -defaultIconButton: IconButton { - iconPosition: point(-1px, -1px); -} - -defaultMultiSelectItem: MultiSelectItem { - padding: margins(6px, 7px, 12px, 0px); - maxWidth: 128px; - height: 32px; - style: defaultTextStyle; - textBg: contactsBgOver; - textFg: windowFg; - textActiveBg: activeButtonBg; - textActiveFg: activeButtonFg; - deleteFg: activeButtonFg; - deleteCross: CrossAnimation { - size: 32px; - skip: 10px; - stroke: 2px; - minScale: 0.3; - } - duration: 150; - minScale: 0.3; -} - -widgetSlideDuration: 200; -widgetFadeDuration: 200; - -fieldSearchIcon: icon {{ "box_search", menuIconFg, point(9px, 8px) }}; -boxFieldSearchIcon: icon {{ "box_search", menuIconFg, point(10px, 9px) }}; - -SettingsSlider { - height: pixels; - barTop: pixels; - barSkip: pixels; - barStroke: pixels; - barFg: color; - barFgActive: color; - labelTop: pixels; - labelStyle: TextStyle; - labelFg: color; - labelFgActive: color; - duration: int; - rippleBottomSkip: pixels; - rippleBg: color; - rippleBgActive: color; - ripple: RippleAnimation; -} - -defaultSettingsSlider: SettingsSlider { - height: 39px; - barTop: 5px; - barSkip: 3px; - barStroke: 3px; - barFg: sliderBgInactive; - barFgActive: sliderBgActive; - labelTop: 17px; - labelStyle: defaultTextStyle; - labelFg: windowActiveTextFg; - labelFgActive: windowActiveTextFg; - duration: 150; -} - -defaultTabsSlider: SettingsSlider(defaultSettingsSlider) { - height: 53px; - barTop: 50px; - barSkip: 0px; - barFg: transparent; - labelTop: 19px; - labelStyle: semiboldTextStyle; - labelFg: windowSubTextFg; - labelFgActive: lightButtonFg; - rippleBottomSkip: 1px; - rippleBg: windowBgOver; - rippleBgActive: lightButtonBgOver; - ripple: defaultRippleAnimation; -} - defaultRoundShadow: Shadow { left: icon {{ "round_shadow_left", windowShadowFg }}; topLeft: icon {{ "round_shadow_top_left", windowShadowFg }}; @@ -902,48 +811,6 @@ defaultPanelAnimation: PanelAnimation { shadow: defaultRoundShadow; } -defaultContinuousSlider: MediaSlider { - width: 3px; - activeFg: mediaPlayerActiveFg; - inactiveFg: mediaPlayerInactiveFg; - activeFgOver: mediaPlayerActiveFg; - inactiveFgOver: mediaPlayerInactiveFg; - activeFgDisabled: mediaPlayerInactiveFg; - inactiveFgDisabled: windowBg; - receivedTillFg: mediaPlayerInactiveFg; - seekSize: size(9px, 9px); - duration: 150; -} - -defaultRoundCheckbox: RoundCheckbox { - border: windowBg; - bgActive: windowBgActive; - width: 2px; - duration: 160; - bgDuration: 0.75; - fgDuration: 1.; -} -defaultPeerListCheckIcon: icon {{ - "default_checkbox_check", - overviewCheckFgActive, - point(3px, 6px) -}}; -defaultPeerListCheck: RoundCheckbox(defaultRoundCheckbox) { - size: 20px; - sizeSmall: 0.3; - bgInactive: overviewCheckBg; - bgActive: overviewCheckBgActive; - check: defaultPeerListCheckIcon; -} -defaultPeerListCheckbox: RoundImageCheckbox { - imageRadius: 21px; - imageSmallRadius: 18px; - selectWidth: 2px; - selectFg: windowBgActive; - selectDuration: 150; - check: defaultPeerListCheck; -} - defaultMenuArrow: icon {{ "dropdown_submenu_arrow", menuSubmenuArrowFg }}; defaultMenuToggle: Toggle(defaultToggle) { untoggledFg: menuIconFg; @@ -991,6 +858,220 @@ defaultPopupMenu: PopupMenu { duration: 150; showDuration: 200; } + +defaultInputField: InputField { + textBg: windowBg; + textFg: windowFg; + textMargins: margins(0px, 26px, 0px, 4px); + textAlign: align(topleft); + + placeholderFg: windowSubTextFg; + placeholderFgActive: windowActiveTextFg; + placeholderFgError: attentionButtonFg; + placeholderMargins: margins(0px, 0px, 0px, 0px); + placeholderAlign: align(topleft); + placeholderScale: 0.9; + placeholderShift: -20px; + placeholderFont: font(semibold 14px); + duration: 150; + + borderFg: inputBorderFg; + borderFgActive: activeLineFg; + borderFgError: activeLineFgError; + + border: 1px; + borderActive: 2px; + + font: boxTextFont; + menu: defaultPopupMenu; + + heightMin: 52px; + heightMax: 148px; +} + +defaultIconButton: IconButton { + iconPosition: point(-1px, -1px); +} + +defaultMultiSelectItem: MultiSelectItem { + padding: margins(6px, 7px, 12px, 0px); + maxWidth: 128px; + height: 32px; + style: defaultTextStyle; + textBg: contactsBgOver; + textFg: windowFg; + textActiveBg: activeButtonBg; + textActiveFg: activeButtonFg; + deleteFg: activeButtonFg; + deleteCross: CrossAnimation { + size: 32px; + skip: 10px; + stroke: 2px; + minScale: 0.3; + } + duration: 150; + minScale: 0.3; +} + +defaultMultiSelectSearchField: InputField(defaultInputField) { + textBg: transparent; + textMargins: margins(2px, 7px, 2px, 0px); + + placeholderFg: placeholderFg; + placeholderFgActive: placeholderFgActive; + placeholderFgError: placeholderFgActive; + placeholderMargins: margins(2px, 0px, 2px, 0px); + placeholderScale: 0.; + placeholderFont: normalFont; + + border: 0px; + borderActive: 0px; + + heightMin: 32px; + + font: normalFont; +} + +fieldSearchIcon: icon {{ "box_search", menuIconFg, point(9px, 8px) }}; +boxFieldSearchIcon: icon {{ "box_search", menuIconFg, point(10px, 9px) }}; + +defaultMultiSelectSearchCancel: CrossButton { + width: 44px; + height: 44px; + + cross: CrossAnimation { + size: 36px; + skip: 12px; + stroke: 2px; + minScale: 0.3; + } + crossFg: boxTitleCloseFg; + crossFgOver: boxTitleCloseFgOver; + crossPosition: point(4px, 4px); + + duration: 150; + loadingPeriod: 1000; + ripple: RippleAnimation(defaultRippleAnimation) { + color: windowBgOver; + } +} +defaultMultiSelect: MultiSelect { + bg: boxSearchBg; + padding: margins(8px, 6px, 8px, 6px); + maxHeight: 104px; + scroll: ScrollArea(defaultSolidScroll) { + deltat: 3px; + deltab: 3px; + round: 1px; + width: 8px; + deltax: 3px; + hiding: 1000; + } + + item: defaultMultiSelectItem; + itemSkip: 8px; + + field: defaultMultiSelectSearchField; + fieldMinWidth: 42px; + fieldIcon: boxFieldSearchIcon; + fieldIconSkip: 36px; + + fieldCancel: defaultMultiSelectSearchCancel; + fieldCancelSkip: 40px; +} + +widgetSlideDuration: 200; +widgetFadeDuration: 200; + +SettingsSlider { + height: pixels; + barTop: pixels; + barSkip: pixels; + barStroke: pixels; + barFg: color; + barFgActive: color; + labelTop: pixels; + labelStyle: TextStyle; + labelFg: color; + labelFgActive: color; + duration: int; + rippleBottomSkip: pixels; + rippleBg: color; + rippleBgActive: color; + ripple: RippleAnimation; +} + +defaultSettingsSlider: SettingsSlider { + height: 39px; + barTop: 5px; + barSkip: 3px; + barStroke: 3px; + barFg: sliderBgInactive; + barFgActive: sliderBgActive; + labelTop: 17px; + labelStyle: defaultTextStyle; + labelFg: windowActiveTextFg; + labelFgActive: windowActiveTextFg; + duration: 150; +} + +defaultTabsSlider: SettingsSlider(defaultSettingsSlider) { + height: 53px; + barTop: 50px; + barSkip: 0px; + barFg: transparent; + labelTop: 19px; + labelStyle: semiboldTextStyle; + labelFg: windowSubTextFg; + labelFgActive: lightButtonFg; + rippleBottomSkip: 1px; + rippleBg: windowBgOver; + rippleBgActive: lightButtonBgOver; + ripple: defaultRippleAnimation; +} + +defaultContinuousSlider: MediaSlider { + width: 3px; + activeFg: mediaPlayerActiveFg; + inactiveFg: mediaPlayerInactiveFg; + activeFgOver: mediaPlayerActiveFg; + inactiveFgOver: mediaPlayerInactiveFg; + activeFgDisabled: mediaPlayerInactiveFg; + inactiveFgDisabled: windowBg; + receivedTillFg: mediaPlayerInactiveFg; + seekSize: size(9px, 9px); + duration: 150; +} + +defaultRoundCheckbox: RoundCheckbox { + border: windowBg; + bgActive: windowBgActive; + width: 2px; + duration: 160; + bgDuration: 0.75; + fgDuration: 1.; +} +defaultPeerListCheckIcon: icon {{ + "default_checkbox_check", + overviewCheckFgActive, + point(3px, 6px) +}}; +defaultPeerListCheck: RoundCheckbox(defaultRoundCheckbox) { + size: 20px; + sizeSmall: 0.3; + bgInactive: overviewCheckBg; + bgActive: overviewCheckBgActive; + check: defaultPeerListCheckIcon; +} +defaultPeerListCheckbox: RoundImageCheckbox { + imageRadius: 21px; + imageSmallRadius: 18px; + selectWidth: 2px; + selectFg: windowBgActive; + selectDuration: 150; + check: defaultPeerListCheck; +} + defaultInnerDropdown: InnerDropdown { padding: margins(10px, 10px, 10px, 10px); shadow: defaultRoundShadow; @@ -1117,12 +1198,15 @@ PeerListItem { chatNamePosition: point; chatDescPosition: point; nameStyle: TextStyle; + nameFg: color; + nameFgChecked: color; statusPosition: point; photoSize: pixels; maximalWidth: pixels; button: OutlineButton; checkbox: RoundImageCheckbox; + disabledCheckFg: color; statusFg: color; statusFgOver: color; statusFgActive: color; @@ -1130,6 +1214,8 @@ PeerListItem { PeerList { padding: margins; + bg: color; + about: FlatLabel; item: PeerListItem; } @@ -1155,17 +1241,37 @@ defaultPeerListItem: PeerListItem { linkFont: semiboldFont; linkFontOver: semiboldFont; } + nameFg: contactsNameFg; + nameFgChecked: windowActiveTextFg; statusPosition: point(68px, 31px); photoSize: 46px; button: defaultPeerListButton; checkbox: defaultPeerListCheckbox; + disabledCheckFg: menuIconFg; statusFg: windowSubTextFg; statusFgOver: windowSubTextFgOver; statusFgActive: windowActiveTextFg; } +boxTextStyle: TextStyle(defaultTextStyle) { + font: font(boxFontSize); + linkFont: font(boxFontSize); + linkFontOver: font(boxFontSize underline); +} + +defaultPeerListAbout: FlatLabel(defaultFlatLabel) { + minWidth: 240px; + textFg: membersAboutLimitFg; + align: align(top); + style: TextStyle(boxTextStyle) { + lineHeight: 22px; + } +} + defaultPeerList: PeerList { padding: margins(0px, 0px, 0px, 0px); + bg: contactsBg; + about: defaultPeerListAbout; item: defaultPeerListItem; } @@ -1252,6 +1358,11 @@ defaultSettingsToggle: Toggle(defaultToggle) { defaultSettingsToggleOver: Toggle(defaultSettingsToggle) { untoggledFg: menuIconFgOver; } +defaultSettingsRightLabel: FlatLabel(defaultFlatLabel) { + textFg: windowActiveTextFg; + style: boxTextStyle; + maxHeight: 20px; +} defaultSettingsButton: SettingsButton { textFg: windowBoldFg; textFgOver: windowBoldFgOver; @@ -1259,6 +1370,7 @@ defaultSettingsButton: SettingsButton { textBgOver: windowBgOver; font: boxTextFont; + rightLabel: defaultSettingsRightLabel; height: 20px; padding: margins(22px, 10px, 22px, 8px); @@ -1297,6 +1409,75 @@ defaultToast: Toast { durationSlide: 160; } +callMuteMainBlobMinRadius: 57px; +callMuteMainBlobMaxRadius: 63px; +callMuteMinorBlobMinRadius: 64px; +callMuteMinorBlobMaxRadius: 74px; +callMuteMajorBlobMinRadius: 67px; +callMuteMajorBlobMaxRadius: 77px; + +callMuteButtonActiveIcon: icon {{ "calls/voice_unmuted_large", groupCallIconFg }}; +callMuteButtonActiveInner: IconButton { + width: 136px; + height: 165px; + + iconPosition: point(-1px, 50px); + icon: callMuteButtonActiveIcon; +} +callMuteButtonLabel: FlatLabel(defaultFlatLabel) { + textFg: groupCallMembersFg; + style: TextStyle(defaultTextStyle) { + font: font(14px); + linkFont: font(14px); + linkFontOver: font(14px underline); + } +} +callMuteButtonSublabel: FlatLabel(defaultFlatLabel) { + textFg: groupCallMemberNotJoinedStatus; +} +callMuteButtonLabelsSkip: 5px; +callMuteButtonSublabelSkip: 19px; +callMuteButtonActive: CallButton { + button: callMuteButtonActiveInner; + bg: groupCallLive1; + bgSize: 100px; + bgPosition: point(18px, 18px); + outerRadius: 18px; + outerBg: callAnswerBgOuter; + label: callMuteButtonLabel; +} +callMuteButtonMuted: CallButton(callMuteButtonActive) { + button: IconButton(callMuteButtonActiveInner) { + icon: icon {{ "calls/voice_muted_large", groupCallIconFg }}; + } + bg: groupCallMuted1; + label: callMuteButtonLabel; +} +callMuteButtonConnecting: CallButton(callMuteButtonMuted) { + button: IconButton(callMuteButtonActiveInner) { + icon: icon {{ "calls/voice_muted_large", groupCallIconFg }}; + } + bg: callIconBg; + label: callMuteButtonLabel; +} +callMuteButtonLabelAdditional: 5px; + +callMuteCrossLine: CrossLineAnimation { + fg: groupCallIconFg; + icon: callMuteButtonActiveIcon; + startPosition: point(7px, 2px); + endPosition: point(34px, 30px); + stroke: 4px; +} + +callConnectingRadial: InfiniteRadialAnimation(defaultInfiniteRadialAnimation) { + color: lightButtonFg; + thickness: 4px; + size: size(100px, 100px); +} + +shakeShift: 4px; + // Windows specific title windowTitleButton: IconButton {