diff --git a/CMakeLists.txt b/CMakeLists.txt index 209df61..6188df5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -151,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/wrap/vertical_layout.cpp b/ui/wrap/vertical_layout.cpp index e0c836a..d588384 100644 --- a/ui/wrap/vertical_layout.cpp +++ b/ui/wrap/vertical_layout.cpp @@ -48,6 +48,25 @@ 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); + } +} + +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 +79,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 +119,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..bac99e0 --- /dev/null +++ b/ui/wrap/vertical_layout_reorder.cpp @@ -0,0 +1,214 @@ +// 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 +#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.shift = 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; + for (auto i = 0, count = int(_entries.size()); i != count; ++i) { + moveToShift(i, 0); + } + + _layout->reorderRows(index, _currentDesiredIndex); + base::reorder(_entries, index, result); + + _updates.fire({ widget, index, result, State::Applied }); +} + +void VerticalLayoutReorder::moveToShift(int index, int shift) { + _layout->setVerticalShift(index, shift); + _entries[index].shift = 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..11578b7 --- /dev/null +++ b/ui/wrap/vertical_layout_reorder.h @@ -0,0 +1,68 @@ +// This file is part of Desktop App Toolkit, +// a set of libraries for developing nice desktop applications. +// +// For license and copyright information please follow this link: +// https://github.com/desktop-app/legal/blob/master/LEGAL +// +#pragma once + +namespace 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; + int shift = 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); + + 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