#pragma once #include #include #include #include #include #include #include class QTimer; namespace ZShell::components { // A ListView replacement that lays delegates out the way SnapLayout lays out // children: delegates are placed on device-pixel boundaries, the gaps between // them are whole device pixels, siblings that move together keep their relative // offsets, and a delegate that only moves is not resized. Like SnapLayout is to // RowLayout/ColumnLayout, this is to ListView: a Flickable with its own // placement, so there is no base-view positioner to fight with. // // Delegates receive `index` and `modelData` initial properties, so a delegate // declares `required property int index` and `required property var modelData`. // The cross-axis size (height in a horizontal view, width in a vertical one) is // set by the view, as ListView does; the main-axis size comes from the delegate // unless snapSizes is true. class SnapView : public QQuickFlickable, public QQuickItemChangeListener { Q_OBJECT QML_ELEMENT Q_PROPERTY( QAbstractItemModel* model READ model WRITE setModel NOTIFY modelChanged FINAL) Q_PROPERTY( QQmlComponent* delegate READ delegate WRITE setDelegate NOTIFY delegateChanged FINAL) // Layout Q_PROPERTY( Qt::Orientation orientation READ orientation WRITE setOrientation NOTIFY orientationChanged FINAL) Q_PROPERTY( qreal spacing READ spacing WRITE setSpacing NOTIFY spacingChanged FINAL) Q_PROPERTY( qreal cacheBuffer READ cacheBuffer WRITE setCacheBuffer NOTIFY cacheBufferChanged FINAL) Q_PROPERTY( qreal estimatedSize READ estimatedSize WRITE setEstimatedSize NOTIFY estimatedSizeChanged FINAL) // Resize delegates to device pixels along the main axis too. Off by // default, because delegates usually derive that size from their content // and resizing them would break the binding. Placement is always // device-pixel snapped. Q_PROPERTY( bool snapSizes READ snapSizes WRITE setSnapSizes NOTIFY snapSizesChanged FINAL) // Spread delegate creation over frames instead of creating everything in // the first polish pass. Q_PROPERTY( bool asynchronous READ asynchronous WRITE setAsynchronous NOTIFY asynchronousChanged FINAL) // State Q_PROPERTY(int count READ count NOTIFY countChanged FINAL) Q_PROPERTY( int currentIndex READ currentIndex WRITE setCurrentIndex NOTIFY currentIndexChanged FINAL) Q_PROPERTY( QQuickItem* currentItem READ currentItem NOTIFY currentItemChanged FINAL) signals: void modelChanged(); void delegateChanged(); void orientationChanged(); void spacingChanged(); void cacheBufferChanged(); void estimatedSizeChanged(); void snapSizesChanged(); void asynchronousChanged(); void countChanged(); void currentIndexChanged(); void currentItemChanged(); public: explicit SnapView(QQuickItem* parent = nullptr); ~SnapView() override; [[nodiscard]] QAbstractItemModel* model() const; void setModel(QAbstractItemModel* model); [[nodiscard]] QQmlComponent* delegate() const; void setDelegate(QQmlComponent* delegate); [[nodiscard]] Qt::Orientation orientation() const; void setOrientation(Qt::Orientation orientation); [[nodiscard]] qreal spacing() const; void setSpacing(qreal spacing); [[nodiscard]] qreal cacheBuffer() const; void setCacheBuffer(qreal buffer); [[nodiscard]] qreal estimatedSize() const; void setEstimatedSize(qreal size); [[nodiscard]] bool snapSizes() const; void setSnapSizes(bool snapSizes); [[nodiscard]] bool asynchronous() const; void setAsynchronous(bool asynchronous); [[nodiscard]] int count() const; [[nodiscard]] int currentIndex() const; void setCurrentIndex(int index); [[nodiscard]] QQuickItem* currentItem() const; Q_INVOKABLE void positionViewAtBeginning(); Q_INVOKABLE void positionViewAtEnd(); Q_INVOKABLE void positionViewAtIndex(int index); protected: void componentComplete() override; void geometryChange( const QRectF& newGeometry, const QRectF& oldGeometry) override; void updatePolish() override; void itemChange(ItemChange change, const ItemChangeData& value) override; // QQuickItemChangeListener void itemGeometryChanged( QQuickItem* item, QQuickGeometryChange change, const QRectF& oldGeometry) override; void itemDestroyed(QQuickItem* item) override; private: struct Row { qreal exactOffset = 0.0; qreal exactSize = 0.0; }; // Main-axis size last measured from a delegate, kept per model row so the // content extent stays right while delegates outside the viewport are // destroyed, the way ListView keeps contentHeight. QVector m_rowSizes; // Placement, mirroring SnapLayout::arrangeChildren: exact positions come // from cumulative row sizes, each near edge is rounded on its own so gaps // stay whole device pixels, and siblings keep their relative offsets. void layoutAndPlace(); void updateDelegates(); [[nodiscard]] QQuickItem* createDelegate(int index); void destroyDelegate(int index); void updateDelegateData(int index); [[nodiscard]] qreal effectiveSpacing() const; [[nodiscard]] qreal devicePixelRatio() const; // Main-axis size of a row: the delegate's own size if it exists, otherwise // the size cached when it was last visible, otherwise the estimate. [[nodiscard]] qreal rowSize(int index) const; [[nodiscard]] qint64 snappedDeviceMainSize(QQuickItem* item); [[nodiscard]] int firstVisibleIndex() const; [[nodiscard]] int lastVisibleIndex() const; [[nodiscard]] QPointF contentOrigin() const; void trackOriginResidual(); [[nodiscard]] QVariantMap delegateProperties(int index); // The model's role names, cached because some models rebuild the hash on // every call and this runs once per delegate per refresh. [[nodiscard]] const QHash& roleNames(); void invalidateRoleNames(); void rebuildRows(); // Sizes are measured along the main axis, so switching axis invalidates // every cached size. void invalidateMainAxisCaches(); // Re-keys the delegate map when rows shift, so an existing item keeps its // place in the model instead of pointing at the wrong row. void shiftDelegateIndices(int from, int delta); void updateAncestorListeners(); void clearListeners(); void connectModel(); void disconnectModel(); void onRowsInserted(const QModelIndex& parent, int first, int last); void onRowsAboutToBeRemoved(const QModelIndex& parent, int first, int last); void onRowsRemoved(const QModelIndex& parent, int first, int last); void onDataChanged( const QModelIndex& topLeft, const QModelIndex& bottomRight); void onModelReset(); QPointer m_model; QPointer m_delegate; Qt::Orientation m_orientation = Qt::Vertical; qreal m_spacing = 0.0; qreal m_cacheBuffer = 0.0; qreal m_estimatedSize = -1.0; bool m_snapSizes = false; bool m_asynchronous = false; int m_currentIndex = -1; QVector m_rows; QHash m_items; QVector m_pendingCreate; // Device-pixel main size of a delegate, so an item that only moves keeps // the size it already has rather than re-rounding a drifting exact size. QHash m_sizeDevCache; // Cached model role names; valid until the model or its roles change. QHash m_roleNames; bool m_roleNamesValid = false; // Fires the next asynchronous creation batch on the next event loop turn, // so batches land on different frames instead of one polish flush. QTimer* m_asyncTimer = nullptr; // Items watched for sub-pixel moves. QPointer, as SnapLayout does, so a // teardown order that destroys an item before the view cannot leave a // dangling pointer to remove a listener from. QVector> m_delegateListeners; QVector> m_ancestorListeners; qint64 m_originResidualX = -1; qint64 m_originResidualY = -1; bool m_placing = false; bool m_componentComplete = false; QList m_modelConnections; }; } // namespace ZShell::components