From 700ad61969ac9ae9339b4335f0b119ad552bb103 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Fri, 3 Apr 2026 18:33:30 +1100 Subject: [PATCH] feat: listview async delegate creation and destruction --- modules/sidebar/NotifGroupList.qml | 1 + .../src/Caelestia/Components/lazylistview.cpp | 97 ++++++++++++++----- .../src/Caelestia/Components/lazylistview.hpp | 9 ++ 3 files changed, 81 insertions(+), 26 deletions(-) diff --git a/modules/sidebar/NotifGroupList.qml b/modules/sidebar/NotifGroupList.qml index e33b3eea..fab8c9b0 100644 --- a/modules/sidebar/NotifGroupList.qml +++ b/modules/sidebar/NotifGroupList.qml @@ -33,6 +33,7 @@ LazyListView { implicitHeight: contentHeight spacing: Math.round(Appearance.spacing.small / 2) + asynchronous: true removeDuration: Appearance.anim.durations.normal diff --git a/plugin/src/Caelestia/Components/lazylistview.cpp b/plugin/src/Caelestia/Components/lazylistview.cpp index b8c37976..07c4d31e 100644 --- a/plugin/src/Caelestia/Components/lazylistview.cpp +++ b/plugin/src/Caelestia/Components/lazylistview.cpp @@ -4,6 +4,13 @@ #include #include +namespace { + +constexpr int ASYNC_BATCH_CREATE = 2; +constexpr int ASYNC_BATCH_DESTROY = 4; + +} // namespace + namespace caelestia::components { // --- LazyListViewAttached --- @@ -195,6 +202,17 @@ void LazyListView::setEstimatedHeight(qreal height) { polish(); } +bool LazyListView::asynchronous() const { + return m_asynchronous; +} + +void LazyListView::setAsynchronous(bool async) { + if (m_asynchronous == async) + return; + m_asynchronous = async; + emit asynchronousChanged(); +} + qreal LazyListView::effectiveEstimatedHeight() const { if (m_estimatedHeight >= 0) return m_estimatedHeight; @@ -531,41 +549,68 @@ void LazyListView::syncDelegates() { visibleIndices.insert(i); } - // Destroy delegates outside visible range (if not animating) + // Collect delegates to destroy (outside visible range and not animating) QList toRemove; for (auto it = m_delegates.begin(); it != m_delegates.end(); ++it) { - if (!visibleIndices.contains(it.key()) && !it->animation) { + if (!visibleIndices.contains(it.key()) && !it->animation) toRemove.append(it.key()); - } - } - for (int idx : toRemove) { - auto entry = m_delegates.take(idx); - destroyDelegate(entry); } - // Create delegates for newly visible indices + // Batch destroy + const int destroyBudget = m_asynchronous ? ASYNC_BATCH_DESTROY : static_cast(toRemove.size()); + QVector removedEntries; + removedEntries.reserve(std::min(destroyBudget, static_cast(toRemove.size()))); + int destroyed = 0; + for (int idx : toRemove) { + if (destroyed >= destroyBudget) + break; + removedEntries.append(m_delegates.take(idx)); + ++destroyed; + } + for (auto& entry : removedEntries) + destroyDelegate(entry); + + // Collect indices to create + QList toCreate; if (first >= 0) { for (int i = first; i <= last; ++i) { - if (m_delegates.contains(i)) - continue; - - auto entry = createDelegate(i); - if (entry.item) { - // Measure height (prefer attached preferredHeight, fall back to implicitHeight) - const qreal h = delegateHeight(entry.item); - if (!m_layout[i].heightKnown || !qFuzzyCompare(m_layout[i].height, h)) { - if (m_layout[i].heightKnown) - untrackHeight(m_layout[i].height); - m_layout[i].height = h; - 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)); - } + if (!m_delegates.contains(i)) + toCreate.append(i); } } + + // Batch create + const int createBudget = m_asynchronous ? ASYNC_BATCH_CREATE : static_cast(toCreate.size()); + int created = 0; + bool layoutChanged = false; + for (int i : toCreate) { + if (created >= createBudget) + break; + + auto entry = createDelegate(i); + if (entry.item) { + const qreal h = delegateHeight(entry.item); + if (!m_layout[i].heightKnown || !qFuzzyCompare(m_layout[i].height, h)) { + if (m_layout[i].heightKnown) + untrackHeight(m_layout[i].height); + m_layout[i].height = h; + m_layout[i].heightKnown = true; + trackHeight(h); + layoutChanged = true; + } + entry.item->setY(m_layout[i].targetY - m_contentY); + m_delegates.insert(i, std::move(entry)); + ++created; + } + } + + if (layoutChanged) + relayout(); + + // If async and there's remaining work, schedule another pass + if (m_asynchronous && + (destroyed < static_cast(toRemove.size()) || created < static_cast(toCreate.size()))) + polish(); } LazyListView::DelegateEntry LazyListView::createDelegate(int modelIndex) { diff --git a/plugin/src/Caelestia/Components/lazylistview.hpp b/plugin/src/Caelestia/Components/lazylistview.hpp index 7147169c..e24be426 100644 --- a/plugin/src/Caelestia/Components/lazylistview.hpp +++ b/plugin/src/Caelestia/Components/lazylistview.hpp @@ -73,6 +73,9 @@ class LazyListView : public QQuickItem { // Sizing Q_PROPERTY(qreal estimatedHeight READ estimatedHeight WRITE setEstimatedHeight NOTIFY estimatedHeightChanged) + // Async + Q_PROPERTY(bool asynchronous READ asynchronous WRITE setAsynchronous NOTIFY asynchronousChanged) + // Add Animation Q_PROPERTY(int addDuration READ addDuration WRITE setAddDuration NOTIFY addDurationChanged) Q_PROPERTY(QEasingCurve addCurve READ addCurve WRITE setAddCurve NOTIFY addCurveChanged) @@ -130,6 +133,10 @@ public: [[nodiscard]] qreal estimatedHeight() const; void setEstimatedHeight(qreal height); + // Async + [[nodiscard]] bool asynchronous() const; + void setAsynchronous(bool async); + // Add Animation [[nodiscard]] int addDuration() const; void setAddDuration(int duration); @@ -178,6 +185,7 @@ signals: void useCustomViewportChanged(); void cacheBufferChanged(); void estimatedHeightChanged(); + void asynchronousChanged(); void addDurationChanged(); void addCurveChanged(); void addFromOpacityChanged(); @@ -261,6 +269,7 @@ private: qreal m_estimatedHeight = -1; qreal m_knownHeightSum = 0; int m_knownHeightCount = 0; + bool m_asynchronous = false; int m_addDuration = 300; QEasingCurve m_addCurve;