From 051b8caee6d742cd5602120a9e6b3c13da9b5fd4 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Fri, 3 Apr 2026 03:51:33 +1100 Subject: [PATCH] fix: move anim logic to qml Fixes closing in quick succession not updating movement correctly --- modules/sidebar/NotifDockList.qml | 23 ++- .../src/Caelestia/Components/lazylistview.cpp | 162 ++++++++---------- .../src/Caelestia/Components/lazylistview.hpp | 15 +- 3 files changed, 101 insertions(+), 99 deletions(-) diff --git a/modules/sidebar/NotifDockList.qml b/modules/sidebar/NotifDockList.qml index f2dea1c2..8517870b 100644 --- a/modules/sidebar/NotifDockList.qml +++ b/modules/sidebar/NotifDockList.qml @@ -72,9 +72,12 @@ LazyListView { } } - LazyListView.preferredHeight: closed ? 0 : notifInner.implicitHeight + LazyListView.preferredHeight: closed ? 0 : notifInner.nonAnimHeight implicitHeight: notifInner.implicitHeight + opacity: LazyListView.removing || closed || LazyListView.adding ? 0 : 1 + scale: LazyListView.removing || closed ? 0.6 : LazyListView.adding ? 0 : 1 + hoverEnabled: true cursorShape: pressed ? Qt.ClosedHandCursor : undefined acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton @@ -114,6 +117,24 @@ LazyListView { visibilities: root.visibilities } + Behavior on y { + Anim { + duration: Appearance.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + } + } + + Behavior on opacity { + Anim {} + } + + Behavior on scale { + Anim { + duration: Appearance.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + } + } + Behavior on x { Anim { duration: Appearance.anim.durations.expressiveDefaultSpatial diff --git a/plugin/src/Caelestia/Components/lazylistview.cpp b/plugin/src/Caelestia/Components/lazylistview.cpp index d4baab49..c92f6963 100644 --- a/plugin/src/Caelestia/Components/lazylistview.cpp +++ b/plugin/src/Caelestia/Components/lazylistview.cpp @@ -2,6 +2,7 @@ #include #include +#include namespace caelestia::components { @@ -21,12 +22,33 @@ void LazyListViewAttached::setPreferredHeight(qreal height) { emit preferredHeightChanged(); } +bool LazyListViewAttached::adding() const { + return m_adding; +} + +void LazyListViewAttached::setAdding(bool adding) { + if (m_adding == adding) + return; + m_adding = adding; + emit addingChanged(); +} + +bool LazyListViewAttached::removing() const { + return m_removing; +} + +void LazyListViewAttached::setRemoving(bool removing) { + if (m_removing == removing) + return; + m_removing = removing; + emit removingChanged(); +} + // --- LazyListView --- LazyListView::LazyListView(QQuickItem* parent) : QQuickItem(parent) { setFlag(ItemHasContents, false); - setClip(true); } LazyListViewAttached* LazyListView::qmlAttachedProperties(QObject* object) { @@ -345,48 +367,22 @@ void LazyListView::updatePolish() { relayout(); syncDelegates(); - // Animate newly created delegates that were pending add animation - QSet pendingAdds; - m_pendingAddAnimations.swap(pendingAdds); - for (int idx : std::as_const(pendingAdds)) { - if (m_delegates.contains(idx) && m_addDuration > 0) - startAddAnimation(m_delegates[idx]); - } - - // Position delegates, animating displacement if a model change occurred - const bool animate = m_animateDisplacement; - m_animateDisplacement = false; - + // Position delegates — QML Behavior on y handles the animation for (auto& entry : m_delegates) { - if (!entry.item || entry.pendingRemoval || entry.animation) + if (!entry.item || entry.pendingRemoval) continue; const int idx = entry.modelIndex; if (idx < 0 || idx >= static_cast(m_layout.size())) continue; - const qreal targetY = m_layout[idx].targetY - m_contentY; - const qreal currentY = entry.item->y(); + if (m_layout[idx].heightKnown && qFuzzyIsNull(m_layout[idx].height)) + continue; - if (animate && !qFuzzyCompare(currentY, targetY) && m_moveDuration > 0 - && !pendingAdds.contains(idx)) { - startMoveAnimation(entry, currentY); - } else if (!entry.animation) { - entry.item->setY(targetY); - } + // Use setProperty to go through the QML property system, + // which triggers Behaviors (setY bypasses them). + entry.item->setProperty("y", m_layout[idx].targetY - m_contentY); } - - // Flush delegate pool — any delegates not reclaimed are truly removed - for (auto& pooled : m_delegatePool) { - pooled.pendingRemoval = true; - if (m_removeDuration > 0 && pooled.item) { - startRemoveAnimation(pooled); - m_dyingDelegates.append(std::move(pooled)); - } else { - destroyDelegate(pooled); - } - } - m_delegatePool.clear(); } // --- Layout Engine --- @@ -491,6 +487,8 @@ void LazyListView::syncDelegates() { m_layout[i].heightKnown = true; trackHeight(h); } + // Position immediately so it doesn't flash at y=0 + entry.item->setY(m_layout[i].targetY - m_contentY); m_delegates.insert(i, std::move(entry)); } } @@ -504,26 +502,8 @@ LazyListView::DelegateEntry LazyListView::createDelegate(int modelIndex) { if (!m_delegate || !m_model) return entry; - // Try to reclaim a delegate from the pool (reuse after remove+insert cycle) const auto roleNames = m_model->roleNames(); const auto role = roleNames.isEmpty() ? Qt::DisplayRole : roleNames.constBegin().key(); - const auto targetData = m_model->data(m_model->index(modelIndex, 0), role); - - for (auto it = m_delegatePool.begin(); it != m_delegatePool.end(); ++it) { - if (!it->item) - continue; - const auto poolData = it->item->property("modelData"); - if (poolData == targetData) { - entry = std::move(*it); - m_delegatePool.erase(it); - entry.modelIndex = modelIndex; - entry.pendingRemoval = false; - updateDelegateData(entry); - entry.item->setParentItem(this); - entry.item->setWidth(width()); - return entry; - } - } // Use the delegate component's creation context so the delegate // can access ids and properties from the scope where it was defined. @@ -572,8 +552,19 @@ LazyListView::DelegateEntry LazyListView::createDelegate(int modelIndex) { entry.item->setParentItem(this); entry.item->setWidth(width()); + + // Set adding = true before completeCreate so bindings see it during initial evaluation. + // Cleared after creation so the transition from true→false triggers QML Behaviors. + auto* addingAttached = qobject_cast( + qmlAttachedPropertiesObject(entry.item, true)); + if (addingAttached) + addingAttached->setAdding(true); + m_delegate->completeCreate(); + if (addingAttached) + addingAttached->setAdding(false); + // Shared height-change handler — captures item pointer instead of index // so it remains valid after model inserts/removes/moves shift indices. auto onHeightChanged = [this, item = entry.item] { @@ -713,10 +704,6 @@ void LazyListView::resetContent() { destroyDelegate(entry); m_dyingDelegates.clear(); - for (auto& entry : m_delegatePool) - destroyDelegate(entry); - m_delegatePool.clear(); - if (m_activeAnimations != 0) { m_activeAnimations = 0; emit settledChanged(); @@ -726,7 +713,6 @@ void LazyListView::resetContent() { m_knownHeightSum = 0; m_knownHeightCount = 0; m_pendingAddAnimations.clear(); - m_animateDisplacement = false; // Rebuild layout from model m_layout.clear(); @@ -766,7 +752,6 @@ void LazyListView::onRowsInserted(const QModelIndex& parent, int first, int last // Queue add animations and mark displacement for (int i = first; i <= last; ++i) m_pendingAddAnimations.insert(i); - m_animateDisplacement = true; emit countChanged(); polish(); @@ -776,14 +761,36 @@ void LazyListView::onRowsAboutToBeRemoved(const QModelIndex& parent, int first, if (parent.isValid()) return; - // Pool removed delegates — they may be reused if the model re-inserts the same data for (int i = first; i <= last; ++i) { if (!m_delegates.contains(i)) continue; auto entry = m_delegates.take(i); + entry.pendingRemoval = true; stopAnimation(entry); - m_delegatePool.append(std::move(entry)); + + if (m_removeDuration > 0 && entry.item) { + // Signal the delegate via attached property — QML handles the visual transition + auto* attached = qobject_cast( + qmlAttachedPropertiesObject(entry.item, false)); + if (attached) + attached->setRemoving(true); + + // Schedule destruction after the remove animation duration + auto* item = entry.item; + QTimer::singleShot(m_removeDuration, this, [this, item] { + for (auto it = m_dyingDelegates.begin(); it != m_dyingDelegates.end(); ++it) { + if (it->item == item) { + destroyDelegate(*it); + m_dyingDelegates.erase(it); + return; + } + } + }); + m_dyingDelegates.append(std::move(entry)); + } else { + destroyDelegate(entry); + } } } @@ -814,7 +821,6 @@ void LazyListView::onRowsRemoved(const QModelIndex& parent, int first, int last) } m_delegates = std::move(shifted); - m_animateDisplacement = true; emit countChanged(); polish(); @@ -860,7 +866,6 @@ void LazyListView::onRowsMoved(const QModelIndex& parent, int start, int end, } m_delegates = std::move(remapped); - m_animateDisplacement = true; polish(); } @@ -999,39 +1004,6 @@ void LazyListView::startRemoveAnimation(DelegateEntry& entry) { group->start(QAbstractAnimation::DeleteWhenStopped); } -void LazyListView::startMoveAnimation(DelegateEntry& entry, qreal fromY) { - if (!entry.item || m_moveDuration <= 0) - return; - - const int idx = entry.modelIndex; - if (idx < 0 || idx >= static_cast(m_layout.size())) - return; - - const qreal toY = m_layout[idx].targetY - m_contentY; - if (qFuzzyCompare(fromY, toY)) - return; - - stopAnimation(entry); - - auto* group = new QParallelAnimationGroup(this); - - auto* yAnim = new QPropertyAnimation(entry.item, "y"); - yAnim->setDuration(m_moveDuration); - yAnim->setEasingCurve(m_moveCurve); - yAnim->setStartValue(fromY); - yAnim->setEndValue(toY); - group->addAnimation(yAnim); - - entry.item->setY(fromY); - entry.animation = group; - ++m_activeAnimations; - if (m_activeAnimations == 1) - emit settledChanged(); - - connect(group, &QAbstractAnimation::finished, this, &LazyListView::onAnimationFinished); - group->start(QAbstractAnimation::DeleteWhenStopped); -} - void LazyListView::stopAnimation(DelegateEntry& entry) { if (!entry.animation) return; diff --git a/plugin/src/Caelestia/Components/lazylistview.hpp b/plugin/src/Caelestia/Components/lazylistview.hpp index c4770552..b934a2f4 100644 --- a/plugin/src/Caelestia/Components/lazylistview.hpp +++ b/plugin/src/Caelestia/Components/lazylistview.hpp @@ -18,6 +18,8 @@ class LazyListViewAttached : public QObject { Q_OBJECT Q_PROPERTY(qreal preferredHeight READ preferredHeight WRITE setPreferredHeight NOTIFY preferredHeightChanged) + Q_PROPERTY(bool adding READ adding NOTIFY addingChanged) + Q_PROPERTY(bool removing READ removing NOTIFY removingChanged) public: explicit LazyListViewAttached(QObject* parent = nullptr); @@ -25,11 +27,21 @@ public: [[nodiscard]] qreal preferredHeight() const; void setPreferredHeight(qreal height); + [[nodiscard]] bool adding() const; + void setAdding(bool adding); + + [[nodiscard]] bool removing() const; + void setRemoving(bool removing); + signals: void preferredHeightChanged(); + void addingChanged(); + void removingChanged(); private: qreal m_preferredHeight = -1; + bool m_adding = false; + bool m_removing = false; }; class LazyListView : public QQuickItem { @@ -220,7 +232,6 @@ private: // Animation void startAddAnimation(DelegateEntry& entry); void startRemoveAnimation(DelegateEntry& entry); - void startMoveAnimation(DelegateEntry& entry, qreal fromY); void stopAnimation(DelegateEntry& entry); void onAnimationFinished(); @@ -256,11 +267,9 @@ private: QVector m_layout; QHash m_delegates; QVector m_dyingDelegates; - QVector m_delegatePool; int m_activeAnimations = 0; bool m_componentComplete = false; - bool m_animateDisplacement = false; QSet m_pendingAddAnimations; QList m_modelConnections;