diff --git a/modules/sidebar/NotifGroupList.qml b/modules/sidebar/NotifGroupList.qml index fe7510b5..e33b3eea 100644 --- a/modules/sidebar/NotifGroupList.qml +++ b/modules/sidebar/NotifGroupList.qml @@ -1,13 +1,12 @@ -pragma ComponentBehavior: Bound - import QtQuick import QtQuick.Layouts import Quickshell +import Caelestia.Components import qs.components import qs.services import qs.config -Item { +LazyListView { id: root required property Props props @@ -16,19 +15,8 @@ Item { required property Flickable container required property DrawerVisibilities visibilities - readonly property real nonAnimHeight: { - let h = -root.spacing; - for (let i = 0; i < repeater.count; i++) { - const item = repeater.itemAt(i) as NotifDelegate; - if (item && !item.modelData.closed && !item.previewHidden) - h += item.nonAnimHeight + root.spacing; - } - return h; - } - - readonly property int spacing: Math.round(Appearance.spacing.small / 2) + readonly property real nonAnimHeight: layoutHeight property bool showAllNotifs - property bool flag signal requestToggleExpand(expand: bool) @@ -42,7 +30,15 @@ Item { } Layout.fillWidth: true - implicitHeight: nonAnimHeight + implicitHeight: contentHeight + + spacing: Math.round(Appearance.spacing.small / 2) + + removeDuration: Appearance.anim.durations.normal + + useCustomViewport: true + viewport: Qt.rect(0, container.contentY - mapToItem(container.contentItem, 0, 0).y, + width, container.height) Timer { id: clearTimer @@ -51,164 +47,116 @@ Item { onTriggered: root.showAllNotifs = false } - Repeater { - id: repeater - - model: ScriptModel { - values: root.showAllNotifs ? root.notifs : root.notifs.slice(0, Config.notifs.groupPreviewNum + 1) - onValuesChanged: root.flagChanged() - } - - delegate: NotifDelegate {} + model: ScriptModel { + values: root.showAllNotifs ? root.notifs : root.notifs.slice(0, Config.notifs.groupPreviewNum + 1) } - Behavior on implicitHeight { - Anim { - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial - } - } + delegate: Component { + MouseArea { + id: notif - component NotifDelegate: MouseArea { - id: notif + required property int index + required property NotifData modelData - required property int index - required property NotifData modelData - - readonly property alias nonAnimHeight: notifInner.nonAnimHeight - readonly property bool previewHidden: { - if (root.expanded) - return false; - - let extraHidden = 0; - for (let i = 0; i < index; i++) - if (root.notifs[i].closed) - extraHidden++; - - return index >= Config.notifs.groupPreviewNum + extraHidden; - } - property int startY - - y: { - root.flag; // Force update - let y = 0; - for (let i = 0; i < index; i++) { - const item = repeater.itemAt(i) as NotifDelegate; - if (item && !item.modelData.closed && !item.previewHidden) - y += item.nonAnimHeight + root.spacing; - } - return y; - } - - containmentMask: QtObject { - function contains(p: point): bool { - if (!root.container.contains(notif.mapToItem(root.container, p))) + readonly property bool previewHidden: { + if (root.expanded) return false; - return notifInner.contains(p); + + let extraHidden = 0; + for (let i = 0; i < index; i++) + if (root.notifs[i]?.closed) + extraHidden++; + + return index >= Config.notifs.groupPreviewNum + extraHidden; } - } + property int startY - opacity: previewHidden ? 0 : 1 - scale: previewHidden ? 0.7 : 1 + Component.onCompleted: modelData?.lock(this) + Component.onDestruction: modelData?.unlock(this) - implicitWidth: root.width - implicitHeight: notifInner.implicitHeight + LazyListView.preferredHeight: modelData?.closed || previewHidden ? 0 : notifInner.nonAnimHeight + LazyListView.visibleHeight: modelData?.closed || previewHidden ? 0 : notifInner.implicitHeight + implicitHeight: notifInner.implicitHeight - hoverEnabled: true - cursorShape: notifInner.body?.hoveredLink ? Qt.PointingHandCursor : pressed ? Qt.ClosedHandCursor : undefined - acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton - preventStealing: !root.expanded - enabled: !modelData.closed + opacity: LazyListView.removing || modelData?.closed || previewHidden || LazyListView.adding ? 0 : 1 + scale: LazyListView.removing || previewHidden ? 0.7 : LazyListView.adding ? 0.7 : 1 - drag.target: this - drag.axis: Drag.XAxis + hoverEnabled: true + cursorShape: notifInner.body?.hoveredLink ? Qt.PointingHandCursor : pressed ? Qt.ClosedHandCursor : undefined + acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton + preventStealing: !root.expanded + enabled: !(modelData?.closed ?? true) - onPressed: event => { - startY = event.y; - if (event.button === Qt.RightButton) - root.requestToggleExpand(!root.expanded); - else if (event.button === Qt.MiddleButton) - modelData.close(); - } - onPositionChanged: event => { - if (pressed && !root.expanded) { - const diffY = event.y - startY; - if (Math.abs(diffY) > Config.notifs.expandThreshold) - root.requestToggleExpand(diffY > 0); + drag.target: this + drag.axis: Drag.XAxis + + onPressed: event => { + startY = event.y; + if (event.button === Qt.RightButton) + root.requestToggleExpand(!root.expanded); + else if (event.button === Qt.MiddleButton) + modelData?.close(); } - } - onReleased: event => { - if (Math.abs(x) < width * Config.notifs.clearThreshold) - x = 0; - else - modelData.close(); - } - - Component.onCompleted: modelData.lock(this) - Component.onDestruction: modelData.unlock(this) - - ParallelAnimation { - Component.onCompleted: running = !notif.previewHidden - - Anim { - target: notif - property: "opacity" - from: 0 - to: 1 + onPositionChanged: event => { + if (pressed && !root.expanded) { + const diffY = event.y - startY; + if (Math.abs(diffY) > Config.notifs.expandThreshold) + root.requestToggleExpand(diffY > 0); + } } - Anim { - target: notif - property: "scale" - from: 0.7 - to: 1 + onReleased: event => { + if (Math.abs(x) < width * Config.notifs.clearThreshold) + x = 0; + else + modelData?.close(); } - } - ParallelAnimation { - running: notif.modelData.closed - onFinished: notif.modelData.unlock(notif) + ParallelAnimation { + running: notif.modelData?.closed ?? false + onFinished: notif.modelData?.unlock(notif) - Anim { - target: notif - property: "opacity" - to: 0 + Anim { + target: notif + property: "opacity" + to: 0 + } + Anim { + target: notif + property: "x" + to: notif.x >= 0 ? notif.width : -notif.width + } } - Anim { - target: notif - property: "x" - to: notif.x >= 0 ? notif.width : -notif.width + + Notif { + id: notifInner + + anchors.fill: parent + modelData: notif.modelData + props: root.props + expanded: root.expanded + visibilities: root.visibilities } - } - Notif { - id: notifInner - - anchors.fill: parent - modelData: notif.modelData - props: root.props - expanded: root.expanded - visibilities: root.visibilities - } - - Behavior on opacity { - Anim {} - } - - Behavior on scale { - Anim {} - } - - Behavior on x { - Anim { - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + Behavior on y { + Anim { + duration: Appearance.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + } } - } - Behavior on y { - Anim { - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + Behavior on opacity { + Anim {} + } + + Behavior on scale { + Anim {} + } + + Behavior on x { + Anim { + duration: Appearance.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + } } } } diff --git a/plugin/src/Caelestia/Components/lazylistview.cpp b/plugin/src/Caelestia/Components/lazylistview.cpp index 47db7556..88703a7f 100644 --- a/plugin/src/Caelestia/Components/lazylistview.cpp +++ b/plugin/src/Caelestia/Components/lazylistview.cpp @@ -126,6 +126,10 @@ qreal LazyListView::contentHeight() const { return m_contentHeight; } +qreal LazyListView::layoutHeight() const { + return m_layoutHeight; +} + qreal LazyListView::contentY() const { return m_contentY; } @@ -413,33 +417,55 @@ void LazyListView::updatePolish() { // --- Layout Engine --- void LazyListView::relayout() { - // Layout positioning uses preferredHeight (final/non-animated) + // Layout positioning uses preferredHeight (final/non-animated). + // Only add spacing between items with non-zero height. qreal y = 0; + bool hasLayoutItem = false; for (auto& record : m_layout) { record.targetY = y; - y += (record.heightKnown ? record.height : effectiveEstimatedHeight()) + m_spacing; + const qreal layoutH = record.heightKnown ? record.height : effectiveEstimatedHeight(); + if (layoutH > 0) { + if (hasLayoutItem) + y += m_spacing; + hasLayoutItem = true; + y += layoutH; + } } - // Content height tracks actual visible heights so scrolling follows animations + if (!qFuzzyCompare(m_layoutHeight, y)) { + m_layoutHeight = y; + emit layoutHeightChanged(); + } + + // Content height tracks actual visible heights so scrolling follows animations. + // Only add spacing between items with non-zero visible height. qreal visY = 0; + bool hasVisItem = false; for (int i = 0; i < static_cast(m_layout.size()); ++i) { qreal h; if (m_delegates.contains(i) && m_delegates[i].item) h = delegateVisibleHeight(m_delegates[i].item); else h = m_layout[i].heightKnown ? m_layout[i].height : effectiveEstimatedHeight(); - visY += h + m_spacing; + if (h > 0) { + if (hasVisItem) + visY += m_spacing; + hasVisItem = true; + visY += h; + } } - qreal maxBottom = m_layout.isEmpty() ? 0 : visY - m_spacing; // Account for dying delegates still visually present for (const auto& dying : std::as_const(m_dyingDelegates)) { - if (dying.item) - maxBottom = std::max(maxBottom, dying.item->y() + delegateVisibleHeight(dying.item)); + if (!dying.item) + continue; + const qreal dyingH = delegateVisibleHeight(dying.item); + if (dyingH > 0) + visY = std::max(visY, dying.item->y() + dyingH); } - if (!qFuzzyCompare(m_contentHeight, maxBottom)) { - m_contentHeight = maxBottom; + if (!qFuzzyCompare(m_contentHeight, visY)) { + m_contentHeight = visY; emit contentHeightChanged(); } } @@ -525,7 +551,9 @@ void LazyListView::syncDelegates() { if (entry.item) { // Measure height (prefer attached preferredHeight, fall back to implicitHeight) const qreal h = delegateHeight(entry.item); - if (h > 0 && !m_layout[i].heightKnown) { + 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); @@ -625,6 +653,9 @@ LazyListView::DelegateEntry LazyListView::createDelegate(int modelIndex) { if (wasKnown) untrackHeight(oldH); trackHeight(h); + // Relayout immediately so layoutHeight/contentHeight update + // synchronously for parent bindings, then polish for delegate sync. + relayout(); polish(); } return; diff --git a/plugin/src/Caelestia/Components/lazylistview.hpp b/plugin/src/Caelestia/Components/lazylistview.hpp index d2cc4fe6..f3f92208 100644 --- a/plugin/src/Caelestia/Components/lazylistview.hpp +++ b/plugin/src/Caelestia/Components/lazylistview.hpp @@ -62,6 +62,7 @@ class LazyListView : public QQuickItem { // Layout Q_PROPERTY(qreal spacing READ spacing WRITE setSpacing NOTIFY spacingChanged) Q_PROPERTY(qreal contentHeight READ contentHeight NOTIFY contentHeightChanged) + Q_PROPERTY(qreal layoutHeight READ layoutHeight NOTIFY layoutHeightChanged) Q_PROPERTY(qreal contentY READ contentY WRITE setContentY NOTIFY contentYChanged) // Viewport & Lazy Loading @@ -110,6 +111,7 @@ public: void setSpacing(qreal spacing); [[nodiscard]] qreal contentHeight() const; + [[nodiscard]] qreal layoutHeight() const; [[nodiscard]] qreal contentY() const; void setContentY(qreal contentY); @@ -170,6 +172,7 @@ signals: void delegateChanged(); void spacingChanged(); void contentHeightChanged(); + void layoutHeightChanged(); void contentYChanged(); void viewportChanged(); void useCustomViewportChanged(); @@ -248,6 +251,7 @@ private: qreal m_spacing = 0; qreal m_contentHeight = 0; + qreal m_layoutHeight = 0; qreal m_contentY = 0; QRectF m_viewport;