diff --git a/CMakeLists.txt b/CMakeLists.txt index 88c28e6..6188df5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -135,6 +135,8 @@ PRIVATE ui/widgets/popup_menu.h ui/widgets/scroll_area.cpp ui/widgets/scroll_area.h + ui/widgets/side_bar_button.cpp + ui/widgets/side_bar_button.h ui/widgets/shadow.cpp ui/widgets/shadow.h ui/widgets/tooltip.cpp @@ -149,6 +151,8 @@ PRIVATE ui/wrap/slide_wrap.h ui/wrap/vertical_layout.cpp ui/wrap/vertical_layout.h + ui/wrap/vertical_layout_reorder.cpp + ui/wrap/vertical_layout_reorder.h ui/wrap/wrap.h ui/abstract_button.cpp ui/abstract_button.h diff --git a/ui/colors.palette b/ui/colors.palette index 7e154c3..4516f3d 100644 --- a/ui/colors.palette +++ b/ui/colors.palette @@ -592,3 +592,14 @@ walletSubBalanceFg: #f9f9f9; // wallet balance label text walletTopLabelFg: #999999; // wallet top updated label text walletTopIconFg: walletTopLabelFg; // wallet top refresh and menu icons walletTopIconRipple: #ffffff12; // wallet top menu icon ripple effect + +sideBarBg: #293a4c; // filters side bar background +sideBarBgActive: #17212b; // filters side bar active background +sideBarBgRipple: #1e2b38; // filters side bar ripple effect +sideBarTextFg: #8897a6; // filters side bar text +sideBarTextFgActive: #64b9fa; // filters side bar active item text +sideBarIconFg: #8393a3; // filters side bar icon +sideBarIconFgActive: #5eb5f7; // filters side bar active item icon +sideBarBadgeBg: #5eb5f7; // filters side bar badge background +sideBarBadgeBgMuted: #8393a3; // filters side bar unimportant badge background +sideBarBadgeFg: #ffffff; // filters side bar badge text diff --git a/ui/layers/generic_box.cpp b/ui/layers/generic_box.cpp index 2e1f643..94888e8 100644 --- a/ui/layers/generic_box.cpp +++ b/ui/layers/generic_box.cpp @@ -25,4 +25,8 @@ void GenericBox::addSkip(int height) { addRow(object_ptr(this, height)); } +not_null GenericBox::verticalLayout() { + return _content.data(); +} + } // namespace Ui diff --git a/ui/layers/generic_box.h b/ui/layers/generic_box.h index 90158f8..058f05c 100644 --- a/ui/layers/generic_box.h +++ b/ui/layers/generic_box.h @@ -77,6 +77,8 @@ public: } } + [[nodiscard]] not_null verticalLayout(); + using BoxContent::setNoContentMargin; protected: diff --git a/ui/layers/layer_widget.cpp b/ui/layers/layer_widget.cpp index 91df272..1dd165d 100644 --- a/ui/layers/layer_widget.cpp +++ b/ui/layers/layer_widget.cpp @@ -633,12 +633,21 @@ void LayerStackWidget::prepareForAnimation() { show(); } if (_mainMenu) { + if (Ui::InFocusChain(_mainMenu)) { + setFocus(); + } _mainMenu->hide(); } if (_specialLayer) { + if (Ui::InFocusChain(_specialLayer)) { + setFocus(); + } _specialLayer->hide(); } if (const auto layer = currentLayer()) { + if (Ui::InFocusChain(layer)) { + setFocus(); + } layer->hide(); } } diff --git a/ui/platform/mac/ui_utility_mac.mm b/ui/platform/mac/ui_utility_mac.mm index 3cac656..803d1a7 100644 --- a/ui/platform/mac/ui_utility_mac.mm +++ b/ui/platform/mac/ui_utility_mac.mm @@ -41,7 +41,7 @@ void InitOnTopPanel(not_null panel) { Assert([platformWindow isKindOfClass:[NSPanel class]]); auto platformPanel = static_cast(platformWindow); - [platformPanel setLevel:NSPopUpMenuWindowLevel]; + [platformPanel setLevel:NSModalPanelWindowLevel]; [platformPanel setCollectionBehavior:NSWindowCollectionBehaviorCanJoinAllSpaces|NSWindowCollectionBehaviorStationary|NSWindowCollectionBehaviorFullScreenAuxiliary|NSWindowCollectionBehaviorIgnoresCycle]; [platformPanel setHidesOnDeactivate:NO]; //[platformPanel setFloatingPanel:YES]; diff --git a/ui/widgets/side_bar_button.cpp b/ui/widgets/side_bar_button.cpp new file mode 100644 index 0000000..193d23a --- /dev/null +++ b/ui/widgets/side_bar_button.cpp @@ -0,0 +1,183 @@ +// 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/side_bar_button.h" + +#include "ui/effects/ripple_animation.h" + +#include + +namespace Ui { +namespace { + +constexpr auto kMaxLabelLines = 3; + +} // namespace + +SideBarButton::SideBarButton( + not_null parent, + const QString &title, + const style::SideBarButton &st) +: RippleButton(parent, st.ripple) +, _st(st) +, _text(_st.minTextWidth) { + _text.setText(_st.style, title); + setAttribute(Qt::WA_OpaquePaintEvent); + + style::PaletteChanged( + ) | rpl::start_with_next([=] { + _iconCache = _iconCacheActive = QImage(); + update(); + }, lifetime()); +} + +void SideBarButton::setActive(bool active) { + if (_active == active) { + return; + } + _active = active; + update(); +} + +void SideBarButton::setBadge(const QString &badge, bool muted) { + if (_badge.toString() == badge && _badgeMuted == muted) { + return; + } + _badge.setText(_st.badgeStyle, badge); + _badgeMuted = muted; + const auto width = badge.isEmpty() + ? 0 + : std::max(_st.badgeHeight, _badge.maxWidth() + 2 * _st.badgeSkip); + if (_iconCacheBadgeWidth != width) { + _iconCacheBadgeWidth = width; + _iconCache = _iconCacheActive = QImage(); + } + update(); +} + +void SideBarButton::setIconOverride( + const style::icon *iconOverride, + const style::icon *iconOverrideActive) { + _iconOverride = iconOverride; + _iconOverrideActive = iconOverrideActive; + update(); +} + +int SideBarButton::resizeGetHeight(int newWidth) { + auto result = _st.minHeight; + const auto text = std::min( + _text.countHeight(newWidth - _st.textSkip * 2), + _st.style.font->height * kMaxLabelLines); + const auto add = text - _st.style.font->height; + return result + std::max(add, 0); +} + +void SideBarButton::paintEvent(QPaintEvent *e) { + auto p = Painter(this); + const auto clip = e->rect(); + + p.fillRect(clip, _active ? _st.textBgActive : _st.textBg); + + RippleButton::paintRipple(p, 0, 0); + + const auto &icon = computeIcon(); + const auto x = (_st.iconPosition.x() < 0) + ? (width() - icon.width()) / 2 + : _st.iconPosition.x(); + const auto y = (_st.iconPosition.y() < 0) + ? (height() - icon.height()) / 2 + : _st.iconPosition.y(); + if (_iconCacheBadgeWidth) { + validateIconCache(); + p.drawImage(x, y, _active ? _iconCacheActive : _iconCache); + } else { + icon.paint(p, x, y, width()); + } + p.setPen(_active ? _st.textFgActive : _st.textFg); + _text.drawElided( + p, + _st.textSkip, + _st.textTop, + (width() - 2 * _st.textSkip), + kMaxLabelLines, + style::al_top); + + if (_iconCacheBadgeWidth) { + const auto desiredLeft = width() / 2 + _st.badgePosition.x(); + const auto x = std::min( + desiredLeft, + width() - _iconCacheBadgeWidth - st::defaultScrollArea.width); + const auto y = _st.badgePosition.y(); + + auto hq = PainterHighQualityEnabler(p); + p.setPen(Qt::NoPen); + p.setBrush((_badgeMuted && !_active) + ? _st.badgeBgMuted + : _st.badgeBg); + const auto r = _st.badgeHeight / 2; + p.drawRoundedRect(x, y, _iconCacheBadgeWidth, _st.badgeHeight, r, r); + + p.setPen(_st.badgeFg); + _badge.draw( + p, + x + (_iconCacheBadgeWidth - _badge.maxWidth()) / 2, + y + (_st.badgeHeight - _st.badgeStyle.font->height) / 2, + width()); + } +} + +const style::icon &SideBarButton::computeIcon() const { + return _active + ? (_iconOverrideActive + ? *_iconOverrideActive + : !_st.iconActive.empty() + ? _st.iconActive + : _iconOverride + ? *_iconOverride + : _st.icon) + : _iconOverride + ? *_iconOverride + : _st.icon; +} + +void SideBarButton::validateIconCache() { + Expects(_st.iconPosition.x() < 0); + Expects(_st.iconPosition.y() >= 0); + + if (!(_active ? _iconCacheActive : _iconCache).isNull()) { + return; + } + const auto &icon = computeIcon(); + auto image = QImage( + icon.size() * style::DevicePixelRatio(), + QImage::Format_ARGB32_Premultiplied); + image.setDevicePixelRatio(style::DevicePixelRatio()); + image.fill(Qt::transparent); + { + auto p = QPainter(&image); + icon.paint(p, 0, 0, icon.width()); + p.setCompositionMode(QPainter::CompositionMode_Source); + p.setBrush(Qt::transparent); + auto pen = QPen(Qt::transparent); + pen.setWidth(2 * _st.badgeStroke); + p.setPen(pen); + auto hq = PainterHighQualityEnabler(p); + const auto desiredLeft = (icon.width() / 2) + _st.badgePosition.x(); + const auto x = std::min( + desiredLeft, + (width() + - _iconCacheBadgeWidth + - st::defaultScrollArea.width + - (width() / 2) + + (icon.width() / 2))); + const auto y = _st.badgePosition.y() - _st.iconPosition.y(); + const auto r = _st.badgeHeight / 2.; + p.drawRoundedRect(x, y, _iconCacheBadgeWidth, _st.badgeHeight, r, r); + } + (_active ? _iconCacheActive : _iconCache) = std::move(image); +} + +} // namespace Ui diff --git a/ui/widgets/side_bar_button.h b/ui/widgets/side_bar_button.h new file mode 100644 index 0000000..4bacb17 --- /dev/null +++ b/ui/widgets/side_bar_button.h @@ -0,0 +1,115 @@ +// 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/widgets/buttons.h" +#include "ui/text/text.h" + +namespace style { +struct SideBarButton; +} // namespace style + +namespace Ui { + +class RippleAnimation; + +class SideBarButton final : public Ui::RippleButton { +public: + SideBarButton( + not_null parent, + const QString &title, + const style::SideBarButton &st); + + void setActive(bool active); + void setBadge(const QString &badge, bool muted); + void setIconOverride( + const style::icon *iconOverride, + const style::icon *iconOverrideActive = nullptr); + + int resizeGetHeight(int newWidth) override; + +private: + void paintEvent(QPaintEvent *e) override; + + [[nodiscard]] const style::icon &computeIcon() const; + void validateIconCache(); + + const style::SideBarButton &_st; + const style::icon *_iconOverride = nullptr; + const style::icon *_iconOverrideActive = nullptr; + Ui::Text::String _text; + Ui::Text::String _badge; + QImage _iconCache; + QImage _iconCacheActive; + int _iconCacheBadgeWidth = 0; + bool _active = false; + bool _badgeMuted = false; + +}; +// +//class SideBarMenu final { +//public: +// struct Item { +// QString id; +// QString title; +// QString badge; +// not_null icon; +// not_null iconActive; +// int iconTop = 0; +// }; +// +// SideBarMenu(not_null parent, const style::SideBarMenu &st); +// ~SideBarMenu(); +// +// [[nodiscard]] not_null widget() const; +// +// void setGeometry(QRect geometry); +// void setItems(std::vector items); +// void setActive( +// const QString &id, +// anim::type animated = anim::type::normal); +// [[nodiscard]] rpl::producer activateRequests() const; +// +// [[nodiscard]] rpl::lifetime &lifetime(); +// +//private: +// struct MenuItem { +// Item data; +// Ui::Text::String text; +// mutable std::unique_ptr ripple; +// int top = 0; +// int height = 0; +// }; +// void setup(); +// void paint(Painter &p, QRect clip) const; +// [[nodiscard]] int countContentHeight(int width, int outerHeight); +// +// void mouseMove(QPoint position); +// void mousePress(Qt::MouseButton button); +// void mouseRelease(Qt::MouseButton button); +// +// void setSelected(int selected); +// void setPressed(int pressed); +// void addRipple(MenuItem &item, QPoint position); +// void repaint(const QString &id); +// [[nodiscard]] MenuItem *itemById(const QString &id); +// +// const style::SideBarMenu &_st; +// +// Ui::RpWidget _outer; +// const not_null _scroll; +// const not_null _inner; +// std::vector _items; +// int _selected = -1; +// int _pressed = -1; +// +// QString _activeId; +// rpl::event_stream _activateRequests; +// +//}; + +} // namespace Ui diff --git a/ui/widgets/widgets.style b/ui/widgets/widgets.style index 04eabf0..e6e9fae 100644 --- a/ui/widgets/widgets.style +++ b/ui/widgets/widgets.style @@ -544,6 +544,30 @@ WindowTitle { closeIconActiveOver: icon; } +SideBarButton { + icon: icon; + iconActive: icon; + iconPosition: point; + textTop: pixels; + textSkip: pixels; + minTextWidth: pixels; + minHeight: pixels; + style: TextStyle; + badgeStyle: TextStyle; + badgeSkip: pixels; + badgeHeight: pixels; + badgeStroke: pixels; + badgePosition: point; + textBg: color; + textBgActive: color; + textFg: color; + textFgActive: color; + badgeBg: color; + badgeBgMuted: color; + badgeFg: color; + ripple: RippleAnimation; +} + defaultLabelSimple: LabelSimple { font: normalFont; maxWidth: 0px; @@ -884,6 +908,26 @@ defaultRoundCheckbox: RoundCheckbox { 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) { @@ -1061,6 +1105,7 @@ PeerListItem { maximalWidth: pixels; button: OutlineButton; + checkbox: RoundImageCheckbox; statusFg: color; statusFgOver: color; statusFgActive: color; @@ -1096,6 +1141,7 @@ defaultPeerListItem: PeerListItem { statusPosition: point(68px, 31px); photoSize: 46px; button: defaultPeerListButton; + checkbox: defaultPeerListCheckbox; statusFg: windowSubTextFg; statusFgOver: windowSubTextFgOver; statusFgActive: windowActiveTextFg; @@ -1207,6 +1253,19 @@ defaultSettingsButton: SettingsButton { ripple: defaultRippleAnimation; } +defaultSideBarButton: SideBarButton { + textBg: sideBarBg; + textBgActive: sideBarBgActive; + textFg: sideBarTextFg; + textFgActive: sideBarTextFgActive; + badgeBg: sideBarBadgeBg; + badgeBgMuted: sideBarBadgeBgMuted; + badgeFg: sideBarBadgeFg; + ripple: RippleAnimation(defaultRippleAnimation) { + color: sideBarBgRipple; + } +} + // Windows specific title windowTitleButton: IconButton { diff --git a/ui/wrap/vertical_layout.cpp b/ui/wrap/vertical_layout.cpp index e0c836a..c43aa0a 100644 --- a/ui/wrap/vertical_layout.cpp +++ b/ui/wrap/vertical_layout.cpp @@ -48,6 +48,26 @@ int VerticalLayout::naturalWidth() const { return result; } +void VerticalLayout::setVerticalShift(int index, int shift) { + Expects(index >= 0 && index < _rows.size()); + + auto &row = _rows[index]; + if (const auto delta = shift - row.verticalShift) { + row.verticalShift = shift; + row.widget->move(row.widget->x(), row.widget->y() + delta); + row.widget->update(); + } +} + +void VerticalLayout::reorderRows(int oldIndex, int newIndex) { + Expects(oldIndex >= 0 && oldIndex < _rows.size()); + Expects(newIndex >= 0 && newIndex < _rows.size()); + Expects(!_inResize); + + base::reorder(_rows, oldIndex, newIndex); + resizeToWidth(width()); +} + int VerticalLayout::resizeGetHeight(int newWidth) { _inResize = true; auto guard = gsl::finally([&] { _inResize = false; }); @@ -60,7 +80,7 @@ int VerticalLayout::resizeGetHeight(int newWidth) { row.widget, row.margin, newWidth, - result); + result + row.verticalShift); result += row.margin.top() + row.widget->heightNoMargins() + row.margin.bottom(); @@ -100,18 +120,12 @@ RpWidget *VerticalLayout::insertChild( object_ptr child, const style::margins &margin) { Expects(atPosition >= 0 && atPosition <= _rows.size()); + Expects(!_inResize); if (const auto weak = AttachParentChild(this, child)) { _rows.insert( begin(_rows) + atPosition, { std::move(child), margin }); - const auto margins = getMargins(); - updateChildGeometry( - margins, - weak, - margin, - width() - margins.left() - margins.right(), - height() - margins.top() - margins.bottom()); weak->heightValue( ) | rpl::start_with_next_done([=] { if (!_inResize) { diff --git a/ui/wrap/vertical_layout.h b/ui/wrap/vertical_layout.h index e0a6759..37851b3 100644 --- a/ui/wrap/vertical_layout.h +++ b/ui/wrap/vertical_layout.h @@ -15,9 +15,14 @@ class VerticalLayout : public RpWidget { public: using RpWidget::RpWidget; - int count() const { + [[nodiscard]] int count() const { return _rows.size(); } + [[nodiscard]] not_null widgetAt(int index) const { + Expects(index >= 0 && index < count()); + + return _rows[index].widget.data(); + } template < typename Widget, @@ -46,6 +51,9 @@ public: QMargins getMargins() const override; int naturalWidth() const override; + void setVerticalShift(int index, int shift); + void reorderRows(int oldIndex, int newIndex); + protected: int resizeGetHeight(int newWidth) override; void visibleTopBottomUpdated( @@ -69,6 +77,7 @@ private: struct Row { object_ptr widget; style::margins margin; + int verticalShift = 0; }; std::vector _rows; bool _inResize = false; diff --git a/ui/wrap/vertical_layout_reorder.cpp b/ui/wrap/vertical_layout_reorder.cpp new file mode 100644 index 0000000..e312114 --- /dev/null +++ b/ui/wrap/vertical_layout_reorder.cpp @@ -0,0 +1,270 @@ +// 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/wrap/vertical_layout_reorder.h" + +#include "ui/wrap/vertical_layout.h" +#include "styles/style_basic.h" + +#include +#include + +namespace Ui { + +VerticalLayoutReorder::VerticalLayoutReorder( + not_null layout) +: _layout(layout) { +} + +void VerticalLayoutReorder::cancel() { + if (_currentWidget) { + cancelCurrent(indexOf(_currentWidget)); + } + _lifetime.destroy(); + for (auto i = 0, count = _layout->count(); i != count; ++i) { + _layout->setVerticalShift(i, 0); + } + _entries.clear(); +} + +void VerticalLayoutReorder::start() { + const auto count = _layout->count(); + if (count < 2) { + return; + } + for (auto i = 0; i != count; ++i) { + const auto widget = _layout->widgetAt(i); + widget->events( + ) | rpl::start_with_next_done([=](not_null e) { + switch (e->type()) { + case QEvent::MouseMove: + mouseMove( + widget, + static_cast(e.get())->globalPos()); + break; + case QEvent::MouseButtonPress: + mousePress( + widget, + static_cast(e.get())->button(), + static_cast(e.get())->globalPos()); + break; + case QEvent::MouseButtonRelease: + mouseRelease( + widget, + static_cast(e.get())->button()); + break; + } + }, [=] { + cancel(); + }, _lifetime); + _entries.push_back({ widget }); + } +} + +void VerticalLayoutReorder::mouseMove( + not_null widget, + QPoint position) { + if (_currentWidget != widget) { + return; + } else if (_currentState != State::Started) { + checkForStart(position); + } else { + updateOrder(indexOf(_currentWidget), position); + } +} + +void VerticalLayoutReorder::checkForStart(QPoint position) { + const auto shift = position.y() - _currentStart; + const auto delta = QApplication::startDragDistance(); + if (std::abs(shift) <= delta) { + return; + } + _currentWidget->raise(); + _currentState = State::Started; + _currentStart += (shift > 0) ? delta : -delta; + + const auto index = indexOf(_currentWidget); + _currentDesiredIndex = index; + _updates.fire({ _currentWidget, index, index, _currentState }); + + updateOrder(index, position); +} + +void VerticalLayoutReorder::updateOrder(int index, QPoint position) { + const auto shift = position.y() - _currentStart; + auto ¤t = _entries[index]; + current.shiftAnimation.stop(); + current.shift = current.finalShift = shift; + _layout->setVerticalShift(index, shift); + + const auto count = _entries.size(); + const auto currentHeight = current.widget->height(); + const auto currentMiddle = current.widget->y() + currentHeight / 2; + _currentDesiredIndex = index; + if (shift > 0) { + auto top = current.widget->y() - shift; + for (auto next = index + 1; next != count; ++next) { + const auto &entry = _entries[next]; + top += entry.widget->height(); + if (currentMiddle < top) { + moveToShift(next, 0); + } else { + _currentDesiredIndex = next; + moveToShift(next, -currentHeight); + } + } + for (auto prev = index - 1; prev >= 0; --prev) { + moveToShift(prev, 0); + } + } else { + for (auto next = index + 1; next != count; ++next) { + moveToShift(next, 0); + } + for (auto prev = index - 1; prev >= 0; --prev) { + const auto &entry = _entries[prev]; + if (currentMiddle >= entry.widget->y() - entry.shift + currentHeight) { + moveToShift(prev, 0); + } else { + _currentDesiredIndex = prev; + moveToShift(prev, currentHeight); + } + } + } +} + +void VerticalLayoutReorder::mousePress( + not_null widget, + Qt::MouseButton button, + QPoint position) { + if (button != Qt::LeftButton) { + return; + } + cancelCurrent(); + _currentWidget = widget; + _currentStart = position.y(); +} + +void VerticalLayoutReorder::mouseRelease( + not_null widget, + Qt::MouseButton button) { + if (button != Qt::LeftButton) { + return; + } + finishCurrent(); +} + +void VerticalLayoutReorder::cancelCurrent() { + if (_currentWidget) { + cancelCurrent(indexOf(_currentWidget)); + } +} + +void VerticalLayoutReorder::cancelCurrent(int index) { + Expects(_currentWidget != nullptr); + + if (_currentState == State::Started) { + _currentState = State::Cancelled; + _updates.fire({ _currentWidget, index, index, _currentState }); + } + _currentWidget = nullptr; + for (auto i = 0, count = int(_entries.size()); i != count; ++i) { + moveToShift(i, 0); + } +} + +void VerticalLayoutReorder::finishCurrent() { + if (!_currentWidget) { + return; + } + const auto index = indexOf(_currentWidget); + if (_currentDesiredIndex == index || _currentState != State::Started) { + cancelCurrent(index); + return; + } + const auto result = _currentDesiredIndex; + const auto widget = _currentWidget; + _currentState = State::Cancelled; + _currentWidget = nullptr; + + auto ¤t = _entries[index]; + const auto height = current.widget->height(); + if (index < result) { + auto sum = 0; + for (auto i = index; i != result; ++i) { + auto &entry = _entries[i + 1]; + const auto widget = entry.widget; + entry.deltaShift += height; + updateShift(widget, i + 1); + sum += widget->height(); + } + current.finalShift -= sum; + } else if (index > result) { + auto sum = 0; + for (auto i = result; i != index; ++i) { + auto &entry = _entries[i]; + const auto widget = entry.widget; + entry.deltaShift -= height; + updateShift(widget, i); + sum += widget->height(); + } + current.finalShift += sum; + } + if (!(current.finalShift + current.deltaShift)) { + current.shift = 0; + _layout->setVerticalShift(index, 0); + } + base::reorder(_entries, index, result); + _layout->reorderRows(index, _currentDesiredIndex); + for (auto i = 0, count = int(_entries.size()); i != count; ++i) { + moveToShift(i, 0); + } + + _updates.fire({ widget, index, result, State::Applied }); +} + +void VerticalLayoutReorder::moveToShift(int index, int shift) { + auto &entry = _entries[index]; + if (entry.finalShift + entry.deltaShift == shift) { + return; + } + const auto widget = entry.widget; + entry.shiftAnimation.start( + [=] { updateShift(widget, index); }, + entry.finalShift, + shift - entry.deltaShift, + st::slideWrapDuration); + entry.finalShift = shift - entry.deltaShift; +} + +void VerticalLayoutReorder::updateShift( + not_null widget, + int indexHint) { + Expects(indexHint >= 0 && indexHint < _entries.size()); + + const auto index = (_entries[indexHint].widget == widget) + ? indexHint + : indexOf(widget); + auto &entry = _entries[index]; + entry.shift = std::round(entry.shiftAnimation.value(entry.finalShift)) + + entry.deltaShift; + if (entry.deltaShift && !entry.shiftAnimation.animating()) { + entry.finalShift += entry.deltaShift; + entry.deltaShift = 0; + } + _layout->setVerticalShift(index, entry.shift); +} + +int VerticalLayoutReorder::indexOf(not_null widget) const { + const auto i = ranges::find(_entries, widget, &Entry::widget); + Assert(i != end(_entries)); + return i - begin(_entries); +} + +auto VerticalLayoutReorder::updates() const -> rpl::producer { + return _updates.events(); +} + +} // namespace Ui diff --git a/ui/wrap/vertical_layout_reorder.h b/ui/wrap/vertical_layout_reorder.h new file mode 100644 index 0000000..d5e6769 --- /dev/null +++ b/ui/wrap/vertical_layout_reorder.h @@ -0,0 +1,74 @@ +// 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 Ui { + +class RpWidget; +class VerticalLayout; + +class VerticalLayoutReorder final { +public: + enum class State : uchar { + Started, + Applied, + Cancelled, + }; + struct Single { + not_null widget; + int oldPosition = 0; + int newPosition = 0; + State state = State::Started; + }; + + VerticalLayoutReorder(not_null layout); + + void start(); + void cancel(); + [[nodiscard]] rpl::producer updates() const; + +private: + struct Entry { + not_null widget; + Ui::Animations::Simple shiftAnimation; + int shift = 0; + int finalShift = 0; + int deltaShift = 0; + }; + + void mouseMove(not_null widget, QPoint position); + void mousePress( + not_null widget, + Qt::MouseButton button, + QPoint position); + void mouseRelease(not_null widget, Qt::MouseButton button); + + void checkForStart(QPoint position); + void updateOrder(int index, QPoint position); + void cancelCurrent(); + void finishCurrent(); + void cancelCurrent(int index); + + [[nodiscard]] int indexOf(not_null widget) const; + void moveToShift(int index, int shift); + void updateShift(not_null widget, int indexHint); + + const not_null _layout; + + RpWidget *_currentWidget = nullptr; + int _currentStart = 0; + int _currentDesiredIndex = 0; + State _currentState = State::Cancelled; + std::vector _entries; + rpl::event_stream _updates; + rpl::lifetime _lifetime; + +}; + +} // namespace Ui