diff --git a/gyp/sources.txt b/gyp/sources.txt index 1cecbff..8169d48 100644 --- a/gyp/sources.txt +++ b/gyp/sources.txt @@ -10,10 +10,20 @@ <(src_loc)/ui/effects/numbers_animation.h <(src_loc)/ui/effects/panel_animation.cpp <(src_loc)/ui/effects/panel_animation.h +<(src_loc)/ui/effects/radial_animation.cpp +<(src_loc)/ui/effects/radial_animation.h <(src_loc)/ui/effects/ripple_animation.cpp <(src_loc)/ui/effects/ripple_animation.h <(src_loc)/ui/image/image_prepare.cpp <(src_loc)/ui/image/image_prepare.h +<(src_loc)/ui/layers/box_content.cpp +<(src_loc)/ui/layers/box_content.h +<(src_loc)/ui/layers/box_layer_widget.cpp +<(src_loc)/ui/layers/box_layer_widget.h +<(src_loc)/ui/layers/generic_box.cpp +<(src_loc)/ui/layers/generic_box.h +<(src_loc)/ui/layers/layer_widget.cpp +<(src_loc)/ui/layers/layer_widget.h <(src_loc)/ui/platform/ui_platform_utility.h <(src_loc)/ui/platform/linux/ui_platform_utility_linux.cpp <(src_loc)/ui/platform/linux/ui_platform_utility_linux.h @@ -44,6 +54,8 @@ <(src_loc)/ui/text/text_isolated_emoji.h <(src_loc)/ui/text/text_utilities.cpp <(src_loc)/ui/text/text_utilities.h +<(src_loc)/ui/widgets/box_content_divider.cpp +<(src_loc)/ui/widgets/box_content_divider.h <(src_loc)/ui/widgets/buttons.cpp <(src_loc)/ui/widgets/buttons.h <(src_loc)/ui/widgets/checkbox.cpp diff --git a/icons/box_divider_bottom.png b/icons/box_divider_bottom.png new file mode 100644 index 0000000..31fee84 Binary files /dev/null and b/icons/box_divider_bottom.png differ diff --git a/icons/box_divider_bottom@2x.png b/icons/box_divider_bottom@2x.png new file mode 100644 index 0000000..87fb656 Binary files /dev/null and b/icons/box_divider_bottom@2x.png differ diff --git a/icons/box_divider_bottom@3x.png b/icons/box_divider_bottom@3x.png new file mode 100644 index 0000000..99d8729 Binary files /dev/null and b/icons/box_divider_bottom@3x.png differ diff --git a/icons/box_divider_top.png b/icons/box_divider_top.png new file mode 100644 index 0000000..6dcdc14 Binary files /dev/null and b/icons/box_divider_top.png differ diff --git a/icons/box_divider_top@2x.png b/icons/box_divider_top@2x.png new file mode 100644 index 0000000..5bc32a7 Binary files /dev/null and b/icons/box_divider_top@2x.png differ diff --git a/icons/box_divider_top@3x.png b/icons/box_divider_top@3x.png new file mode 100644 index 0000000..938e1e1 Binary files /dev/null and b/icons/box_divider_top@3x.png differ diff --git a/lib_ui.gyp b/lib_ui.gyp index 7507026..38933ab 100644 --- a/lib_ui.gyp +++ b/lib_ui.gyp @@ -34,6 +34,7 @@ 'style_files': [ '<(src_loc)/ui/colors.palette', '<(src_loc)/ui/basic.style', + '<(src_loc)/ui/layers/layers.style', '<(src_loc)/ui/widgets/widgets.style', ], 'dependent_style_files': [ diff --git a/ui/effects/radial_animation.cpp b/ui/effects/radial_animation.cpp new file mode 100644 index 0000000..3a3ff77 --- /dev/null +++ b/ui/effects/radial_animation.cpp @@ -0,0 +1,309 @@ +// 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/radial_animation.h" + +#include "ui/painter.h" +#include "styles/style_widgets.h" + +namespace Ui { +namespace { + +constexpr auto kFullArcLength = 360 * 16; +constexpr auto kQuarterArcLength = (kFullArcLength / 4); +constexpr auto kMinArcLength = (kFullArcLength / 360); +constexpr auto kAlmostFullArcLength = (kFullArcLength - kMinArcLength); + +} // namespace + +const int RadialState::kFull = kFullArcLength; + +void RadialAnimation::start(float64 prg) { + _firstStart = _lastStart = _lastTime = crl::now(); + const auto iprg = qRound(qMax(prg, 0.0001) * kAlmostFullArcLength); + const auto iprgstrict = qRound(prg * kAlmostFullArcLength); + _arcEnd = anim::value(iprgstrict, iprg); + _animation.start(); +} + +bool RadialAnimation::update(float64 prg, bool finished, crl::time ms) { + const auto iprg = qRound(qMax(prg, 0.0001) * kAlmostFullArcLength); + const auto result = (iprg != qRound(_arcEnd.to())); + if (_finished != finished) { + _arcEnd.start(iprg); + _finished = finished; + _lastStart = _lastTime; + } else if (result) { + _arcEnd.start(iprg); + _lastStart = _lastTime; + } + _lastTime = ms; + + const auto dt = float64(ms - _lastStart); + const auto fulldt = float64(ms - _firstStart); + const auto opacitydt = _finished + ? (_lastStart - _firstStart) + : fulldt; + _opacity = qMin(opacitydt / st::radialDuration, 1.); + if (anim::Disabled()) { + _arcEnd.update(1., anim::linear); + if (finished) { + stop(); + } + } else if (!finished) { + _arcEnd.update(1. - (st::radialDuration / (st::radialDuration + dt)), anim::linear); + } else if (dt >= st::radialDuration) { + _arcEnd.update(1., anim::linear); + stop(); + } else { + auto r = dt / st::radialDuration; + _arcEnd.update(r, anim::linear); + _opacity *= 1 - r; + } + auto fromstart = fulldt / st::radialPeriod; + _arcStart.update(fromstart - std::floor(fromstart), anim::linear); + return result; +} + +void RadialAnimation::stop() { + _firstStart = _lastStart = _lastTime = 0; + _arcEnd = anim::value(); + _animation.stop(); +} + +void RadialAnimation::draw( + QPainter &p, + const QRect &inner, + int32 thickness, + style::color color) const { + const auto state = computeState(); + + auto o = p.opacity(); + p.setOpacity(o * state.shown); + + auto pen = color->p; + auto was = p.pen(); + pen.setWidth(thickness); + pen.setCapStyle(Qt::RoundCap); + p.setPen(pen); + + { + PainterHighQualityEnabler hq(p); + p.drawArc(inner, state.arcFrom, state.arcLength); + } + + p.setPen(was); + p.setOpacity(o); +} + +RadialState RadialAnimation::computeState() const { + auto length = kMinArcLength + qRound(_arcEnd.current()); + auto from = kQuarterArcLength + - length + - (anim::Disabled() ? 0 : qRound(_arcStart.current())); + if (style::RightToLeft()) { + from = kQuarterArcLength - (from - kQuarterArcLength) - length; + if (from < 0) from += kFullArcLength; + } + return { _opacity, from, length }; +} + +void InfiniteRadialAnimation::start(crl::time skip) { + const auto now = crl::now(); + if (_workFinished <= now && (_workFinished || !_workStarted)) { + _workStarted = std::max(now + _st.sineDuration - skip, crl::time(1)); + _workFinished = 0; + } + if (!_animation.animating()) { + _animation.start(); + } +} + +void InfiniteRadialAnimation::stop(anim::type animated) { + const auto now = crl::now(); + if (anim::Disabled() || animated == anim::type::instant) { + _workFinished = now; + } + if (!_workFinished) { + const auto zero = _workStarted - _st.sineDuration; + const auto index = (now - zero + _st.sinePeriod - _st.sineShift) + / _st.sinePeriod; + _workFinished = zero + + _st.sineShift + + (index * _st.sinePeriod) + + _st.sineDuration; + } else if (_workFinished <= now) { + _animation.stop(); + } +} + +void InfiniteRadialAnimation::draw( + QPainter &p, + QPoint position, + int outerWidth) { + draw(p, position, _st.size, outerWidth); +} + +void InfiniteRadialAnimation::draw( + QPainter &p, + QPoint position, + QSize size, + int outerWidth) { + const auto state = computeState(); + + auto o = p.opacity(); + p.setOpacity(o * state.shown); + + const auto rect = style::rtlrect( + position.x(), + position.y(), + size.width(), + size.height(), + outerWidth); + const auto was = p.pen(); + const auto brush = p.brush(); + if (anim::Disabled()) { + anim::DrawStaticLoading(p, rect, _st.thickness, _st.color); + } else { + auto pen = _st.color->p; + pen.setWidth(_st.thickness); + pen.setCapStyle(Qt::RoundCap); + p.setPen(pen); + + { + PainterHighQualityEnabler hq(p); + p.drawArc( + rect, + state.arcFrom, + state.arcLength); + } + } + p.setPen(was); + p.setBrush(brush); + p.setOpacity(o); +} + +RadialState InfiniteRadialAnimation::computeState() { + const auto now = crl::now(); + const auto linear = kFullArcLength + - int(((now * kFullArcLength) / _st.linearPeriod) % kFullArcLength); + if (!_workStarted || (_workFinished && _workFinished <= now)) { + const auto shown = 0.; + _animation.stop(); + return { + shown, + linear, + kFullArcLength }; + } + if (anim::Disabled()) { + const auto shown = 1.; + return { 1., 0, kFullArcLength }; + } + const auto min = int(std::round(kFullArcLength * _st.arcMin)); + const auto max = int(std::round(kFullArcLength * _st.arcMax)); + if (now <= _workStarted) { + // zero .. _workStarted + const auto zero = _workStarted - _st.sineDuration; + const auto shown = (now - zero) / float64(_st.sineDuration); + const auto length = anim::interpolate( + kFullArcLength, + min, + anim::sineInOut(1., std::clamp(shown, 0., 1.))); + return { + shown, + linear, + length }; + } else if (!_workFinished || now <= _workFinished - _st.sineDuration) { + // _workStared .. _workFinished - _st.sineDuration + const auto shown = 1.; + const auto cycles = (now - _workStarted) / _st.sinePeriod; + const auto relative = (now - _workStarted) % _st.sinePeriod; + const auto smallDuration = _st.sineShift - _st.sineDuration; + const auto largeDuration = _st.sinePeriod + - _st.sineShift + - _st.sineDuration; + const auto basic = int((linear + + min + + (cycles * (kFullArcLength + min - max))) % kFullArcLength); + if (relative <= smallDuration) { + // localZero .. growStart + return { + shown, + basic - min, + min }; + } else if (relative <= smallDuration + _st.sineDuration) { + // growStart .. growEnd + const auto growLinear = (relative - smallDuration) / + float64(_st.sineDuration); + const auto growProgress = anim::sineInOut(1., growLinear); + const auto length = anim::interpolate(min, max, growProgress); + return { + shown, + basic - length, + length }; + } else if (relative <= _st.sinePeriod - _st.sineDuration) { + // growEnd .. shrinkStart + return { + shown, + basic - max, + max }; + } else { + // shrinkStart .. shrinkEnd + const auto shrinkLinear = (relative + - (_st.sinePeriod - _st.sineDuration)) + / float64(_st.sineDuration); + const auto shrinkProgress = anim::sineInOut(1., shrinkLinear); + const auto shrink = anim::interpolate( + 0, + max - min, + shrinkProgress); + return { + shown, + basic - max, + max - shrink }; // interpolate(max, min, shrinkProgress) + } + } else { + // _workFinished - _st.sineDuration .. _workFinished + const auto hidden = (now - (_workFinished - _st.sineDuration)) + / float64(_st.sineDuration); + const auto cycles = (_workFinished - _workStarted) / _st.sinePeriod; + const auto basic = int((linear + + min + + cycles * (kFullArcLength + min - max)) % kFullArcLength); + const auto length = anim::interpolate( + min, + kFullArcLength, + anim::sineInOut(1., std::clamp(hidden, 0., 1.))); + return { + 1. - hidden, + basic - length, + length }; + } + //const auto frontPeriods = time / st.sinePeriod; + //const auto frontCurrent = time % st.sinePeriod; + //const auto frontProgress = anim::sineInOut( + // st.arcMax - st.arcMin, + // std::min(frontCurrent, crl::time(st.sineDuration)) + // / float64(st.sineDuration)); + //const auto backTime = std::max(time - st.sineShift, 0LL); + //const auto backPeriods = backTime / st.sinePeriod; + //const auto backCurrent = backTime % st.sinePeriod; + //const auto backProgress = anim::sineInOut( + // st.arcMax - st.arcMin, + // std::min(backCurrent, crl::time(st.sineDuration)) + // / float64(st.sineDuration)); + //const auto front = linear + std::round((st.arcMin + frontProgress + frontPeriods * (st.arcMax - st.arcMin)) * kFullArcLength); + //const auto from = linear + std::round((backProgress + backPeriods * (st.arcMax - st.arcMin)) * kFullArcLength); + //const auto length = (front - from); + + //return { + // _opacity, + // from, + // length + //}; +} + +} // namespace Ui diff --git a/ui/effects/radial_animation.h b/ui/effects/radial_animation.h new file mode 100644 index 0000000..2581372 --- /dev/null +++ b/ui/effects/radial_animation.h @@ -0,0 +1,110 @@ +// 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/animations.h" + +namespace style { +struct InfiniteRadialAnimation; +} // namespace style + +namespace Ui { + +struct RadialState { + static const int kFull; + + float64 shown = 0.; + int arcFrom = 0; + int arcLength = kFull; +}; + +class RadialAnimation { +public: + template + RadialAnimation(Callback &&callback); + + float64 opacity() const { + return _opacity; + } + bool animating() const { + return _animation.animating(); + } + + void start(float64 prg); + bool update(float64 prg, bool finished, crl::time ms); + void stop(); + + void draw( + QPainter &p, + const QRect &inner, + int32 thickness, + style::color color) const; + + RadialState computeState() const; + +private: + crl::time _firstStart = 0; + crl::time _lastStart = 0; + crl::time _lastTime = 0; + float64 _opacity = 0.; + anim::value _arcEnd; + anim::value _arcStart; + Ui::Animations::Basic _animation; + bool _finished = false; + +}; + +template +inline RadialAnimation::RadialAnimation(Callback &&callback) +: _arcStart(0, RadialState::kFull) +, _animation(std::forward(callback)) { +} + + +class InfiniteRadialAnimation { +public: + template + InfiniteRadialAnimation( + Callback &&callback, + const style::InfiniteRadialAnimation &st); + + bool animating() const { + return _animation.animating(); + } + + void start(crl::time skip = 0); + void stop(anim::type animated = anim::type::normal); + + void draw( + QPainter &p, + QPoint position, + int outerWidth); + void draw( + QPainter &p, + QPoint position, + QSize size, + int outerWidth); + + RadialState computeState(); + +private: + const style::InfiniteRadialAnimation &_st; + crl::time _workStarted = 0; + crl::time _workFinished = 0; + Ui::Animations::Basic _animation; + +}; + +template +inline InfiniteRadialAnimation::InfiniteRadialAnimation( + Callback &&callback, + const style::InfiniteRadialAnimation &st) +: _st(st) +, _animation(std::forward(callback)) { +} + +} // namespace Ui diff --git a/ui/layers/box_content.cpp b/ui/layers/box_content.cpp new file mode 100644 index 0000000..6eeca0e --- /dev/null +++ b/ui/layers/box_content.cpp @@ -0,0 +1,254 @@ +// 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/layers/box_content.h" + +#include "ui/widgets/buttons.h" +#include "ui/widgets/scroll_area.h" +#include "ui/widgets/labels.h" +#include "ui/widgets/shadow.h" +#include "ui/wrap/fade_wrap.h" +#include "ui/text/text_utilities.h" +#include "ui/painter.h" +#include "base/timer.h" +#include "styles/style_layers.h" +#include "styles/palette.h" + +namespace Ui { + +void BoxContent::setTitle(rpl::producer title) { + getDelegate()->setTitle(std::move(title) | Text::ToWithEntities()); +} + +QPointer BoxContent::addButton( + rpl::producer text, + Fn clickCallback) { + return addButton( + std::move(text), + std::move(clickCallback), + st::defaultBoxButton); +} + +QPointer BoxContent::addLeftButton( + rpl::producer text, + Fn clickCallback) { + return getDelegate()->addLeftButton( + std::move(text), + std::move(clickCallback), + st::defaultBoxButton); +} + +void BoxContent::setInner(object_ptr inner) { + setInner(std::move(inner), st::boxLayerScroll); +} + +void BoxContent::setInner(object_ptr inner, const style::ScrollArea &st) { + if (inner) { + getDelegate()->setLayerType(true); + _scroll.create(this, st); + _scroll->setGeometryToLeft(0, _innerTopSkip, width(), 0); + _scroll->setOwnedWidget(std::move(inner)); + if (_topShadow) { + _topShadow->raise(); + _bottomShadow->raise(); + } else { + _topShadow.create(this); + _bottomShadow.create(this); + } + if (!_preparing) { + // We didn't set dimensions yet, this will be called from finishPrepare(); + finishScrollCreate(); + } + } else { + getDelegate()->setLayerType(false); + _scroll.destroyDelayed(); + _topShadow.destroyDelayed(); + _bottomShadow.destroyDelayed(); + } +} + +void BoxContent::finishPrepare() { + _preparing = false; + if (_scroll) { + finishScrollCreate(); + } + setInnerFocus(); +} + +void BoxContent::finishScrollCreate() { + Expects(_scroll != nullptr); + + if (!_scroll->isHidden()) { + _scroll->show(); + } + updateScrollAreaGeometry(); + connect(_scroll, SIGNAL(scrolled()), this, SLOT(onScroll())); + connect(_scroll, SIGNAL(innerResized()), this, SLOT(onInnerResize())); +} + +void BoxContent::scrollToWidget(not_null widget) { + if (_scroll) { + _scroll->scrollToWidget(widget); + } +} + +void BoxContent::onScrollToY(int top, int bottom) { + if (_scroll) { + _scroll->scrollToY(top, bottom); + } +} + +void BoxContent::onDraggingScrollDelta(int delta) { + _draggingScrollDelta = _scroll ? delta : 0; + if (_draggingScrollDelta) { + if (!_draggingScrollTimer) { + _draggingScrollTimer.create(this); + _draggingScrollTimer->setSingleShot(false); + connect(_draggingScrollTimer, SIGNAL(timeout()), this, SLOT(onDraggingScrollTimer())); + } + _draggingScrollTimer->start(15); + } else { + _draggingScrollTimer.destroy(); + } +} + +void BoxContent::onDraggingScrollTimer() { + auto delta = (_draggingScrollDelta > 0) ? qMin(_draggingScrollDelta * 3 / 20 + 1, int32(kMaxScrollSpeed)) : qMax(_draggingScrollDelta * 3 / 20 - 1, -int32(kMaxScrollSpeed)); + _scroll->scrollToY(_scroll->scrollTop() + delta); +} + +void BoxContent::updateInnerVisibleTopBottom() { + if (auto widget = static_cast(_scroll ? _scroll->widget() : nullptr)) { + auto top = _scroll->scrollTop(); + widget->setVisibleTopBottom(top, top + _scroll->height()); + } +} + +void BoxContent::updateShadowsVisibility() { + if (!_scroll) return; + + auto top = _scroll->scrollTop(); + _topShadow->toggle( + (top > 0 || _innerTopSkip > 0), + anim::type::normal); + _bottomShadow->toggle( + (top < _scroll->scrollTopMax() || _innerBottomSkip > 0), + anim::type::normal); +} + +void BoxContent::onScroll() { + updateInnerVisibleTopBottom(); + updateShadowsVisibility(); +} + +void BoxContent::onInnerResize() { + updateInnerVisibleTopBottom(); + updateShadowsVisibility(); +} + +void BoxContent::setDimensionsToContent( + int newWidth, + not_null content) { + content->resizeToWidth(newWidth); + content->heightValue( + ) | rpl::start_with_next([=](int height) { + setDimensions(newWidth, height); + }, content->lifetime()); +} + +void BoxContent::setInnerTopSkip(int innerTopSkip, bool scrollBottomFixed) { + if (_innerTopSkip != innerTopSkip) { + auto delta = innerTopSkip - _innerTopSkip; + _innerTopSkip = innerTopSkip; + if (_scroll && width() > 0) { + auto scrollTopWas = _scroll->scrollTop(); + updateScrollAreaGeometry(); + if (scrollBottomFixed) { + _scroll->scrollToY(scrollTopWas + delta); + } + } + } +} + +void BoxContent::setInnerBottomSkip(int innerBottomSkip) { + if (_innerBottomSkip != innerBottomSkip) { + auto delta = innerBottomSkip - _innerBottomSkip; + _innerBottomSkip = innerBottomSkip; + if (_scroll && width() > 0) { + updateScrollAreaGeometry(); + } + } +} + +void BoxContent::setInnerVisible(bool scrollAreaVisible) { + if (_scroll) { + _scroll->setVisible(scrollAreaVisible); + } +} + +QPixmap BoxContent::grabInnerCache() { + auto isTopShadowVisible = !_topShadow->isHidden(); + auto isBottomShadowVisible = !_bottomShadow->isHidden(); + if (isTopShadowVisible) _topShadow->setVisible(false); + if (isBottomShadowVisible) _bottomShadow->setVisible(false); + auto result = GrabWidget(this, _scroll->geometry()); + if (isTopShadowVisible) _topShadow->setVisible(true); + if (isBottomShadowVisible) _bottomShadow->setVisible(true); + return result; +} + +void BoxContent::resizeEvent(QResizeEvent *e) { + if (_scroll) { + updateScrollAreaGeometry(); + } +} + +void BoxContent::keyPressEvent(QKeyEvent *e) { + if (e->key() == Qt::Key_Escape && !_closeByEscape) { + e->accept(); + } else { + RpWidget::keyPressEvent(e); + } +} + +void BoxContent::updateScrollAreaGeometry() { + auto newScrollHeight = height() - _innerTopSkip - _innerBottomSkip; + auto changed = (_scroll->height() != newScrollHeight); + _scroll->setGeometryToLeft(0, _innerTopSkip, width(), newScrollHeight); + _topShadow->entity()->resize(width(), st::lineWidth); + _topShadow->moveToLeft(0, _innerTopSkip); + _bottomShadow->entity()->resize(width(), st::lineWidth); + _bottomShadow->moveToLeft( + 0, + height() - _innerBottomSkip - st::lineWidth); + if (changed) { + updateInnerVisibleTopBottom(); + + auto top = _scroll->scrollTop(); + _topShadow->toggle( + (top > 0 || _innerTopSkip > 0), + anim::type::instant); + _bottomShadow->toggle( + (top < _scroll->scrollTopMax() || _innerBottomSkip > 0), + anim::type::instant); + } +} + +object_ptr BoxContent::doTakeInnerWidget() { + return _scroll->takeWidget(); +} + +void BoxContent::paintEvent(QPaintEvent *e) { + Painter p(this); + + if (testAttribute(Qt::WA_OpaquePaintEvent)) { + for (auto rect : e->region().rects()) { + p.fillRect(rect, st::boxBg); + } + } +} + +} // namespace Ui diff --git a/ui/layers/box_content.h b/ui/layers/box_content.h new file mode 100644 index 0000000..9f21c0a --- /dev/null +++ b/ui/layers/box_content.h @@ -0,0 +1,341 @@ +// 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 "base/flags.h" +#include "ui/wrap/padding_wrap.h" +#include "ui/widgets/labels.h" +#include "ui/layers/layer_widget.h" +#include "ui/effects/animation_value.h" +#include "ui/text/text_entity.h" +#include "ui/rp_widget.h" + +namespace style { +struct RoundButton; +struct IconButton; +struct ScrollArea; +} // namespace style + +namespace Ui { +class GenericBox; +} // namespace Ui + +template +inline object_ptr Box(Args &&...args) { + const auto parent = static_cast(nullptr); + return object_ptr(parent, std::forward(args)...); +} + +namespace Ui { + +class RoundButton; +class IconButton; +class ScrollArea; +class FlatLabel; +class FadeShadow; +class BoxContent; + +class BoxContentDelegate { +public: + virtual void setLayerType(bool layerType) = 0; + virtual void setTitle(rpl::producer title) = 0; + virtual void setAdditionalTitle(rpl::producer additional) = 0; + virtual void setCloseByOutsideClick(bool close) = 0; + + virtual void clearButtons() = 0; + virtual QPointer addButton( + rpl::producer text, + Fn clickCallback, + const style::RoundButton &st) = 0; + virtual QPointer addLeftButton( + rpl::producer text, + Fn clickCallback, + const style::RoundButton &st) = 0; + virtual QPointer addTopButton( + const style::IconButton &st, + Fn clickCallback) = 0; + virtual void showLoading(bool show) = 0; + virtual void updateButtonsPositions() = 0; + + virtual void showBox( + object_ptr box, + LayerOptions options, + anim::type animated) = 0; + virtual void setDimensions( + int newWidth, + int maxHeight, + bool forceCenterPosition = false) = 0; + virtual void setNoContentMargin(bool noContentMargin) = 0; + virtual bool isBoxShown() const = 0; + virtual void closeBox() = 0; + + template + QPointer show( + object_ptr content, + LayerOptions options = LayerOption::KeepOther, + anim::type animated = anim::type::normal) { + auto result = QPointer(content.data()); + showBox(std::move(content), options, animated); + return result; + } + + virtual QPointer outerContainer() = 0; + +}; + +class BoxContent : public RpWidget { + Q_OBJECT + +public: + BoxContent() { + setAttribute(Qt::WA_OpaquePaintEvent); + } + + bool isBoxShown() const { + return getDelegate()->isBoxShown(); + } + void closeBox() { + getDelegate()->closeBox(); + } + + void setTitle(rpl::producer title); + void setTitle(rpl::producer title) { + getDelegate()->setTitle(std::move(title)); + } + void setAdditionalTitle(rpl::producer additional) { + getDelegate()->setAdditionalTitle(std::move(additional)); + } + void setCloseByEscape(bool close) { + _closeByEscape = close; + } + void setCloseByOutsideClick(bool close) { + getDelegate()->setCloseByOutsideClick(close); + } + + void scrollToWidget(not_null widget); + + void clearButtons() { + getDelegate()->clearButtons(); + } + QPointer addButton( + rpl::producer text, + Fn clickCallback = nullptr); + QPointer addLeftButton( + rpl::producer text, + Fn clickCallback = nullptr); + QPointer addTopButton( + const style::IconButton &st, + Fn clickCallback = nullptr) { + return getDelegate()->addTopButton(st, std::move(clickCallback)); + } + QPointer addButton( + rpl::producer text, + const style::RoundButton &st) { + return getDelegate()->addButton(std::move(text), nullptr, st); + } + QPointer addButton( + rpl::producer text, + Fn clickCallback, + const style::RoundButton &st) { + return getDelegate()->addButton( + std::move(text), + std::move(clickCallback), + st); + } + void showLoading(bool show) { + getDelegate()->showLoading(show); + } + void updateButtonsGeometry() { + getDelegate()->updateButtonsPositions(); + } + + virtual void setInnerFocus() { + setFocus(); + } + + rpl::producer<> boxClosing() const { + return _boxClosingStream.events(); + } + void notifyBoxClosing() { + _boxClosingStream.fire({}); + } + + void setDelegate(not_null newDelegate) { + _delegate = newDelegate; + _preparing = true; + prepare(); + finishPrepare(); + } + not_null getDelegate() const { + return _delegate; + } + +public slots: + void onScrollToY(int top, int bottom = -1); + + void onDraggingScrollDelta(int delta); + +protected: + virtual void prepare() = 0; + + void setLayerType(bool layerType) { + getDelegate()->setLayerType(layerType); + } + + void setNoContentMargin(bool noContentMargin) { + if (_noContentMargin != noContentMargin) { + _noContentMargin = noContentMargin; + setAttribute(Qt::WA_OpaquePaintEvent, !_noContentMargin); + } + getDelegate()->setNoContentMargin(noContentMargin); + } + void setDimensions( + int newWidth, + int maxHeight, + bool forceCenterPosition = false) { + getDelegate()->setDimensions( + newWidth, + maxHeight, + forceCenterPosition); + } + void setDimensionsToContent( + int newWidth, + not_null content); + void setInnerTopSkip(int topSkip, bool scrollBottomFixed = false); + void setInnerBottomSkip(int bottomSkip); + + template + QPointer setInnerWidget( + object_ptr inner, + const style::ScrollArea &st, + int topSkip = 0, + int bottomSkip = 0) { + auto result = QPointer(inner.data()); + setInnerTopSkip(topSkip); + setInnerBottomSkip(bottomSkip); + setInner(std::move(inner), st); + return result; + } + + template + QPointer setInnerWidget( + object_ptr inner, + int topSkip = 0, + int bottomSkip = 0) { + auto result = QPointer(inner.data()); + setInnerTopSkip(topSkip); + setInnerBottomSkip(bottomSkip); + setInner(std::move(inner)); + return result; + } + + template + object_ptr takeInnerWidget() { + return object_ptr::fromRaw( + static_cast(doTakeInnerWidget().release())); + } + + void setInnerVisible(bool scrollAreaVisible); + QPixmap grabInnerCache(); + + void resizeEvent(QResizeEvent *e) override; + void paintEvent(QPaintEvent *e) override; + void keyPressEvent(QKeyEvent *e) override; + +private slots: + void onScroll(); + void onInnerResize(); + + void onDraggingScrollTimer(); + +private: + void finishPrepare(); + void finishScrollCreate(); + void setInner(object_ptr inner); + void setInner(object_ptr inner, const style::ScrollArea &st); + void updateScrollAreaGeometry(); + void updateInnerVisibleTopBottom(); + void updateShadowsVisibility(); + object_ptr doTakeInnerWidget(); + + BoxContentDelegate *_delegate = nullptr; + + bool _preparing = false; + bool _noContentMargin = false; + bool _closeByEscape = true; + int _innerTopSkip = 0; + int _innerBottomSkip = 0; + object_ptr _scroll = { nullptr }; + object_ptr _topShadow = { nullptr }; + object_ptr _bottomShadow = { nullptr }; + + object_ptr _draggingScrollTimer = { nullptr }; + int _draggingScrollDelta = 0; + + rpl::event_stream<> _boxClosingStream; + +}; + +class BoxPointer { +public: + BoxPointer() = default; + BoxPointer(const BoxPointer &other) = default; + BoxPointer(BoxPointer &&other) : _value(base::take(other._value)) { + } + BoxPointer(BoxContent *value) : _value(value) { + } + BoxPointer &operator=(const BoxPointer &other) { + if (_value != other._value) { + destroy(); + _value = other._value; + } + return *this; + } + BoxPointer &operator=(BoxPointer &&other) { + if (_value != other._value) { + destroy(); + _value = base::take(other._value); + } + return *this; + } + BoxPointer &operator=(BoxContent *other) { + if (_value != other) { + destroy(); + _value = other; + } + return *this; + } + ~BoxPointer() { + destroy(); + } + + BoxContent *get() const { + return _value.data(); + } + operator BoxContent*() const { + return get(); + } + explicit operator bool() const { + return get(); + } + BoxContent *operator->() const { + return get(); + } + +private: + void destroy() { + if (const auto value = base::take(_value)) { + value->closeBox(); + } + } + + QPointer _value; + +}; + +} // namespace Ui diff --git a/ui/layers/box_layer_widget.cpp b/ui/layers/box_layer_widget.cpp new file mode 100644 index 0000000..cfe7a5c --- /dev/null +++ b/ui/layers/box_layer_widget.cpp @@ -0,0 +1,351 @@ +// 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/layers/box_layer_widget.h" + +#include "ui/effects/radial_animation.h" +#include "ui/widgets/buttons.h" +#include "ui/widgets/scroll_area.h" +#include "ui/widgets/labels.h" +#include "ui/widgets/shadow.h" +#include "ui/wrap/fade_wrap.h" +#include "ui/text/text_utilities.h" +#include "ui/image/image_prepare.h" +#include "ui/painter.h" +#include "base/timer.h" +#include "styles/style_layers.h" +#include "styles/palette.h" + +namespace Ui { + +struct BoxLayerWidget::LoadingProgress { + LoadingProgress( + Fn &&callback, + const style::InfiniteRadialAnimation &st); + + InfiniteRadialAnimation animation; + base::Timer removeTimer; +}; + +BoxLayerWidget::LoadingProgress::LoadingProgress( + Fn &&callback, + const style::InfiniteRadialAnimation &st) +: animation(std::move(callback), st) { +} + +BoxLayerWidget::BoxLayerWidget( + not_null layer, + object_ptr content) +: LayerWidget(layer) +, _layer(layer) +, _content(std::move(content)) +, _roundRect(ImageRoundRadius::Small, st::boxBg) { + _content->setParent(this); + _content->setDelegate(this); + + _additionalTitle.changes( + ) | rpl::start_with_next([=] { + updateSize(); + update(); + }, lifetime()); +} + +BoxLayerWidget::~BoxLayerWidget() = default; + +void BoxLayerWidget::setLayerType(bool layerType) { + _layerType = layerType; + updateTitlePosition(); +} + +int BoxLayerWidget::titleHeight() const { + return _layerType ? st::boxLayerTitleHeight : st::boxTitleHeight; +} + +int BoxLayerWidget::buttonsHeight() const { + const auto padding = _layerType + ? st::boxLayerButtonPadding + : st::boxButtonPadding; + return padding.top() + st::defaultBoxButton.height + padding.bottom(); +} + +int BoxLayerWidget::buttonsTop() const { + const auto padding = _layerType + ? st::boxLayerButtonPadding + : st::boxButtonPadding; + return height() - padding.bottom() - st::defaultBoxButton.height; +} + +QRect BoxLayerWidget::loadingRect() const { + const auto padding = _layerType + ? st::boxLayerButtonPadding + : st::boxButtonPadding; + const auto size = st::boxLoadingSize; + const auto skipx = _layerType + ? st::boxLayerTitlePosition.x() + : st::boxTitlePosition.x(); + const auto skipy = (st::defaultBoxButton.height - size) / 2; + return QRect( + skipx, + height() - padding.bottom() - skipy - size, + size, + size); +} + +void BoxLayerWidget::paintEvent(QPaintEvent *e) { + Painter p(this); + auto clip = e->rect(); + auto paintTopRounded = clip.intersects(QRect(0, 0, width(), st::boxRadius)); + auto paintBottomRounded = clip.intersects(QRect(0, height() - st::boxRadius, width(), st::boxRadius)); + if (paintTopRounded || paintBottomRounded) { + auto parts = RectPart::None | 0; + if (paintTopRounded) parts |= RectPart::FullTop; + if (paintBottomRounded) parts |= RectPart::FullBottom; + _roundRect.paint(p, rect(), parts); + } + 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); + } + } + if (!_additionalTitle.current().isEmpty() + && clip.intersects(QRect(0, 0, width(), titleHeight()))) { + paintAdditionalTitle(p); + } + if (_loadingProgress) { + const auto rect = loadingRect(); + _loadingProgress->animation.draw( + p, + rect.topLeft(), + rect.size(), + width()); + } +} + +void BoxLayerWidget::paintAdditionalTitle(Painter &p) { + p.setFont(st::boxLayerTitleAdditionalFont); + p.setPen(st::boxTitleAdditionalFg); + p.drawTextLeft(_titleLeft + (_title ? _title->width() : 0) + st::boxLayerTitleAdditionalSkip, _titleTop + st::boxTitleFont->ascent - st::boxLayerTitleAdditionalFont->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); + update(); +} + +void BoxLayerWidget::setTitle(rpl::producer title) { + const auto wasTitle = hasTitle(); + if (title) { + _title.create(this, std::move(title), st::boxTitle); + _title->show(); + updateTitlePosition(); + } else { + _title.destroy(); + } + if (wasTitle != hasTitle()) { + updateSize(); + } +} + +void BoxLayerWidget::setAdditionalTitle(rpl::producer additional) { + _additionalTitle = std::move(additional); +} + +void BoxLayerWidget::setCloseByOutsideClick(bool close) { + _closeByOutsideClick = close; +} + +bool BoxLayerWidget::closeByOutsideClick() const { + return _closeByOutsideClick; +} + +bool BoxLayerWidget::hasTitle() const { + return (_title != nullptr) || !_additionalTitle.current().isEmpty(); +} + +void BoxLayerWidget::showBox( + object_ptr box, + LayerOptions options, + anim::type animated) { + _layer->showBox(std::move(box), options, animated); +} + +void BoxLayerWidget::updateSize() { + setDimensions(width(), _maxContentHeight); +} + +void BoxLayerWidget::updateButtonsPositions() { + if (!_buttons.empty() || _leftButton) { + auto padding = _layerType ? st::boxLayerButtonPadding : st::boxButtonPadding; + auto right = padding.right(); + auto top = buttonsTop(); + if (_leftButton) { + _leftButton->moveToLeft(right, top); + } + for (const auto &button : _buttons) { + button->moveToRight(right, top); + right += button->width() + padding.left(); + } + } + if (_topButton) { + _topButton->moveToRight(0, 0); + } +} + +QPointer BoxLayerWidget::outerContainer() { + return parentWidget(); +} + +void BoxLayerWidget::updateTitlePosition() { + _titleLeft = _layerType ? st::boxLayerTitlePosition.x() : st::boxTitlePosition.x(); + _titleTop = _layerType ? st::boxLayerTitlePosition.y() : st::boxTitlePosition.y(); + if (_title) { + _title->resizeToWidth(qMin(_title->naturalWidth(), width() - _titleLeft * 2)); + _title->moveToLeft(_titleLeft, _titleTop); + } +} + +void BoxLayerWidget::clearButtons() { + for (auto &button : base::take(_buttons)) { + button.destroy(); + } + _leftButton.destroy(); + _topButton = nullptr; +} + +QPointer BoxLayerWidget::addButton( + rpl::producer text, + Fn clickCallback, + const style::RoundButton &st) { + _buttons.emplace_back(this, std::move(text), st); + auto result = QPointer(_buttons.back()); + result->setClickedCallback(std::move(clickCallback)); + result->show(); + result->widthValue( + ) | rpl::start_with_next([=] { + updateButtonsPositions(); + }, result->lifetime()); + return result; +} + +QPointer BoxLayerWidget::addLeftButton( + rpl::producer text, + Fn clickCallback, + const style::RoundButton &st) { + _leftButton = object_ptr(this, std::move(text), st); + auto result = QPointer(_leftButton); + result->setClickedCallback(std::move(clickCallback)); + result->show(); + result->widthValue( + ) | rpl::start_with_next([=] { + updateButtonsPositions(); + }, result->lifetime()); + return result; +} + +QPointer BoxLayerWidget::addTopButton(const style::IconButton &st, Fn clickCallback) { + _topButton = base::make_unique_q(this, st); + auto result = QPointer(_topButton.get()); + result->setClickedCallback(std::move(clickCallback)); + result->show(); + updateButtonsPositions(); + return result; +} + +void BoxLayerWidget::showLoading(bool show) { + const auto &st = st::boxLoadingAnimation; + if (!show) { + if (_loadingProgress && !_loadingProgress->removeTimer.isActive()) { + _loadingProgress->removeTimer.callOnce( + st.sineDuration + st.sinePeriod); + _loadingProgress->animation.stop(); + } + return; + } + if (!_loadingProgress) { + const auto callback = [=] { + if (!anim::Disabled()) { + const auto t = st::boxLoadingAnimation.thickness; + update(loadingRect().marginsAdded({ t, t, t, t })); + } + }; + _loadingProgress = std::make_unique( + callback, + st::boxLoadingAnimation); + _loadingProgress->removeTimer.setCallback([=] { + _loadingProgress = nullptr; + }); + } else { + _loadingProgress->removeTimer.cancel(); + } + _loadingProgress->animation.start(); +} + + +void BoxLayerWidget::setDimensions(int newWidth, int maxHeight, bool forceCenterPosition) { + _maxContentHeight = maxHeight; + + auto fullHeight = countFullHeight(); + if (width() != newWidth || _fullHeight != fullHeight) { + _fullHeight = fullHeight; + if (parentWidget()) { + auto oldGeometry = geometry(); + resize(newWidth, countRealHeight()); + auto newGeometry = geometry(); + auto parentHeight = parentWidget()->height(); + if (newGeometry.top() + newGeometry.height() + st::boxVerticalMargin > parentHeight + || forceCenterPosition) { + const auto top1 = parentHeight - int(st::boxVerticalMargin) - newGeometry.height(); + const auto top2 = (parentHeight - newGeometry.height()) / 2; + const auto newTop = forceCenterPosition + ? std::min(top1, top2) + : std::max(top1, top2); + if (newTop != newGeometry.top()) { + move(newGeometry.left(), newTop); + resizeEvent(0); + } + } + parentWidget()->update(oldGeometry.united(geometry()).marginsAdded(st::boxRoundShadow.extend)); + } else { + resize(newWidth, 0); + } + } +} + +int BoxLayerWidget::countRealHeight() const { + return qMin(_fullHeight, parentWidget()->height() - 2 * st::boxVerticalMargin); +} + +int BoxLayerWidget::countFullHeight() const { + return contentTop() + _maxContentHeight + buttonsHeight(); +} + +int BoxLayerWidget::contentTop() const { + return hasTitle() ? titleHeight() : (_noContentMargin ? 0 : st::boxTopMargin); +} + +void BoxLayerWidget::resizeEvent(QResizeEvent *e) { + updateButtonsPositions(); + updateTitlePosition(); + + auto top = contentTop(); + _content->resize(width(), height() - top - buttonsHeight()); + _content->moveToLeft(0, top); + + LayerWidget::resizeEvent(e); +} + +void BoxLayerWidget::keyPressEvent(QKeyEvent *e) { + if (e->key() == Qt::Key_Escape) { + closeBox(); + } else { + LayerWidget::keyPressEvent(e); + } +} + +} // namespace Ui diff --git a/ui/layers/box_layer_widget.h b/ui/layers/box_layer_widget.h new file mode 100644 index 0000000..2c7861b --- /dev/null +++ b/ui/layers/box_layer_widget.h @@ -0,0 +1,142 @@ +// 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 "base/flags.h" +#include "ui/layers/layer_widget.h" +#include "ui/layers/box_content.h" +#include "ui/wrap/padding_wrap.h" +#include "ui/widgets/labels.h" +#include "ui/effects/animation_value.h" +#include "ui/text/text_entity.h" +#include "ui/round_rect.h" +#include "ui/rp_widget.h" + +class Painter; + +namespace style { +struct RoundButton; +struct IconButton; +struct ScrollArea; +} // namespace style + +namespace Ui { + +class RoundButton; +class IconButton; +class ScrollArea; +class FlatLabel; +class FadeShadow; + +class BoxLayerWidget : public LayerWidget, public BoxContentDelegate { +public: + BoxLayerWidget( + not_null layer, + object_ptr content); + ~BoxLayerWidget(); + + void parentResized() override; + + void setLayerType(bool layerType) override; + void setTitle(rpl::producer title) override; + void setAdditionalTitle(rpl::producer additional) override; + void showBox( + object_ptr box, + LayerOptions options, + anim::type animated) override; + + void clearButtons() override; + QPointer addButton( + rpl::producer text, + Fn clickCallback, + const style::RoundButton &st) override; + QPointer addLeftButton( + rpl::producer text, + Fn clickCallback, + const style::RoundButton &st) override; + QPointer addTopButton( + const style::IconButton &st, + Fn clickCallback) override; + void showLoading(bool show) override; + void updateButtonsPositions() override; + QPointer outerContainer() override; + + void setDimensions( + int newWidth, + int maxHeight, + bool forceCenterPosition = false) override; + + void setNoContentMargin(bool noContentMargin) override { + if (_noContentMargin != noContentMargin) { + _noContentMargin = noContentMargin; + updateSize(); + } + } + + bool isBoxShown() const override { + return !isHidden(); + } + void closeBox() override { + closeLayer(); + } + + void setCloseByOutsideClick(bool close) override; + bool closeByOutsideClick() const override; + +protected: + void keyPressEvent(QKeyEvent *e) override; + void resizeEvent(QResizeEvent *e) override; + void paintEvent(QPaintEvent *e) override; + + void doSetInnerFocus() override { + _content->setInnerFocus(); + } + void closeHook() override { + _content->notifyBoxClosing(); + } + +private: + struct LoadingProgress; + + void paintAdditionalTitle(Painter &p); + void updateTitlePosition(); + + [[nodiscard]] bool hasTitle() const; + [[nodiscard]] int titleHeight() const; + [[nodiscard]] int buttonsHeight() const; + [[nodiscard]] int buttonsTop() const; + [[nodiscard]] int contentTop() const; + [[nodiscard]] int countFullHeight() const; + [[nodiscard]] int countRealHeight() const; + [[nodiscard]] QRect loadingRect() const; + void updateSize(); + + not_null _layer; + int _fullHeight = 0; + + bool _noContentMargin = false; + int _maxContentHeight = 0; + object_ptr _content; + + RoundRect _roundRect; + object_ptr _title = { nullptr }; + Fn _titleFactory; + rpl::variable _additionalTitle; + int _titleLeft = 0; + int _titleTop = 0; + bool _layerType = false; + bool _closeByOutsideClick = true; + + std::vector> _buttons; + object_ptr _leftButton = { nullptr }; + base::unique_qptr _topButton = { nullptr }; + std::unique_ptr _loadingProgress; + +}; + +} // namespace Ui diff --git a/ui/layers/generic_box.cpp b/ui/layers/generic_box.cpp new file mode 100644 index 0000000..2e1f643 --- /dev/null +++ b/ui/layers/generic_box.cpp @@ -0,0 +1,28 @@ +// 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/layers/generic_box.h" + +#include "ui/wrap/vertical_layout.h" +#include "ui/wrap/padding_wrap.h" +#include "ui/wrap/wrap.h" +#include "styles/style_layers.h" + +namespace Ui { + +void GenericBox::prepare() { + _init(this); + + auto wrap = object_ptr(this, std::move(_content)); + setDimensionsToContent(_width ? _width : st::boxWidth, wrap.data()); + setInnerWidget(std::move(wrap)); +} + +void GenericBox::addSkip(int height) { + addRow(object_ptr(this, height)); +} + +} // namespace Ui diff --git a/ui/layers/generic_box.h b/ui/layers/generic_box.h new file mode 100644 index 0000000..86e81bc --- /dev/null +++ b/ui/layers/generic_box.h @@ -0,0 +1,157 @@ +// 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/layers/box_content.h" +#include "ui/wrap/vertical_layout.h" + +#include + +namespace st { +extern const style::margins &boxRowPadding; +} // namespace st + +namespace Ui { + +class GenericBox : public BoxContent { +public: + // InitMethod::operator()(not_null box, InitArgs...) + // init(box, args...) + template < + typename InitMethod, + typename ...InitArgs, + typename = decltype(std::declval>()( + std::declval>(), + std::declval>()...))> + GenericBox( + QWidget*, + InitMethod &&init, + InitArgs &&...args); + + void setWidth(int width) { + _width = width; + } + void setFocusCallback(Fn callback) { + _focus = callback; + } + + int rowsCount() const { + return _content->count(); + } + + template < + typename Widget, + typename = std::enable_if_t< + std::is_base_of_v>> + Widget *insertRow( + int atPosition, + object_ptr &&child, + const style::margins &margin = st::boxRowPadding) { + return _content->insert( + atPosition, + std::move(child), + margin); + } + + template < + typename Widget, + typename = std::enable_if_t< + std::is_base_of_v>> + Widget *addRow( + object_ptr &&child, + const style::margins &margin = st::boxRowPadding) { + return _content->add(std::move(child), margin); + } + + void addSkip(int height); + + void setInnerFocus() override { + if (_focus) { + _focus(); + } + } + +protected: + void prepare() override; + +private: + template + struct Initer { + template < + typename OtherMethod, + typename ...OtherArgs, + typename = std::enable_if_t< + std::is_constructible_v>> + Initer(OtherMethod &&method, OtherArgs &&...args); + + void operator()(not_null box); + + template + void call( + not_null box, + std::index_sequence); + + InitMethod method; + std::tuple args; + }; + + template + auto MakeIniter(InitMethod &&method, InitArgs &&...args) + -> Initer, std::decay_t...>; + + FnMut)> _init; + Fn _focus; + object_ptr _content; + int _width = 0; + +}; + +template +template +GenericBox::Initer::Initer( + OtherMethod &&method, + OtherArgs &&...args) +: method(std::forward(method)) +, args(std::forward(args)...) { +} + +template +inline void GenericBox::Initer::operator()( + not_null box) { + call(box, std::make_index_sequence()); +} + +template +template +inline void GenericBox::Initer::call( + not_null box, + std::index_sequence) { + std::invoke(method, box, std::get(std::move(args))...); +} + +template +inline auto GenericBox::MakeIniter(InitMethod &&method, InitArgs &&...args) +-> Initer, std::decay_t...> { + return { + std::forward(method), + std::forward(args)... + }; +} + +template +inline GenericBox::GenericBox( + QWidget*, + InitMethod &&init, + InitArgs &&...args) +: _init( + MakeIniter( + std::forward(init), + std::forward(args)...)) +, _content(this) { +} + +} // namespace Ui diff --git a/ui/layers/layer_widget.cpp b/ui/layers/layer_widget.cpp new file mode 100644 index 0000000..e6bb7dd --- /dev/null +++ b/ui/layers/layer_widget.cpp @@ -0,0 +1,856 @@ +// 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/layers/layer_widget.h" + +#include "ui/layers/box_layer_widget.h" +#include "ui/widgets/shadow.h" +#include "ui/image/image_prepare.h" +#include "ui/ui_utility.h" +#include "ui/round_rect.h" +#include "styles/style_layers.h" +#include "styles/style_widgets.h" +#include "styles/palette.h" + +#include + +namespace Ui { + +class LayerStackWidget::BackgroundWidget : public TWidget { +public: + explicit BackgroundWidget(QWidget *parent); + + void setDoneCallback(Fn callback) { + _doneCallback = std::move(callback); + } + + void setLayerBoxes(const QRect &specialLayerBox, const QRect &layerBox); + void setCacheImages( + QPixmap &&bodyCache, + QPixmap &&mainMenuCache, + QPixmap &&specialLayerCache, + QPixmap &&layerCache); + void removeBodyCache(); + void startAnimation(Action action); + void skipAnimation(Action action); + void finishAnimating(); + + bool animating() const { + return _a_mainMenuShown.animating() || _a_specialLayerShown.animating() || _a_layerShown.animating(); + } + +protected: + void paintEvent(QPaintEvent *e) override; + +private: + bool isShown() const { + return _mainMenuShown || _specialLayerShown || _layerShown; + } + void checkIfDone(); + void setMainMenuShown(bool shown); + void setSpecialLayerShown(bool shown); + void setLayerShown(bool shown); + void checkWasShown(bool wasShown); + void animationCallback(); + + QPixmap _bodyCache; + QPixmap _mainMenuCache; + int _mainMenuCacheWidth = 0; + QPixmap _specialLayerCache; + QPixmap _layerCache; + RoundRect _roundRect; + + Fn _doneCallback; + + bool _wasAnimating = false; + bool _inPaintEvent = false; + Ui::Animations::Simple _a_shown; + Ui::Animations::Simple _a_mainMenuShown; + Ui::Animations::Simple _a_specialLayerShown; + Ui::Animations::Simple _a_layerShown; + + QRect _specialLayerBox, _specialLayerCacheBox; + QRect _layerBox, _layerCacheBox; + int _mainMenuRight = 0; + + bool _mainMenuShown = false; + bool _specialLayerShown = false; + bool _layerShown = false; + +}; + +LayerStackWidget::BackgroundWidget::BackgroundWidget(QWidget *parent) +: TWidget(parent) +, _roundRect(ImageRoundRadius::Small, st::boxBg) { +} + +void LayerStackWidget::BackgroundWidget::setCacheImages( + QPixmap &&bodyCache, + QPixmap &&mainMenuCache, + QPixmap &&specialLayerCache, + QPixmap &&layerCache) { + _bodyCache = std::move(bodyCache); + _mainMenuCache = std::move(mainMenuCache); + _specialLayerCache = std::move(specialLayerCache); + _layerCache = std::move(layerCache); + _specialLayerCacheBox = _specialLayerBox; + _layerCacheBox = _layerBox; + setAttribute(Qt::WA_OpaquePaintEvent, !_bodyCache.isNull()); +} + +void LayerStackWidget::BackgroundWidget::removeBodyCache() { + if (!_bodyCache.isNull()) { + _bodyCache = {}; + setAttribute(Qt::WA_OpaquePaintEvent, false); + } +} + +void LayerStackWidget::BackgroundWidget::startAnimation(Action action) { + if (action == Action::ShowMainMenu) { + setMainMenuShown(true); + } else if (action != Action::HideLayer + && action != Action::HideSpecialLayer) { + setMainMenuShown(false); + } + if (action == Action::ShowSpecialLayer) { + setSpecialLayerShown(true); + } else if (action == Action::ShowMainMenu + || action == Action::HideAll + || action == Action::HideSpecialLayer) { + setSpecialLayerShown(false); + } + if (action == Action::ShowLayer) { + setLayerShown(true); + } else if (action != Action::ShowSpecialLayer + && action != Action::HideSpecialLayer) { + setLayerShown(false); + } + _wasAnimating = true; + checkIfDone(); +} + +void LayerStackWidget::BackgroundWidget::skipAnimation(Action action) { + startAnimation(action); + finishAnimating(); +} + +void LayerStackWidget::BackgroundWidget::checkIfDone() { + if (!_wasAnimating || _inPaintEvent || animating()) { + return; + } + _wasAnimating = false; + _mainMenuCache = _specialLayerCache = _layerCache = QPixmap(); + removeBodyCache(); + if (_doneCallback) { + _doneCallback(); + } +} + +void LayerStackWidget::BackgroundWidget::setMainMenuShown(bool shown) { + auto wasShown = isShown(); + if (_mainMenuShown != shown) { + _mainMenuShown = shown; + _a_mainMenuShown.start([this] { animationCallback(); }, _mainMenuShown ? 0. : 1., _mainMenuShown ? 1. : 0., st::boxDuration, anim::easeOutCirc); + } + _mainMenuCacheWidth = (_mainMenuCache.width() / style::DevicePixelRatio()) + - st::boxRoundShadow.extend.right(); + _mainMenuRight = _mainMenuShown ? _mainMenuCacheWidth : 0; + checkWasShown(wasShown); +} + +void LayerStackWidget::BackgroundWidget::setSpecialLayerShown(bool shown) { + auto wasShown = isShown(); + if (_specialLayerShown != shown) { + _specialLayerShown = shown; + _a_specialLayerShown.start([this] { animationCallback(); }, _specialLayerShown ? 0. : 1., _specialLayerShown ? 1. : 0., st::boxDuration); + } + checkWasShown(wasShown); +} + +void LayerStackWidget::BackgroundWidget::setLayerShown(bool shown) { + auto wasShown = isShown(); + if (_layerShown != shown) { + _layerShown = shown; + _a_layerShown.start([this] { animationCallback(); }, _layerShown ? 0. : 1., _layerShown ? 1. : 0., st::boxDuration); + } + checkWasShown(wasShown); +} + +void LayerStackWidget::BackgroundWidget::checkWasShown(bool wasShown) { + if (isShown() != wasShown) { + _a_shown.start([this] { animationCallback(); }, wasShown ? 1. : 0., wasShown ? 0. : 1., st::boxDuration, anim::easeOutCirc); + } +} + +void LayerStackWidget::BackgroundWidget::setLayerBoxes(const QRect &specialLayerBox, const QRect &layerBox) { + _specialLayerBox = specialLayerBox; + _layerBox = layerBox; + update(); +} + +void LayerStackWidget::BackgroundWidget::paintEvent(QPaintEvent *e) { + Painter p(this); + + _inPaintEvent = true; + auto guard = gsl::finally([this] { + _inPaintEvent = false; + crl::on_main(this, [=] { checkIfDone(); }); + }); + + if (!_bodyCache.isNull()) { + p.drawPixmap(0, 0, _bodyCache); + } + + auto specialLayerBox = _specialLayerCache.isNull() ? _specialLayerBox : _specialLayerCacheBox; + auto layerBox = _layerCache.isNull() ? _layerBox : _layerCacheBox; + + auto mainMenuProgress = _a_mainMenuShown.value(-1); + auto mainMenuRight = (_mainMenuCache.isNull() || mainMenuProgress < 0) ? _mainMenuRight : (mainMenuProgress < 0) ? _mainMenuRight : anim::interpolate(0, _mainMenuCacheWidth, mainMenuProgress); + if (mainMenuRight) { + // Move showing boxes to the right while main menu is hiding. + if (!_specialLayerCache.isNull()) { + specialLayerBox.moveLeft(specialLayerBox.left() + mainMenuRight / 2); + } + if (!_layerCache.isNull()) { + layerBox.moveLeft(layerBox.left() + mainMenuRight / 2); + } + } + auto bgOpacity = _a_shown.value(isShown() ? 1. : 0.); + auto specialLayerOpacity = _a_specialLayerShown.value(_specialLayerShown ? 1. : 0.); + auto layerOpacity = _a_layerShown.value(_layerShown ? 1. : 0.); + if (bgOpacity == 0.) { + return; + } + + p.setOpacity(bgOpacity); + auto overSpecialOpacity = (layerOpacity * specialLayerOpacity); + auto bg = myrtlrect(mainMenuRight, 0, width() - mainMenuRight, height()); + + if (_mainMenuCache.isNull() && mainMenuRight > 0) { + // All cache images are taken together with their shadows, + // so we paint shadow only when there is no cache. + Ui::Shadow::paint(p, myrtlrect(0, 0, mainMenuRight, height()), width(), st::boxRoundShadow, RectPart::Right); + } + + if (_specialLayerCache.isNull() && !specialLayerBox.isEmpty()) { + // All cache images are taken together with their shadows, + // so we paint shadow only when there is no cache. + auto sides = RectPart::Left | RectPart::Right; + auto topCorners = (specialLayerBox.y() > 0); + auto bottomCorners = (specialLayerBox.y() + specialLayerBox.height() < height()); + if (topCorners) { + sides |= RectPart::Top; + } + if (bottomCorners) { + sides |= RectPart::Bottom; + } + if (topCorners || bottomCorners) { + p.setClipRegion(QRegion(rect()) - specialLayerBox.marginsRemoved(QMargins(st::boxRadius, 0, st::boxRadius, 0)) - specialLayerBox.marginsRemoved(QMargins(0, st::boxRadius, 0, st::boxRadius))); + } + Ui::Shadow::paint(p, specialLayerBox, width(), st::boxRoundShadow, sides); + + if (topCorners || bottomCorners) { + // In case of painting the shadow above the special layer we get + // glitches in the corners, so we need to paint the corners once more. + p.setClipping(false); + auto parts = (topCorners ? (RectPart::TopLeft | RectPart::TopRight) : RectPart::None) + | (bottomCorners ? (RectPart::BottomLeft | RectPart::BottomRight) : RectPart::None); + _roundRect.paint(p, specialLayerBox, parts); + } + } + + if (!layerBox.isEmpty() && !_specialLayerCache.isNull() && overSpecialOpacity < bgOpacity) { + // In case of moving special layer below the background while showing a box + // we need to fill special layer rect below its cache with a complex opacity + // (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()) { + p.fillRect(rect, st::layerBg); + } + p.setOpacity((bgOpacity - overSpecialOpacity) / (1. - (overSpecialOpacity * st::layerBg->c.alphaF()))); + p.fillRect(specialLayerBox, st::layerBg); + p.setOpacity(bgOpacity); + } else { + p.fillRect(bg, st::layerBg); + } + + if (!_specialLayerCache.isNull() && specialLayerOpacity > 0) { + p.setOpacity(specialLayerOpacity); + auto cacheLeft = specialLayerBox.x() - st::boxRoundShadow.extend.left(); + auto cacheTop = specialLayerBox.y() - (specialLayerBox.y() > 0 ? st::boxRoundShadow.extend.top() : 0); + p.drawPixmapLeft(cacheLeft, cacheTop, width(), _specialLayerCache); + } + if (!layerBox.isEmpty()) { + if (!_specialLayerCache.isNull()) { + p.setOpacity(overSpecialOpacity); + p.fillRect(specialLayerBox, st::layerBg); + } + if (_layerCache.isNull()) { + p.setOpacity(layerOpacity); + Ui::Shadow::paint(p, layerBox, width(), st::boxRoundShadow); + } + } + if (!_layerCache.isNull() && layerOpacity > 0) { + p.setOpacity(layerOpacity); + p.drawPixmapLeft(layerBox.topLeft() - QPoint(st::boxRoundShadow.extend.left(), st::boxRoundShadow.extend.top()), width(), _layerCache); + } + if (!_mainMenuCache.isNull() && mainMenuRight > 0) { + p.setOpacity(1.); + auto shownWidth = mainMenuRight + st::boxRoundShadow.extend.right(); + auto sourceWidth = shownWidth * style::DevicePixelRatio(); + auto sourceRect = style::rtlrect(_mainMenuCache.width() - sourceWidth, 0, sourceWidth, _mainMenuCache.height(), _mainMenuCache.width()); + p.drawPixmapLeft(0, 0, shownWidth, height(), width(), _mainMenuCache, sourceRect); + } +} + +void LayerStackWidget::BackgroundWidget::finishAnimating() { + _a_shown.stop(); + _a_mainMenuShown.stop(); + _a_specialLayerShown.stop(); + _a_layerShown.stop(); + checkIfDone(); +} + +void LayerStackWidget::BackgroundWidget::animationCallback() { + update(); + checkIfDone(); +} + +LayerStackWidget::LayerStackWidget(QWidget *parent) +: RpWidget(parent) +, _background(this) { + setGeometry(parentWidget()->rect()); + hide(); + _background->setDoneCallback([this] { animationDone(); }); +} + +void LayerWidget::setInnerFocus() { + if (!isAncestorOf(window()->focusWidget())) { + doSetInnerFocus(); + } +} + +bool LayerWidget::overlaps(const QRect &globalRect) { + if (isHidden()) { + return false; + } + auto testRect = QRect(mapFromGlobal(globalRect.topLeft()), globalRect.size()); + if (testAttribute(Qt::WA_OpaquePaintEvent)) { + return rect().contains(testRect); + } + if (QRect(0, st::boxRadius, width(), height() - 2 * st::boxRadius).contains(testRect)) { + return true; + } + if (QRect(st::boxRadius, 0, width() - 2 * st::boxRadius, height()).contains(testRect)) { + return true; + } + return false; +} + +void LayerWidget::mousePressEvent(QMouseEvent *e) { + e->accept(); +} + +void LayerWidget::resizeEvent(QResizeEvent *e) { + if (_resizedCallback) { + _resizedCallback(); + } +} + +void LayerStackWidget::setHideByBackgroundClick(bool hide) { + _hideByBackgroundClick = hide; +} + +void LayerStackWidget::keyPressEvent(QKeyEvent *e) { + if (e->key() == Qt::Key_Escape) { + hideCurrent(anim::type::normal); + } +} + +void LayerStackWidget::mousePressEvent(QMouseEvent *e) { + Ui::PostponeCall(this, [=] { backgroundClicked(); }); +} + +void LayerStackWidget::backgroundClicked() { + if (!_hideByBackgroundClick) { + return; + } + if (const auto layer = currentLayer()) { + if (!layer->closeByOutsideClick()) { + return; + } + } else if (const auto special = _specialLayer.data()) { + if (!special->closeByOutsideClick()) { + return; + } + } + hideCurrent(anim::type::normal); +} + +void LayerStackWidget::hideCurrent(anim::type animated) { + return currentLayer() ? hideLayers(animated) : hideAll(animated); +} + +void LayerStackWidget::hideLayers(anim::type animated) { + startAnimation([] {}, [&] { + clearLayers(); + }, Action::HideLayer, animated); +} + +void LayerStackWidget::hideAll(anim::type animated) { + startAnimation([] {}, [&] { + clearLayers(); + clearSpecialLayer(); + _mainMenu.destroy(); + }, Action::HideAll, animated); +} + +void LayerStackWidget::hideTopLayer(anim::type animated) { + if (_specialLayer || _mainMenu) { + hideLayers(animated); + } else { + hideAll(animated); + } +} + +void LayerStackWidget::removeBodyCache() { + _background->removeBodyCache(); + setAttribute(Qt::WA_OpaquePaintEvent, false); +} + +bool LayerStackWidget::layerShown() const { + return _specialLayer || currentLayer() || _mainMenu; +} + +void LayerStackWidget::setCacheImages() { + auto bodyCache = QPixmap(), mainMenuCache = QPixmap(); + auto specialLayerCache = QPixmap(); + if (_specialLayer) { + Ui::SendPendingMoveResizeEvents(_specialLayer); + auto sides = RectPart::Left | RectPart::Right; + if (_specialLayer->y() > 0) { + sides |= RectPart::Top; + } + if (_specialLayer->y() + _specialLayer->height() < height()) { + sides |= RectPart::Bottom; + } + specialLayerCache = Ui::Shadow::grab(_specialLayer, st::boxRoundShadow, sides); + } + auto layerCache = QPixmap(); + if (auto layer = currentLayer()) { + layerCache = Ui::Shadow::grab(layer, st::boxRoundShadow); + } + if (isAncestorOf(window()->focusWidget())) { + setFocus(); + } + if (_mainMenu) { + removeBodyCache(); + hideChildren(); + bodyCache = Ui::GrabWidget(parentWidget()); + showChildren(); + mainMenuCache = Ui::Shadow::grab(_mainMenu, st::boxRoundShadow, RectPart::Right); + } + setAttribute(Qt::WA_OpaquePaintEvent, !bodyCache.isNull()); + updateLayerBoxes(); + _background->setCacheImages(std::move(bodyCache), std::move(mainMenuCache), std::move(specialLayerCache), std::move(layerCache)); +} + +void LayerStackWidget::closeLayer(not_null layer) { + const auto weak = Ui::MakeWeak(layer.get()); + if (weak->inFocusChain()) { + setFocus(); + } + if (!weak || !weak->setClosing()) { + // This layer is already closing. + return; + } else if (!weak) { + // setClosing() could've killed the layer. + return; + } + + if (layer == _specialLayer) { + hideAll(anim::type::normal); + } else if (layer == currentLayer()) { + if (_layers.size() == 1) { + hideCurrent(anim::type::normal); + } else { + const auto taken = std::move(_layers.back()); + _layers.pop_back(); + + layer = currentLayer(); + layer->parentResized(); + if (!_background->animating()) { + layer->show(); + showFinished(); + } + } + } else { + for (auto i = _layers.begin(), e = _layers.end(); i != e; ++i) { + if (layer == i->get()) { + const auto taken = std::move(*i); + _layers.erase(i); + break; + } + } + } +} + +void LayerStackWidget::updateLayerBoxes() { + const auto layerBox = [&] { + if (const auto layer = currentLayer()) { + return layer->geometry(); + } + return QRect(); + }(); + const auto specialLayerBox = _specialLayer + ? _specialLayer->geometry() + : QRect(); + _background->setLayerBoxes(specialLayerBox, layerBox); + update(); +} + +void LayerStackWidget::finishAnimating() { + _background->finishAnimating(); +} + +bool LayerStackWidget::canSetFocus() const { + return (currentLayer() || _specialLayer || _mainMenu); +} + +void LayerStackWidget::setInnerFocus() { + if (_background->animating()) { + setFocus(); + } else if (auto l = currentLayer()) { + l->setInnerFocus(); + } else if (_specialLayer) { + _specialLayer->setInnerFocus(); + } else if (_mainMenu) { + _mainMenu->setInnerFocus(); + } +} + +bool LayerStackWidget::contentOverlapped(const QRect &globalRect) { + if (isHidden()) { + return false; + } + if (_specialLayer && _specialLayer->overlaps(globalRect)) { + return true; + } + if (auto layer = currentLayer()) { + return layer->overlaps(globalRect); + } + return false; +} + +template +void LayerStackWidget::startAnimation( + SetupNew setupNewWidgets, + ClearOld clearOldWidgets, + Action action, + anim::type animated) { + if (animated == anim::type::instant) { + setupNewWidgets(); + clearOldWidgets(); + prepareForAnimation(); + _background->skipAnimation(action); + } else { + setupNewWidgets(); + setCacheImages(); + const auto weak = Ui::MakeWeak(this); + clearOldWidgets(); + if (weak) { + prepareForAnimation(); + _background->startAnimation(action); + } + } +} + +void LayerStackWidget::resizeEvent(QResizeEvent *e) { + const auto weak = Ui::MakeWeak(this); + _background->setGeometry(rect()); + if (!weak) { + return; + } + if (_specialLayer) { + _specialLayer->parentResized(); + if (!weak) { + return; + } + } + if (const auto layer = currentLayer()) { + layer->parentResized(); + if (!weak) { + return; + } + } + if (_mainMenu) { + _mainMenu->parentResized(); + if (!weak) { + return; + } + } + updateLayerBoxes(); +} + +void LayerStackWidget::showBox( + object_ptr box, + LayerOptions options, + anim::type animated) { + if (options & LayerOption::KeepOther) { + if (options & LayerOption::ShowAfterOther) { + prependBox(std::move(box), animated); + } else { + appendBox(std::move(box), animated); + } + } else { + replaceBox(std::move(box), animated); + } +} + +void LayerStackWidget::replaceBox( + object_ptr box, + anim::type animated) { + const auto pointer = pushBox(std::move(box), animated); + const auto removeTill = ranges::find( + _layers, + pointer, + &std::unique_ptr::get); + _closingLayers.insert( + end(_closingLayers), + std::make_move_iterator(begin(_layers)), + std::make_move_iterator(removeTill)); + _layers.erase(begin(_layers), removeTill); + clearClosingLayers(); +} + +void LayerStackWidget::prepareForAnimation() { + if (isHidden()) { + show(); + } + if (_mainMenu) { + _mainMenu->hide(); + } + if (_specialLayer) { + _specialLayer->hide(); + } + if (const auto layer = currentLayer()) { + layer->hide(); + } +} + +void LayerStackWidget::animationDone() { + bool hidden = true; + if (_mainMenu) { + _mainMenu->show(); + hidden = false; + } + if (_specialLayer) { + _specialLayer->show(); + hidden = false; + } + if (auto layer = currentLayer()) { + layer->show(); + hidden = false; + } + setAttribute(Qt::WA_OpaquePaintEvent, false); + if (hidden) { + _hideFinishStream.fire({}); + } else { + showFinished(); + } +} + +rpl::producer<> LayerStackWidget::hideFinishEvents() const { + return _hideFinishStream.events(); +} + +void LayerStackWidget::showFinished() { + fixOrder(); + sendFakeMouseEvent(); + updateLayerBoxes(); + if (_specialLayer) { + _specialLayer->showFinished(); + } + if (auto layer = currentLayer()) { + layer->showFinished(); + } + if (canSetFocus()) { + setInnerFocus(); + } +} + +void LayerStackWidget::showSpecialLayer( + object_ptr layer, + anim::type animated) { + startAnimation([&] { + _specialLayer.destroy(); + _specialLayer = std::move(layer); + initChildLayer(_specialLayer); + }, [&] { + _mainMenu.destroy(); + }, Action::ShowSpecialLayer, animated); +} + +bool LayerStackWidget::showSectionInternal( + not_null memento, + const Window::SectionShow ¶ms) { + if (_specialLayer) { + return _specialLayer->showSectionInternal(memento, params); + } + return false; +} + +void LayerStackWidget::hideSpecialLayer(anim::type animated) { + startAnimation([] {}, [&] { + clearSpecialLayer(); + _mainMenu.destroy(); + }, Action::HideSpecialLayer, animated); +} + +void LayerStackWidget::showMainMenu( + object_ptr layer, + anim::type animated) { + startAnimation([&] { + _mainMenu = std::move(layer); + initChildLayer(_mainMenu); + _mainMenu->moveToLeft(0, 0); + }, [&] { + clearLayers(); + _specialLayer.destroy(); + }, Action::ShowMainMenu, animated); +} + +void LayerStackWidget::appendBox( + object_ptr box, + anim::type animated) { + pushBox(std::move(box), animated); +} + +LayerWidget *LayerStackWidget::pushBox( + object_ptr box, + anim::type animated) { + auto oldLayer = currentLayer(); + if (oldLayer) { + if (oldLayer->inFocusChain()) setFocus(); + oldLayer->hide(); + } + _layers.push_back( + std::make_unique(this, std::move(box))); + const auto raw = _layers.back().get(); + initChildLayer(raw); + + if (_layers.size() > 1) { + if (!_background->animating()) { + raw->setVisible(true); + showFinished(); + } + } else { + startAnimation([] {}, [&] { + _mainMenu.destroy(); + }, Action::ShowLayer, animated); + } + + return raw; +} + +void LayerStackWidget::prependBox( + object_ptr box, + anim::type animated) { + if (_layers.empty()) { + replaceBox(std::move(box), animated); + return; + } + _layers.insert( + begin(_layers), + std::make_unique(this, std::move(box))); + const auto raw = _layers.front().get(); + raw->hide(); + initChildLayer(raw); +} + +bool LayerStackWidget::takeToThirdSection() { + return _specialLayer + ? _specialLayer->takeToThirdSection() + : false; +} + +void LayerStackWidget::clearLayers() { + _closingLayers.insert( + end(_closingLayers), + std::make_move_iterator(begin(_layers)), + std::make_move_iterator(end(_layers))); + _layers.clear(); + clearClosingLayers(); +} + +void LayerStackWidget::clearClosingLayers() { + const auto weak = Ui::MakeWeak(this); + while (!_closingLayers.empty()) { + const auto index = _closingLayers.size() - 1; + const auto layer = _closingLayers.back().get(); + if (layer->inFocusChain()) { + setFocus(); + } + + // This may destroy LayerStackWidget (by calling Ui::hideLayer). + // So each time we check a weak pointer (if we are still alive). + layer->setClosing(); + + // setClosing() could destroy 'this' or could call clearLayers(). + if (weak && !_closingLayers.empty()) { + // We could enqueue more closing layers, so we remove by index. + Assert(index < _closingLayers.size()); + Assert(_closingLayers[index].get() == layer); + _closingLayers.erase(begin(_closingLayers) + index); + } else { + // Everything was destroyed in clearLayers or ~LayerStackWidget. + break; + } + } +} + +void LayerStackWidget::clearSpecialLayer() { + if (_specialLayer) { + _specialLayer->setClosing(); + _specialLayer.destroy(); + } +} + +void LayerStackWidget::initChildLayer(LayerWidget *layer) { + layer->setParent(this); + layer->setClosedCallback([=] { closeLayer(layer); }); + layer->setResizedCallback([=] { updateLayerBoxes(); }); + Ui::SendPendingMoveResizeEvents(layer); + layer->parentResized(); +} + +void LayerStackWidget::fixOrder() { + if (const auto layer = currentLayer()) { + _background->raise(); + layer->raise(); + } else if (_specialLayer) { + _specialLayer->raise(); + } + if (_mainMenu) { + _mainMenu->raise(); + } +} + +void LayerStackWidget::sendFakeMouseEvent() { + SendSynteticMouseEvent(this, QEvent::MouseMove, Qt::NoButton); +} + +LayerStackWidget::~LayerStackWidget() { + // Some layer destructors call back into LayerStackWidget. + while (!_layers.empty() || !_closingLayers.empty()) { + hideAll(anim::type::instant); + clearClosingLayers(); + } +} + +} // namespace Ui diff --git a/ui/layers/layer_widget.h b/ui/layers/layer_widget.h new file mode 100644 index 0000000..6fa7116 --- /dev/null +++ b/ui/layers/layer_widget.h @@ -0,0 +1,200 @@ +// This file is part of Desktop App Toolkit, +// a set of libraries for developing nice desktop applications. +// +// For license and copyright information please follow this link: +// https://github.com/desktop-app/legal/blob/master/LEGAL +// +#pragma once + +#include "ui/rp_widget.h" +#include "ui/effects/animations.h" +#include "base/object_ptr.h" +#include "base/flags.h" + +namespace Window { +class SectionMemento; +struct SectionShow; +} // namespace Window + +namespace Ui { + +class BoxContent; + +enum class LayerOption { + CloseOther = (1 << 0), + KeepOther = (1 << 1), + ShowAfterOther = (1 << 2), +}; +using LayerOptions = base::flags; +inline constexpr auto is_flag_type(LayerOption) { return true; }; + +class LayerWidget : public Ui::RpWidget { +public: + using RpWidget::RpWidget; + + virtual void parentResized() = 0; + virtual void showFinished() { + } + void setInnerFocus(); + bool setClosing() { + if (!_closing) { + _closing = true; + closeHook(); + return true; + } + return false; + } + + bool overlaps(const QRect &globalRect); + + void setClosedCallback(Fn callback) { + _closedCallback = std::move(callback); + } + void setResizedCallback(Fn callback) { + _resizedCallback = std::move(callback); + } + virtual bool takeToThirdSection() { + return false; + } + virtual bool showSectionInternal( + not_null memento, + const Window::SectionShow ¶ms) { + return false; + } + virtual bool closeByOutsideClick() const { + return true; + } + +protected: + void closeLayer() { + if (const auto callback = base::take(_closedCallback)) { + callback(); + } + } + void mousePressEvent(QMouseEvent *e) override; + void resizeEvent(QResizeEvent *e) override; + virtual void doSetInnerFocus() { + setFocus(); + } + virtual void closeHook() { + } + +private: + bool _closing = false; + Fn _closedCallback; + Fn _resizedCallback; + +}; + +class LayerStackWidget : public Ui::RpWidget { +public: + LayerStackWidget(QWidget *parent); + + void finishAnimating(); + rpl::producer<> hideFinishEvents() const; + + void showBox( + object_ptr box, + LayerOptions options, + anim::type animated); + void showSpecialLayer( + object_ptr layer, + anim::type animated); + void showMainMenu( + object_ptr menu, + anim::type animated); + bool takeToThirdSection(); + + bool canSetFocus() const; + void setInnerFocus(); + + bool contentOverlapped(const QRect &globalRect); + + void hideSpecialLayer(anim::type animated); + void hideLayers(anim::type animated); + void hideAll(anim::type animated); + void hideTopLayer(anim::type animated); + void setHideByBackgroundClick(bool hide); + void removeBodyCache(); + + bool showSectionInternal( + not_null memento, + const Window::SectionShow ¶ms); + + bool layerShown() const; + + ~LayerStackWidget(); + +protected: + void keyPressEvent(QKeyEvent *e) override; + void mousePressEvent(QMouseEvent *e) override; + void resizeEvent(QResizeEvent *e) override; + +private: + void appendBox( + object_ptr box, + anim::type animated); + void prependBox( + object_ptr box, + anim::type animated); + void replaceBox( + object_ptr box, + anim::type animated); + void backgroundClicked(); + + LayerWidget *pushBox( + object_ptr box, + anim::type animated); + void showFinished(); + void hideCurrent(anim::type animated); + void closeLayer(not_null layer); + + enum class Action { + ShowMainMenu, + ShowSpecialLayer, + ShowLayer, + HideSpecialLayer, + HideLayer, + HideAll, + }; + template + void startAnimation( + SetupNew setupNewWidgets, + ClearOld clearOldWidgets, + Action action, + anim::type animated); + + void prepareForAnimation(); + void animationDone(); + + void setCacheImages(); + void clearLayers(); + void clearSpecialLayer(); + void initChildLayer(LayerWidget *layer); + void updateLayerBoxes(); + void fixOrder(); + void sendFakeMouseEvent(); + void clearClosingLayers(); + + LayerWidget *currentLayer() { + return _layers.empty() ? nullptr : _layers.back().get(); + } + const LayerWidget *currentLayer() const { + return const_cast(this)->currentLayer(); + } + + std::vector> _layers; + std::vector> _closingLayers; + + object_ptr _specialLayer = { nullptr }; + object_ptr _mainMenu = { nullptr }; + + class BackgroundWidget; + object_ptr _background; + bool _hideByBackgroundClick = true; + + rpl::event_stream<> _hideFinishStream; + +}; + +} // namespace Ui diff --git a/ui/layers/layers.style b/ui/layers/layers.style new file mode 100644 index 0000000..a7cae08 --- /dev/null +++ b/ui/layers/layers.style @@ -0,0 +1,150 @@ +// 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 +// +using "ui/basic.style"; + +using "ui/widgets/widgets.style"; + +ServiceCheck { + margin: margins; + diameter: pixels; + shift: pixels; + thickness: pixels; + tip: point; + small: pixels; + large: pixels; + stroke: pixels; + color: color; + duration: int; +} + +boxDuration: 200; +boxRadius: 3px; + +boxButtonFont: font(boxFontSize semibold); +defaultBoxButton: RoundButton(defaultLightButton) { + width: -24px; + height: 36px; + font: boxButtonFont; +} + +boxTextStyle: TextStyle(defaultTextStyle) { + font: font(boxFontSize); + linkFont: font(boxFontSize); + linkFontOver: font(boxFontSize underline); +} + +boxLabelStyle: TextStyle(boxTextStyle) { + lineHeight: 22px; +} + +attentionBoxButton: RoundButton(defaultBoxButton) { + textFg: attentionButtonFg; + textFgOver: attentionButtonFgOver; + textBgOver: attentionButtonBgOver; + + ripple: RippleAnimation(defaultRippleAnimation) { + color: attentionButtonBgRipple; + } +} + +defaultBoxCheckbox: Checkbox(defaultCheckbox) { + width: -46px; + textPosition: point(12px, 1px); + style: boxTextStyle; +} + +boxRoundShadow: Shadow { + left: icon {{ "round_shadow_box_left", windowShadowFg }}; + topLeft: icon {{ "round_shadow_box_top_left", windowShadowFg }}; + top: icon {{ "round_shadow_box_top", windowShadowFg }}; + topRight: icon {{ "round_shadow_box_top_left-flip_horizontal", windowShadowFg }}; + right: icon {{ "round_shadow_box_left-flip_horizontal", windowShadowFg }}; + bottomRight: icon {{ "round_shadow_box_bottom_left-flip_horizontal", windowShadowFg }}; + bottom: icon {{ "round_shadow_box_bottom", windowShadowFg }}; + bottomLeft: icon {{ "round_shadow_box_bottom_left", windowShadowFg }}; + extend: margins(10px, 10px, 10px, 10px); + fallback: windowShadowFgFallback; +} + +boxTitleFont: font(17px semibold); +boxTitle: FlatLabel(defaultFlatLabel) { + textFg: boxTitleFg; + maxHeight: 24px; + style: TextStyle(defaultTextStyle) { + font: boxTitleFont; + linkFont: boxTitleFont; + linkFontOver: font(17px semibold underline); + } +} +boxTitlePosition: point(23px, 16px); +boxTitleHeight: 56px; +boxLayerTitlePosition: point(23px, 16px); +boxLayerTitleHeight: 56px; +boxLayerTitleAdditionalSkip: 9px; +boxLayerTitleAdditionalFont: normalFont; +boxLayerScroll: defaultSolidScroll; + +boxRowPadding: margins(23px, 0px, 23px, 0px); + +boxTopMargin: 6px; + +boxTitleClose: IconButton(defaultIconButton) { + width: boxTitleHeight; + height: boxTitleHeight; + + icon: boxTitleCloseIcon; + iconOver: boxTitleCloseIconOver; + + rippleAreaPosition: point(6px, 6px); + rippleAreaSize: 44px; + ripple: RippleAnimation(defaultRippleAnimation) { + color: windowBgOver; + } +} + +boxLinkButton: LinkButton(defaultLinkButton) { + font: boxTextFont; + overFont: font(boxFontSize underline); +} + +boxOptionListPadding: margins(0px, 0px, 0px, 0px); +boxOptionListSkip: 20px; +boxOptionInputSkip: 6px; + +boxVerticalMargin: 10px; +boxWidth: 320px; +boxWideWidth: 364px; +boxPadding: margins(23px, 30px, 23px, 8px); +boxMaxListHeight: 492px; +boxLittleSkip: 10px; +boxMediumSkip: 20px; + +boxButtonPadding: margins(8px, 12px, 13px, 12px); +boxLayerButtonPadding: margins(8px, 8px, 8px, 8px); +boxLabel: FlatLabel(defaultFlatLabel) { + minWidth: 274px; + align: align(topleft); + style: boxLabelStyle; +} +boxDividerLabel: FlatLabel(boxLabel) { + minWidth: 245px; + align: align(topleft); + textFg: windowSubTextFg; + style: defaultTextStyle; +} + +boxLoadingAnimation: InfiniteRadialAnimation(defaultInfiniteRadialAnimation) { + color: windowSubTextFg; + thickness: 2px; +} +boxLoadingSize: 20px; + +boxDividerBg: windowBgOver; +boxDividerFg: windowShadowFg; +boxDividerTop: icon {{ "box_divider_top", boxDividerFg }}; +boxDividerBottom: icon {{ "box_divider_bottom", boxDividerFg }}; +boxDividerHeight: 10px; diff --git a/ui/round_rect.cpp b/ui/round_rect.cpp index 540aac3..3de09bf 100644 --- a/ui/round_rect.cpp +++ b/ui/round_rect.cpp @@ -12,7 +12,6 @@ namespace Ui { - void DrawRoundedRect( QPainter &p, const QRect &rect, @@ -75,8 +74,11 @@ RoundRect::RoundRect( }, _lifetime); } -void RoundRect::paint(QPainter &p, const QRect &rect) const { - DrawRoundedRect(p, rect, _color, _corners); +void RoundRect::paint( + QPainter &p, + const QRect &rect, + RectParts parts) const { + DrawRoundedRect(p, rect, _color, _corners, parts); } } // namespace Ui diff --git a/ui/round_rect.h b/ui/round_rect.h index 55d112e..672605a 100644 --- a/ui/round_rect.h +++ b/ui/round_rect.h @@ -25,7 +25,10 @@ class RoundRect final { public: RoundRect(ImageRoundRadius radius, const style::color &color); - void paint(QPainter &p, const QRect &rect) const; + void paint( + QPainter &p, + const QRect &rect, + RectParts parts = RectPart::Full) const; private: style::color _color; diff --git a/ui/widgets/box_content_divider.cpp b/ui/widgets/box_content_divider.cpp new file mode 100644 index 0000000..c0b2218 --- /dev/null +++ b/ui/widgets/box_content_divider.cpp @@ -0,0 +1,42 @@ +// 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/box_content_divider.h" + +#include "styles/style_layers.h" + +#include +#include + +namespace Ui { + +BoxContentDivider::BoxContentDivider(QWidget *parent) +: BoxContentDivider(parent, st::boxDividerHeight) { +} + +BoxContentDivider::BoxContentDivider(QWidget *parent, int height) +: RpWidget(parent) { + resize(width(), height); +} + +void BoxContentDivider::paintEvent(QPaintEvent *e) { + QPainter p(this); + p.fillRect(e->rect(), st::boxDividerBg); + const auto dividerFillTop = QRect( + 0, + 0, + width(), + st::boxDividerTop.height()); + st::boxDividerTop.fill(p, dividerFillTop); + const auto dividerFillBottom = myrtlrect( + 0, + height() - st::boxDividerBottom.height(), + width(), + st::boxDividerBottom.height()); + st::boxDividerBottom.fill(p, dividerFillBottom); +} + +} // namespace Ui diff --git a/ui/widgets/box_content_divider.h b/ui/widgets/box_content_divider.h new file mode 100644 index 0000000..572beba --- /dev/null +++ b/ui/widgets/box_content_divider.h @@ -0,0 +1,23 @@ +// This file is part of Desktop App Toolkit, +// a set of libraries for developing nice desktop applications. +// +// For license and copyright information please follow this link: +// https://github.com/desktop-app/legal/blob/master/LEGAL +// +#pragma once + +#include "ui/rp_widget.h" + +namespace Ui { + +class BoxContentDivider : public Ui::RpWidget { +public: + BoxContentDivider(QWidget *parent); + BoxContentDivider(QWidget *parent, int height); + +protected: + void paintEvent(QPaintEvent *e) override; + +}; + +} // namespace Ui diff --git a/ui/widgets/labels.cpp b/ui/widgets/labels.cpp index ad05dc1..a29544c 100644 --- a/ui/widgets/labels.cpp +++ b/ui/widgets/labels.cpp @@ -9,6 +9,7 @@ #include "ui/text/text_entity.h" #include "ui/effects/animation_value.h" #include "ui/widgets/popup_menu.h" +#include "ui/widgets/box_content_divider.h" #include "ui/basic_click_handlers.h" // UrlClickHandler #include "ui/inactive_press.h" @@ -886,4 +887,14 @@ void FlatLabel::paintEvent(QPaintEvent *e) { } } +int DividerLabel::naturalWidth() const { + return -1; +} + +void DividerLabel::resizeEvent(QResizeEvent *e) { + _background->lower(); + _background->setGeometry(rect()); + return PaddingWrap::resizeEvent(e); +} + } // namespace Ui diff --git a/ui/widgets/labels.h b/ui/widgets/labels.h index fc9e60a..3ae79fb 100644 --- a/ui/widgets/labels.h +++ b/ui/widgets/labels.h @@ -10,6 +10,7 @@ #include "ui/wrap/padding_wrap.h" #include "ui/text/text.h" #include "ui/click_handler.h" +#include "ui/widgets/box_content_divider.h" #include "styles/style_widgets.h" #include @@ -19,6 +20,7 @@ class QTouchEvent; namespace Ui { class PopupMenu; +class BoxContentDivider; class CrossFadeAnimation { public: @@ -216,4 +218,19 @@ private: }; +class DividerLabel : public PaddingWrap { +public: + using PaddingWrap::PaddingWrap; + + int naturalWidth() const override; + +protected: + void resizeEvent(QResizeEvent *e) override; + +private: + object_ptr _background + = object_ptr(this); + +}; + } // namespace Ui