From 4e75f2144613756171f5cc8dbc03c7e2d442a576 Mon Sep 17 00:00:00 2001 From: zach Date: Sun, 16 Aug 2026 20:28:21 +0200 Subject: [PATCH] improve sizing and dynamic width for view --- Modules/Launcher/CarouselView.qml | 8 +- Modules/Launcher/ContentList.qml | 2 +- Plugins/ZShell/Components/carouselview.cpp | 814 +++++++++++++++++---- Plugins/ZShell/Components/carouselview.hpp | 37 +- 4 files changed, 701 insertions(+), 160 deletions(-) diff --git a/Modules/Launcher/CarouselView.qml b/Modules/Launcher/CarouselView.qml index ca11095..595dfa2 100755 --- a/Modules/Launcher/CarouselView.qml +++ b/Modules/Launcher/CarouselView.qml @@ -15,8 +15,6 @@ CarouselView { readonly property var values: Wallpapers.query(root.search.text.split(" ").slice(1).join(" ")) required property var visibilities - // Forwarded verbatim as a property of the same name onto every delegate - // instance (the delegate declares `required property PersistentProperties visibilities`). delegateProperties: ({ visibilities: root.visibilities }) @@ -29,9 +27,9 @@ CarouselView { if (!screen) return 0; const barMargins = Config.bar.border * 2; - let margins = 0; + let margins = Math.round(screen.width / 6); if (visibilities.utilities || visibilities.sidebar) - margins = panels.utilities.implicitWidth; + margins = Config.sidebar.sizes.width; return screen.width - (barMargins + margins) * 2 - Config.bar.rounding * 4; } minEdgeWidth: 56 @@ -40,7 +38,7 @@ CarouselView { delegate: Component { WallpaperTile { - } // your existing delegate file, unchanged + } } Component.onCompleted: { diff --git a/Modules/Launcher/ContentList.qml b/Modules/Launcher/ContentList.qml index c9ab7f1..f34d16f 100644 --- a/Modules/Launcher/ContentList.qml +++ b/Modules/Launcher/ContentList.qml @@ -110,9 +110,9 @@ Item { anchors.bottom: parent.bottom anchors.horizontalCenter: parent.horizontalCenter anchors.top: parent.top + width: root.implicitWidth sourceComponent: CarouselView { - // content: root.content panels: root.panels search: root.search visibilities: root.visibilities diff --git a/Plugins/ZShell/Components/carouselview.cpp b/Plugins/ZShell/Components/carouselview.cpp index e7a8bda..73ba4f4 100644 --- a/Plugins/ZShell/Components/carouselview.cpp +++ b/Plugins/ZShell/Components/carouselview.cpp @@ -4,17 +4,19 @@ #include #include #include -#include -#include + #include namespace ZShell::components { namespace { + constexpr qreal kDragThreshold = 4.0; constexpr qreal kFlickFriction = 0.0022; constexpr qreal kMinFlickVelocity = 60.0; constexpr qreal kMaxFlickVelocity = 6000.0; +constexpr int kMaxLayoutCapacity = 64; + } // namespace CarouselView::CarouselView(QQuickItem* parent) : QQuickItem(parent) { @@ -24,32 +26,39 @@ CarouselView::CarouselView(QQuickItem* parent) : QQuickItem(parent) { m_glideAnim = new QVariantAnimation(this); m_glideAnim->setEasingCurve(m_glideEasingType); + connect( m_glideAnim, &QVariantAnimation::valueChanged, this, [this](const QVariant& v) { m_contentX = v.toReal(); + wrapContentIfNeeded(); updateCurrentIndexFromContentX(true); relayout(); }); + connect(m_glideAnim, &QVariantAnimation::finished, this, [this]() { updateCurrentIndexFromContentX(true); + relayout(); }); m_flickAnim = new QVariantAnimation(this); m_flickAnim->setEasingCurve(QEasingCurve::Linear); + connect( m_flickAnim, &QVariantAnimation::valueChanged, this, [this](const QVariant& v) { m_contentX = v.toReal(); + wrapContentIfNeeded(); updateCurrentIndexFromContentX(true); relayout(); }); + connect(m_flickAnim, &QVariantAnimation::finished, this, [this]() { snapToNearest(); }); @@ -61,104 +70,186 @@ CarouselView::~CarouselView() { void CarouselView::setDelegate(QQmlComponent* c) { if (m_delegate == c) return; + m_delegate = c; + releaseAllItems(); + emit delegateChanged(); + relayout(); } void CarouselView::setModel(const QVariantList& m) { m_model = m; + emit modelChanged(); - int newCount = m_model.size(); + const int newCount = m_model.size(); + if (newCount == 0) { m_currentRealIndex = -1; + releaseAllItems(); rebuildArrangement(); - return; - } - if (!m_initializedLayout) { - m_centerBlockStart = (kLoopMultiplier / 2) * newCount; - m_currentVirtualIndex = m_centerBlockStart; - m_contentX = contentXForIndex(m_currentVirtualIndex); - m_currentRealIndex = realIndexOf(m_currentVirtualIndex); - m_previewRealIndex = m_currentRealIndex; - m_initializedLayout = true; - rebuildArrangement(); - relayout(); + return; } + if (!m_initializedLayout) { + m_centerBlockStart = 0; + m_currentVirtualIndex = 0; + + m_contentX = contentXForIndex(0); + + m_currentRealIndex = realIndexOf(m_currentVirtualIndex); + + m_previewRealIndex = m_currentRealIndex; + + m_initializedLayout = true; + + m_layoutCapacity = + qMax(m_layoutCapacity, layoutCapacityForWidth(m_maxWidth)); + + setImplicitWidth(targetWidthForMaxWidth(m_maxWidth)); + + rebuildArrangement(); + relayout(); + + return; + } + + m_layoutCapacity = + qMax(m_layoutCapacity, layoutCapacityForWidth(m_maxWidth)); + + setImplicitWidth(targetWidthForMaxWidth(m_maxWidth)); + rebuildArrangement(); + centerInstantlyOnReal(qBound(0, m_currentRealIndex, newCount - 1)); } void CarouselView::setFocalWidth(qreal v) { if (qFuzzyCompare(m_focalWidth, v)) return; + m_focalWidth = v; + emit focalWidthChanged(); + + m_layoutCapacity = + qMax(m_layoutCapacity, layoutCapacityForWidth(m_maxWidth)); + + setImplicitWidth(targetWidthForMaxWidth(m_maxWidth)); + rebuildArrangement(); relayout(); } void CarouselView::setMinEdgeWidth(qreal v) { if (qFuzzyCompare(m_minEdgeWidth, v)) return; + m_minEdgeWidth = v; + emit minEdgeWidthChanged(); + + m_layoutCapacity = + qMax(m_layoutCapacity, layoutCapacityForWidth(m_maxWidth)); + + setImplicitWidth(targetWidthForMaxWidth(m_maxWidth)); + rebuildArrangement(); relayout(); } void CarouselView::setItemSpacing(qreal v) { if (qFuzzyCompare(m_itemSpacing, v)) return; + m_itemSpacing = v; + emit itemSpacingChanged(); + + m_layoutCapacity = + qMax(m_layoutCapacity, layoutCapacityForWidth(m_maxWidth)); + + setImplicitWidth(targetWidthForMaxWidth(m_maxWidth)); + rebuildArrangement(); relayout(); } void CarouselView::setTileHeight(qreal v) { if (qFuzzyCompare(m_tileHeight, v)) return; + m_tileHeight = v; + emit tileHeightChanged(); + for (const Slot& s : std::as_const(m_slots)) { - if (s.active && s.item) + if (s.active && s.item) { QQmlProperty::write( s.item, QStringLiteral("tileHeight"), m_tileHeight); + } } } void CarouselView::setMaxWidth(qreal v) { if (qFuzzyCompare(m_maxWidth, v)) return; + m_maxWidth = v; + emit maxWidthChanged(); + + /* + * Advertise the target width immediately. + * + * The parent QML object can then animate its own implicitWidth + * immediately instead of waiting for the delegate geometry. + */ + const qreal targetWidth = targetWidthForMaxWidth(m_maxWidth); + + setImplicitWidth(targetWidth); + + /* + * Grow the instantiated delegate pool if the new maximum needs + * more items. Never throw them away during a resize. + */ + m_layoutCapacity = + qMax(m_layoutCapacity, layoutCapacityForWidth(m_maxWidth)); + rebuildArrangement(); relayout(); } void CarouselView::setGlideDuration(int v) { if (m_glideDuration == v) return; + m_glideDuration = v; + emit glideDurationChanged(); } void CarouselView::setGlideEasingType(QEasingCurve::Type t) { if (m_glideEasingType == t) return; + m_glideEasingType = t; + m_glideAnim->setEasingCurve(t); + emit glideEasingTypeChanged(); } void CarouselView::setDelegateProperties(const QVariantMap& v) { m_delegateProperties = v; + emit delegatePropertiesChanged(); + for (const Slot& s : std::as_const(m_slots)) { - if (s.active && s.item) { - for (auto it = m_delegateProperties.constBegin(); - it != m_delegateProperties.constEnd(); - ++it) - QQmlProperty::write(s.item, it.key(), it.value()); + if (!s.active || !s.item) continue; + + for (auto it = m_delegateProperties.constBegin(); + it != m_delegateProperties.constEnd(); + ++it) { + QQmlProperty::write(s.item, it.key(), it.value()); } } } @@ -169,141 +260,319 @@ void CarouselView::setCurrentIndex(int idx) { void CarouselView::geometryChange(const QRectF& newGeo, const QRectF& oldGeo) { QQuickItem::geometryChange(newGeo, oldGeo); - if (!qFuzzyCompare(newGeo.width(), oldGeo.width()) || - !qFuzzyCompare(newGeo.height(), oldGeo.height())) { - rebuildArrangement(); - if (m_initializedLayout) - m_contentX = contentXForIndex(m_currentVirtualIndex); - relayout(); + + const qreal oldWidth = oldGeo.width(); + + const qreal newWidth = newGeo.width(); + + if (qFuzzyCompare(oldWidth, newWidth)) { + return; } + + /* + * The carousel is horizontally centered. + * + * Changing its width therefore moves the local coordinate system + * by half the width delta. Keep the logical content stationary by + * moving contentX by the opposite amount. + * + * Crucially, do this directly instead of recomputing a logical + * index and converting it back. That avoids quantization and + * floating-point drift on every animation frame. + */ + const qreal delta = (newWidth - oldWidth) / 2.0; + + m_contentX -= delta; + + /* + * A running glide/flick animation is expressed in contentX + * coordinates too. Shift both endpoints by exactly the same + * amount so the animation continues seamlessly in the new + * geometry. + */ + auto shiftAnimation = [delta](QVariantAnimation* anim) { + if (anim->state() != QAbstractAnimation::Running) { + return; + } + + anim->setStartValue(anim->startValue().toReal() - delta); + + anim->setEndValue(anim->endValue().toReal() - delta); + }; + + shiftAnimation(m_glideAnim); + shiftAnimation(m_flickAnim); + + rebuildArrangement(); + relayout(); +} + +CarouselView::Arrangement CarouselView::arrangementForCount(int n) const { + Arrangement a; + + const qreal F = m_focalWidth; + + const qreal M = m_minEdgeWidth; + + const qreal sp = m_itemSpacing; + + if (F <= 0) { + a.n = 0; + a.width = 0; + a.outerSpacing = 0; + a.sizes = {F}; + a.centers = {0}; + + return a; + } + + if (n <= 0) { + a.n = 0; + a.width = F; + a.outerSpacing = 0; + + a.sizes = {F}; + a.centers = {0}; + + return a; + } + + a.n = n; + a.outerSpacing = sp; + + a.sizes.reserve(n + 1); + a.centers.reserve(n + 1); + + a.sizes.append(F); + + const qreal ratio = std::pow(M / F, 1.0 / n); + + qreal sideWidth = 0; + + for (int i = 1; i <= n; ++i) { + const qreal size = (i == n) ? M : F * std::pow(ratio, i); + + a.sizes.append(size); + sideWidth += size; + } + + /* + * This is exactly the original full arrangement width. + */ + a.width = F + 2.0 * n * sp + 2.0 * sideWidth; + + a.centers.append(0); + + qreal center = 0; + + for (int i = 1; i <= n; ++i) { + center += a.sizes[i - 1] / 2.0 + sp + a.sizes[i] / 2.0; + + a.centers.append(center); + } + + a.effectiveExtraWidth = 0; + + for (int i = 1; i <= n; ++i) { + a.effectiveExtraWidth += pitch() - a.sizes[i]; + } + + return a; +} + +int CarouselView::layoutCapacityForWidth(qreal width) const { + if (width <= 0 || m_focalWidth <= 0) { + return 0; + } + + int bestN = 0; + + for (int n = 1; n <= kMaxLayoutCapacity; ++n) { + const Arrangement a = arrangementForCount(n); + + if (a.width > width) break; + + bestN = n; + } + + return bestN; +} + +qreal CarouselView::targetWidthForMaxWidth(qreal maxWidth) const { + if (maxWidth <= 0 || m_focalWidth <= 0) { + return 0; + } + + /* + * This deliberately preserves the old behavior: + * + * choose the largest complete arrangement whose outermost + * delegate can remain at minEdgeWidth. + * + * Therefore the component's desired width can be smaller than + * maxWidth. + */ + const int n = layoutCapacityForWidth(maxWidth); + + return arrangementForCount(n).width; } void CarouselView::rebuildArrangement() { Arrangement& a = m_arrangement; + a = Arrangement{}; - const qreal W = m_maxWidth; - const qreal F = m_focalWidth; - const qreal M = m_minEdgeWidth; - const qreal sp = m_itemSpacing; + const qreal W = width() > 0 ? width() : targetWidthForMaxWidth(m_maxWidth); + + if (W <= 0 || m_focalWidth <= 0) { + a = arrangementForCount(0); + a.width = W; - if (W <= 0 || F <= 0) { - a.sizes = {F}; - a.centers = {0}; return; } - int bestN = 0; - QVector bestSizes = {F}; - qreal bestWidth = F; + if (m_layoutCapacity <= 0) { + m_layoutCapacity = layoutCapacityForWidth(m_maxWidth); + } - for (int n = 1;; ++n) { - const qreal ratio = std::pow(M / F, 1.0 / n); - QVector sizes = {F}; - qreal sideWidth = 0; + if (m_layoutCapacity <= 0) { + a = arrangementForCount(0); + a.width = W; - for (int i = 1; i <= n; ++i) { - const qreal size = (i == n) ? M : F * std::pow(ratio, i); - sizes.push_back(size); - sideWidth += size; + return; + } + + const Arrangement full = arrangementForCount(m_layoutCapacity); + + /* + * If the current width can display the complete capacity + * arrangement, use that exact geometry. + */ + if (W >= full.width) { + a = full; + return; + } + + /* + * Otherwise find the complete arrangement immediately above W. + * + * Example: + * + * width(8) >= W >= width(7) + * + * means we're in the 8 -> 7 transition. + */ + int highN = 1; + + for (int n = 1; n <= m_layoutCapacity; ++n) { + const Arrangement candidate = arrangementForCount(n); + + if (W < candidate.width) { + highN = n; + break; } - - const qreal requiredWidth = F + 2 * n * sp + 2 * sideWidth; - if (requiredWidth > W) break; - - bestN = n; - bestSizes = sizes; - bestWidth = requiredWidth; - - if (n > 64) break; } - a.n = bestN; - a.sizes = bestSizes; - a.width = bestWidth; + const int lowN = qMax(0, highN - 1); + + const Arrangement high = arrangementForCount(highN); + + const Arrangement low = arrangementForCount(lowN); + + if (high.width <= low.width) { + a = low; + return; + } + + qreal t = (high.width - W) / (high.width - low.width); + + t = qBound(0.0, t, 1.0); + + /* + * Keep the outgoing slot instantiated and visible while it + * smoothly transitions to zero. + */ + a.n = highN; + a.width = W; + + a.sizes.resize(highN + 1); + + a.sizes[0] = m_focalWidth; + + for (int i = 1; i < highN; ++i) { + a.sizes[i] = high.sizes[i] + (low.sizes[i] - high.sizes[i]) * t; + } + + a.sizes[highN] = high.sizes[highN] * (1.0 - t); + + /* + * All normal gaps stay constant. + * + * Only the gap immediately before the disappearing delegate + * collapses together with that delegate. + */ + a.outerSpacing = m_itemSpacing * (1.0 - t); + + a.centers.append(0); - QVector centers = {0}; qreal center = 0; - for (int i = 1; i <= bestN; ++i) { - center += bestSizes[i - 1] / 2.0 + sp + bestSizes[i] / 2.0; - centers.push_back(center); - } - a.centers = centers; - qreal extra = 0; - for (int i = 1; i <= bestN; ++i) - extra += pitch() - bestSizes[i]; - a.effectiveExtraWidth = extra; + for (int i = 1; i <= highN; ++i) { + const qreal gap = i == highN ? a.outerSpacing : m_itemSpacing; - setImplicitWidth(m_maxWidth); - setWidth(a.width); -} + center += a.sizes[i - 1] / 2.0 + gap + a.sizes[i] / 2.0; -qreal CarouselView::keylineCenter(int io) const { - const int n = m_arrangement.n; - const int i = std::abs(io); - if (i == 0) return 0; - const int sign = io < 0 ? -1 : 1; - - if (i <= n) return sign * m_arrangement.centers[i]; - - if (i == n + 1) { - const qreal lastCenter = m_arrangement.centers[n]; - const qreal lastSize = m_arrangement.sizes[n]; - return sign * (lastCenter + lastSize / 2.0 + m_itemSpacing + - m_minEdgeWidth / 2.0); + a.centers.append(center); } - const qreal outgoingCenter = std::abs(keylineCenter(sign * (n + 1))); - return sign * (outgoingCenter + m_minEdgeWidth / 2.0 + m_itemSpacing); + a.effectiveExtraWidth = 0; + + for (int i = 1; i <= highN; ++i) { + a.effectiveExtraWidth += pitch() - a.sizes[i]; + } } qreal CarouselView::keylineSize(int io) const { const int n = m_arrangement.n; + const int i = std::abs(io); + if (i == 0) return m_focalWidth; + if (i <= n) return m_arrangement.sizes[i]; - if (i == n + 1) return m_minEdgeWidth; + return 0; } -qreal CarouselView::sampleCenter(qreal childLoc) const { - const int n = m_arrangement.n; - if (n <= 0) return 0; - - const int minIo = -(n + 2); - const int maxIo = n + 2; - const qreal p = pitch(); - - const int a = static_cast(std::floor(childLoc / p)); - if (a < minIo) return keylineCenter(minIo); - if (a >= maxIo) return keylineCenter(maxIo); - - const int b = a + 1; - const qreal t = (childLoc - a * p) / p; - return keylineCenter(a) + (keylineCenter(b) - keylineCenter(a)) * t; -} - qreal CarouselView::sampleSize(qreal childLoc) const { const int n = m_arrangement.n; + if (n <= 0) return 0; const int minIo = -(n + 2); + const int maxIo = n + 2; + const qreal p = pitch(); const int a = static_cast(std::floor(childLoc / p)); + if (a < minIo) return keylineSize(minIo); + if (a >= maxIo) return keylineSize(maxIo); const int b = a + 1; + const qreal t = (childLoc - a * p) / p; + return keylineSize(a) + (keylineSize(b) - keylineSize(a)) * t; } int CarouselView::realIndexOf(int virtualIndex) const { const int count = m_model.size(); + if (count == 0) return -1; + return ((virtualIndex % count) + count) % count; } @@ -322,51 +591,71 @@ qreal CarouselView::snapTargetX(qreal x) const { void CarouselView::wrapContentIfNeeded() { const int count = m_model.size(); + if (count == 0) return; const qreal blockWidth = count * pitch(); + const qreal centerX = contentXForIndex(m_centerBlockStart); + const int blocksFromCenter = static_cast(std::round((m_contentX - centerX) / blockWidth)); - if (std::abs(blocksFromCenter) >= 2) { - const qreal shift = blocksFromCenter * blockWidth; - m_contentX -= shift; + if (std::abs(blocksFromCenter) < 2) return; - if (m_glideAnim->state() == QAbstractAnimation::Running) { - const qreal newEnd = m_glideAnim->endValue().toReal() - shift; - m_glideAnim->setEndValue(newEnd); - } - if (m_flickAnim->state() == QAbstractAnimation::Running) { - const qreal newEnd = m_flickAnim->endValue().toReal() - shift; - m_flickAnim->setEndValue(newEnd); - } - const int indexShift = static_cast(std::round(shift / pitch())); - for (Slot& s : m_slots) { - if (s.active) s.virtualIndex -= indexShift; + const int indexShift = blocksFromCenter * count; + + const qreal shift = indexShift * pitch(); + + m_contentX -= shift; + m_currentVirtualIndex -= indexShift; + + auto shiftAnimation = [shift](QVariantAnimation* anim) { + if (anim->state() != QAbstractAnimation::Running) { + return; } + + anim->setStartValue(anim->startValue().toReal() - shift); + + anim->setEndValue(anim->endValue().toReal() - shift); + }; + + shiftAnimation(m_glideAnim); + shiftAnimation(m_flickAnim); + + for (Slot& s : m_slots) { + if (s.active) s.virtualIndex -= indexShift; } } void CarouselView::updateCurrentIndexFromContentX(bool emitPreview) { const int count = m_model.size(); + if (count == 0) return; const int newVirtual = static_cast( std::round((m_contentX + width() / 2.0 - pitch() / 2.0) / pitch())); - if (newVirtual == m_currentVirtualIndex) return; + + if (newVirtual == m_currentVirtualIndex) { + return; + } m_currentVirtualIndex = newVirtual; + const int newReal = realIndexOf(newVirtual); if (newReal != m_currentRealIndex) { m_currentRealIndex = newReal; + emit currentIndexChanged(); } + if (emitPreview && newReal != m_previewRealIndex) { m_previewRealIndex = newReal; - if (newReal >= 0) + + if (newReal >= 0) { emit previewIndexChanged(newReal, m_model.at(newReal)); + } } } @@ -374,38 +663,54 @@ QQuickItem* CarouselView::acquireItem() { if (!m_delegate) return nullptr; QQmlContext* ctx = QQmlEngine::contextForObject(this); + QVariantMap initial; + initial.insert(QStringLiteral("isCurrent"), false); + initial.insert(QStringLiteral("modelData"), QVariant()); + initial.insert(QStringLiteral("tileHeight"), m_tileHeight); + initial.insert(QStringLiteral("tileWidth"), m_focalWidth); + for (auto it = m_delegateProperties.constBegin(); it != m_delegateProperties.constEnd(); - ++it) + ++it) { initial.insert(it.key(), it.value()); + } QObject* obj = m_delegate->createWithInitialProperties(initial, ctx); + if (!obj) { qWarning( "CarouselView: failed to create delegate instance: %s", qPrintable(m_delegate->errorString())); + return nullptr; } + QQuickItem* item = qobject_cast(obj); + if (!item) { qWarning("CarouselView: delegate root is not an Item"); + delete obj; + return nullptr; } item->setParentItem(this); + QQmlEngine::setObjectOwnership(item, QQmlEngine::CppOwnership); const QMetaMethod activateSignal = item->metaObject()->method( item->metaObject()->indexOfSignal("requestActivate()")); + if (activateSignal.isValid()) { static const QMetaMethod slot = metaObject()->method( metaObject()->indexOfSlot("handleDelegateActivate()")); + connect(item, activateSignal, this, slot); } @@ -414,39 +719,172 @@ QQuickItem* CarouselView::acquireItem() { void CarouselView::applyPropertiesToItem(QQuickItem* item, int realIndex) const { if (!item) return; - const bool isCurrent = (realIndex == m_currentRealIndex); + + const bool isCurrent = realIndex == m_currentRealIndex; + QQmlProperty::write(item, QStringLiteral("isCurrent"), isCurrent); + QQmlProperty::write( item, QStringLiteral("modelData"), realIndex >= 0 && realIndex < m_model.size() ? m_model.at(realIndex) : QVariant()); + QQmlProperty::write(item, QStringLiteral("tileHeight"), m_tileHeight); } void CarouselView::updateSlotContent(Slot& slot, int virtualIndex) { if (!slot.item) slot.item = acquireItem(); + if (!slot.item) return; slot.virtualIndex = virtualIndex; + slot.realIndex = realIndexOf(virtualIndex); + slot.active = slot.realIndex >= 0; + slot.item->setVisible(slot.active); - if (slot.active) applyPropertiesToItem(slot.item, slot.realIndex); + + if (slot.active) { + applyPropertiesToItem(slot.item, slot.realIndex); + } +} + +qreal CarouselView::continuousIndex() const { + return (m_contentX + width() / 2.0 - pitch() / 2.0) / pitch(); } void CarouselView::positionSlot(const Slot& slot) const { if (!slot.item || !slot.active) return; - const qreal childLoc = slot.virtualIndex * pitch() + pitch() / 2.0 - - m_contentX - width() / 2.0; - const qreal center = sampleCenter(childLoc); + const qreal current = continuousIndex(); + + const int leftIndex = static_cast(std::floor(current)); + + const qreal fraction = current - leftIndex; + + const int v = slot.virtualIndex; + + const qreal childLoc = (qreal(v) - current) * pitch(); + const qreal size = sampleSize(childLoc); - const qreal slotX = childLoc + width() / 2.0 - pitch() / 2.0; - const qreal itemX = (center - childLoc) + (pitch() - size) / 2.0 + slotX; + if (size <= 0.5) { + slot.item->setVisible(false); + return; + } + + const int n = m_arrangement.n; + + const auto rightGap = [this, n](int rightIndex) { + return rightIndex == n ? m_arrangement.outerSpacing : m_itemSpacing; + }; + + const auto leftGap = [this, n](int leftIndex) { + return leftIndex == -n ? m_arrangement.outerSpacing : m_itemSpacing; + }; + + qreal center = 0; + + if (std::abs(fraction) < 1e-6) { + if (v == leftIndex) { + center = 0; + } else if (v < leftIndex) { + qreal x = 0; + + qreal previousSize = sampleSize(0); + + for (int i = leftIndex - 1; i >= v; --i) { + const qreal s = sampleSize((qreal(i) - current) * pitch()); + + const qreal gap = leftGap(i); + + x -= previousSize / 2.0 + gap + s / 2.0; + + previousSize = s; + } + + center = x; + } else { + qreal x = 0; + + qreal previousSize = sampleSize(0); + + for (int i = leftIndex + 1; i <= v; ++i) { + const qreal s = sampleSize((qreal(i) - current) * pitch()); + + const qreal gap = rightGap(i); + + x += previousSize / 2.0 + gap + s / 2.0; + + previousSize = s; + } + + center = x; + } + } else { + const qreal leftSize = + sampleSize((qreal(leftIndex) - current) * pitch()); + + const qreal rightSize = + sampleSize((qreal(leftIndex + 1) - current) * pitch()); + + qreal centerGap = m_itemSpacing; + + if (leftIndex == -n || leftIndex + 1 == n) { + centerGap = m_arrangement.outerSpacing; + } + + const qreal distance = leftSize / 2.0 + centerGap + rightSize / 2.0; + + const qreal leftCenter = -fraction * distance; + + const qreal rightCenter = (1.0 - fraction) * distance; + + if (v == leftIndex) { + center = leftCenter; + } else if (v == leftIndex + 1) { + center = rightCenter; + } else if (v < leftIndex) { + qreal x = leftCenter; + + qreal previousSize = leftSize; + + for (int i = leftIndex - 1; i >= v; --i) { + const qreal s = sampleSize((qreal(i) - current) * pitch()); + + const qreal gap = leftGap(i); + + x -= previousSize / 2.0 + gap + s / 2.0; + + previousSize = s; + } + + center = x; + } else { + qreal x = rightCenter; + + qreal previousSize = rightSize; + + for (int i = leftIndex + 2; i <= v; ++i) { + const qreal s = sampleSize((qreal(i) - current) * pitch()); + + const qreal gap = rightGap(i); + + x += previousSize / 2.0 + gap + s / 2.0; + + previousSize = s; + } + + center = x; + } + } + + const qreal itemX = width() / 2.0 + center - size / 2.0; QQmlProperty::write(slot.item, QStringLiteral("tileWidth"), size); + slot.item->setX(itemX); slot.item->setY(0); slot.item->setVisible(size > 0.5); @@ -456,59 +894,83 @@ void CarouselView::releaseAllItems() { for (Slot& s : m_slots) { if (s.item) s.item->deleteLater(); } + m_slots.clear(); } void CarouselView::relayout() { const int count = m_model.size(); + if (count == 0 || width() <= 0 || m_focalWidth <= 0) { - for (Slot& s : m_slots) + for (Slot& s : m_slots) { if (s.item) s.item->setVisible(false); + } + return; } - const int n = m_arrangement.n; - const int half = n + 3; + /* + * Keep enough slots for the largest layout we've needed. + * The currently visible count is determined by m_arrangement. + */ + const int half = m_layoutCapacity + 2; + const int lo = m_currentVirtualIndex - half; + const int hi = m_currentVirtualIndex + half; + const int needed = hi - lo + 1; while (m_slots.size() < needed) m_slots.append(Slot{}); QHash virtualToSlot; + virtualToSlot.reserve(m_slots.size()); + for (int i = 0; i < m_slots.size(); ++i) { - if (m_slots[i].active) virtualToSlot.insert(m_slots[i].virtualIndex, i); + if (m_slots[i].active) { + virtualToSlot.insert(m_slots[i].virtualIndex, i); + } } QVector slotUsedThisPass(m_slots.size(), false); for (int v = lo; v <= hi; ++v) { auto it = virtualToSlot.find(v); + if (it != virtualToSlot.end()) { slotUsedThisPass[it.value()] = true; + applyPropertiesToItem( m_slots[it.value()].item, m_slots[it.value()].realIndex); + continue; } int freeSlot = -1; + for (int i = 0; i < m_slots.size(); ++i) { if (slotUsedThisPass[i]) continue; + if (m_slots[i].active && m_slots[i].virtualIndex >= lo && - m_slots[i].virtualIndex <= hi) + m_slots[i].virtualIndex <= hi) { continue; + } + freeSlot = i; break; } + if (freeSlot < 0) { m_slots.append(Slot{}); slotUsedThisPass.append(false); + freeSlot = m_slots.size() - 1; } updateSlotContent(m_slots[freeSlot], v); + slotUsedThisPass[freeSlot] = true; } @@ -519,39 +981,53 @@ void CarouselView::relayout() { } } - for (const Slot& s : std::as_const(m_slots)) + for (const Slot& s : std::as_const(m_slots)) { positionSlot(s); + } } void CarouselView::cancelAnimations() { - if (m_glideAnim->state() == QAbstractAnimation::Running) + if (m_glideAnim->state() == QAbstractAnimation::Running) { m_glideAnim->stop(); - if (m_flickAnim->state() == QAbstractAnimation::Running) + } + + if (m_flickAnim->state() == QAbstractAnimation::Running) { m_flickAnim->stop(); + } } void CarouselView::glideTo(qreal dest) { if (std::abs(dest - m_contentX) < 0.5) return; cancelAnimations(); + m_glideAnim->setDuration(m_glideDuration); + m_glideAnim->setStartValue(m_contentX); + m_glideAnim->setEndValue(dest); + m_glideAnim->start(); } void CarouselView::snapToNearest() { - glideTo(snapTargetX(m_contentX)); + if (m_model.isEmpty()) return; + + glideTo(contentXForIndex(m_currentVirtualIndex)); } void CarouselView::goToIndex(int realIndex) { const int count = m_model.size(); + if (count == 0) return; + realIndex = qBound(0, realIndex, count - 1); const int curBlock = static_cast(std::floor(qreal(m_currentVirtualIndex) / count)); + const int current = m_currentVirtualIndex; + int target = curBlock * count + realIndex; if (target - current > count / 2) @@ -564,11 +1040,13 @@ void CarouselView::goToIndex(int realIndex) { void CarouselView::incrementCurrentIndex() { if (m_model.isEmpty()) return; + glideTo(contentXForIndex(m_currentVirtualIndex + 1)); } void CarouselView::decrementCurrentIndex() { if (m_model.isEmpty()) return; + glideTo(contentXForIndex(m_currentVirtualIndex - 1)); } @@ -578,14 +1056,18 @@ void CarouselView::jumpToIndex(int realIndex) { void CarouselView::centerInstantlyOnReal(int realIndex) { const int count = m_model.size(); + if (count == 0) return; + realIndex = qBound(0, realIndex, count - 1); cancelAnimations(); const int curBlock = static_cast(std::floor(qreal(m_currentVirtualIndex) / count)); + const int current = m_currentVirtualIndex; + int target = curBlock * count + realIndex; if (target - current > count / 2) @@ -594,12 +1076,17 @@ void CarouselView::centerInstantlyOnReal(int realIndex) { target += count; m_currentVirtualIndex = target; + m_contentX = contentXForIndex(target); - const bool changed = (realIndex != m_currentRealIndex); + const bool changed = realIndex != m_currentRealIndex; + m_currentRealIndex = realIndex; + m_previewRealIndex = realIndex; + if (changed) emit currentIndexChanged(); + emit previewIndexChanged(realIndex, m_model.at(realIndex)); relayout(); @@ -608,12 +1095,18 @@ void CarouselView::centerInstantlyOnReal(int realIndex) { void CarouselView::mousePressEvent(QMouseEvent* event) { m_dragging = true; m_dragActive = false; + m_pressPos = event->position(); + m_pressContentX = m_contentX; + m_dragTimer.start(); + m_lastMoveX = event->position().x(); + m_lastMoveT = 0; m_velocity = 0; + event->accept(); } @@ -621,27 +1114,36 @@ void CarouselView::mouseMoveEvent(QMouseEvent* event) { if (!m_dragging) return; const qreal dx = event->position().x() - m_pressPos.x(); + if (!m_dragActive && std::abs(dx) > kDragThreshold) { m_dragActive = true; m_pressContentX = m_contentX; + cancelAnimations(); grabMouse(); } + if (!m_dragActive) return; m_contentX = m_pressContentX - dx; + wrapContentIfNeeded(); updateCurrentIndexFromContentX(true); relayout(); const qint64 t = m_dragTimer.elapsed(); + const qint64 dt = t - m_lastMoveT; + if (dt > 0) { const qreal instVel = (m_lastMoveX - event->position().x()) / (dt / 1000.0); + m_velocity = m_velocity * 0.7 + instVel * 0.3; } + m_lastMoveX = event->position().x(); + m_lastMoveT = t; event->accept(); @@ -649,33 +1151,49 @@ void CarouselView::mouseMoveEvent(QMouseEvent* event) { void CarouselView::mouseReleaseEvent(QMouseEvent* event) { if (!m_dragging) return; + m_dragging = false; + ungrabMouse(); if (!m_dragActive) { + cancelAnimations(); + snapToNearest(); + event->accept(); return; } + m_dragActive = false; qreal v = qBound(-kMaxFlickVelocity, m_velocity, kMaxFlickVelocity); + if (std::abs(v) < kMinFlickVelocity) { snapToNearest(); + event->accept(); return; } - const qreal duration = std::abs(v) / (kFlickFriction * 1000.0); // ms + const qreal duration = std::abs(v) / (kFlickFriction * 1000.0); + const qreal distance = (v * (duration / 1000.0)) / 2.0; + const qreal rawDest = m_contentX + distance; - const qreal dest = snapTargetX(rawDest); + + const qreal dest = contentXForIndex(indexForContentX(rawDest)); cancelAnimations(); + m_flickAnim->setDuration( static_cast(qBound(120, duration, 900))); + m_flickAnim->setEasingCurve(QEasingCurve::OutCubic); + m_flickAnim->setStartValue(m_contentX); + m_flickAnim->setEndValue(dest); + m_flickAnim->start(); event->accept(); @@ -688,26 +1206,32 @@ void CarouselView::mouseUngrabEvent() { void CarouselView::wheelEvent(QWheelEvent* event) { const int steps = event->angleDelta().y() / 120; + if (steps > 0) decrementCurrentIndex(); else if (steps < 0) incrementCurrentIndex(); + event->accept(); } void CarouselView::handleDelegateActivate() { QQuickItem* item = qobject_cast(sender()); + if (!item) return; for (const Slot& s : std::as_const(m_slots)) { - if (s.item == item && s.active) { - if (s.realIndex == m_currentRealIndex) { - emit activated(s.realIndex, m_model.at(s.realIndex)); - } else { - goToIndex(s.realIndex); - } - return; + if (s.item != item || !s.active) { + continue; } + + if (s.realIndex == m_currentRealIndex) { + emit activated(s.realIndex, m_model.at(s.realIndex)); + } else { + goToIndex(s.realIndex); + } + + return; } } diff --git a/Plugins/ZShell/Components/carouselview.hpp b/Plugins/ZShell/Components/carouselview.hpp index 2925cf9..a5b2ef9 100644 --- a/Plugins/ZShell/Components/carouselview.hpp +++ b/Plugins/ZShell/Components/carouselview.hpp @@ -1,15 +1,15 @@ #pragma once +#include +#include +#include +#include #include #include #include -#include #include -#include -#include -#include +#include #include -#include namespace ZShell::components { @@ -112,6 +112,7 @@ class CarouselView : public QQuickItem { protected: void geometryChange(const QRectF& newGeo, const QRectF& oldGeo) override; + void mousePressEvent(QMouseEvent* event) override; void mouseMoveEvent(QMouseEvent* event) override; void mouseReleaseEvent(QMouseEvent* event) override; @@ -125,8 +126,11 @@ class CarouselView : public QQuickItem { struct Arrangement { int n = 0; qreal width = 0; + qreal outerSpacing = 0; + QVector sizes; QVector centers; + qreal effectiveExtraWidth = 0; }; @@ -139,25 +143,36 @@ class CarouselView : public QQuickItem { void rebuildArrangement(); void relayout(); + + Arrangement arrangementForCount(int n) const; + + int layoutCapacityForWidth(qreal width) const; + qreal targetWidthForMaxWidth(qreal maxWidth) const; + void updateSlotContent(Slot& slot, int virtualIndex); + void positionSlot(const Slot& slot) const; + void applyPropertiesToItem(QQuickItem* item, int realIndex) const; qreal pitch() const { return m_focalWidth + m_itemSpacing; } + qreal contentXForIndex(qreal index) const; int indexForContentX(qreal x) const; qreal snapTargetX(qreal x) const; + void snapToNearest(); void glideTo(qreal dest); void cancelAnimations(); - qreal keylineCenter(int io) const; qreal keylineSize(int io) const; - qreal sampleCenter(qreal childLoc) const; qreal sampleSize(qreal childLoc) const; + qreal continuousIndex() const; int realIndexOf(int virtualIndex) const; + void updateCurrentIndexFromContentX(bool emitPreview); + void wrapContentIfNeeded(); void centerInstantlyOnReal(int realIndex); @@ -176,12 +191,13 @@ class CarouselView : public QQuickItem { Arrangement m_arrangement; + int m_layoutCapacity = 0; + qreal m_contentX = 0; int m_currentVirtualIndex = 0; int m_currentRealIndex = -1; int m_previewRealIndex = -1; - static constexpr int kLoopMultiplier = 401; int m_centerBlockStart = 0; QList m_slots; @@ -192,16 +208,19 @@ class CarouselView : public QQuickItem { bool m_dragging = false; bool m_dragActive = false; + QPointF m_pressPos; qreal m_pressContentX = 0; + QElapsedTimer m_dragTimer; qreal m_lastMoveX = 0; qint64 m_lastMoveT = 0; qreal m_velocity = 0; + QVariantAnimation* m_flickAnim = nullptr; bool m_initializedLayout = false; bool m_relayoutQueued = false; }; -}; // namespace ZShell::components +} // namespace ZShell::components